@oceanbase/codemod 1.0.0-alpha.0 → 1.0.0-alpha.10
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 +158 -0
- package/bin/cli.js +196 -30
- package/bin/upgrade-list.json +8 -5
- package/package.json +7 -6
- package/transforms/__testfixtures__/less-to-cssvar/basic.input.less +16 -0
- package/transforms/__testfixtures__/less-to-cssvar/basic.output.less +14 -0
- package/transforms/__testfixtures__/less-to-cssvar/color-scales.input.less +23 -0
- package/transforms/__testfixtures__/less-to-cssvar/color-scales.output.less +21 -0
- package/transforms/__testfixtures__/less-to-cssvar/complex-values.input.less +22 -0
- package/transforms/__testfixtures__/less-to-cssvar/complex-values.output.less +20 -0
- package/transforms/__testfixtures__/less-to-cssvar/control-tokens.input.less +29 -0
- package/transforms/__testfixtures__/less-to-cssvar/control-tokens.output.less +27 -0
- package/transforms/__testfixtures__/less-to-cssvar/css-modules-global.input.less +21 -0
- package/transforms/__testfixtures__/less-to-cssvar/css-modules-global.output.less +19 -0
- package/transforms/__testfixtures__/less-to-cssvar/custom-prefix.input.less +9 -0
- package/transforms/__testfixtures__/less-to-cssvar/custom-prefix.output.less +7 -0
- package/transforms/__testfixtures__/less-to-cssvar/fill-tokens.input.less +36 -0
- package/transforms/__testfixtures__/less-to-cssvar/fill-tokens.output.less +34 -0
- package/transforms/__testfixtures__/less-to-cssvar/mixed-values.input.less +21 -0
- package/transforms/__testfixtures__/less-to-cssvar/mixed-values.output.less +19 -0
- package/transforms/__testfixtures__/less-to-cssvar/multiple-imports.input.less +9 -0
- package/transforms/__testfixtures__/less-to-cssvar/multiple-imports.output.less +8 -0
- package/transforms/__testfixtures__/less-to-cssvar/nested-selectors.input.less +24 -0
- package/transforms/__testfixtures__/less-to-cssvar/nested-selectors.output.less +22 -0
- package/transforms/__testfixtures__/less-to-cssvar/no-transform.input.less +8 -0
- package/transforms/__testfixtures__/less-to-cssvar/no-transform.output.less +8 -0
- package/transforms/__testfixtures__/less-to-cssvar/obui-import.input.less +7 -0
- package/transforms/__testfixtures__/less-to-cssvar/obui-import.output.less +5 -0
- package/transforms/__testfixtures__/less-to-cssvar/status-colors.input.less +25 -0
- package/transforms/__testfixtures__/less-to-cssvar/status-colors.output.less +23 -0
- 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 +4 -0
- package/transforms/__testfixtures__/less-to-token/case-insensitive.output.less +4 -0
- package/transforms/__testfixtures__/less-to-token/exist-import-url.input.less +10 -0
- package/transforms/__testfixtures__/less-to-token/exist-import-url.output.less +10 -0
- package/transforms/__testfixtures__/less-to-token/exist-import.input.less +10 -0
- package/transforms/__testfixtures__/less-to-token/exist-import.output.less +10 -0
- package/transforms/__testfixtures__/sass-to-cssvar/basic.input.scss +18 -0
- package/transforms/__testfixtures__/sass-to-cssvar/basic.output.scss +18 -0
- package/transforms/__testfixtures__/sass-to-cssvar/custom-prefix.input.scss +5 -0
- package/transforms/__testfixtures__/sass-to-cssvar/custom-prefix.output.scss +5 -0
- package/transforms/__testfixtures__/sass-to-cssvar/no-transform.input.scss +6 -0
- package/transforms/__testfixtures__/sass-to-cssvar/no-transform.output.scss +6 -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 -2
- package/transforms/__testfixtures__/style-to-token/antd-style.output.js +2 -1
- package/transforms/__testfixtures__/style-to-token/class-component.input.js +1 -1
- package/transforms/__testfixtures__/style-to-token/existed-useToken.input.js +1 -1
- package/transforms/__testfixtures__/style-to-token/function-component.input.js +3 -3
- package/transforms/__testfixtures__/style-to-token/function-component.output.js +2 -2
- package/transforms/__testfixtures__/style-to-token/function.input.js +2 -2
- package/transforms/__testfixtures__/style-to-token/hooks.input.js +1 -1
- package/transforms/__testfixtures__/style-to-token/nested-object.input.js +12 -0
- package/transforms/__testfixtures__/style-to-token/nested-object.output.js +13 -0
- package/transforms/__testfixtures__/style-to-token/single-function.input.js +5 -0
- package/transforms/__testfixtures__/style-to-token/single-function.output.js +6 -0
- package/transforms/__testfixtures__/style-to-token/template-string.input.js +23 -0
- package/transforms/__testfixtures__/style-to-token/template-string.output.js +25 -0
- package/transforms/__tests__/less-to-cssvar.test.ts +180 -0
- package/transforms/__tests__/less-to-token.test.ts +2 -0
- package/transforms/__tests__/sass-to-cssvar.test.ts +67 -0
- package/transforms/__tests__/style-to-token.test.ts +4 -0
- package/transforms/less-to-cssvar.js +505 -0
- package/transforms/less-to-token.js +47 -7
- package/transforms/obui-to-oceanbase-design-and-ui.js +0 -4
- package/transforms/sass-to-cssvar.js +194 -0
- package/transforms/style-to-token.js +236 -25
- package/transforms/utils/index.js +8 -0
- package/transforms/utils/path-utils.js +40 -0
- package/transforms/utils/token.js +24 -2
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const postcss = require('postcss');
|
|
4
|
+
const postcssScss = require('postcss-scss');
|
|
5
|
+
const isDirectory = require('is-directory');
|
|
6
|
+
const { shouldExcludePath } = require('./utils/path-utils');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get SASS/SCSS tokens from @oceanbase/design theme Less file
|
|
10
|
+
* Since we're converting SASS variables to CSS variables based on the same design tokens,
|
|
11
|
+
* we can reuse the Less token list
|
|
12
|
+
*/
|
|
13
|
+
function getSassTokensFromTheme() {
|
|
14
|
+
try {
|
|
15
|
+
// Try to find the Less theme file from @oceanbase/design
|
|
16
|
+
const themeLessPath = require.resolve('@oceanbase/design/es/theme/style/default.less');
|
|
17
|
+
const content = fs.readFileSync(themeLessPath, 'utf-8');
|
|
18
|
+
|
|
19
|
+
// Extract all @variableName: definitions
|
|
20
|
+
const tokenRegex = /@([a-zA-Z][a-zA-Z0-9]*):/g;
|
|
21
|
+
const tokens = new Set();
|
|
22
|
+
let match;
|
|
23
|
+
while ((match = tokenRegex.exec(content)) !== null) {
|
|
24
|
+
tokens.add(match[1]);
|
|
25
|
+
}
|
|
26
|
+
return Array.from(tokens);
|
|
27
|
+
} catch (e) {
|
|
28
|
+
// Fallback: try lib path
|
|
29
|
+
try {
|
|
30
|
+
const themeLessPath = require.resolve('@oceanbase/design/lib/theme/style/default.less');
|
|
31
|
+
const content = fs.readFileSync(themeLessPath, 'utf-8');
|
|
32
|
+
|
|
33
|
+
const tokenRegex = /@([a-zA-Z][a-zA-Z0-9]*):/g;
|
|
34
|
+
const tokens = new Set();
|
|
35
|
+
let match;
|
|
36
|
+
while ((match = tokenRegex.exec(content)) !== null) {
|
|
37
|
+
tokens.add(match[1]);
|
|
38
|
+
}
|
|
39
|
+
return Array.from(tokens);
|
|
40
|
+
} catch (e2) {
|
|
41
|
+
// Final fallback: use theme token object
|
|
42
|
+
try {
|
|
43
|
+
const { default: defaultTheme } = require('@oceanbase/design/lib/theme/default');
|
|
44
|
+
return Object.keys(defaultTheme.token || {});
|
|
45
|
+
} catch (e3) {
|
|
46
|
+
console.warn('Warning: Could not load tokens from @oceanbase/design, using empty list');
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Get SASS variable tokens dynamically from @oceanbase/design theme
|
|
54
|
+
const SASS_TOKENS = getSassTokensFromTheme();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Convert camelCase to kebab-case
|
|
58
|
+
* @param {string} str - camelCase string
|
|
59
|
+
* @returns {string} - kebab-case string
|
|
60
|
+
*/
|
|
61
|
+
function camelToKebab(str) {
|
|
62
|
+
return str.replace(/([a-z0-9])([A-Z])/g, '$1-$2').toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Find all sass/scss files in the directory
|
|
67
|
+
* @param {string} dir - Directory or file path
|
|
68
|
+
* @returns {string[]} - Array of file paths
|
|
69
|
+
*/
|
|
70
|
+
const findAllSassFiles = dir => {
|
|
71
|
+
const sassFiles = [];
|
|
72
|
+
const isDir = isDirectory.sync(dir);
|
|
73
|
+
if (isDir) {
|
|
74
|
+
const files = fs.readdirSync(dir);
|
|
75
|
+
files.forEach(file => {
|
|
76
|
+
const filePath = path.join(dir, file);
|
|
77
|
+
if (isDirectory.sync(filePath)) {
|
|
78
|
+
if (shouldExcludePath(filePath)) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
sassFiles.push(...findAllSassFiles(filePath));
|
|
82
|
+
} else if (file.endsWith('.sass') || file.endsWith('.scss')) {
|
|
83
|
+
sassFiles.push(filePath);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
} else if (dir.endsWith('.sass') || dir.endsWith('.scss')) {
|
|
87
|
+
sassFiles.push(dir);
|
|
88
|
+
}
|
|
89
|
+
return sassFiles;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Transform SASS variable to CSS variable
|
|
94
|
+
* @param {string} value - CSS value that may contain SASS variables
|
|
95
|
+
* @param {string} prefix - CSS variable prefix (default: 'ant')
|
|
96
|
+
* @returns {string} - Transformed value with CSS variables
|
|
97
|
+
*/
|
|
98
|
+
function transformSassVarToCssVar(value, prefix = 'ant') {
|
|
99
|
+
if (!value || typeof value !== 'string') {
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let result = value;
|
|
104
|
+
|
|
105
|
+
// Match SASS variables like $colorPrimary, $fontSize, etc.
|
|
106
|
+
// Support both $tokenName and #{tokenName} syntax
|
|
107
|
+
const sassVarRegex = /\$([a-zA-Z][a-zA-Z0-9]*)/g;
|
|
108
|
+
|
|
109
|
+
result = result.replace(sassVarRegex, (match, varName) => {
|
|
110
|
+
// Check if this is a known token
|
|
111
|
+
if (SASS_TOKENS.includes(varName)) {
|
|
112
|
+
const kebabName = camelToKebab(varName);
|
|
113
|
+
return `var(--${prefix}-${kebabName})`;
|
|
114
|
+
}
|
|
115
|
+
// Return original if not a known token
|
|
116
|
+
return match;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Transform a single SASS/SCSS file to use CSS variables
|
|
124
|
+
* @param {string} file - File path
|
|
125
|
+
* @param {object} options - Transform options
|
|
126
|
+
* @param {string} options.prefix - CSS variable prefix (default: 'ant')
|
|
127
|
+
* @returns {Promise<{content: string, hasTransformations: boolean}>} - Transformed content
|
|
128
|
+
*/
|
|
129
|
+
async function transform(file, options = {}) {
|
|
130
|
+
const { prefix = 'ant' } = options;
|
|
131
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
132
|
+
const { root: ast } = await postcss([]).process(content, {
|
|
133
|
+
syntax: postcssScss,
|
|
134
|
+
from: file,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Track whether any transformations were made
|
|
138
|
+
let hasTransformations = false;
|
|
139
|
+
|
|
140
|
+
// Traverse AST
|
|
141
|
+
ast.walk(node => {
|
|
142
|
+
if (node.type === 'decl') {
|
|
143
|
+
// Transform property values that contain SASS variables
|
|
144
|
+
const newValue = transformSassVarToCssVar(node.value, prefix);
|
|
145
|
+
if (newValue !== node.value) {
|
|
146
|
+
node.value = newValue;
|
|
147
|
+
hasTransformations = true;
|
|
148
|
+
}
|
|
149
|
+
} else if (node.type === 'atrule') {
|
|
150
|
+
// Transform SASS variables in at-rule params (e.g., media queries)
|
|
151
|
+
if (node.params) {
|
|
152
|
+
const newParams = transformSassVarToCssVar(node.params, prefix);
|
|
153
|
+
if (newParams !== node.params) {
|
|
154
|
+
node.params = newParams;
|
|
155
|
+
hasTransformations = true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
content: ast.toString(postcssScss.stringify),
|
|
163
|
+
hasTransformations,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Run the sass-to-cssvar transformation
|
|
169
|
+
* @param {string} file - File or directory path
|
|
170
|
+
* @param {object} options - Transform options
|
|
171
|
+
* @param {string} options.prefix - CSS variable prefix (default: 'ant')
|
|
172
|
+
*/
|
|
173
|
+
async function sassToCssvar(file, options = {}) {
|
|
174
|
+
const { prefix = 'ant' } = options;
|
|
175
|
+
const allSassFiles = findAllSassFiles(file);
|
|
176
|
+
|
|
177
|
+
for (const item of allSassFiles) {
|
|
178
|
+
const { content, hasTransformations } = await transform(item, { prefix });
|
|
179
|
+
|
|
180
|
+
if (hasTransformations) {
|
|
181
|
+
fs.writeFileSync(item, content);
|
|
182
|
+
console.log(` Transformed: ${item}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
transform,
|
|
189
|
+
sassToCssvar,
|
|
190
|
+
transformSassVarToCssVar,
|
|
191
|
+
getSassTokensFromTheme,
|
|
192
|
+
camelToKebab,
|
|
193
|
+
SASS_TOKENS,
|
|
194
|
+
};
|
|
@@ -38,15 +38,35 @@ function wrapJSXValue(value, isJSXAttribute) {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
// 检查是否为 React 组件或 Hook
|
|
41
|
-
function isReactComponentOrHook(functionName) {
|
|
42
|
-
|
|
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;
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
// 检查 BlockStatement 中是否包含 token 使用
|
|
46
66
|
function hasTokenUsage(j, path) {
|
|
47
67
|
return (
|
|
48
|
-
j(path).find(j.
|
|
49
|
-
|
|
68
|
+
j(path).find(j.MemberExpression, {
|
|
69
|
+
object: { name: 'token' },
|
|
50
70
|
}).length > 0
|
|
51
71
|
);
|
|
52
72
|
}
|
|
@@ -122,7 +142,7 @@ function addTokenImportToBlockStatement(j, root, path) {
|
|
|
122
142
|
const includeJSXElement = j(path).find(j.JSXElement).length > 0;
|
|
123
143
|
const functionName = getFunctionName(path);
|
|
124
144
|
|
|
125
|
-
if (includeJSXElement && isReactComponentOrHook(functionName)) {
|
|
145
|
+
if (includeJSXElement && isReactComponentOrHook(functionName, path)) {
|
|
126
146
|
const insertString = 'const { token } = theme.useToken()';
|
|
127
147
|
path.get('body').value.unshift(j.expressionStatement(j.identifier(insertString)));
|
|
128
148
|
addSubmoduleImport(j, root, {
|
|
@@ -130,6 +150,13 @@ function addTokenImportToBlockStatement(j, root, path) {
|
|
|
130
150
|
importedName: 'theme',
|
|
131
151
|
importKind: 'value',
|
|
132
152
|
});
|
|
153
|
+
} else if (includeJSXElement) {
|
|
154
|
+
// 对于非 React 组件的函数,直接导入 token
|
|
155
|
+
addSubmoduleImport(j, root, {
|
|
156
|
+
moduleName: '@oceanbase/design',
|
|
157
|
+
importedName: 'token',
|
|
158
|
+
importKind: 'value',
|
|
159
|
+
});
|
|
133
160
|
}
|
|
134
161
|
}
|
|
135
162
|
|
|
@@ -201,17 +228,6 @@ function processStringLiterals(j, root) {
|
|
|
201
228
|
const { key, token, formattedValue } = tokenParse(path.value.value);
|
|
202
229
|
const isJSXAttribute = path.parentPath.value.type === 'JSXAttribute';
|
|
203
230
|
|
|
204
|
-
console.log(
|
|
205
|
-
'Processing string:',
|
|
206
|
-
path.value.value,
|
|
207
|
-
'isJSXAttribute:',
|
|
208
|
-
isJSXAttribute,
|
|
209
|
-
'formattedValue:',
|
|
210
|
-
formattedValue,
|
|
211
|
-
'key:',
|
|
212
|
-
key
|
|
213
|
-
);
|
|
214
|
-
|
|
215
231
|
if (formattedValue === key) {
|
|
216
232
|
// 完全匹配的情况,直接返回 token.xxx
|
|
217
233
|
const memberExpression = j.memberExpression(j.identifier('token'), j.identifier(token));
|
|
@@ -244,12 +260,130 @@ function processStringLiterals(j, root) {
|
|
|
244
260
|
return hasChanged;
|
|
245
261
|
}
|
|
246
262
|
|
|
263
|
+
// 处理模板字符串中的颜色值
|
|
264
|
+
function processTemplateLiterals(j, root) {
|
|
265
|
+
let hasChanged = false;
|
|
266
|
+
|
|
267
|
+
const templateList = root.find(j.TemplateLiteral);
|
|
268
|
+
|
|
269
|
+
if (templateList.length > 0) {
|
|
270
|
+
templateList.forEach(path => {
|
|
271
|
+
const templateLiteral = path.value;
|
|
272
|
+
const quasis = templateLiteral.quasis;
|
|
273
|
+
const expressions = templateLiteral.expressions || [];
|
|
274
|
+
|
|
275
|
+
// 检查每个模板字符串片段是否包含需要转换的颜色值
|
|
276
|
+
let needsReconstruction = false;
|
|
277
|
+
const newQuasis = [];
|
|
278
|
+
const newExpressions = [];
|
|
279
|
+
|
|
280
|
+
for (let i = 0; i < quasis.length; i++) {
|
|
281
|
+
const quasi = quasis[i];
|
|
282
|
+
let value = quasi.value.raw;
|
|
283
|
+
|
|
284
|
+
// 查找需要转换的颜色值
|
|
285
|
+
const colorMatch = value.match(
|
|
286
|
+
/rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|hsl\([^)]+\)|hsla?\([^)]+\)/g
|
|
287
|
+
);
|
|
288
|
+
if (colorMatch) {
|
|
289
|
+
hasChanged = true;
|
|
290
|
+
needsReconstruction = true;
|
|
291
|
+
|
|
292
|
+
// 收集所有需要替换的匹配项及其 token
|
|
293
|
+
const replacements = [];
|
|
294
|
+
colorMatch.forEach(match => {
|
|
295
|
+
const { token } = tokenParse(match);
|
|
296
|
+
if (token) {
|
|
297
|
+
const index = value.indexOf(match);
|
|
298
|
+
replacements.push({ index, match, token });
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
// 按位置排序,从后往前处理以避免位置偏移
|
|
303
|
+
replacements.sort((a, b) => b.index - a.index);
|
|
304
|
+
|
|
305
|
+
let processedValue = value;
|
|
306
|
+
replacements.forEach(({ index, match, token }) => {
|
|
307
|
+
const before = processedValue.substring(0, index);
|
|
308
|
+
const after = processedValue.substring(index + match.length);
|
|
309
|
+
|
|
310
|
+
// 如果前面有内容,创建一个 quasi
|
|
311
|
+
if (before) {
|
|
312
|
+
newQuasis.push(j.templateElement({ raw: before, cooked: before }, false));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 创建 token 表达式
|
|
316
|
+
newExpressions.push(j.memberExpression(j.identifier('token'), j.identifier(token)));
|
|
317
|
+
|
|
318
|
+
// 剩余部分继续处理
|
|
319
|
+
processedValue = after;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
// 如果有剩余部分,添加到 quasis
|
|
323
|
+
if (processedValue || newQuasis.length === 0) {
|
|
324
|
+
const isTail = i === quasis.length - 1 && expressions.length === 0;
|
|
325
|
+
newQuasis.push(
|
|
326
|
+
j.templateElement({ raw: processedValue || '', cooked: processedValue || '' }, isTail)
|
|
327
|
+
);
|
|
328
|
+
} else if (newQuasis.length > 0) {
|
|
329
|
+
// 确保最后一个 quasi 标记为 tail(如果这是最后一个 quasi 且没有更多表达式)
|
|
330
|
+
const isTail = i === quasis.length - 1 && expressions.length === 0;
|
|
331
|
+
if (isTail) {
|
|
332
|
+
const lastQuasi = newQuasis[newQuasis.length - 1];
|
|
333
|
+
lastQuasi.tail = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} else {
|
|
337
|
+
// 如果没有颜色值需要转换,保持原样
|
|
338
|
+
newQuasis.push(quasi);
|
|
339
|
+
if (i < expressions.length) {
|
|
340
|
+
newExpressions.push(expressions[i]);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// 如果需要重构,替换整个模板字符串
|
|
346
|
+
if (needsReconstruction) {
|
|
347
|
+
// 确保最后一个 quasi 标记为 tail
|
|
348
|
+
if (newQuasis.length > 0) {
|
|
349
|
+
const lastQuasi = newQuasis[newQuasis.length - 1];
|
|
350
|
+
lastQuasi.tail = true;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// 合并原有的表达式(如果有)
|
|
354
|
+
const allExpressions = [...newExpressions];
|
|
355
|
+
// 添加原有的表达式(在 quasis 之后)
|
|
356
|
+
for (let i = 0; i < expressions.length; i++) {
|
|
357
|
+
allExpressions.push(expressions[i]);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
path.replace(j.templateLiteral(newQuasis, allExpressions));
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
// 为包含 token 使用的顶级 BlockStatement 添加导入
|
|
365
|
+
root
|
|
366
|
+
.find(j.BlockStatement)
|
|
367
|
+
.filter(path => isTopBlockStatement(path))
|
|
368
|
+
.forEach(path => {
|
|
369
|
+
if (hasTokenUsage(j, path) && !hasUseTokenStatement(j, path)) {
|
|
370
|
+
addTokenImportToBlockStatement(j, root, path);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return hasChanged;
|
|
376
|
+
}
|
|
377
|
+
|
|
247
378
|
function importComponent(j, root, options) {
|
|
248
379
|
let hasChanged = false;
|
|
249
380
|
|
|
250
381
|
// 处理字符串字面量
|
|
251
382
|
hasChanged = processStringLiterals(j, root) || hasChanged;
|
|
252
383
|
|
|
384
|
+
// 处理模板字符串中的颜色值
|
|
385
|
+
hasChanged = processTemplateLiterals(j, root) || hasChanged;
|
|
386
|
+
|
|
253
387
|
// 处理对象属性值(如 fontSize: 14)
|
|
254
388
|
const objectPropertyChanged = processObjectProperties(j, root);
|
|
255
389
|
hasChanged = objectPropertyChanged || hasChanged;
|
|
@@ -261,13 +395,14 @@ function importComponent(j, root, options) {
|
|
|
261
395
|
function processObjectProperties(j, root) {
|
|
262
396
|
let hasChanged = false;
|
|
263
397
|
|
|
264
|
-
|
|
398
|
+
// 处理数字字面量
|
|
399
|
+
const numericPropertyList = root.find(j.ObjectProperty, {
|
|
265
400
|
key: { type: 'Identifier' },
|
|
266
401
|
value: { type: 'NumericLiteral' },
|
|
267
402
|
});
|
|
268
403
|
|
|
269
|
-
if (
|
|
270
|
-
|
|
404
|
+
if (numericPropertyList.length > 0) {
|
|
405
|
+
numericPropertyList.replaceWith(path => {
|
|
271
406
|
const propertyName = path.value.key.name;
|
|
272
407
|
const propertyValue = path.value.value.value;
|
|
273
408
|
|
|
@@ -275,16 +410,39 @@ function processObjectProperties(j, root) {
|
|
|
275
410
|
if (tokenResult) {
|
|
276
411
|
hasChanged = true;
|
|
277
412
|
const isJSXAttribute = path.parentPath.value.type === 'JSXAttribute';
|
|
278
|
-
const
|
|
279
|
-
|
|
413
|
+
const memberExpression = j.memberExpression(
|
|
414
|
+
j.identifier('token'),
|
|
415
|
+
j.identifier(tokenResult.token)
|
|
416
|
+
);
|
|
417
|
+
return j.objectProperty(j.identifier(propertyName), memberExpression);
|
|
280
418
|
}
|
|
281
419
|
return path.value;
|
|
282
420
|
});
|
|
421
|
+
}
|
|
283
422
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
423
|
+
// 处理字符串字面量(如 fontSize: '14px')
|
|
424
|
+
const stringPropertyList = root.find(j.ObjectProperty, {
|
|
425
|
+
key: { type: 'Identifier' },
|
|
426
|
+
value: { type: 'StringLiteral' },
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
if (stringPropertyList.length > 0) {
|
|
430
|
+
stringPropertyList.replaceWith(path => {
|
|
431
|
+
const propertyName = path.value.key.name;
|
|
432
|
+
const propertyValue = path.value.value.value;
|
|
433
|
+
|
|
434
|
+
const tokenResult = propertyTokenParse(propertyName, propertyValue);
|
|
435
|
+
if (tokenResult) {
|
|
436
|
+
hasChanged = true;
|
|
437
|
+
const isJSXAttribute = path.parentPath.value.type === 'JSXAttribute';
|
|
438
|
+
const memberExpression = j.memberExpression(
|
|
439
|
+
j.identifier('token'),
|
|
440
|
+
j.identifier(tokenResult.token)
|
|
441
|
+
);
|
|
442
|
+
return j.objectProperty(j.identifier(propertyName), memberExpression);
|
|
443
|
+
}
|
|
444
|
+
return path.value;
|
|
445
|
+
});
|
|
288
446
|
}
|
|
289
447
|
|
|
290
448
|
return hasChanged;
|
|
@@ -292,6 +450,7 @@ function processObjectProperties(j, root) {
|
|
|
292
450
|
|
|
293
451
|
// 为对象属性添加 token 导入
|
|
294
452
|
function addTokenImportsForObjectProperties(j, root) {
|
|
453
|
+
// 处理 BlockStatement 中的 token 使用
|
|
295
454
|
root
|
|
296
455
|
.find(j.BlockStatement)
|
|
297
456
|
.filter(path => isTopBlockStatement(path))
|
|
@@ -304,6 +463,31 @@ function addTokenImportsForObjectProperties(j, root) {
|
|
|
304
463
|
}
|
|
305
464
|
}
|
|
306
465
|
});
|
|
466
|
+
|
|
467
|
+
// 处理顶层导出语句中的 token 使用
|
|
468
|
+
const hasTokenUsageInRoot =
|
|
469
|
+
root.find(j.MemberExpression, {
|
|
470
|
+
object: { name: 'token' },
|
|
471
|
+
}).length > 0;
|
|
472
|
+
|
|
473
|
+
if (hasTokenUsageInRoot) {
|
|
474
|
+
// 检查是否应该添加顶层 token 导入
|
|
475
|
+
if (shouldAddTopLevelTokenImport(j, root)) {
|
|
476
|
+
// 检查是否有 @oceanbase/design 的导入
|
|
477
|
+
const hasOceanbaseImport =
|
|
478
|
+
root.find(j.ImportDeclaration, {
|
|
479
|
+
source: { value: '@oceanbase/design' },
|
|
480
|
+
}).length > 0;
|
|
481
|
+
|
|
482
|
+
if (hasOceanbaseImport) {
|
|
483
|
+
// 如果有 @oceanbase/design 导入,添加到现有导入
|
|
484
|
+
addTokenToExistingImport(j, root);
|
|
485
|
+
} else {
|
|
486
|
+
// 如果没有 @oceanbase/design 导入,添加顶层 token 导入
|
|
487
|
+
addTopLevelTokenImport(j, root);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
307
491
|
}
|
|
308
492
|
|
|
309
493
|
// 检查是否应该添加顶层 token 导入
|
|
@@ -385,6 +569,33 @@ module.exports = (file, api, options) => {
|
|
|
385
569
|
processCreateStylesParams(j, root);
|
|
386
570
|
}
|
|
387
571
|
|
|
572
|
+
// 为对象属性添加 token 导入(只在没有其他 token 导入逻辑时调用)
|
|
573
|
+
if (hasChanged) {
|
|
574
|
+
// 检查是否已经有其他 token 导入逻辑在处理
|
|
575
|
+
const hasOtherTokenLogic =
|
|
576
|
+
root.find(j.CallExpression, {
|
|
577
|
+
callee: { name: 'createStyles' },
|
|
578
|
+
}).length > 0 ||
|
|
579
|
+
root.find(j.CallExpression, {
|
|
580
|
+
callee: {
|
|
581
|
+
type: 'MemberExpression',
|
|
582
|
+
object: { name: 'theme' },
|
|
583
|
+
property: { name: 'useToken' },
|
|
584
|
+
},
|
|
585
|
+
}).length > 0 ||
|
|
586
|
+
root
|
|
587
|
+
.find(j.ImportDeclaration, {
|
|
588
|
+
source: { value: '@oceanbase/design' },
|
|
589
|
+
})
|
|
590
|
+
.find(j.ImportSpecifier, {
|
|
591
|
+
imported: { name: 'theme' },
|
|
592
|
+
}).length > 0;
|
|
593
|
+
|
|
594
|
+
if (!hasOtherTokenLogic) {
|
|
595
|
+
addTokenImportsForObjectProperties(j, root);
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
388
599
|
// 如果有变化,检查是否需要添加 token 导入
|
|
389
600
|
if (hasChanged) {
|
|
390
601
|
// 检查是否有 token 使用
|
|
@@ -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));
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common directories to exclude when traversing files
|
|
3
|
+
* These are typically build outputs, caches, dependencies, and temporary directories
|
|
4
|
+
*/
|
|
5
|
+
const EXCLUDED_DIRS = [
|
|
6
|
+
'node_modules',
|
|
7
|
+
'.umi',
|
|
8
|
+
'.umi-production',
|
|
9
|
+
'.git',
|
|
10
|
+
'dist',
|
|
11
|
+
'build',
|
|
12
|
+
'coverage',
|
|
13
|
+
'.cache',
|
|
14
|
+
'.next',
|
|
15
|
+
'.turbo',
|
|
16
|
+
'.vite',
|
|
17
|
+
'.nuxt',
|
|
18
|
+
'.output',
|
|
19
|
+
'.temp',
|
|
20
|
+
'.tmp',
|
|
21
|
+
'temp',
|
|
22
|
+
'tmp',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check if a path should be excluded
|
|
27
|
+
* @param {string} filePath - File path to check
|
|
28
|
+
* @returns {boolean} - True if should be excluded
|
|
29
|
+
*/
|
|
30
|
+
function shouldExcludePath(filePath) {
|
|
31
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
32
|
+
return EXCLUDED_DIRS.some(
|
|
33
|
+
dir => normalizedPath.includes(`/${dir}/`) || normalizedPath.endsWith(`/${dir}`)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
EXCLUDED_DIRS,
|
|
39
|
+
shouldExcludePath,
|
|
40
|
+
};
|
|
@@ -36,17 +36,24 @@ const TOKEN_MAP = {
|
|
|
36
36
|
'#1890ff': 'colorInfo',
|
|
37
37
|
'#40a9ff': 'colorInfo',
|
|
38
38
|
'#006aff': 'colorInfo',
|
|
39
|
+
'#1843ff': 'colorInfo',
|
|
40
|
+
'#597ef7': 'colorInfo',
|
|
39
41
|
'#f7f9fb': 'colorInfoBg',
|
|
42
|
+
'#91a9f8': 'colorInfoBg',
|
|
40
43
|
'#e6f7ff': 'colorInfoBgHover',
|
|
41
44
|
'#f3f9ff': 'colorInfoBgHover',
|
|
42
45
|
'#e6f7ff': 'colorInfoBgHover',
|
|
43
46
|
'#73d13d': 'colorSuccess',
|
|
44
47
|
'#52c41a': 'colorSuccess',
|
|
45
48
|
'#faad14': 'colorWarning',
|
|
49
|
+
'#ffa940': 'colorWarning',
|
|
46
50
|
'#fef6e7': 'colorWarningBg',
|
|
51
|
+
'#fed59c': 'colorWarningBg',
|
|
47
52
|
'#ff4d4f': 'colorError',
|
|
48
53
|
'#f5222d': 'colorError',
|
|
49
54
|
'#f8636b': 'colorError',
|
|
55
|
+
'#f93939': 'colorError',
|
|
56
|
+
'#eb4444': 'colorError',
|
|
50
57
|
'#d9d9d9': 'colorBorder',
|
|
51
58
|
'#bfbfbf': 'colorBorder',
|
|
52
59
|
'#e8e8e8': 'colorBorder',
|
|
@@ -67,7 +74,9 @@ const TOKEN_MAP = {
|
|
|
67
74
|
'rgba(0,0,0,0.85)': 'colorText',
|
|
68
75
|
'rgba(0,0,0,0.65)': 'colorTextSecondary',
|
|
69
76
|
'rgba(0,0,0,0.45)': 'colorTextTertiary',
|
|
70
|
-
'#5c6b8a': '
|
|
77
|
+
'#5c6b8a': 'colorTextSecondary',
|
|
78
|
+
'#5C6B8A': 'colorTextSecondary',
|
|
79
|
+
'#ced5e3': '@colorTextPlaceholder',
|
|
71
80
|
'rgba(0,0,0,0.25)': 'colorTextQuaternary',
|
|
72
81
|
'rgba(0,0,0,.85)': 'colorText',
|
|
73
82
|
'rgba(0,0,0,.65)': 'colorTextSecondary',
|
|
@@ -97,6 +106,7 @@ const TOKEN_MAP = {
|
|
|
97
106
|
'rgba(0,0,0,0.06)': 'colorFillSecondary',
|
|
98
107
|
'rgba(0,0,0,0.04)': 'colorFillTertiary',
|
|
99
108
|
'rgba(0,0,0,0.02)': 'colorFillQuaternary',
|
|
109
|
+
'rgba(0,0,0,0.03)': 'colorFillQuaternary',
|
|
100
110
|
'#f5f6fa': 'colorBgLayout',
|
|
101
111
|
'#edeff2': 'colorBgLayout',
|
|
102
112
|
// obui legacy style => token
|
|
@@ -122,12 +132,12 @@ const TOKEN_MAP = {
|
|
|
122
132
|
'#cdd5e4': 'colorBorder',
|
|
123
133
|
'#f5f8fe': 'colorBgLayout',
|
|
124
134
|
'#f5f7fa': 'colorBgLayout',
|
|
135
|
+
'#f8fafe': 'colorBgLayout',
|
|
125
136
|
'rgba(140,140,140,0.1)': 'colorBgLayout',
|
|
126
137
|
'rgb(240,242,245)': 'colorBgLayout',
|
|
127
138
|
'#132039': 'colorText',
|
|
128
139
|
'#364563': 'colorTextSecondary',
|
|
129
140
|
'#8592ad': 'colorTextTertiary',
|
|
130
|
-
'#f8fafe': 'colorFillQuaternary',
|
|
131
141
|
};
|
|
132
142
|
|
|
133
143
|
const TOKEN_MAP_KEYS = Object.keys(TOKEN_MAP).map(key => formatValue(key));
|
|
@@ -170,6 +180,18 @@ const PROPERTY_TOKEN_MAP = {
|
|
|
170
180
|
15: 'fontSizeLG',
|
|
171
181
|
16: 'fontSizeLG',
|
|
172
182
|
},
|
|
183
|
+
fontWeight: {
|
|
184
|
+
300: 'fontWeightWeak',
|
|
185
|
+
400: 'fontWeight',
|
|
186
|
+
500: 'fontWeightStrong',
|
|
187
|
+
600: 'fontWeightStrong',
|
|
188
|
+
},
|
|
189
|
+
borderRadius: {
|
|
190
|
+
2: 'borderRadiusSM',
|
|
191
|
+
4: 'borderRadius',
|
|
192
|
+
6: 'borderRadiusMD',
|
|
193
|
+
8: 'borderRadiusLG',
|
|
194
|
+
},
|
|
173
195
|
};
|
|
174
196
|
|
|
175
197
|
function propertyTokenParse(propertyName, value) {
|