How to build a vue3 migration tool
Opened this issue · 0 comments
Recently, I'm working on a tool named vue23 that aims to migrate vue2 project to vue3 automatically.
Currently, it supports wrap variables hosted in data
with reactive
method and rename vue2 lifecycle hooks. Next it will support computed
, watch
, provide & inject
and typescript
.
Why I build this.
vue3 comes up with composition api which is significantly different with vue2. Migrating existing project to vue3 one by one manually may take huge works. This tool can help developers automatically migrating and say no to 996.
How it works.
vue23 is built up on babel, vue-template-compiler and typescript. The basic workflow likes below digram.
To complete this post, you may need some basic knowledge about AST and babel
reactive
vue2 puts all reactive variables into data
and uses object.defineproperty
to track dependencies.
In vue3, variables has to be wrapped with reactive
method and host in setup
lifecycle. To archive this, we can modify AST using @babel/traverse
. @babel/traverse
supplies traverse method that walks the whole AST tree using visitor pattern, so we can modify, replace, remove AST nodes conveniently when visiting.
Here is the core code about migrating reative.
{
ReturnStatement(path) {
const parent = path.getFunctionParent();
if (t.isObjectMethod(parent.node)) {
const gp = parent as NodePath<t.ObjectMethod>;
setupNode = gp;
if (t.isIdentifier(gp.node.key, { name: KeyWords.Data})) {
// rename 'data' to 'setup'
gp.node.key.name = KeyWords.Setup;
setupNode = gp;
// @TODO extract to another function
// make varibale reactivity
if (t.isIdentifier(path.node.argument)) {
const n = resolveTopIdentifier(path.node.argument.name, path)
if (n?.isVariableDeclarator()) {
let args = [];
if (n.node.init) {
args.push(n.node.init);
}
const call = wrapWithReactive(args)
const v = t.variableDeclarator(n.node.id, call);
n.replaceWith(v);
}
options.reactive = true;
} else if (path.node.argument) {
const call = wrapWithReactive([path.node.argument])
if (path.scope.hasOwnBinding('state')) {
path.scope.rename('state')
}
let re;
if (t.isBlockStatement(path.parentPath.node)) {
const nstateIdentifier = t.identifier('state');
gp.scope.push({ id: nstateIdentifier, init: call, kind: 'const' })
re = t.returnStatement(
t.objectExpression([t.objectProperty(nstateIdentifier, nstateIdentifier)])
);
} else {
re = t.returnStatement(
t.objectExpression([t.objectProperty(call, call)])
);
}
options.reactive = true;
path.replaceWith(re);
}
}
}
}
}
As you can see, we do the modification in ReturnStatement
, in this method we have to check whether the ReturnStatement
is hosted in data
method and wrap the returned obj with reactive
. Also we need to handle edge cases like the return statement yields a variable b
, b
references another variable a
which is an object. In this case we have to wrap a
rather than b
.
Lifecycle Hooks
Update lifecycle hooks is quite straight. We can break down it into two steps.
{
Program: {
exit(path) {
importDependencies(path, options);
options = defaultImportOptions;
if (lifecycleHooksPath.length) {
const node = setupNode ? setupNode.node : generateSetupNode(lifecycleHooksPath[0].path);
const exps = lifecycleHooksPath.reduce((previous, current) => {
return previous.concat(current.exps);
}, [] as t.Statement[]);
if (t.isObjectMethod(node)) {
insertStatements(node.body.body, exps)
} else if (t.isObjectProperty(node) && t.isFunctionExpression(node.value)) {
const value = node.value as t.FunctionExpression;
insertStatements(value.body.body, exps)
}
lifecycleHooksPath = []
}
},
},
ObjectMethod(path) {
if (isLifecycleHook(path.node.key.name)) {
lifecycleHooksPath.push({ exps: convertHook(path), path });
}
},
ObjectProperty(path) {
if (isLifecycleHook(path.node.key.name) && t.isFunctionExpression(path.node.value)) {
lifecycleHooksPath.push({ exps: convertHook(path), path });
}
},
}
As above, we use ObjectMethod
and ObjectProperty
methods to collect lifecycle hooks, insert lifecycle hooks into setup
in Program
method.
All above is the basic the intro about vue23. If you are interested in this project, please feel free to contact me.