道源的博客

技术为道,事业为源,愿以道化源。

0%

umi-codemod

这一篇文章主要读 umi 里的@umijs/codemod的代码。这个包作用是通过umi-codemod命令对将 umi3 升级到 umi4 的一个工具。

前言

这一篇文章主要读 umi 里的@umijs/codemod的代码。这个包作用是通过umi-codemod命令对将 umi3 升级到 umi4 的一个工具。

源码阅读

执行umi-codemod命令会先运行 codemod/bin/umi-codemod.js 这个文件代码,这个文件会判断 node 版本,因为 umi4 的 node 版本必须 >= 14。

然后是执行 codemod/src/cli.ts 这个文件,这个文件是整个 codemod 流程的主文件,可以很清晰看到 codemod 干了什么事情。准备阶段处理(codemod/src/prepare.ts)、检查项目进行验证(codemod/src/checker.ts)、修改配置(codemod/src/runner/config.ts)、修改 eslint 文件(codemod/src/runner/eslintrc.ts)、修改 js 文件(codemod/src/runner/javascript.ts)、修改 css 文件(codemod/src/runner/css.ts)、修改 package.json 文件(codemod/src/runner/packageJSON.ts)

准备阶段处理(codemod/src/prepare.ts)

这个阶段主要是提供 context 让其他阶段使用。context 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
return {
// 用户配置文件
config,
// app绝对路径
absAppJSPath,
// package.json
pkg,
// 路径
pkgPath,
// 文件路径
files,
// 文件内容缓存,map结构
fileCache,
// 意想不到的布局配置
unexpectedLayoutConfig,
// 依赖
deps: {
includes: {} as Record<string, string>,
excludes: [] as string[],
},
// 开发依赖
devDeps: {
includes: {} as Record<string, string>,
excludes: [] as string[],
},
// 命令参数
args: opts.args,
};

config

在’.umirc.ts’、’.umirc.js’、’config/config.ts’、’config/config.js’中拿到优先级最高的作为用户配置,通过 esbuild 处理成 node 可读取的缓存文件(主要用来处理 ts 文件,很棒),用 require 的方式拿到用户的 json 配置,json 配置存到 config 里。

问题:如果有多个配置会丢失配置。我的建议是每个 config 都应该被处理。

file、fileCache

使用 fast-glob 快速获取 src 目录下的文件(js,jsx,ts,tsx)路径,存到 file 里。

循环 file,通过 fs 读取文件,路径为 key,文件字符串为内容,存到 fileCache 的 map 结构里。

pkg、pkgPath

cwd+package.json 作为 package.json 路径,存到 pkgPath 里。

通过 require 方式拿到 package.json 内容,存到 pkg 里。

unexpectedLayoutConfig、absSrcPath、absAppJSPath

拿到想不到(config.layout 中不是’name’, ‘title’, ‘locale’)的布局配置,存到 unexpectedLayoutConfig 里。
cwd+src 或者 cwd 作为 src 绝对路径,存到 absSrcPath 里。

absSrcPath+’app.tsx’、 ‘app.ts’、 ‘app.jsx’、 ‘app.js’其中一个路径存在作为 app 路径,存到 absAppJSPath 里。

检查项目进行验证(codemod/src/checker.ts)

这个阶段主要做一些常规检查,给出警告和错误。如果有错误就退出命令。通过创建错误数组,然后有错误添加进数组,最后看数组是否有错误,有就退出,这种错误后置的思路值得借鉴。

检查

  • git 检查
    • [错误]目录是不是 git 仓库(保证是根目录)
    • [错误]git 状态是不是清理干净了
  • config 检查
    • [错误]mpa、ssr 项目不支持升级
    • [警告]singular 属性给警告提示
  • pkg 检查
    • [错误]alita 只支持 2 到 3 升级
    • [错误]umi 只支持 3 升级

处理错误

打印错误
有错误结束程序

修改配置(codemod/src/runner/config.ts)

