用于React和TypeScript的Codemods

1,055 阅读5分钟

codemod是一种对代码进行修改的自动化方式。我把它看作是类固醇的查找和替换。

Codemod对库的维护者很有帮助,因为它允许他们对API的公共部分进行废弃和破坏性的修改,同时将使用该库的开发者的升级成本降到最低。

例如,Material UI有以下的codemod,用于更新到第五版:

npx @mui/codemod v5.0.0/preset-safe <path>

这篇文章涵盖了为可重用的React和TypeScript组件库创建codemod。

Codemods for React and TypeScript

照片: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解析器。

AST Explorer typescript parser

配置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:

Button JSX element

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

kind JSX attribute

下面是完整的转换函数:

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.jsontest 脚本中加入这个内容:

{
  "scripts": {
    "test": "jest",    ...
  },
}

运行npm test ,就可以运行测试了:

Test result

我们的测试通过了 😊

运行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

因此,运行该命令会打印出更新后的源代码:

Dry codemod run

更新后的源代码正是我们所需要的😊

运行下面的命令可以运行转换并执行更新:

npm run codemod -- --parser=tsx -t dist/button-kind-to-variant.js  src/HomePage.tsx

很好 😊

这个例子的codemod在我的GitHub