@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,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
|
-
*
|
|
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 (
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 (
|
|
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 (
|
|
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',
|