This example swaps out function name with another
source.js
console.log('this should be a warning');
mod.js
// A codemod is simply a module that exports a function with 2 parameters: `file` and `api`.
module.exports = function mod(file, api) {
// The jscodeshift function may be used frequently in a codemod, to do everything from creating an AST from source, to traversing that AST.
const jsCS = api.jscodeshift;
// Convert file code into an AST. This AST will be wrapped in a jscodeshift collection.
const root = jsCS(file.source);
// - The `find` method that is exposed on all collections. In this case, find is called on the `root` collection, which tells jscodeshift to search the entire source.
// - The `find` method takes 2 arguments:
// - TypeDefinition: All available type definitions are exposed as properties on `api.jscodeshift`. The simplest way to identify the correct type definition you need is to highlight the code you are looking to transform in the top-left panel of AST Explorer. Then, in the top-right panel, look for the type property for the corresponding node. Any type property you see in the AST will always have a corresponding property of the same name on `api.jscodeshift`.
// - (Optional) Filter: A filter can be either a function that returns true/false, or an Object that can be used to partially match against all nodes found for the specified TypeDefinition. In this example, an Object that matches unique pieces of the `callee` property is used.
const consoleLogCalls = root.find(jsCS.CallExpression, {
callee: {
object: { name: 'console' },
property: { name: 'log' },
},
});
// A collection exposes many of the common methods you would expect to see on Array.prototype. In this case, `forEach` is used to tell jscodeshift to work with each path in the collection, one at a time.
consoleLogCalls.forEach(p => {
// Although collections and paths expose many helpful APIs to declaratively modify the AST, you can always grab a reference to the node wrapped in a path, and mutate it directly. In this example, we are renaming the callee's `property.name`, to change it from `log` to `warn`.
p.node.callee.property.name = 'warn';
});
// - jscodeshift expects that a codemod will return a string representation of the new source code when the transformation is complete. To obtain a String from our AST, we can call `toSource()` on the `root` collection. The `toSource` function calls `recast` directly, and accepts an object with any options that can be provided to recast.
// - You can alter the default configs for `recast` by passing an Object to `toSource`.
return root.toSource();
}
Look for an Object reference and replace it with another.
// Look for `this.someReactRef.current`
const refName = 'someReactRef';
root.find(jsCS.MemberExpression, {
object: {
object: {
object: { type: 'ThisExpression' },
property: { name: refName },
},
property: { name: 'current' },
},
})
.replaceWith((np) => {
// MemberExpression instructs that a prop (Identifier) belongs to a parent.
return jsCS.memberExpression(jsCS.identifier('fu'), jsCS.identifier('bar'));
});
// results in: fu.bar
Construct a function call
jsCS.callExpression(
jsCS.identifier('test'),
[jsCS.literal('fu'), jsCS.literal('bar')]
)
// results in: test('fu', 'bar')
Replacing one line with multiple lines.
Ideally you could just use replaceWith
, but that seems to only handle replacing one item with another. replaceWith
accepts an Array as a return, but errors if you have more than one item in it.
In order to accomplish this, you have to get the body
of the current items context, find the current item's reference, and the swap it out with the new items.
// util to traverse upwards until it finds the items `body`
const getParentBody = (np) => {
return (np.name === 'body') ? np.node.body : getParentBody(np.parentPath);
};
root
.find(jsCS.CallExpression, {
callee: {
object: { type: 'ThisExpression' },
property: { name: 'setState' },
},
})
.forEach((np) => {
const assignments = np.node.arguments[0].properties.map((n) => {
return jsCS.expressionStatement(jsCS.assignmentExpression('=', n.key, n.value));
});
const body = getParentBody(np);
const bodyNdx = body.findIndex((n, ndx) => n.start === np.node.start); // `start` is the line the item is on
body.splice(bodyNdx, 1, ...assignments); // remove `1` item, and insert new items
});
// before
if (true) {
var1 = '1';
this.setState({ var2: '2', var3: '3' });
var4 = '4';
}
// after
if (true) {
var1 = '1';
var2 = '2';
var3 = '3';
var4 = '4';
}
Remove 'this.' from function calls and maintain arguments.
// look for something like `this.fnCall(arg1, arg2);`
root
.find(jsCS.ExpressionStatement, {
expression: {
callee: {
object: { type: 'ThisExpression' },
},
},
})
.replaceWith((np) => {
const n = np.node.expression;
return jsCS.callStatement(n.callee.property, n.arguments);
});
// result: `fnCall(arg1, arg2);`
Create misc. custom lines
jsCS.program([
jsCS.variableDeclaration(
'const',
[
jsCS.variableDeclarator(jsCS.identifier('fu'), jsCS.literal('bar'))
]
),
jsCS.variableDeclaration(
'const',
[
jsCS.variableDeclarator(jsCS.identifier('bar'), jsCS.literal('fu'))
]
)
]);
// result: `const fu = 'bar';\nconst bar = 'fu';`
Creating statement and expression nodes, the easy way
One thing to note, it seems that variables don't always work when added in the template. In the cases where I don't see my existing variables inlined into a template, I just use the template result as a base, and access the Node's props directly and add in any other nodes that way (usually via an Array push
).
jsCS.template.statement`const fu = 'bar';` // result: VariableDeclaration Node
jsCS.template.statement` // result: The first VariableDeclaration Node
const fu = 'bar';
const bar = 'fu';
`
jsCS.template.statements` // result: An Array of VariableDeclaration Nodes (note, using statements NOT statement)
const fu = 'bar';
const bar = 'fu';
`
jsCS.template.statements` // result: An Array with one ExpressionStatement Node (since it's a JSX element)
<div className="fu">
Hello World
</div>
`
jsCS.template.expression`3 + 3` // result: BinaryExpression Node
jsCS.template.expression`num++` // result: UpdateExpression Node
First you'll need to install jscodeshift
globally
npm i -g jscodeshift
The below example uses the files from the Write a CodeMod section.
jscodeshift \
--dry \ # don't modify the source file
--print \ # print the result out
--transform=mod.js \ # path to the file that will transform the source file
source.js
Looked all over can couldn't find documentation on all the possible builder functions/options, had to cobble together a way to print out everything. The definitions may be here, couldn't find the lowercase equivelants, but they seem to call out what arguments/types are needed by the builders.
While on AST Explorer I added this in at the top of transformer definition.
module.exports = function transformer(file, api) {
- const jsCS = api.jscodeshift;
+ const jsCS = window.jsCS = api.jscodeshift;
Looks like all builders
can be viewed via jsCS.types.builders
.
Now I can run stuff in the DevTools Console.
// Anything starting with a capitol letter is most likely a type (that you'd use in `find`) so only care about items beginning with a lowercase letter.
console.log(Object.keys(jsCS).filter(fn => /^[a-z]/.test(fn)).sort().join('\n'));