Skip to content

Writing Codemods - Manipulating Source Code

Basics

In a nutshell, a vue-metamorph codemod is a function that you define, which is passed several ASTs - the script ASTs, the template AST, and the style ASTs. Your function can traverse and mutate these ASTs by changing properties, or adding/removing nodes, and vue-metamorph will detect your changes and apply them to your source code file.

In a .js or .ts file, the template AST will always be null.

Hello, World!

A simple codemod that changes all string literals to 'Hello, world!' would look like:

ts
import type {  } from 'vue-metamorph';

const :  = {
  : 'codemod',
  : 'change string literals to hello, world',

  ({ , , , , : { ,  } }) {
    // codemod plugins self-report the number of transforms it made
    // this is only used to print the stats in CLI output
    let  = 0;

    // scriptASTs is an array of Program ASTs
    // in a js/ts file, this array will only have one item
    // in a vue file, this array will have one item for each <script> block
    for (const  of ) {
      // traverseScriptAST is an alias for the ast-types 'visit' function
      // see: https://github.com/benjamn/ast-types#ast-traversal
      (, {
        () {
          if (typeof .. === 'string') {
            // mutate the node
            .. = 'Hello, world!';
            ++;
          }

          return this.();
        }
      });
    }

    if () {
      // traverseTemplateAST is an alias for the vue-eslint-parser 'AST.traverseNodes' function
      // see: https://github.com/vuejs/vue-eslint-parser/blob/master/src/ast/traverse.ts#L118
      (, {
        () {
          if (. === 'Literal' && typeof . === 'string') {
            // mutate the node
            . = 'Hello, world!';
            ++;
          }
        },
        () {

        },
      });
    }

    return ;
  }
}

TIP

Codemods can choose which files to operate on using the filename parameter. For example, if a codemod should only touch files ending in .spec.js or .spec.ts:

ts
const codemod = {
  transform({ filename }) {
    if (!/\.spec\.[jt]s/g.test(filename)) {
      return;
    }

    // ...
  }
}

HTML Code Comments

Certain <template> node types (VExpressionContainer, VText, VStartTag, VEndTag, HtmlComment) have a leadingComment property that contains a HtmlComment node that is attached to the node and will be printed directly prior to it.

Note: a VExpressionContainer's leadingComment will only be printed if the VExpressionContainer is a direct child of a VElement.

Code Formatting

The code printed by vue-metamorph will not be formatted perfectly. vue-metamorph's aim is only to print syntactically correct code. It's recommended to use a code formatter such as eslint, prettier, or similar to fix this formatting in accordance with your project's code style conventions.

CSS

CSS codemods are supported as of vue-metamorph v3.1.0. Supported syntaxes include css, sass, scss, less and stylus.

Each codemod plugin will be passed an array of PostCSS Root objects. Use the PostCSS API to make changes to the stylesheets.

AST Explorer

AST Explorer is an invaluable tool for visualizing what the AST for a code snippet will look like. vue-metamorph uses vue-eslint-parser for the <template> AST. As the most detailed parser available for Vue files, it suits this use case fairly well, even if it was really meant for eslint.

Make sure to choose the correct parser:

Source TypeParser
Vue SFC <template>vue-eslint-parser / @babel/parser
Vue SFC <script>@babel/parser
Vue SFC <style>postcss
Vue SFC <style lang="scss">postcss (parser=scss)
Vue SFC <style lang="sass">postcss (parser=sass)
Vue SFC <style lang="less">postcss (parser=less)
Vue SFC <style lang="stylus">postcss-styl (AST visualizer)
JavaScript@babel/parser
TypeScript@babel/parser
CSSpostcss
LESSpostcss (parser=less)
SASSpostcss (parser=sass)
SCSSpostcss (parser=scss)
Styluspostcss-styl (AST visualizer)

When using @babel/parser with AST Explorer, enable this list of plugins to get an accurate representation of the AST you'll be working with.

Testing

As you're developing your codemod, using automated testing is highly recommended! Generally, we know what we want our output to look like for any given input, and since codemods are pure functions, they are easy to test for using test-driven development. Anytime you run into an edge case in your codebase, add a test case for it!

For example, if we had a codemod that removed all v-if directives, define your input, expected output, and then assert that the transformation produces the expected output:

ts
import { transform } from 'vue-metamorph';

it('should remove all v-ifs', () => {
  const source = `
<template>
  <div v-if="someCondition">
    <span v-if="anotherCondition">Hello, world!</span>
  </div>
</template>
`;

  const expected = `
<template>
  <div>
    <span>Hello, world!</span>
  </div>
</template>
`;

  expect(transform(source, 'file.vue', [myCodemod]).code).toBe(expected);
});