@oceanbase/codemod 1.0.0-alpha.1 → 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.
Files changed (63) hide show
  1. package/README.md +158 -0
  2. package/bin/cli.js +196 -30
  3. package/package.json +7 -6
  4. package/transforms/__testfixtures__/less-to-cssvar/basic.input.less +16 -0
  5. package/transforms/__testfixtures__/less-to-cssvar/basic.output.less +14 -0
  6. package/transforms/__testfixtures__/less-to-cssvar/color-scales.input.less +23 -0
  7. package/transforms/__testfixtures__/less-to-cssvar/color-scales.output.less +21 -0
  8. package/transforms/__testfixtures__/less-to-cssvar/complex-values.input.less +22 -0
  9. package/transforms/__testfixtures__/less-to-cssvar/complex-values.output.less +20 -0
  10. package/transforms/__testfixtures__/less-to-cssvar/control-tokens.input.less +29 -0
  11. package/transforms/__testfixtures__/less-to-cssvar/control-tokens.output.less +27 -0
  12. package/transforms/__testfixtures__/less-to-cssvar/css-modules-global.input.less +21 -0
  13. package/transforms/__testfixtures__/less-to-cssvar/css-modules-global.output.less +19 -0
  14. package/transforms/__testfixtures__/less-to-cssvar/custom-prefix.input.less +9 -0
  15. package/transforms/__testfixtures__/less-to-cssvar/custom-prefix.output.less +7 -0
  16. package/transforms/__testfixtures__/less-to-cssvar/fill-tokens.input.less +36 -0
  17. package/transforms/__testfixtures__/less-to-cssvar/fill-tokens.output.less +34 -0
  18. package/transforms/__testfixtures__/less-to-cssvar/mixed-values.input.less +21 -0
  19. package/transforms/__testfixtures__/less-to-cssvar/mixed-values.output.less +19 -0
  20. package/transforms/__testfixtures__/less-to-cssvar/multiple-imports.input.less +9 -0
  21. package/transforms/__testfixtures__/less-to-cssvar/multiple-imports.output.less +8 -0
  22. package/transforms/__testfixtures__/less-to-cssvar/nested-selectors.input.less +24 -0
  23. package/transforms/__testfixtures__/less-to-cssvar/nested-selectors.output.less +22 -0
  24. package/transforms/__testfixtures__/less-to-cssvar/no-transform.input.less +8 -0
  25. package/transforms/__testfixtures__/less-to-cssvar/no-transform.output.less +8 -0
  26. package/transforms/__testfixtures__/less-to-cssvar/obui-import.input.less +7 -0
  27. package/transforms/__testfixtures__/less-to-cssvar/obui-import.output.less +5 -0
  28. package/transforms/__testfixtures__/less-to-cssvar/status-colors.input.less +25 -0
  29. package/transforms/__testfixtures__/less-to-cssvar/status-colors.output.less +23 -0
  30. package/transforms/__testfixtures__/less-to-token/antd-v4-less-to-token.input.less +2 -0
  31. package/transforms/__testfixtures__/less-to-token/antd-v4-less-to-token.output.less +2 -0
  32. package/transforms/__testfixtures__/less-to-token/case-insensitive.input.less +4 -0
  33. package/transforms/__testfixtures__/less-to-token/case-insensitive.output.less +4 -0
  34. package/transforms/__testfixtures__/less-to-token/exist-import-url.input.less +10 -0
  35. package/transforms/__testfixtures__/less-to-token/exist-import-url.output.less +10 -0
  36. package/transforms/__testfixtures__/less-to-token/exist-import.input.less +10 -0
  37. package/transforms/__testfixtures__/less-to-token/exist-import.output.less +10 -0
  38. package/transforms/__testfixtures__/sass-to-cssvar/basic.input.scss +18 -0
  39. package/transforms/__testfixtures__/sass-to-cssvar/basic.output.scss +18 -0
  40. package/transforms/__testfixtures__/sass-to-cssvar/custom-prefix.input.scss +5 -0
  41. package/transforms/__testfixtures__/sass-to-cssvar/custom-prefix.output.scss +5 -0
  42. package/transforms/__testfixtures__/sass-to-cssvar/no-transform.input.scss +6 -0
  43. package/transforms/__testfixtures__/sass-to-cssvar/no-transform.output.scss +6 -0
  44. package/transforms/__testfixtures__/style-to-token/antd-style.input.js +1 -0
  45. package/transforms/__testfixtures__/style-to-token/antd-style.output.js +1 -0
  46. package/transforms/__testfixtures__/style-to-token/function-component.input.js +2 -2
  47. package/transforms/__testfixtures__/style-to-token/function-component.output.js +2 -2
  48. package/transforms/__testfixtures__/style-to-token/nested-object.input.js +12 -0
  49. package/transforms/__testfixtures__/style-to-token/nested-object.output.js +13 -0
  50. package/transforms/__testfixtures__/style-to-token/single-function.output.js +1 -2
  51. package/transforms/__testfixtures__/style-to-token/template-string.input.js +23 -0
  52. package/transforms/__testfixtures__/style-to-token/template-string.output.js +25 -0
  53. package/transforms/__tests__/less-to-cssvar.test.ts +180 -0
  54. package/transforms/__tests__/less-to-token.test.ts +2 -0
  55. package/transforms/__tests__/sass-to-cssvar.test.ts +67 -0
  56. package/transforms/__tests__/style-to-token.test.ts +2 -0
  57. package/transforms/less-to-cssvar.js +505 -0
  58. package/transforms/less-to-token.js +47 -7
  59. package/transforms/obui-to-oceanbase-design-and-ui.js +0 -4
  60. package/transforms/sass-to-cssvar.js +194 -0
  61. package/transforms/style-to-token.js +183 -12
  62. package/transforms/utils/path-utils.js +40 -0
  63. package/transforms/utils/token.js +24 -2
