# Plugin
# custom babel-plugin
这里我们记录下怎么实现一个自定义 babel-plugin
Plugin Development 文档 (opens new window)
首先我们需要安装几个依赖
依赖 | 版本 | 作用 |
---|---|---|
@babel/core | ^7.21.3 | babel 转译代码 |
@babel/preset-react | ^7.18.6 | 转译react代码 |
father | ^4.1.7 | ts转commonjs打包工具 |
np | ^7.6.3 | npm包发布工具 |
umi-test | ^1.9.7 | babel-plugin单元测试( jest ) |
@babel/helper-module-imports | ^1.9.7 | 修改 Ast 代码工具 |
我们再看下 package.json scripts 命令配置
"scripts": {
"build": "father build", // 打包
"test": "umi-test", // 单元测试
"prepublishOnly": "npm run build && father doctor && np --no-cleanup --yolo --no-publish --any-branch" // npm publish 会触发的声明周期 主要做了打包和项目依赖检测还有发布代码
},
1
2
3
4
5
2
3
4
5
我们再看下项目的结构
.
├── README.md
├── __tests__ // 单元测试文件夹
│ ├── index.test.js // 单元测试主要执行文件 动态读取 packages 下的 actual.js 并编译 和 expected.js 做对比
│ └── packages
│ ├── antd-custom-name
│ │ ├── actual.js
│ │ └── expected.js
│ ├── antd-library-directory
│ │ ├── actual.js
│ │ └── expected.js
│ ├── antd-name
│ │ ├── actual.js
│ │ └── expected.js
│ ├── antd-style-css-name
│ │ ├── actual.js
│ │ └── expected.js
│ └── antd-style-name
│ ├── actual.js
│ └── expected.js
├── index.d.ts
├── lib // 打包文件
│ ├── Plugin.js
│ ├── index.js
│ └── utls.js
├── package.json
└── src // 主代码
├── Plugin.ts
├── index.ts
└── utls.ts
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
29
30
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
29
30
我们主要看下 文件夹 src 和 test 文件夹
src/index.ts
自定义 babel-plugin 官网文档 (opens new window)比较详细, 可以参考
import { addDefault, addSideEffect } from "@babel/helper-module-imports";
import { windowPath } from "./utls";
import { join } from "path";
export interface Opts_Props {
libraryName: string;
libraryDirectory?: string;
style: boolean | "css";
types: any;
customNameCB: ((name: string, file: any) => string) | undefined;
index?: number;
}
export default class Plugin implements Opts_Props {
libraryName: string; // 依赖仓库的名 比如 antd
libraryDirectory: string; // 需要自定义配置的依赖路径 默认是 lib
style: boolean | "css"; // 自定义引入样式文件路径格式
pluginStateKey: string;
types: any; // [babel-types](https://babel.dev/docs/babel-types)
customNameCB: ((name: string, file: any) => string) | undefined; // 自定义依赖路径拼接
index?: number | undefined;
constructor(
libraryName: string,
libraryDirectory: string | undefined,
style: boolean | "css" | undefined,
customNameCB: ((name: string, file: any) => string) | undefined,
types: any,
index = 0
) {
this.libraryName = libraryName;
this.libraryDirectory =
typeof libraryDirectory === "undefined" ? "lib" : libraryDirectory;
this.style = style || false;
this.customNameCB = customNameCB;
this.types = types;
this.pluginStateKey = `pluginStateKey${index}`;
}
// 基于 state 维护好字典 pluginState
getPluginState(state) {
if (!state[this.pluginStateKey]) {
state[this.pluginStateKey] = {};
}
return state[this.pluginStateKey];
}
// 初始化字典
ProgramEnter(path, state) {
const pluginState = this.getPluginState(state);
pluginState.specifiers = Object.create(null);
pluginState.pateToRemove = []; // 待删除节点列表
pluginState.selectedMethods = []; // 已选中(格式化)节点列表
}
// 删除已经被标记需要删除的节点
ProgramExit(path, state) {
this.getPluginState(state).pateToRemove.forEach(
(p) => !p.removed && p.remove()
);
}
// ast import 节点编译触发的钩子
// 这里主要就是拿到左侧的值 比如说 Button 放入字典 pluginState.specifiers 中, 后续使用, 并把该 import 节点放入 pateToRemove 中, 在节点退出的时候删除该节点
ImportDeclaration(path, state) {
const { node } = path; // 节点
if (!node) return;
const { value } = node.source;
const { libraryName, types } = this;
const pluginState = this.getPluginState(state);
if (value === libraryName) {
// 拿到左侧的值
node.specifiers.forEach((spec) => {
// https://babeljs.io/docs/babel-types.html
// 判断是否是解构内的值 也就是 Button 之类的
if (types.isImportSpecifier(spec)) {
pluginState.specifiers[spec.local.name] = spec.imported.name;
}
});
pluginState.pateToRemove.push(path);
}
}
// 引入的依赖执行的时候会触发该钩子
// 主要就是拿到对应的 pluginState, 遍历 arguments 拿到需要引入的遍历名, 触发 importMethod 用于新增 import 代码
CallExpression(path, state) {
const { node } = path;
const file = path && path.hub && path.hub.file;
const pluginState = this.getPluginState(state);
node.arguments = node.arguments.map((arg) => {
const { name } = arg;
if (
pluginState.specifiers[name] &&
path.scope.hasBinding(name) &&
path.scope.getBinding(name).path.type === "ImportSpecifier"
) {
this.importMethod(pluginState.specifiers[name], file, pluginState);
}
return arg;
});
}
// 新增代码
// 通过 @babel/helper-module-imports 提供新增 ast 代码的方法 新增格式化后的代码 包括组件和组件的样式
// 注意: 需要通过 pluginState.selectedMethods 做好缓存, 避免同一个依赖多次触发新增代码, 不然新增的代码会递增出现异常, 具体可以自己试一下
importMethod(methodName, file, pluginState) {
const { customNameCB, libraryDirectory, libraryName, style } = this;
if (!pluginState.selectedMethods[methodName]) {
const path = windowPath(
customNameCB
? customNameCB(methodName, file)
: windowPath(join(libraryName, libraryDirectory, methodName))
);
// 防止重复添加 复用节点
pluginState.selectedMethods[methodName] = addDefault(file.path, path, {
nameHint: methodName,
});
if (style === true) {
addSideEffect(file.path, `${path}/style`);
} else if (style === "css") {
addSideEffect(file.path, `${path}/style/css`);
}
}
return { ...pluginState.selectedMethods[methodName] };
}
}
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# 至此 自定义 babel-plugin 已经写完了
← 学习 Babel