codemod是一种对代码进行修改的自动化方式。我把它看作是类固醇的查找和替换。
Codemod对库的维护者很有帮助,因为它允许他们对API的公共部分进行废弃和破坏性的修改,同时将使用该库的开发者的升级成本降到最低。
例如,Material UI有以下的codemod,用于更新到第五版:
npx @mui/codemod v5.0.0/preset-safe <path>
这篇文章涵盖了为可重用的React和TypeScript组件库创建codemod。

照片:Michael AleoonUnsplash
jscodeshift
jscodeshift是一个由Facebook建立的库,用于帮助创建转换。变换是将消耗的代码改变到我们的库中,以针对一个新的版本。
为了安装jscodeshift 和TypeScript类型,我们在终端运行以下命令:
npm install jscodeshift
npm install --save-dev @types/jscodeshift
AST Explorer
变换是基于代码的抽象语法树(AST)。AST Explorer是一个网站,可以帮助我们了解一些代码的AST。
要为React和TypeScript配置AST Explorer,我们选择工具栏上的typecript解析器。

配置TypeScript
在tsconfig.json ,使用exclude 字段排除测试夹具文件是很重要的(后面会有更多关于测试夹具的内容)。我们还使用outDir 字段来指定转置变换的位置:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "node",
"esModuleInterop": true,
"target": "esnext",
"strict": false,
"jsx": "preserve",
"lib": ["es2017"],
"outDir": "dist" },
"exclude": ["transforms/__testfixtures__/**"]}
配置ESLint
如果使用ESLint,我们同样需要使用ignorePatterns 字段来忽略测试夹具:
{
"root": true,
"env": { "node": true },
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": ["plugin:@typescript-eslint/recommended", "plugin:prettier/recommended"],
"ignorePatterns": ["**/__testfixtures__"], "rules": {
"@typescript-eslint/explicit-function-return-type": "off"
}
}
创建一个转换来改变一个组件道具的名称
我们将创建一个转换,将Button 元素上的kind 道具改为variant 。我们将把这个转换称为button-kind-to-variant 。
文件结构
所有的转换都将放在transforms 文件夹中。测试将在transforms 文件夹中的__tests__ 文件夹中。下面是文件结构的样子:
transforms
└── button-kind-to-variant.ts // the transform
└── __tests__
└── button-kind-to-variant.ts // test for the transform (references test fixtures below)
└── __testfixtures__
└── button-kind-to-variant
└── basic.input.tsx // Code snippet to test
└── basic.output.tsx // expected result test code snippet
创建一个测试
我们将在写转换之前写一个测试。这将使我们清楚地知道转换需要做什么。让我们从测试夹具开始,它只是输入和预期输出的代码片段:
// basic.input.tsx
import { Button } from "@my/reusable-components";
function SomeComponent() {
return <Button kind="round">test</Button>;
}
// basic.output.tsx
import { Button } from "@my/reusable-components";
function SomeComponent() {
return <Button variant="round">test</Button>;
}
所以,我们期望kind 的道具在Button 元素上改变为variant 的道具。
注意:如果你使用prettier来自动格式化代码,建议排除测试夹具文件,这样你就可以调整它们的格式,使之与jscodeshift 中的内容一致:
// .prettierignore
**/__testfixtures__
转换的测试代码是相当通用的,为每个测试夹具创建一个测试。改变的部分是转换名称的变量和测试夹具名称的数组:
// button-kind-to-variant.ts
jest.autoMockOff();
import { defineTest } from 'jscodeshift/dist/testUtils';
const name = 'button-kind-to-variant';const fixtures = ['basic'] as const;
describe(name, () => {
fixtures.forEach((test) =>
defineTest(__dirname, name, null, `${name}/${test}`, {
parser: 'tsx',
}),
);
});
创建转换
这里是我们转换的开始:
import { API, FileInfo, JSXIdentifier } from 'jscodeshift';
export default function transformer(file: FileInfo, api: API) {
const j = api.jscodeshift;
const root = j(file.source);
// TODO find Button JSX elements
// TODO find `kind` prop
// TODO change `kind` to `variant`
return root.toSource();
}
jscodeshift 期待一个函数作为默认的输出来进行转换。该函数接收关于源文件和 API的信息。 API可以查询源文件并对其进行修改。jscodeshift jscodeshift
在编写转换代码之前,我们可以使用AST Explorer来感受一下AST的结构。毫不奇怪,Button 是一个JsxElement:

...而kind 道具是一个JsxAttribute :

下面是完整的转换函数:
export default function transformer(file: FileInfo, api: API) {
const j = api.jscodeshift;
const root = j(file.source);
// find Button JSX elements
root .findJSXElements('Button') // find `kind` prop
.find(j.JSXAttribute, { name: { type: 'JSXIdentifier', name: 'kind', }, }) // change `kind` to `variant`
.forEach((jsxAttribute) => { const identifier = jsxAttribute.node.name as JSXIdentifier; identifier.name = 'variant'; });
return root.toSource();
}
findJSXElements 方法被用来定位所有的Button 元素。对于找到的Button 元素,find 方法被用来获取kind 属性。find 方法返回一个匹配项目的集合,所以我们使用forEach 方法来遍历这个集合。然后我们将属性名称改为'variant' 。
运行测试
我们运行jest 来运行测试。因此,我们可以在package.json 的test 脚本中加入这个内容:
{
"scripts": {
"test": "jest", ...
},
}
运行npm test ,就可以运行测试了:

我们的测试通过了 😊
运行codemod
在运行codemod之前,我们需要将转换的内容转译成JavaScript。我们可以使用package.json 中的build 脚本来完成,它可以调用TypeScript编译器,tsc :
{
"scripts": {
"build": "tsc", ...
},
}
运行npm run build ,将把转换的JavaScript版本放在一个dist 的文件夹中。
为了运行codemod,我们需要运行jscodeshift CLI。我们可以在package.json 中放入一个codemod 脚本来做到这一点:
{
"scripts": {
"codemod": "jscodeshift",
...
},
}
通常情况下,codemod将在一个单独的项目中的代码上执行。在这个例子中,我们将对这个项目中的一个文件执行codemod,地址是src\HomePage.tsx 。运行下面的命令将在HomePage.tsx 文件上执行转换的模拟运行:
npm run codemod -- --parser=tsx -t dist/button-kind-to-variant.js src/HomePage.tsx --print --dry
下面是对参数的解释:
--parser=tsx表示TypeScript解析器被用来解析codemod所应用的文件。我们需要指定这个,因为默认的解析器是babel。-t后面的文件指定了转换文件。在我们的例子中是dist/button-kind-to-variant.js。- 变换路径后的文件或目录指定了代码修正所要应用的文件。在我们的例子中是
src/HomePage.tsx。 --print(或 )指定将转换后的文件输出到终端。-p--dry(或 )指定只进行模拟运行(而不是改变源文件)。d
因此,运行该命令会打印出更新后的源代码:

更新后的源代码正是我们所需要的😊
运行下面的命令可以运行转换并执行更新:
npm run codemod -- --parser=tsx -t dist/button-kind-to-variant.js src/HomePage.tsx
很好 😊
这个例子的codemod在我的GitHub上