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:
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
:
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 Type | Parser |
---|---|
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 |
CSS | postcss |
LESS | postcss (parser=less) |
SASS | postcss (parser=sass) |
SCSS | postcss (parser=scss) |
Stylus | postcss-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:
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);
});