本文作者:IMWeb 杨文坚 原文出处:IMWeb社区 未经同意,禁止转载
Standard Component 项目需要一个基于 AST 的 Javascript Transformer 编写工具,用于从一种类型的组件 transform 到 Standard Component。本来,想用著名的 esprima,来编写相应工具。但后来发现,Facebook 已经开发了 jscodeshift,重造一个轮子明显是多余的。
所以,jscodeshift 是什么鬼?
jscodeshift 是一个 Javscript Codemod 工具,官方对 Codemod 的解释是:
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 也是基于 esprima 的,相比 esprima 及 estools 工具集,其通过 path 可以很容易的在 AST 上遍历 node。
OK,前戏不多言,直接上例子,项目主要依赖下面这些库:
简单重构,比如生命周期,初始化完成finished
,改名成为了ready
。
先写好测试用用例:
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')
}
});`)
然后我们将需要修改的代码粘贴进 AST explorer:
注意红色框出来的 node,好的开始写 codemod。
function transformer(file, api) {
const j = api.jscodeshift
// TODO 等下要写的过滤函数
// 把 Identifier 节点的 name 从 finished 改成 ready 就行了
const replaceFishined = p => {
Object.assign(p.node, { name: 'ready' })
return p.node
}
return (
// 读取文件
j(file.source)
// 找到 Identifier 节点,且其名字为 finished
.find(j.Identifier, { name: 'finished' })
// TODO 要验证一下是不是 Base.extend 里面的
.replaceWith(replaceFishined)
.toSource()
)
}
module.exports = transformer
怎么过滤呢?主要就是通过 path 找到他的父节点,然后逐一判断类型和名字是不是符合预期的。
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
}
大功告成:
function transformer(file, api) {
const j = api.jscodeshift
const isProperty = p => {
return (
p.parent.node.type === 'Property' &&
p.parent.node.key.type === 'Identifier' &&
p.parent.node.key.name === 'finished'
)
}
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
}
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 replaceFishined = p => {
Object.assign(p.node, { name: 'ready' })
return p.node
}
return (
j(file.source)
.find(j.Identifier, { name: 'finished' })
.filter(isProperty)
.filter(isArgument)
.replaceWith(replaceFishined)
.toSource()
)
}
module.exports = transformer
测试通过,搞定!