@oceanbase/codemod 0.4.16 → 1.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -8
- package/bin/upgrade-list.json +8 -5
- package/package.json +3 -3
- package/transforms/__testfixtures__/less-to-token/antd-v4-less-to-token.input.less +2 -0
- package/transforms/__testfixtures__/less-to-token/antd-v4-less-to-token.output.less +2 -0
- package/transforms/__testfixtures__/less-to-token/case-insensitive.input.less +2 -0
- package/transforms/__testfixtures__/less-to-token/case-insensitive.output.less +2 -0
- package/transforms/__testfixtures__/less-to-token/mixin.input.less +3 -0
- package/transforms/__testfixtures__/less-to-token/mixin.output.less +3 -0
- package/transforms/__testfixtures__/less-to-token/obui-less-to-token.input.less +2 -0
- package/transforms/__testfixtures__/less-to-token/obui-less-to-token.output.less +2 -0
- package/transforms/__testfixtures__/less-to-token/obui-less-token-to-token.input.less +1 -0
- package/transforms/__testfixtures__/less-to-token/obui-less-token-to-token.output.less +1 -0
- package/transforms/__testfixtures__/style-to-token/anonymous-function.input.js +13 -0
- package/transforms/__testfixtures__/style-to-token/anonymous-function.output.js +14 -0
- package/transforms/__testfixtures__/style-to-token/antd-style.input.js +3 -0
- package/transforms/__testfixtures__/style-to-token/antd-style.output.js +3 -0
- package/transforms/__testfixtures__/style-to-token/block-statement.input.js +4 -0
- package/transforms/__testfixtures__/style-to-token/block-statement.output.js +4 -0
- package/transforms/__testfixtures__/style-to-token/class-component.input.js +2 -2
- package/transforms/__testfixtures__/style-to-token/class-component.output.js +2 -2
- package/transforms/__testfixtures__/style-to-token/existed-useToken.input.js +2 -2
- package/transforms/__testfixtures__/style-to-token/existed-useToken.output.js +2 -2
- package/transforms/__testfixtures__/style-to-token/function-component.input.js +4 -4
- package/transforms/__testfixtures__/style-to-token/function-component.output.js +4 -4
- package/transforms/__testfixtures__/style-to-token/function.input.js +6 -6
- package/transforms/__testfixtures__/style-to-token/function.output.js +7 -7
- package/transforms/__testfixtures__/style-to-token/hooks.input.js +4 -4
- package/transforms/__testfixtures__/style-to-token/hooks.output.js +4 -4
- package/transforms/__testfixtures__/style-to-token/nested-block-statement.input.js +3 -3
- package/transforms/__testfixtures__/style-to-token/nested-block-statement.output.js +3 -3
- package/transforms/__testfixtures__/style-to-token/single-function.input.js +5 -0
- package/transforms/__testfixtures__/style-to-token/single-function.output.js +7 -0
- package/transforms/__testfixtures__/style-to-token/top-identifier.input.js +1 -0
- package/transforms/__testfixtures__/style-to-token/top-identifier.output.js +1 -0
- package/transforms/__tests__/style-to-token.test.ts +2 -0
- package/transforms/less-to-token.js +16 -6
- package/transforms/style-to-token.js +392 -121
- package/transforms/utils/index.js +8 -0
- package/transforms/utils/token.js +39 -0
|
@@ -5,13 +5,13 @@ const Demo = () => {
|
|
|
5
5
|
const { token } = theme.useToken();
|
|
6
6
|
const columns = [{
|
|
7
7
|
render: () => {
|
|
8
|
-
return <Tooltip color={token.colorBgContainer} backgroundColor={token.colorErrorBg} borderColor={token.colorBgLayout} border={`1px solid ${token.colorBgLayout}`} />;
|
|
8
|
+
return <Tooltip color={token.colorBgContainer} backgroundColor={token.colorErrorBg} borderColor={token.colorBgLayout} border={`1px solid ${token.colorBgLayout}`} style={{ fontSize: token.fontSize }} />;
|
|
9
9
|
},
|
|
10
10
|
}];
|
|
11
11
|
return (
|
|
12
12
|
<div>
|
|
13
|
-
<Alert style={{ color: token.colorText, background: token.colorTextSecondary, backgroundColor: token.colorTextTertiary, border: `1px solid ${token.colorBorder}
|
|
14
|
-
<Button style={{ color: token.colorInfo, background: token.colorSuccess, backgroundColor: token.colorWarning, borderColor: token.colorError }}></Button>
|
|
13
|
+
<Alert style={{ color: token.colorText, background: token.colorTextSecondary, backgroundColor: token.colorTextTertiary, border: `1px solid ${token.colorBorder}`, fontSize: token.fontSize }} />
|
|
14
|
+
<Button style={{ color: token.colorInfo, background: token.colorSuccess, backgroundColor: token.colorWarning, borderColor: token.colorError, fontSize: token.fontSizeSM }}></Button>
|
|
15
15
|
</div>
|
|
16
16
|
);
|
|
17
17
|
};
|
|
@@ -3,7 +3,7 @@ const fs = require('fs');
|
|
|
3
3
|
const postcss = require('postcss');
|
|
4
4
|
const postcssLess = require('postcss-less');
|
|
5
5
|
const isDirectory = require('is-directory');
|
|
6
|
-
const { tokenParse } = require('./utils/token');
|
|
6
|
+
const { tokenParse, propertyTokenParse } = require('./utils/token');
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* 搜索目录下所有的less文件
|
|
@@ -42,12 +42,22 @@ async function transform(file) {
|
|
|
42
42
|
// 遍历 AST
|
|
43
43
|
ast.walk(node => {
|
|
44
44
|
if (node.type === 'decl') {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
// 首先尝试基于属性的 token 转换
|
|
46
|
+
// 将 CSS 属性名转换为小驼峰写法(如 font-size -> fontSize)
|
|
47
|
+
const camelCaseProp = node.prop.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
|
|
48
|
+
const propertyResult = propertyTokenParse(camelCaseProp, node.value);
|
|
49
|
+
if (propertyResult) {
|
|
50
|
+
node.value = `@${propertyResult.token}`;
|
|
50
51
|
hasToken = true;
|
|
52
|
+
} else {
|
|
53
|
+
// 然后尝试基于值的 token 转换
|
|
54
|
+
const { key, token, formattedValue } = tokenParse(node.value);
|
|
55
|
+
if (token) {
|
|
56
|
+
node.value = formattedValue.replace(key, `@${token}`);
|
|
57
|
+
hasToken = true;
|
|
58
|
+
} else if (node.value?.includes('@')) {
|
|
59
|
+
hasToken = true;
|
|
60
|
+
}
|
|
51
61
|
}
|
|
52
62
|
} else if (node.type === 'atrule' && node.name === 'import') {
|
|
53
63
|
if (node.params === "'~@oceanbase/design/es/theme/index.less'") {
|
|
@@ -1,28 +1,17 @@
|
|
|
1
1
|
const { upperFirst } = require('lodash');
|
|
2
2
|
const { addSubmoduleImport } = require('./utils');
|
|
3
|
-
const { tokenParse } = require('./utils/token');
|
|
3
|
+
const { tokenParse, propertyTokenParse } = require('./utils/token');
|
|
4
4
|
const { printOptions } = require('./utils/config');
|
|
5
5
|
|
|
6
|
+
// 判断当前 path 是否为顶层 BlockStatement
|
|
6
7
|
function isTopBlockStatement(path) {
|
|
8
|
+
// 判断当前节点类型是否为 BlockStatement
|
|
7
9
|
const isBlockStatement = path.value.type === 'BlockStatement';
|
|
8
10
|
let isTop = isBlockStatement && true;
|
|
9
11
|
path = path.parentPath;
|
|
12
|
+
// 向上遍历父节点,直到遇到 Program 节点
|
|
10
13
|
while (isTop && path.value.type !== 'Program') {
|
|
11
|
-
//
|
|
12
|
-
if (path.value.type === 'BlockStatement') {
|
|
13
|
-
isTop = false;
|
|
14
|
-
break;
|
|
15
|
-
}
|
|
16
|
-
path = path.parentPath;
|
|
17
|
-
}
|
|
18
|
-
return isTop;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function isTopIdentifier(path) {
|
|
22
|
-
let isTop = true;
|
|
23
|
-
path = path.parentPath;
|
|
24
|
-
while (isTop && path.value.type !== 'Program') {
|
|
25
|
-
// isTopIdentifier => not wrapped by BlockStatement
|
|
14
|
+
// 如果父节点也是 BlockStatement,则当前不是顶层 BlockStatement
|
|
26
15
|
if (path.value.type === 'BlockStatement') {
|
|
27
16
|
isTop = false;
|
|
28
17
|
break;
|
|
@@ -32,6 +21,7 @@ function isTopIdentifier(path) {
|
|
|
32
21
|
return isTop;
|
|
33
22
|
}
|
|
34
23
|
|
|
24
|
+
// 判断字符串首字母是否为大写
|
|
35
25
|
function isFirstUpperCase(str) {
|
|
36
26
|
return upperFirst(str) === str;
|
|
37
27
|
}
|
|
@@ -42,7 +32,188 @@ function shorthandProperty(property) {
|
|
|
42
32
|
return property;
|
|
43
33
|
}
|
|
44
34
|
|
|
45
|
-
|
|
35
|
+
// 包装 JSX 属性值
|
|
36
|
+
function wrapJSXValue(value, isJSXAttribute) {
|
|
37
|
+
return isJSXAttribute ? `{${value}}` : value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 检查是否为 React 组件或 Hook
|
|
41
|
+
function isReactComponentOrHook(functionName, path) {
|
|
42
|
+
// 如果有函数名,检查是否以大写字母开头或以 use 开头
|
|
43
|
+
if (functionName) {
|
|
44
|
+
return isFirstUpperCase(functionName) || functionName.startsWith('use');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 对于匿名函数,检查是否是 export default function () {} 的形式
|
|
48
|
+
// 而不是 export default () => {} 的形式
|
|
49
|
+
if (path) {
|
|
50
|
+
const parentType = path.parentPath.value?.type;
|
|
51
|
+
if (parentType === 'FunctionDeclaration') {
|
|
52
|
+
// export default function () {} - 认为是 React 组件
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
if (parentType === 'ArrowFunctionExpression') {
|
|
56
|
+
// export default () => {} - 不认为是 React 组件
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 其他情况,默认不认为是 React 组件
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 检查 BlockStatement 中是否包含 token 使用
|
|
66
|
+
function hasTokenUsage(j, path) {
|
|
67
|
+
return (
|
|
68
|
+
j(path).find(j.Identifier, {
|
|
69
|
+
name: name => name?.includes('token.'),
|
|
70
|
+
}).length > 0
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 检查 BlockStatement 中是否包含 useToken 语句
|
|
75
|
+
function hasUseTokenStatement(j, path) {
|
|
76
|
+
return (
|
|
77
|
+
j(path).find(j.Identifier, {
|
|
78
|
+
name: name => name.includes('useToken'),
|
|
79
|
+
}).length > 0
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 获取函数名称
|
|
84
|
+
function getFunctionName(path) {
|
|
85
|
+
const parentType = path.parentPath.value?.type;
|
|
86
|
+
if (parentType === 'FunctionDeclaration') {
|
|
87
|
+
return path.parentPath.value?.id?.name;
|
|
88
|
+
}
|
|
89
|
+
if (parentType === 'ArrowFunctionExpression') {
|
|
90
|
+
return path.parentPath.parentPath?.value?.id?.name;
|
|
91
|
+
}
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 创建 token 对象模式
|
|
96
|
+
function createTokenObjectPattern(j) {
|
|
97
|
+
return j.objectPattern([
|
|
98
|
+
shorthandProperty(j.property('init', j.identifier('token'), j.identifier('token'))),
|
|
99
|
+
]);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 检查对象模式中是否包含 token 属性
|
|
103
|
+
function hasTokenInObjectPattern(param) {
|
|
104
|
+
return param.properties.some(p => p.type === 'ObjectProperty' && p.key && p.key.name === 'token');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function processCreateStylesParams(j, root) {
|
|
108
|
+
const processedCreateStyles = new Set();
|
|
109
|
+
|
|
110
|
+
root
|
|
111
|
+
.find(j.CallExpression, {
|
|
112
|
+
callee: { name: 'createStyles' },
|
|
113
|
+
})
|
|
114
|
+
.forEach(path => {
|
|
115
|
+
const arrowFunc = path.value.arguments[0];
|
|
116
|
+
if (arrowFunc && arrowFunc.type === 'ArrowFunctionExpression') {
|
|
117
|
+
processedCreateStyles.add(path);
|
|
118
|
+
|
|
119
|
+
if (arrowFunc.params.length > 0) {
|
|
120
|
+
const param = arrowFunc.params[0];
|
|
121
|
+
if (param.type === 'ObjectPattern' && !hasTokenInObjectPattern(param)) {
|
|
122
|
+
// 如果参数对象中没有 token 属性,则插入 token 属性
|
|
123
|
+
param.properties.push(
|
|
124
|
+
shorthandProperty(j.property('init', j.identifier('token'), j.identifier('token')))
|
|
125
|
+
);
|
|
126
|
+
} else if (param.type !== 'ObjectPattern') {
|
|
127
|
+
// 如果参数不是对象结构,则替换为 { token }
|
|
128
|
+
arrowFunc.params[0] = createTokenObjectPattern(j);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
// 如果没有参数,则插入 { token }
|
|
132
|
+
arrowFunc.params = [createTokenObjectPattern(j)];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return processedCreateStyles;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 添加 token 导入到 BlockStatement
|
|
141
|
+
function addTokenImportToBlockStatement(j, root, path) {
|
|
142
|
+
const includeJSXElement = j(path).find(j.JSXElement).length > 0;
|
|
143
|
+
const functionName = getFunctionName(path);
|
|
144
|
+
|
|
145
|
+
if (includeJSXElement && isReactComponentOrHook(functionName, path)) {
|
|
146
|
+
const insertString = 'const { token } = theme.useToken()';
|
|
147
|
+
path.get('body').value.unshift(j.expressionStatement(j.identifier(insertString)));
|
|
148
|
+
addSubmoduleImport(j, root, {
|
|
149
|
+
moduleName: '@oceanbase/design',
|
|
150
|
+
importedName: 'theme',
|
|
151
|
+
importKind: 'value',
|
|
152
|
+
});
|
|
153
|
+
} else if (includeJSXElement) {
|
|
154
|
+
// 对于非 React 组件的函数,直接导入 token
|
|
155
|
+
addSubmoduleImport(j, root, {
|
|
156
|
+
moduleName: '@oceanbase/design',
|
|
157
|
+
importedName: 'token',
|
|
158
|
+
importKind: 'value',
|
|
159
|
+
after: 'react',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 为函数组件和类组件添加 token 到现有导入
|
|
165
|
+
function addTokenToExistingImport(j, root) {
|
|
166
|
+
// 检查是否已经导入了 useToken 或 theme
|
|
167
|
+
const hasUseTokenImport =
|
|
168
|
+
root
|
|
169
|
+
.find(j.ImportDeclaration, {
|
|
170
|
+
source: { value: '@oceanbase/design' },
|
|
171
|
+
})
|
|
172
|
+
.find(j.ImportSpecifier, {
|
|
173
|
+
imported: { name: 'useToken' },
|
|
174
|
+
}).length > 0;
|
|
175
|
+
|
|
176
|
+
const hasThemeImport =
|
|
177
|
+
root
|
|
178
|
+
.find(j.ImportDeclaration, {
|
|
179
|
+
source: { value: '@oceanbase/design' },
|
|
180
|
+
})
|
|
181
|
+
.find(j.ImportSpecifier, {
|
|
182
|
+
imported: { name: 'theme' },
|
|
183
|
+
}).length > 0;
|
|
184
|
+
|
|
185
|
+
// 如果已经有 useToken 或 theme 导入,不需要添加 token 导入
|
|
186
|
+
if (hasUseTokenImport || hasThemeImport) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 查找所有从 @oceanbase/design 的导入
|
|
191
|
+
const importDeclarations = root.find(j.ImportDeclaration, {
|
|
192
|
+
source: { value: '@oceanbase/design' },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (importDeclarations.length === 0) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
importDeclarations.forEach(importPath => {
|
|
200
|
+
const specifiers = importPath.value.specifiers;
|
|
201
|
+
|
|
202
|
+
// 检查是否已经有 token 导入
|
|
203
|
+
const hasTokenImport = specifiers.some(
|
|
204
|
+
spec => spec.type === 'ImportSpecifier' && spec.imported.name === 'token'
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (!hasTokenImport) {
|
|
208
|
+
// 添加 token 导入
|
|
209
|
+
const tokenSpecifier = j.importSpecifier(j.identifier('token'));
|
|
210
|
+
specifiers.push(tokenSpecifier);
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 处理字符串字面量的 token 替换
|
|
216
|
+
function processStringLiterals(j, root) {
|
|
46
217
|
let hasChanged = false;
|
|
47
218
|
|
|
48
219
|
const stringList = root.find(j.StringLiteral, {
|
|
@@ -51,130 +222,197 @@ function importComponent(j, root, options) {
|
|
|
51
222
|
return !!token;
|
|
52
223
|
},
|
|
53
224
|
});
|
|
225
|
+
|
|
54
226
|
if (stringList.length > 0) {
|
|
55
|
-
// replace fixed style to token
|
|
56
227
|
stringList.replaceWith(path => {
|
|
57
228
|
hasChanged = true;
|
|
58
229
|
const { key, token, formattedValue } = tokenParse(path.value.value);
|
|
59
230
|
const isJSXAttribute = path.parentPath.value.type === 'JSXAttribute';
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
231
|
+
|
|
232
|
+
if (formattedValue === key) {
|
|
233
|
+
// 完全匹配的情况,直接返回 token.xxx
|
|
234
|
+
const memberExpression = j.memberExpression(j.identifier('token'), j.identifier(token));
|
|
235
|
+
return isJSXAttribute ? j.jsxExpressionContainer(memberExpression) : memberExpression;
|
|
236
|
+
} else {
|
|
237
|
+
// 部分匹配的情况,返回模板字符串
|
|
238
|
+
const beforeToken = formattedValue.replace(key, '');
|
|
239
|
+
const templateString = j.templateLiteral(
|
|
240
|
+
[
|
|
241
|
+
j.templateElement({ raw: beforeToken, cooked: beforeToken }, false),
|
|
242
|
+
j.templateElement({ raw: '', cooked: '' }, true),
|
|
243
|
+
],
|
|
244
|
+
[j.memberExpression(j.identifier('token'), j.identifier(token))]
|
|
245
|
+
);
|
|
246
|
+
return isJSXAttribute ? j.jsxExpressionContainer(templateString) : templateString;
|
|
66
247
|
}
|
|
67
|
-
return formattedValue === key ? j.identifier(stringValue) : j.identifier(templateStringValue);
|
|
68
248
|
});
|
|
69
249
|
|
|
250
|
+
// 为包含 token 使用的顶级 BlockStatement 添加导入
|
|
70
251
|
root
|
|
71
252
|
.find(j.BlockStatement)
|
|
72
|
-
// avoid duplicate insert for nested block statement
|
|
73
253
|
.filter(path => isTopBlockStatement(path))
|
|
74
254
|
.forEach(path => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
name: name => name?.includes('token.'),
|
|
78
|
-
}).length > 0;
|
|
79
|
-
if (includeToken) {
|
|
80
|
-
const includeJSXElement = j(path).find(j.JSXElement).length > 0;
|
|
81
|
-
const includeUseTokenStatement =
|
|
82
|
-
j(path).find(j.Identifier, {
|
|
83
|
-
name: name => name.includes('useToken'),
|
|
84
|
-
}).length > 0;
|
|
85
|
-
const parentType = path.parentPath.value?.type;
|
|
86
|
-
const functionName =
|
|
87
|
-
parentType === 'FunctionDeclaration'
|
|
88
|
-
? path.parentPath.value?.id?.name
|
|
89
|
-
: parentType === 'ArrowFunctionExpression'
|
|
90
|
-
? path.parentPath.parentPath?.value?.id?.name
|
|
91
|
-
: undefined;
|
|
92
|
-
const calleeName = path.parentPath.parentPath?.parentPath?.value?.callee?.name;
|
|
93
|
-
if (
|
|
94
|
-
includeJSXElement &&
|
|
95
|
-
functionName &&
|
|
96
|
-
// React function component or React hooks
|
|
97
|
-
(isFirstUpperCase(functionName) || functionName.startsWith('use'))
|
|
98
|
-
) {
|
|
99
|
-
// avoid duplicate insert when it's existed
|
|
100
|
-
if (!includeUseTokenStatement) {
|
|
101
|
-
const insertString = 'const { token } = theme.useToken()';
|
|
102
|
-
// insert `const { token } = theme.useToken()`
|
|
103
|
-
path.get('body').value.unshift(j.expressionStatement(j.identifier(insertString)));
|
|
104
|
-
// import `theme` from @oceanbase/design
|
|
105
|
-
addSubmoduleImport(j, root, {
|
|
106
|
-
moduleName: '@oceanbase/design',
|
|
107
|
-
importedName: 'theme',
|
|
108
|
-
importKind: 'value',
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
// antd-style createStyles
|
|
113
|
-
else if (parentType === 'ArrowFunctionExpression' && calleeName === 'createStyles') {
|
|
114
|
-
const arrowFunc = path.parentPath?.parentPath?.value?.[0];
|
|
115
|
-
if (arrowFunc && arrowFunc.type === 'ArrowFunctionExpression') {
|
|
116
|
-
let hasToken = false;
|
|
117
|
-
if (arrowFunc.params.length > 0) {
|
|
118
|
-
const param = arrowFunc.params[0];
|
|
119
|
-
if (param.type === 'ObjectPattern') {
|
|
120
|
-
hasToken = param.properties.some(
|
|
121
|
-
p => p.type === 'ObjectProperty' && p.key && p.key.name === 'token'
|
|
122
|
-
);
|
|
123
|
-
// 如果参数对象中没有 token 属性,则插入 token 属性
|
|
124
|
-
if (!hasToken) {
|
|
125
|
-
param.properties.push(
|
|
126
|
-
shorthandProperty(
|
|
127
|
-
j.property('init', j.identifier('token'), j.identifier('token'))
|
|
128
|
-
)
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
// 如果参数不是对象结构,则替换为 { token }
|
|
133
|
-
arrowFunc.params[0] = j.objectPattern([
|
|
134
|
-
shorthandProperty(
|
|
135
|
-
j.property('init', j.identifier('token'), j.identifier('token'))
|
|
136
|
-
),
|
|
137
|
-
]);
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
// 如果没有参数,则插入 { token }
|
|
141
|
-
arrowFunc.params = [
|
|
142
|
-
j.objectPattern([
|
|
143
|
-
shorthandProperty(
|
|
144
|
-
j.property('init', j.identifier('token'), j.identifier('token'))
|
|
145
|
-
),
|
|
146
|
-
]),
|
|
147
|
-
];
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
} else {
|
|
151
|
-
// React class component and static file (not react component)
|
|
152
|
-
// import `token` from @oceanbase/design
|
|
153
|
-
addSubmoduleImport(j, root, {
|
|
154
|
-
moduleName: '@oceanbase/design',
|
|
155
|
-
importedName: 'token',
|
|
156
|
-
importKind: 'value',
|
|
157
|
-
});
|
|
158
|
-
}
|
|
255
|
+
if (hasTokenUsage(j, path) && !hasUseTokenStatement(j, path)) {
|
|
256
|
+
addTokenImportToBlockStatement(j, root, path);
|
|
159
257
|
}
|
|
160
258
|
});
|
|
259
|
+
}
|
|
161
260
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
261
|
+
return hasChanged;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function importComponent(j, root, options) {
|
|
265
|
+
let hasChanged = false;
|
|
266
|
+
|
|
267
|
+
// 处理字符串字面量
|
|
268
|
+
hasChanged = processStringLiterals(j, root) || hasChanged;
|
|
269
|
+
|
|
270
|
+
// 处理对象属性值(如 fontSize: 14)
|
|
271
|
+
const objectPropertyChanged = processObjectProperties(j, root);
|
|
272
|
+
hasChanged = objectPropertyChanged || hasChanged;
|
|
273
|
+
|
|
274
|
+
return hasChanged;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 处理对象属性
|
|
278
|
+
function processObjectProperties(j, root) {
|
|
279
|
+
let hasChanged = false;
|
|
280
|
+
|
|
281
|
+
// 处理数字字面量
|
|
282
|
+
const numericPropertyList = root.find(j.ObjectProperty, {
|
|
283
|
+
key: { type: 'Identifier' },
|
|
284
|
+
value: { type: 'NumericLiteral' },
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
if (numericPropertyList.length > 0) {
|
|
288
|
+
numericPropertyList.replaceWith(path => {
|
|
289
|
+
const propertyName = path.value.key.name;
|
|
290
|
+
const propertyValue = path.value.value.value;
|
|
291
|
+
|
|
292
|
+
const tokenResult = propertyTokenParse(propertyName, propertyValue);
|
|
293
|
+
if (tokenResult) {
|
|
294
|
+
hasChanged = true;
|
|
295
|
+
const isJSXAttribute = path.parentPath.value.type === 'JSXAttribute';
|
|
296
|
+
const stringValue = wrapJSXValue(`token.${tokenResult.token}`, isJSXAttribute);
|
|
297
|
+
return j.objectProperty(j.identifier(propertyName), j.identifier(stringValue));
|
|
298
|
+
}
|
|
299
|
+
return path.value;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// 处理字符串字面量(如 fontSize: '14px')
|
|
304
|
+
const stringPropertyList = root.find(j.ObjectProperty, {
|
|
305
|
+
key: { type: 'Identifier' },
|
|
306
|
+
value: { type: 'StringLiteral' },
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
if (stringPropertyList.length > 0) {
|
|
310
|
+
stringPropertyList.replaceWith(path => {
|
|
311
|
+
const propertyName = path.value.key.name;
|
|
312
|
+
const propertyValue = path.value.value.value;
|
|
313
|
+
|
|
314
|
+
const tokenResult = propertyTokenParse(propertyName, propertyValue);
|
|
315
|
+
if (tokenResult) {
|
|
316
|
+
hasChanged = true;
|
|
317
|
+
const isJSXAttribute = path.parentPath.value.type === 'JSXAttribute';
|
|
318
|
+
const stringValue = wrapJSXValue(`token.${tokenResult.token}`, isJSXAttribute);
|
|
319
|
+
return j.objectProperty(j.identifier(propertyName), j.identifier(stringValue));
|
|
320
|
+
}
|
|
321
|
+
return path.value;
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// 如果发生了替换,需要添加 token 导入
|
|
326
|
+
if (hasChanged) {
|
|
327
|
+
addTokenImportsForObjectProperties(j, root);
|
|
173
328
|
}
|
|
174
329
|
|
|
175
330
|
return hasChanged;
|
|
176
331
|
}
|
|
177
332
|
|
|
333
|
+
// 为对象属性添加 token 导入
|
|
334
|
+
function addTokenImportsForObjectProperties(j, root) {
|
|
335
|
+
root
|
|
336
|
+
.find(j.BlockStatement)
|
|
337
|
+
.filter(path => isTopBlockStatement(path))
|
|
338
|
+
.forEach(path => {
|
|
339
|
+
if (hasTokenUsage(j, path) && !hasUseTokenStatement(j, path)) {
|
|
340
|
+
const calleeName = path.parentPath.parentPath?.parentPath?.value?.callee?.name;
|
|
341
|
+
// 跳过 createStyles 的情况,因为已经在上面处理了
|
|
342
|
+
if (calleeName !== 'createStyles') {
|
|
343
|
+
addTokenImportToBlockStatement(j, root, path);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// 检查是否应该添加顶层 token 导入
|
|
350
|
+
function shouldAddTopLevelTokenImport(j, root) {
|
|
351
|
+
// 检查是否有 createStyles 调用
|
|
352
|
+
const hasCreateStyles =
|
|
353
|
+
root.find(j.CallExpression, {
|
|
354
|
+
callee: { name: 'createStyles' },
|
|
355
|
+
}).length > 0;
|
|
356
|
+
|
|
357
|
+
// 检查是否使用了 theme.useToken()
|
|
358
|
+
const hasThemeUseToken =
|
|
359
|
+
root.find(j.CallExpression, {
|
|
360
|
+
callee: {
|
|
361
|
+
type: 'MemberExpression',
|
|
362
|
+
object: { name: 'theme' },
|
|
363
|
+
property: { name: 'useToken' },
|
|
364
|
+
},
|
|
365
|
+
}).length > 0;
|
|
366
|
+
|
|
367
|
+
// 检查是否导入了 theme
|
|
368
|
+
const hasThemeImport =
|
|
369
|
+
root
|
|
370
|
+
.find(j.ImportDeclaration, {
|
|
371
|
+
source: { value: '@oceanbase/design' },
|
|
372
|
+
})
|
|
373
|
+
.find(j.ImportSpecifier, {
|
|
374
|
+
imported: { name: 'theme' },
|
|
375
|
+
}).length > 0;
|
|
376
|
+
|
|
377
|
+
// 检查是否已经导入了 token
|
|
378
|
+
const hasTokenImport =
|
|
379
|
+
root
|
|
380
|
+
.find(j.ImportDeclaration, {
|
|
381
|
+
source: { value: '@oceanbase/design' },
|
|
382
|
+
})
|
|
383
|
+
.find(j.ImportSpecifier, {
|
|
384
|
+
imported: { name: 'token' },
|
|
385
|
+
}).length > 0;
|
|
386
|
+
|
|
387
|
+
// 检查是否有 token.xxx 的使用
|
|
388
|
+
const hasTokenUsage =
|
|
389
|
+
root.find(j.MemberExpression, {
|
|
390
|
+
object: { name: 'token' },
|
|
391
|
+
}).length > 0;
|
|
392
|
+
|
|
393
|
+
// 如果已经有 token 导入,不需要添加
|
|
394
|
+
if (hasTokenImport) {
|
|
395
|
+
return false;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// 如果有 createStyles 或 theme.useToken() 或 theme 导入,不需要添加顶层 token 导入
|
|
399
|
+
if (hasCreateStyles || hasThemeUseToken || hasThemeImport) {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 如果有 token.xxx 的使用,需要添加导入
|
|
404
|
+
return hasTokenUsage;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// 添加顶层 token 导入
|
|
408
|
+
function addTopLevelTokenImport(j, root) {
|
|
409
|
+
addSubmoduleImport(j, root, {
|
|
410
|
+
moduleName: '@oceanbase/design',
|
|
411
|
+
importedName: 'token',
|
|
412
|
+
importKind: 'value',
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
178
416
|
module.exports = (file, api, options) => {
|
|
179
417
|
const j = api.jscodeshift;
|
|
180
418
|
const root = j(file.source);
|
|
@@ -182,5 +420,38 @@ module.exports = (file, api, options) => {
|
|
|
182
420
|
let hasChanged = false;
|
|
183
421
|
hasChanged = importComponent(j, root, options) || hasChanged;
|
|
184
422
|
|
|
423
|
+
// 处理 createStyles 函数的参数
|
|
424
|
+
if (hasChanged) {
|
|
425
|
+
processCreateStylesParams(j, root);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// 如果有变化,检查是否需要添加 token 导入
|
|
429
|
+
if (hasChanged) {
|
|
430
|
+
// 检查是否有 token 使用
|
|
431
|
+
const hasTokenUsage =
|
|
432
|
+
root.find(j.MemberExpression, {
|
|
433
|
+
object: { name: 'token' },
|
|
434
|
+
}).length > 0;
|
|
435
|
+
|
|
436
|
+
if (hasTokenUsage) {
|
|
437
|
+
// 使用 shouldAddTopLevelTokenImport 函数来判断是否需要添加 token 导入
|
|
438
|
+
if (shouldAddTopLevelTokenImport(j, root)) {
|
|
439
|
+
// 检查是否有 @oceanbase/design 的导入
|
|
440
|
+
const hasOceanbaseImport =
|
|
441
|
+
root.find(j.ImportDeclaration, {
|
|
442
|
+
source: { value: '@oceanbase/design' },
|
|
443
|
+
}).length > 0;
|
|
444
|
+
|
|
445
|
+
if (hasOceanbaseImport) {
|
|
446
|
+
// 如果有 @oceanbase/design 导入,添加到现有导入
|
|
447
|
+
addTokenToExistingImport(j, root);
|
|
448
|
+
} else {
|
|
449
|
+
// 如果没有 @oceanbase/design 导入,添加顶层 token 导入
|
|
450
|
+
addTopLevelTokenImport(j, root);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
185
456
|
return hasChanged ? root.toSource(options.printOptions || printOptions) : null;
|
|
186
457
|
};
|
|
@@ -136,6 +136,14 @@ function addModuleImport(j, root, { pkgName, importSpecifier, importKind, before
|
|
|
136
136
|
return 1;
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
// 特殊处理:token 总是放在最后
|
|
140
|
+
if (a.imported && a.imported.name === 'token') {
|
|
141
|
+
return 1;
|
|
142
|
+
}
|
|
143
|
+
if (b.imported && b.imported.name === 'token') {
|
|
144
|
+
return -1;
|
|
145
|
+
}
|
|
146
|
+
|
|
139
147
|
return a.imported.name.localeCompare(b.imported.name);
|
|
140
148
|
});
|
|
141
149
|
const importStatement = j.importDeclaration(mergedImportSpecifiers, j.literal(pkgName));
|