How to Automate JavaScript Component Refactoring with jscodeshift
Learn how to use Facebook’s jscodeshift tool to write codemods that automatically rename lifecycle methods in JavaScript components, with step‑by‑step examples, test setup using AVA, and detailed AST traversal logic for precise transformations.
Standard Component project needs an AST‑based JavaScript transformer to convert one type of component to a Standard Component. Instead of reinventing the wheel with esprima, we use Facebook’s jscodeshift.
jscodeshift is a JavaScript codemod tool that helps automate large‑scale refactors while still allowing human oversight.
Codemod is a tool/library to assist you with large‑scale codebase refactors that can be partially automated but still require human oversight and occasional intervention.
jscodeshift builds on esprima and provides convenient path‑based AST traversal.
The project depends on the following libraries:
jscodeshift
ava
jscodeshift-ava-tester
Simple refactor example: rename the lifecycle method
finishedto
ready.
First write test cases with AVA:
<code>import test from 'ava'
import jscodeshift from 'jscodeshift'
import testCodemod from '../test.plugin'
import transformer from '../transformer/old-component/test'
const { testChanged, testUnchanged } = testCodemod(jscodeshift, test, transformer)
testChanged(`import Base from 'base';
export default Base.extend({
finished: () => {
console.log('ready')
}
});`, `import Base from 'base';
export default Base.extend({
ready: () => {
console.log('ready')
}
});`)
testUnchanged(`import Base from 'base';
export default Base.extend({
other: () => {
console.log('other')
}
});`)
</code>Paste the code to be transformed into the AST Explorer (see image).
Identify the red‑boxed node and start writing the codemod.
<code>function transformer(file, api) {
const j = api.jscodeshift
// TODO: filter function
// Replace Identifier name from finished to ready
const replaceFinished = p => {
Object.assign(p.node, { name: 'ready' })
return p.node
}
return (
j(file.source)
.find(j.Identifier, { name: 'finished' })
.filter(isProperty)
.filter(isArgument)
.replaceWith(replaceFinished)
.toSource()
)
}
module.exports = transformer
</code>Filtering logic uses the node’s path to verify its parent is a Property with key name “finished”, that the Identifier appears as an argument of a CallExpression, and that the CallExpression is
Base.extend:
<code>const isProperty = p => {
return (
p.parent.node.type === 'Property' &&
p.parent.node.key.type === 'Identifier' &&
p.parent.node.key.name === 'finished'
)
}
const isArgument = p => {
if (p.parent.parent.parent.node.type === 'CallExpression') {
const call = p.parent.parent.parent.node
return checkCallee(call.callee)
}
return false
}
const checkCallee = node => {
const types = (
node.type === 'MemberExpression' &&
node.object.type === 'Identifier' &&
node.property.type === 'Identifier'
)
const identifiers = (
node.object.name === 'Base' &&
node.property.name === 'extend'
)
return types && identifiers
}
</code>Running the tests confirms the transformation works.
Tencent IMWeb Frontend Team
IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.