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 { 
CodemodPlugin
} from 'vue-metamorph';
const
changeStringLiterals
:
CodemodPlugin
= {
type
: 'codemod',
name
: 'change string literals to hello, world',
transform
({
scriptASTs
,
sfcAST
,
styleASTs
,
filename
,
utils
: {
traverseScriptAST
,
traverseTemplateAST
} }) {
// codemod plugins self-report the number of transforms it made // this is only used to print the stats in CLI output let
transformCount
= 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
scriptAST
of
scriptASTs
) {
// traverseScriptAST is an alias for the ast-types 'visit' function // see: https://github.com/benjamn/ast-types#ast-traversal
traverseScriptAST
(
scriptAST
, {
visitLiteral
(
path
) {
if (typeof
path
.
node
.
value
=== 'string') {
// mutate the node
path
.
node
.
value
= 'Hello, world!';
transformCount
++;
} return this.
traverse
(
path
);
} }); } if (
sfcAST
) {
// 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
traverseTemplateAST
(
sfcAST
, {
enterNode
(
node
) {
if (
node
.
type
=== 'Literal' && typeof
node
.
value
=== 'string') {
// mutate the node
node
.
value
= 'Hello, world!';
transformCount
++;
} },
leaveNode
() {
}, }); } return
transformCount
;
} }

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;
    }

    // ...
  }
}

Code Comment Preservation

Recast does a fairly decent job of preserving code comments in JavaScript and TypeScript, but at this time, HTML comments inside of modified <template> nodes will not be re-printed. Comments inside unchanged <template> nodes will not be affected.

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);
});