如果你遇到很机械但又很费人力的任务,那么就该请
jscodeshift
出山了!
jscodeshift
是一个工具包,用于在多个 JavaScript
或 TypeScript
文件上运行 codemods,它是:
API
。recast
是一个 AST
到 AST
的尽量保留原始代码的风格转换工具。
上图清晰地说明了 jscodeshift
的工作机制,跟 [babel小抄] 讲到的 parse -> transform -> generate 流程基本一致。
codemod[2] 是一个可用于大规模重构的部分自动化 python 工具。举一个官方的 :
codemod -m -d /home/jrosenstein/www --extensions php,html \
'<font *color="?(.*?)"?>(.*?)</font>' \
'<span style="color: \1;">\2</span>'
上面的命令可以将 /home/jrosenstein/www
下 php、html
格式的文件全部 <font>
替换成 ``,并保留字体颜色。
recast[3] 是一个 Node
包,调用 parse
生成 AST
(生成的抽象树支持 ast-types
[4] 的接口),再对 AST 调用 print 方法就能还原成代码。看一个官方的 :
// Let's turn this function declaration into a variable declaration.
const code = [
"function add(a, b) {",
" return a +",
" // Weird formatting, huh?",
" b;",
"}"
].join("\n");
// Parse the code using an interface similar to require("esprima").parse.
const ast = recast.parse(code);
然后写一个操作(manipulate
)函数:
export default function transformer(code, { recast, parsers }) {
// 这里编译器使用 recast 默认的 exprime,也可以换成其他的编译器比如 acorn,具体可见官方 API 用法
const ast = recast.parse(code, { parser: parsers.esprima });
// Grab a reference to the function declaration we just parsed.
const add = ast.program.body[0];
// 确认是一个函数声明语句
const n = recast.types.namedTypes;
n.FunctionDeclaration.assert(add);
// builders 用于创建新的节点,来自于 ast-types
const b = recast.types.builders;
// 将 AST program 节点的 body 数组第一个值赋值为新创建的 var 变量声明
ast.program.body[0] = b.variableDeclaration("var", [
b.variableDeclarator(
add.id,
b.functionExpression(
null, // Anonymize the function expression.
add.params,
add.body
)
)
]);
add.params.push(add.params.shift());
// 调用 print 生成最终的代码
return recast.print(ast).code;
}
最终生成的代码结果是:
var add = function(b, a) {
return a +
b;
};
先简单了解 jscodeshift
会涉及到几个概念——codemod
、recast
,通过官方的 简单了解它们的用法。
前面已经看过 codemod
和 recast
的示例,那本文的主角 jscodeshift
怎么用呢?本节就通过一个工作中会遇到的场景来深入感受工作机制。
前端调试代码都会使上 console
家族函数,比如 console.log
、console.error
、console.warn
等等。而且常常因为 console
写得太多了,上库时偶尔会漏删。虽然 eslint
有 no-console
规则帮你识别还有 console 存余的问题,但这个规则是不支持自动修复的:
需要你根据 eslint
报错信息定位到指定文件,然后将 console
删掉,重新 git add
-> git commit
。这个过程还是挺机械繁琐的,那么能不能在 git commit
的时候自动将变更文件中的 console
删除掉呢?答案当然是可以,本文会通过 jscodeshift
来实现这个需求。
开始写测试用例之前,先把用到的 npm
包安装一下。jscodeshift
的测试套件也是基于 jest
做的封装,所以我们需要安装 jest
包:
yarn add jscodeshift jest -D
jscodeshift
提供了一个测试套件,避免我们写大量的面条代码。unit-testing[5] 有详细的测试工具说明。我们先在根目录建一个 __tests__/remove_console-test.js
文件:
// __tests__/remove_console-test.js
const defineTest = require('jscodeshift/dist/testUtils').defineTest;
defineTest(__dirname, 'remove_console');
调用 defineTest
定义我们的测试名称,这个名称有下面 2 个约定:
transform
文件名需要是 remove_console
;__testfixtures__
目录下我们的输入、输出文件名必须是 remove_console.input.js
和 remove_console.output.js
;整明白上面的约定,接下来我们写 __testfixtures__
的测试用例:
// remove_console.input.js
export const sum = (a, b) => {
console.log('计算下面两个数的和:', a, b);
return a + b;
};
export const minus = (a, b) => {
console.log('计算下面两个数的差:' + a + ',' + b);
return a - b;
};
export const multiply = (a, b) => {
console.warn('计算下面两个数的积:', a, b);
return a * b;
};
export const divide = (a, b) => {
console.error(`计算下面两个数相除 ${a}, ${b}`);
return a / b;
};
输入方面,我们覆盖了 console
的各种家族函数,参数的各种形式。输出方面自然就是全部的 console.XXX
代码全部删除掉:
// remove_console.output.js
export const sum = (a, b) => {
return a + b;
};
export const minus = (a, b) => {
return a - b;
};
export const multiply = (a, b) => {
return a * b;
};
export const divide = (a, b) => {
return a / b;
};
有了测试用例,我们开启 jest 进行测试:
npx jest --watchAll
结果如下:
Nice,全部标红,说明我们的测试工具已经跑起来了,然后一步一步来实现我们的 transform module
。
将我们的用例丢到 astexplorer[6] 分析一下:
依据 AST
分析,要删除 console.XXX
代码,就是要将 AST
中满足以下条件的 ExpressionStatement
删除就可满足。
从上面的 AST
可以分析得出,要删除掉 console
,就是要将满足标红特点的语句表达式从抽象语法树中删除即可。
transform module
初始模板都可以用下面的结构 :
/**
* jscodeshift transform
* @param {Object} fileInfo 处理文件的信息
* @param {Object} api jscodeshift 所有的 api,这部分会在源码解析部分详细说明
* @param {Object} options CLI 传入的参数
* @returns {string} 生成的代码
*/
module.exports = (fileInfo, api, options) => {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// 对 AST 做一系列操作...
return root.toSource();
};
上述代码的 root 就是根 AST,然后就可以通过 ast 上的方法去找到满足条件的节点,然后移除,直接看代码:
/**
* jscodeshift transform
* @param {Object} fileInfo 处理文件的信息
* @param {Object} api jscodeshift 所有的 api,这部分会在源码解析部分详细说明
* @param {Object} options CLI 传入的参数
* @returns {string} 生成的代码
*/
module.exports = (fileInfo, api, options) => {
const j = api.jscodeshift;
const root = j(fileInfo.source);
// 眼睛放光没,找到一个节点既然如此简单
const expressionStatement = root.find(j.ExpressionStatement, {
expression: {
callee: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'console' },
}
},
});
expressionStatement.remove();
return root.toSource();
};
上述代码保存之后可以发现我们的测试用例已经全部通过:
虽然解决了问题,但是上面的代码看起来还是太命令式,jscodeshift 有着很好的链式调用支持:
module.exports = (fileInfo, api, options) => {
const j = api.jscodeshift;
return j(fileInfo.source)
.find(j.ExpressionStatement, {
expression: {
callee: {
type: 'MemberExpression',
object: { type: 'Identifier', name: 'console' },
}
},
})
.remove()
.toSource();
};
保存之后,测试用例依然是全部 passed
的。
这个需求的剩下最后一步通过 husky
在 commit
钩子上添加 npx jscodeshift -t remove_console.js
就完成了。这里就不具体展开讲解,有疑问的欢迎在评论区留言哦。这一节通过一个具体的业务需求,用 TDD
的方式实现一个 codemod
。例子举得比较简单,没有涉及太多的 jscodeshift
的 API
。jscodeshift
文档的不完善是挺蛋疼的。对于 API
的了解,建议可以多看官方文档底下的几个 github
仓库,例如:js-codemod[7] 、react-codemod[8]。
下一篇会剖析 jscodeshift 的源码,让我们深入学习以下几点内容:
[1]recast: https://github.com/benjamn/recast
[2]codemod: https://github.com/facebookarchive/codemod
[3]recast: https://github.com/benjamn/recast
[4]ast-types
: https://github.com/benjamn/ast-types
[5]unit-testing: https://github.com/facebook/jscodeshift#unit-testing
[6]astexplorer: https://astexplorer.net/
[7]js-codemod: https://github.com/cpojer/js-codemod
[8]react-codemod: https://github.com/reactjs/react-codemod
Copyright© 2013-2020
All Rights Reserved 京ICP备2023019179号-8