awxxxxxx/awxxxxxx.github.io

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.
workflow
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.

image

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.
image

Lifecycle Hooks

Update lifecycle hooks is quite straight. We can break down it into two steps.

  1. First, renaming all hooks.
    image

  2. Second, putting all hooks into setup function.
    image

{
  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.