@@ -0,0 +1,505 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const postcss = require('postcss');
4
+ const postcssLess = require('postcss-less');
5
+ const isDirectory = require('is-directory');
6
+ const { glob } = require('glob');
7
+ const { shouldExcludePath } = require('./utils/path-utils');
8
+
9
+ /**
10
+ * Get Less tokens from @oceanbase/design theme Less file
11
+ * This reads all @variableName definitions from the theme file
12
+ */
13
+ function getLessTokensFromTheme() {
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 Less variable tokens dynamically from @oceanbase/design theme
54
+ const LESS_TOKENS = getLessTokensFromTheme();
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 less files in the directory
67
+ * @param dir
68
+ * @returns
69
+ */
70
+ const findAllLessFiles = dir => {
71
+ const lessFiles = [];
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
+ lessFiles.push(...findAllLessFiles(filePath));
82
+ } else if (file.endsWith('.less')) {
83
+ lessFiles.push(filePath);
84
+ }
85
+ });
86
+ } else if (dir.endsWith('.less')) {
87
+ lessFiles.push(dir);
88
+ }
89
+ return lessFiles;
90
+ };
91
+
92
+ /**
93
+ * Transform Less variable to CSS variable
94
+ * @param {string} value - CSS value that may contain Less variables
95
+ * @param {string} prefix - CSS variable prefix (default: 'ant')
96
+ * @returns {string} - Transformed value with CSS variables
97
+ */
98
+ function transformLessVarToCssVar(value, prefix = 'ant') {
99
+ if (!value || typeof value !== 'string') {
100
+ return value;
101
+ }
102
+
103
+ let result = value;
104
+
105
+ // Match Less variables like @colorPrimary, @fontSize, etc.
106
+ // Support both @tokenName and @{tokenName} syntax
107
+ const lessVarRegex = /@\{?([a-zA-Z][a-zA-Z0-9]*)\}?/g;
108
+
109
+ result = result.replace(lessVarRegex, (match, varName) => {
110
+ // Check if this is a known token
111
+ if (LESS_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 Less 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<string>} - 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: postcssLess,
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 Less variables
144
+ const newValue = transformLessVarToCssVar(node.value, prefix);
145
+ if (newValue !== node.value) {
146
+ node.value = newValue;
147
+ hasTransformations = true;
148
+ }
149
+ } else if (node.type === 'atrule') {
150
+ // Remove the theme import
151
+ if (node.name === 'import') {
152
+ if (
153
+ node.params?.includes("'~@oceanbase/design/es/theme/index.less'") ||
154
+ node.params?.includes('"~@oceanbase/design/es/theme/index.less"') ||
155
+ node.params?.includes("'~@alipay/ob-ui/es/theme/index.less'") ||
156
+ node.params?.includes('"~@alipay/ob-ui/es/theme/index.less"')
157
+ ) {
158
+ node.remove();
159
+ hasTransformations = true;
160
+ }
161
+ }
162
+ // Transform Less variables in at-rule params (e.g., media queries)
163
+ if (node.params) {
164
+ const newParams = transformLessVarToCssVar(node.params, prefix);
165
+ if (newParams !== node.params) {
166
+ node.params = newParams;
167
+ hasTransformations = true;
168
+ }
169
+ }
170
+ }
171
+ });
172
+
173
+ return {
174
+ content: ast.toString(postcssLess.stringify),
175
+ hasTransformations,
176
+ };
177
+ }
178
+
179
+ /**
180
+ * Convert Less single-line comments (//) to CSS block comments
181
+ * @param {string} content - File content
182
+ * @returns {string} - Content with converted comments
183
+ */
184
+ function convertLessCommentsToCss(content) {
185
+ // Convert // comments to /* */ comments
186
+ // Match // at the start of a line or after whitespace, but not inside strings or existing block comments
187
+ const lines = content.split('\n');
188
+ const result = lines.map(line => {
189
+ // Skip if line is inside a block comment or contains a URL
190
+ if (line.includes('://')) {
191
+ // Handle URLs - only convert comments that are not part of URLs
192
+ return line.replace(/(?<!:)\/\/(.*)$/gm, (match, comment) => {
193
+ // Check if this is really a comment (not a URL protocol)
194
+ const beforeMatch = line.substring(0, line.lastIndexOf(match));
195
+ if (beforeMatch.match(/https?:$/) || beforeMatch.match(/url\([^)]*$/)) {
196
+ return match; // Keep as is for URLs
197
+ }
198
+ return `/* ${comment.trim()} */`;
199
+ });
200
+ }
201
+ // Convert standalone // comments
202
+ return line.replace(/^(\s*)\/\/(.*)$/, '$1/* $2 */').replace(/(\s)\/\/(.*)$/, '$1/* $2 */');
203
+ });
204
+ return result.join('\n');
205
+ }
206
+
207
+ /**
208
+ * Update import references in JS/TS files
209
+ * @param {string} baseDir - Base directory to search for JS/TS files
210
+ * @param {Array<{oldPath: string, newPath: string}>} renamedFiles - List of renamed files
211
+ */
212
+ async function updateImportReferences(baseDir, renamedFiles) {
213
+ if (renamedFiles.length === 0) return;
214
+
215
+ // Find all JS/TS/JSX/TSX files
216
+ const jsFiles = await glob('**/*.{js,jsx,ts,tsx}', {
217
+ cwd: baseDir,
218
+ absolute: true,
219
+ ignore: ['**/node_modules/**', '**/.umi/**', '**/.umi-production/**'],
220
+ });
221
+
222
+ for (const jsFile of jsFiles) {
223
+ let content = fs.readFileSync(jsFile, 'utf-8');
224
+ let hasChanges = false;
225
+
226
+ for (const { oldPath, newPath } of renamedFiles) {
227
+ // Calculate relative paths from the JS file to the Less/CSS files
228
+ const jsFileDir = path.dirname(jsFile);
229
+
230
+ // Get the relative path from the JS file to the old Less file
231
+ let relativeOldPath = path.relative(jsFileDir, oldPath);
232
+ let relativeNewPath = path.relative(jsFileDir, newPath);
233
+
234
+ // Normalize path separators for the regex
235
+ relativeOldPath = relativeOldPath.replace(/\\/g, '/');
236
+ relativeNewPath = relativeNewPath.replace(/\\/g, '/');
237
+
238
+ // Add ./ prefix if needed
239
+ if (!relativeOldPath.startsWith('.') && !relativeOldPath.startsWith('/')) {
240
+ relativeOldPath = './' + relativeOldPath;
241
+ }
242
+ if (!relativeNewPath.startsWith('.') && !relativeNewPath.startsWith('/')) {
243
+ relativeNewPath = './' + relativeNewPath;
244
+ }
245
+
246
+ // Also handle the case without ./ prefix
247
+ const relativeOldPathNoDot = relativeOldPath.replace(/^\.\//, '');
248
+ const relativeNewPathNoDot = relativeNewPath.replace(/^\.\//, '');
249
+
250
+ // Create regex patterns to match import statements
251
+ // Match: import './style.less', import "./style.less", require('./style.less'), require("./style.less")
252
+ const patterns = [
253
+ // With ./ prefix
254
+ new RegExp(`(['"])${escapeRegExp(relativeOldPath)}\\1`, 'g'),
255
+ // Without ./ prefix (for some bundler configurations)
256
+ new RegExp(`(['"])${escapeRegExp(relativeOldPathNoDot)}\\1`, 'g'),
257
+ ];
258
+
259
+ for (const pattern of patterns) {
260
+ const newContent = content.replace(pattern, (match, quote) => {
261
+ return `${quote}${relativeNewPath}${quote}`;
262
+ });
263
+ if (newContent !== content) {
264
+ content = newContent;
265
+ hasChanges = true;
266
+ }
267
+ }
268
+ }
269
+
270
+ if (hasChanges) {
271
+ fs.writeFileSync(jsFile, content);
272
+ console.log(` Updated imports: ${jsFile}`);
273
+ }
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Escape special regex characters in a string
279
+ * @param {string} string - String to escape
280
+ * @returns {string} - Escaped string
281
+ */
282
+ function escapeRegExp(string) {
283
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
284
+ }
285
+
286
+ // Common global style file name patterns (case insensitive)
287
+ const GLOBAL_STYLE_PATTERNS = [
288
+ /^global\.less$/i,
289
+ /^globals\.less$/i,
290
+ /^reset\.less$/i,
291
+ /^normalize\.less$/i,
292
+ /^base\.less$/i,
293
+ /^common\.less$/i,
294
+ /^app\.less$/i,
295
+ /^index\.less$/i,
296
+ /^main\.less$/i,
297
+ /^style\.less$/i,
298
+ /^styles\.less$/i,
299
+ /^variables\.less$/i,
300
+ /^mixins\.less$/i,
301
+ /^theme\.less$/i,
302
+ ];
303
+
304
+ /**
305
+ * Check if a Less file is imported as a CSS Module in any JS/TS file
306
+ * CSS Module import: import styles from './xxx.less'
307
+ * Global import: import './xxx.less'
308
+ * @param {string} lessFilePath - Path to the Less file
309
+ * @param {string} baseDir - Base directory to search for JS/TS files
310
+ * @returns {Promise<boolean>} - True if imported as CSS Module, false if global or not found
311
+ */
312
+ async function isImportedAsCssModule(lessFilePath, baseDir) {
313
+ const fileName = path.basename(lessFilePath);
314
+
315
+ // Check if file name matches common global style patterns
316
+ for (const pattern of GLOBAL_STYLE_PATTERNS) {
317
+ if (pattern.test(fileName)) {
318
+ return false;
319
+ }
320
+ }
321
+
322
+ // Check if file already has .module in the name
323
+ if (/\.module\.less$/.test(lessFilePath)) {
324
+ return true;
325
+ }
326
+
327
+ try {
328
+ // Find all JS/TS/JSX/TSX files
329
+ const jsFiles = await glob('**/*.{js,jsx,ts,tsx}', {
330
+ cwd: baseDir,
331
+ absolute: true,
332
+ ignore: ['**/node_modules/**', '**/.umi/**', '**/.umi-production/**'],
333
+ });
334
+
335
+ for (const jsFile of jsFiles) {
336
+ const content = fs.readFileSync(jsFile, 'utf-8');
337
+ const jsFileDir = path.dirname(jsFile);
338
+
339
+ // Calculate relative path from JS file to Less file
340
+ let relativePath = path.relative(jsFileDir, lessFilePath).replace(/\\/g, '/');
341
+ if (!relativePath.startsWith('.') && !relativePath.startsWith('/')) {
342
+ relativePath = './' + relativePath;
343
+ }
344
+ const relativePathNoDot = relativePath.replace(/^\.\//, '');
345
+
346
+ // Escape for regex
347
+ const escapedPath = escapeRegExp(relativePath);
348
+ const escapedPathNoDot = escapeRegExp(relativePathNoDot);
349
+
350
+ // Check for CSS Module import pattern: import xxx from './xxx.less'
351
+ // Match: import styles from './xxx.less', import * as styles from './xxx.less'
352
+ const cssModuleImportRegex = new RegExp(
353
+ `import\\s+(?:\\*\\s+as\\s+)?\\w+\\s+from\\s+(['"])(?:${escapedPath}|${escapedPathNoDot})\\1`,
354
+ 'm'
355
+ );
356
+
357
+ // Check for global import pattern: import './xxx.less'
358
+ const globalImportRegex = new RegExp(
359
+ `import\\s+(['"])(?:${escapedPath}|${escapedPathNoDot})\\1`,
360
+ 'm'
361
+ );
362
+
363
+ // Check for require with assignment: const styles = require('./xxx.less')
364
+ const cssModuleRequireRegex = new RegExp(
365
+ `(?:const|let|var)\\s+\\w+\\s*=\\s*require\\s*\\(\\s*(['"])(?:${escapedPath}|${escapedPathNoDot})\\1\\s*\\)`,
366
+ 'm'
367
+ );
368
+
369
+ // Check for require without assignment: require('./xxx.less')
370
+ const globalRequireRegex = new RegExp(
371
+ `(?<!(?:const|let|var)\\s+\\w+\\s*=\\s*)require\\s*\\(\\s*(['"])(?:${escapedPath}|${escapedPathNoDot})\\1\\s*\\)`,
372
+ 'm'
373
+ );
374
+
375
+ if (cssModuleImportRegex.test(content) || cssModuleRequireRegex.test(content)) {
376
+ return true;
377
+ }
378
+
379
+ if (globalImportRegex.test(content) || globalRequireRegex.test(content)) {
380
+ // Found as global import, but continue checking other files
381
+ // in case it's also imported as CSS Module elsewhere
382
+ continue;
383
+ }
384
+ }
385
+
386
+ // Default to false (treat as global) if not found or only found as global import
387
+ return false;
388
+ } catch (e) {
389
+ // If scanning fails, default to user's preference
390
+ return true;
391
+ }
392
+ }
393
+
394
+ /**
395
+ * Get the new file path when renaming from .less to .css or .scss
396
+ * @param {string} filePath - Original file path
397
+ * @param {boolean} shouldAddModule - Whether to add .module suffix
398
+ * @param {string} outputFormat - Output format: 'css' or 'scss' (default: 'css')
399
+ * @returns {string} - New file path
400
+ */
401
+ function getNewCssPath(filePath, shouldAddModule, outputFormat = 'css') {
402
+ const extension = outputFormat === 'scss' ? '.scss' : '.css';
403
+ // Check if file already has .module in the name
404
+ const hasModule = /\.module\.less$/.test(filePath);
405
+
406
+ if (hasModule) {
407
+ // Already has .module, just change extension
408
+ return filePath.replace(/\.less$/, extension);
409
+ } else if (shouldAddModule) {
410
+ // Add .module before extension
411
+ return filePath.replace(/\.less$/, `.module${extension}`);
412
+ } else {
413
+ // Just change extension
414
+ return filePath.replace(/\.less$/, extension);
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Run the less-to-cssvar transformation
420
+ * @param {string} file - File or directory path
421
+ * @param {object} options - Transform options
422
+ * @param {string} options.prefix - CSS variable prefix (default: 'ant')
423
+ * @param {string|boolean} options.renameTo - Target format: 'css', 'scss', or false to keep .less (default: 'css')
424
+ * @param {boolean} options.addModule - Whether to add .module suffix when renaming (default: true)
425
+ * - true (default): Auto-detect based on import style (CSS Module import → .module.css/.scss, global import → .css/.scss)
426
+ * - false: Skip detection, never add .module suffix
427
+ * @param {boolean} options._explicitAddModule - Internal flag: whether addModule was explicitly specified by user
428
+ */
429
+ async function lessToCssvar(file, options = {}) {
430
+ let { prefix = 'ant', renameTo = 'css', addModule = true, _explicitAddModule = false } = options;
431
+ const allLessFiles = findAllLessFiles(file);
432
+ const renamedFiles = [];
433
+ const baseDir = isDirectory.sync(file) ? file : path.dirname(file);
434
+
435
+ // Determine output format
436
+ let outputFormat = 'css';
437
+ let shouldRename = true;
438
+ if (renameTo === false || renameTo === 'false') {
439
+ shouldRename = false;
440
+ // When not renaming, disable addModule by default (only if not explicitly specified)
441
+ // No point in adding .module to .less files, but respect user's explicit choice
442
+ if (!_explicitAddModule && addModule === true) {
443
+ addModule = false;
444
+ }
445
+ } else if (renameTo === 'scss' || renameTo === true) {
446
+ // Support both 'scss' string and true (for backward compatibility)
447
+ outputFormat = renameTo === 'scss' ? 'scss' : 'css';
448
+ } else if (typeof renameTo === 'string') {
449
+ outputFormat = renameTo.toLowerCase() === 'scss' ? 'scss' : 'css';
450
+ }
451
+
452
+ for await (const item of allLessFiles) {
453
+ let { content, hasTransformations } = await transform(item, { prefix });
454
+
455
+ // If renaming to CSS, convert Less comments
456
+ // Note: SCSS supports // comments, so only convert for CSS
457
+ if (shouldRename && outputFormat === 'css') {
458
+ const convertedContent = convertLessCommentsToCss(content);
459
+ if (convertedContent !== content) {
460
+ content = convertedContent;
461
+ hasTransformations = true;
462
+ }
463
+ }
464
+
465
+ fs.writeFileSync(item, content);
466
+
467
+ // Rename .less to .css/.scss if option is enabled
468
+ if (shouldRename && hasTransformations) {
469
+ // Determine whether to add .module suffix
470
+ let shouldAddModule;
471
+ if (addModule) {
472
+ // Auto-detect based on import style
473
+ shouldAddModule = await isImportedAsCssModule(item, baseDir);
474
+ } else {
475
+ // Skip detection, never add .module
476
+ shouldAddModule = false;
477
+ }
478
+
479
+ const newPath = getNewCssPath(item, shouldAddModule, outputFormat);
480
+ if (newPath !== item) {
481
+ fs.renameSync(item, newPath);
482
+ console.log(` Renamed: ${item} -> ${newPath}`);
483
+ renamedFiles.push({ oldPath: item, newPath });
484
+ }
485
+ }
486
+ }
487
+
488
+ // Update import references in JS/TS files
489
+ if (shouldRename && renamedFiles.length > 0) {
490
+ console.log(`\n Updating import references...`);
491
+ await updateImportReferences(baseDir, renamedFiles);
492
+ }
493
+ }
494
+
495
+ module.exports = {
496
+ transform,
497
+ lessToCssvar,
498
+ transformLessVarToCssVar,
499
+ convertLessCommentsToCss,
500
+ updateImportReferences,
501
+ getLessTokensFromTheme,
502
+ getNewCssPath,
503
+ camelToKebab,
504
+ LESS_TOKENS,
505
+ };
@@ -4,9 +4,10 @@ const postcss = require('postcss');
4
4
  const postcssLess = require('postcss-less');
5
5
  const isDirectory = require('is-directory');
6
6
  const { tokenParse, propertyTokenParse } = require('./utils/token');
7
+ const { shouldExcludePath } = require('./utils/path-utils');
7
8
 
8
9
  /**
9
- * 搜索目录下所有的less文件
10
+ * Find all less files in the directory
10
11
  * @param dir
11
12
  * @returns
12
13
  */
@@ -18,7 +19,7 @@ const findAllLessFiles = dir => {
18
19
  files.forEach(file => {
19
20
  const filePath = path.join(dir, file);
20
21
  if (isDirectory.sync(filePath)) {
21
- if (filePath.includes('.umi') || filePath.includes('.umi-production')) {
22
+ if (shouldExcludePath(filePath)) {
22
23
  return;
23
24
  }
24
25
  lessFiles.push(...findAllLessFiles(filePath));
@@ -36,6 +37,7 @@ async function transform(file) {
36
37
  const content = fs.readFileSync(file, 'utf-8');
37
38
  const { root: ast } = await postcss([]).process(content, {
38
39
  syntax: postcssLess,
40
+ from: file, // 添加 from 选项以避免警告
39
41
  });
40
42
  let hasToken = false;
41
43
  let tokenLessImported = false;
@@ -51,18 +53,56 @@ async function transform(file) {
51
53
  hasToken = true;
52
54
  } else {
53
55
  // 然后尝试基于值的 token 转换
54
- const { key, token, formattedValue } = tokenParse(node.value);
55
- if (token) {
56
- node.value = formattedValue.replace(key, `@${token}`);
56
+ let newValue = node.value;
57
+ let valueHasToken = false;
58
+
59
+ // 检查是否为复合值(包含多个值或颜色值)
60
+ const isCompositeValue =
61
+ node.value.includes(',') ||
62
+ /rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|hsl\([^)]+\)|hsla?\([^)]+\)/.test(
63
+ node.value
64
+ );
65
+
66
+ if (isCompositeValue) {
67
+ // 对于复合值,只替换其中的颜色值
68
+ const colorRegex =
69
+ /rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|rgb\([^)]+\)|hsl\([^)]+\)|hsla?\([^)]+\)/g;
70
+ const colorMatches = node.value.match(colorRegex);
71
+ if (colorMatches) {
72
+ colorMatches.forEach(match => {
73
+ const colorResult = tokenParse(match);
74
+ if (colorResult.token) {
75
+ newValue = newValue.replace(match, `@${colorResult.token}`);
76
+ valueHasToken = true;
77
+ }
78
+ });
79
+ }
80
+ } else {
81
+ // 对于简单值,尝试完整的 token 转换
82
+ const { key, token, formattedValue } = tokenParse(node.value);
83
+ if (token) {
84
+ newValue = formattedValue.replace(key, `@${token}`);
85
+ valueHasToken = true;
86
+ }
87
+ }
88
+
89
+ if (valueHasToken) {
90
+ node.value = newValue;
57
91
  hasToken = true;
58
92
  } else if (node.value?.includes('@')) {
59
93
  hasToken = true;
60
94
  }
61
95
  }
62
96
  } else if (node.type === 'atrule' && node.name === 'import') {
63
- if (node.params === "'~@oceanbase/design/es/theme/index.less'") {
97
+ if (
98
+ node.params?.includes("'~@oceanbase/design/es/theme/index.less'") ||
99
+ node.params?.includes('"~@oceanbase/design/es/theme/index.less"')
100
+ ) {
64
101
  tokenLessImported = true;
65
- } else if (node.params === "'~@alipay/ob-ui/es/theme/index.less'") {
102
+ } else if (
103
+ node.params?.includes("'~@alipay/ob-ui/es/theme/index.less'") ||
104
+ node.params?.includes('"~@alipay/ob-ui/es/theme/index.less"')
105
+ ) {
66
106
  node.remove();
67
107
  }
68
108
  }
@@ -21,7 +21,6 @@ module.exports = (file, api, options) => {
21
21
  'DocDialog',
22
22
  'FullscreenBox',
23
23
  'Highlight',
24
- 'GraphToolbar',
25
24
  'IconFont',
26
25
  'Login',
27
26
  'Lottie',
@@ -29,7 +28,6 @@ module.exports = (file, api, options) => {
29
28
  'Password',
30
29
  'Ranger',
31
30
  'SideTip',
32
- 'TaskGraph',
33
31
  'TreeSearch',
34
32
  'Welcome',
35
33
  /* pro-components */
@@ -50,7 +48,6 @@ module.exports = (file, api, options) => {
50
48
  'DialogProps',
51
49
  'DocDialogProps',
52
50
  'FullscreenBoxProps',
53
- 'GraphToolbarProps',
54
51
  'HighlightProps',
55
52
  'IconFontProps',
56
53
  'LoginProps',
@@ -61,7 +58,6 @@ module.exports = (file, api, options) => {
61
58
  'RangerProps',
62
59
  'QuickPickerProps',
63
60
  'SideTipProps',
64
- 'TaskGraphProps',
65
61
  // TreeSearch
66
62
  'TreeSearchProps',
67
63
  'TreeSearchRef',