这个阶段会删除一些 umi4 不支持的配置项和属性、对老配置写法进行升级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 删除key
const KEYS_TO_DELETE = [
'nodeModulesTransform',
'devServer',
'dynamicImportSyntax',
'esbuild',
'fastRefresh',
'runtimeHistory',
'singular',
'webpack5',
'workerLoader',
// 暂不支持,以后会支持
'dynamicImport',
'analyze',
'exportStatic',
// 'mpa',
// 'ssr',
];
// 匹配key的value成功就删除这个属性
const KEYS_TO_DELETE_IF_VALUE_MATCH: Record<string, string> = {
devtool: 'eval-cheap-module-source-map',
};
// 匹配key的value为布尔值进行值替换
const KEYS_FROM_BOOLEAN_TO_OBJECT = { runtimePublicPath: {}, ctoken: {} };
// 替换key值,后面的替换前面的
const KEYS_CHANGE_NAME: [string, string][] = [['antd.config', 'antd.configProvider']];
  • 删除空的 proxy
  • favicon 存在就换成 favicons,并改成数组结构
  • config.layout?.name 换成 config.layout?.title
  • unexpectedLayoutConfig 是 config 的布局配置,通过 babel 迁移到 app 里
  • 有 layout 但没有 layout?.locale,设置 false

最后对收集要删除的属性和要设置的属性进行处理,重新写入。

我的补充

config.inspectorConfig 需要删除
config.dva?.hmr 需要删除

修改 eslint 文件(codemod/src/runner/eslintrc.ts)

这个文件主要处理 eslint 文件。

通过 fs+JSON.parse 获取 json 文件配置,require 获取 js 文件配置。

用户的 plugins、globals、rules 配置不变,extends 替换成umi/eslint

修改 js 文件(codemod/src/runner/javascript.ts)

这个文件主要处理用户的 js 文件。主要通过 Babel 对文件相应的节点进行转换和一些检测提醒。

转换

  • import { dynamic } from 'umi';const AsyncComponent = dynamic({ loader: import('./AsyncComponent') });替换成const AsyncComponent = loadable(() => import('./AsyncComponent'));import loadable from "@loadable/component";

  • 存在 matchPath 调用,提示看文档改

  • history.push({ pathname: '/foo', query: { a: 1 } });替换成history.push({ pathname: '/foo', search: qs.stringify({ a: 1 }) });import * as qs from "query-string";

  • 检测到 props.children 或 children,提示如果是用于嵌套子路由,请改用 <Outlet>

  • history.goBack(); 替换成 history.back();

  • ```js
    function foo(props) {
    props.history, props.location, props.match, props.routes, props.route, props.location;
    }
    // 替换
    import { history, useLocation, useMatch, useRouteData, useAppData } from ‘umi’;
    function foo(props) {
    const { route } = useAppData();
    const { routes } = useRouteData();
    const match = useMatch();
    const location = useLocation();
    history, location, match, routes, route, location;
    }
    // 其他写法
    function foo(props) {
    const { history, match, location, routes, route } = props;
    }
    // 替换
    function foo(props) {
    const match = useMatch();
    const location = useLocation();
    const { routes } = useRouteData();
    const { route } = useAppData();
    }
    import { useAppData, useRouteData, useLocation, useMatch, history } from ‘umi’;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

    - `const { query } = location;`替换成`const query = qs_l_q.parse(location.search)`,增加`import qs_l_q from 'query-string';`
    - `import * as styles from './index.module.less';`替换成`import styles from './index.module.less';`

    ### 修改 css 文件(codemod/src/runner/css.ts)

    主要对 css 文件进行修改。没有写内容。

    ### 修改 package.json 文件(codemod/src/runner/packageJSON.ts)

    这个文件用来修改 package.json 文件,主要改脚本和依赖。

    #### 脚本修改

    强制修改成下面的内容

    ```json
    {
    "lint": "umi lint",
    "lint:fix": "umi lint -fix",
    "postinstall": "umi setup",
    "setup": "umi setup"
    }

依赖

强制修改成下面的内容

1
2
3
{
"umi": "^4.0.0"
}

总结

整体来说这个包还是个试用版,能够处理一部分升级。fs 模块做文件读写,错误后置处理思路、esbuild 处理 ts 导出的 json 格式文件、babel 转换 js 文件内容。codmode 转换不难,花时间的是整理这些规则。

坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道