@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.
Files changed (40) hide show
  1. package/README.md +19 -8
  2. package/bin/upgrade-list.json +8 -5
  3. package/package.json +3 -3
  4. package/transforms/__testfixtures__/less-to-token/antd-v4-less-to-token.input.less +2 -0
  5. package/transforms/__testfixtures__/less-to-token/antd-v4-less-to-token.output.less +2 -0
  6. package/transforms/__testfixtures__/less-to-token/case-insensitive.input.less +2 -0
  7. package/transforms/__testfixtures__/less-to-token/case-insensitive.output.less +2 -0
  8. package/transforms/__testfixtures__/less-to-token/mixin.input.less +3 -0
  9. package/transforms/__testfixtures__/less-to-token/mixin.output.less +3 -0
  10. package/transforms/__testfixtures__/less-to-token/obui-less-to-token.input.less +2 -0
  11. package/transforms/__testfixtures__/less-to-token/obui-less-to-token.output.less +2 -0
  12. package/transforms/__testfixtures__/less-to-token/obui-less-token-to-token.input.less +1 -0
  13. package/transforms/__testfixtures__/less-to-token/obui-less-token-to-token.output.less +1 -0
  14. package/transforms/__testfixtures__/style-to-token/anonymous-function.input.js +13 -0
  15. package/transforms/__testfixtures__/style-to-token/anonymous-function.output.js +14 -0
  16. package/transforms/__testfixtures__/style-to-token/antd-style.input.js +3 -0
  17. package/transforms/__testfixtures__/style-to-token/antd-style.output.js +3 -0
  18. package/transforms/__testfixtures__/style-to-token/block-statement.input.js +4 -0
  19. package/transforms/__testfixtures__/style-to-token/block-statement.output.js +4 -0
  20. package/transforms/__testfixtures__/style-to-token/class-component.input.js +2 -2
  21. package/transforms/__testfixtures__/style-to-token/class-component.output.js +2 -2
  22. package/transforms/__testfixtures__/style-to-token/existed-useToken.input.js +2 -2
  23. package/transforms/__testfixtures__/style-to-token/existed-useToken.output.js +2 -2
  24. package/transforms/__testfixtures__/style-to-token/function-component.input.js +4 -4
  25. package/transforms/__testfixtures__/style-to-token/function-component.output.js +4 -4
  26. package/transforms/__testfixtures__/style-to-token/function.input.js +6 -6
  27. package/transforms/__testfixtures__/style-to-token/function.output.js +7 -7
  28. package/transforms/__testfixtures__/style-to-token/hooks.input.js +4 -4
  29. package/transforms/__testfixtures__/style-to-token/hooks.output.js +4 -4
  30. package/transforms/__testfixtures__/style-to-token/nested-block-statement.input.js +3 -3
  31. package/transforms/__testfixtures__/style-to-token/nested-block-statement.output.js +3 -3
  32. package/transforms/__testfixtures__/style-to-token/single-function.input.js +5 -0
  33. package/transforms/__testfixtures__/style-to-token/single-function.output.js +7 -0
  34. package/transforms/__testfixtures__/style-to-token/top-identifier.input.js +1 -0
  35. package/transforms/__testfixtures__/style-to-token/top-identifier.output.js +1 -0
  36. package/transforms/__tests__/style-to-token.test.ts +2 -0
  37. package/transforms/less-to-token.js +16 -6
  38. package/transforms/style-to-token.js +392 -121
  39. package/transforms/utils/index.js +8 -0
  40. 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
  };
@@ -0,0 +1,5 @@
1
+ import React from 'react';
2
+
3
+ function getComponent() {
4
+ return <div style={{ fontSize: 16 }} />;
5
+ }
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+
3
+ import { token } from '@oceanbase/design';
4
+
5
+ function getComponent() {
6
+ return <div style={{ fontSize: token.fontSizeLG }} />;
7
+ }
@@ -7,4 +7,5 @@ const colorMap = {
7
7
  warning: '#faad14',
8
8
  error: '#ff4d4f',
9
9
  border: '1px solid #d9d9d9',
10
+ fontSize: 14,
10
11
  };
@@ -8,4 +8,5 @@ const colorMap = {
8
8
  warning: token.colorWarning,
9
9
  error: token.colorError,
10
10
  border: `1px solid ${token.colorBorder}`,
11
+ fontSize: token.fontSize,
11
12
  };
@@ -3,7 +3,9 @@ import { defineTest } from 'jscodeshift/src/testUtils';
3
3
  const testUnit = 'style-to-token';
4
4
  const tests = [
5
5
  'function-component',
6
+ 'anonymous-function',
6
7
  'function',
8
+ 'single-function',
7
9
  'hooks',
8
10
  'class-component',
9
11
  'block-statement',
@@ -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
- const { key, token, formattedValue } = tokenParse(node.value);
46
- if (token) {
47
- node.value = formattedValue.replace(key, `@${token}`);
48
- hasToken = true;
49
- } else if (node.value?.includes('@')) {
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
- // isTopBlockStatement => not wrapped by BlockStatement
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
- function importComponent(j, root, options) {
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
- let stringValue = `token.${token}`;
61
- let templateStringValue = `\`${formattedValue.replace(key, `\${token.${token}}`)}\``;
62
- // add {} wrapper for JSXAttribute
63
- if (isJSXAttribute) {
64
- stringValue = `{${stringValue}}`;
65
- templateStringValue = `{${templateStringValue}}`;
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
- const includeToken =
76
- j(path).find(j.Identifier, {
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
- root
163
- .find(j.Identifier)
164
- .filter(path => isTopIdentifier(path) && path.value.name?.includes('token.'))
165
- .forEach(() => {
166
- // import `token` from @oceanbase/design
167
- addSubmoduleImport(j, root, {
168
- moduleName: '@oceanbase/design',
169
- importedName: 'token',
170
- importKind: 'value',
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));