scss-variable-extractor 1.0.0
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/.scssextractrc.example.json +35 -0
- package/LICENSE +21 -0
- package/README.md +423 -0
- package/bin/cli.js +226 -0
- package/jest.config.js +12 -0
- package/package.json +38 -0
- package/src/analyzer.js +285 -0
- package/src/config.js +82 -0
- package/src/generator.js +219 -0
- package/src/index.js +16 -0
- package/src/parser.js +421 -0
- package/src/refactorer.js +209 -0
- package/src/scanner.js +29 -0
- package/templates/_variables.scss.template +56 -0
- package/test/analyzer.test.js +107 -0
- package/test/fixtures/apps/subapp/src/app/component-a/component-a.component.scss +47 -0
- package/test/fixtures/apps/subapp/src/app/component-b/component-b.component.scss +52 -0
- package/test/fixtures/libs/styles/_existing-variables.scss +16 -0
- package/test/generator.test.js +149 -0
- package/test/parser.test.js +131 -0
- package/test/refactorer.test.js +127 -0
- package/test/scanner.test.js +25 -0
package/src/parser.js
ADDED
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Regular expressions for matching various CSS/SCSS value types
|
|
5
|
+
*/
|
|
6
|
+
const PATTERNS = {
|
|
7
|
+
// Hex colors: #RGB, #RRGGBB, #RRGGBBAA
|
|
8
|
+
hexColor: /#([0-9a-fA-F]{3,8})\b/g,
|
|
9
|
+
|
|
10
|
+
// RGB/RGBA colors
|
|
11
|
+
rgbColor: /rgba?\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/g,
|
|
12
|
+
|
|
13
|
+
// Named colors (common ones)
|
|
14
|
+
namedColor: /\b(red|blue|green|white|black|gray|grey|yellow|orange|purple|pink|brown|cyan|magenta|transparent)\b/g,
|
|
15
|
+
|
|
16
|
+
// Pixel values
|
|
17
|
+
pixelValue: /\b(\d+)px\b/g,
|
|
18
|
+
|
|
19
|
+
// Percentage values
|
|
20
|
+
percentValue: /\b(\d+)%\b/g,
|
|
21
|
+
|
|
22
|
+
// Font weight values
|
|
23
|
+
fontWeight: /\b(bold|normal|lighter|bolder|[1-9]00)\b/g,
|
|
24
|
+
|
|
25
|
+
// Decimal values (for opacity, line-height)
|
|
26
|
+
decimalValue: /\b(0?\.\d+|1\.\d+|[2-9]\.\d+)\b/g,
|
|
27
|
+
|
|
28
|
+
// Box shadow
|
|
29
|
+
boxShadow: /(-?\d+px\s+){2,4}(rgba?\([^)]+\)|#[0-9a-fA-F]{3,8}|\w+)/g,
|
|
30
|
+
|
|
31
|
+
// Font family
|
|
32
|
+
fontFamily: /(['"][^'"]+['"](?:\s*,\s*(?:sans-serif|serif|monospace|cursive|fantasy))?)/g,
|
|
33
|
+
|
|
34
|
+
// Transition
|
|
35
|
+
transition: /\b(all|none|[\w-]+)\s+[\d.]+m?s\s+(?:ease|linear|ease-in|ease-out|ease-in-out|cubic-bezier\([^)]+\))/g,
|
|
36
|
+
|
|
37
|
+
// Variable declaration (to skip)
|
|
38
|
+
variableDeclaration: /\$[\w-]+\s*:/g,
|
|
39
|
+
|
|
40
|
+
// URL function (to skip)
|
|
41
|
+
urlFunction: /url\([^)]+\)/g,
|
|
42
|
+
|
|
43
|
+
// Content property (to skip string literals)
|
|
44
|
+
contentProperty: /content\s*:\s*[^;]+;/g,
|
|
45
|
+
|
|
46
|
+
// String interpolation (to skip)
|
|
47
|
+
interpolation: /#\{[^}]+\}/g,
|
|
48
|
+
|
|
49
|
+
// @use and @forward statements (to skip)
|
|
50
|
+
useForward: /@(?:use|forward)\s+[^;]+;/g,
|
|
51
|
+
|
|
52
|
+
// Material palette functions (to skip)
|
|
53
|
+
materialFunctions: /mat\.(?:define-palette|get-color-from-palette|define-(?:light|dark)-theme|all-component-themes)\([^)]*\)/g
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extracts hardcoded values from SCSS content
|
|
58
|
+
* @param {string} content - SCSS file content
|
|
59
|
+
* @param {string} filePath - Path to the file (for context)
|
|
60
|
+
* @returns {Object} - Extracted values categorized by type
|
|
61
|
+
*/
|
|
62
|
+
function parseScss(content, filePath) {
|
|
63
|
+
const extracted = {
|
|
64
|
+
colors: [],
|
|
65
|
+
spacing: [],
|
|
66
|
+
fontSizes: [],
|
|
67
|
+
fontWeights: [],
|
|
68
|
+
fontFamilies: [],
|
|
69
|
+
borderRadius: [],
|
|
70
|
+
shadows: [],
|
|
71
|
+
zIndex: [],
|
|
72
|
+
sizing: [],
|
|
73
|
+
lineHeight: [],
|
|
74
|
+
opacity: [],
|
|
75
|
+
transitions: []
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Remove comments
|
|
79
|
+
let cleanContent = content.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
80
|
+
cleanContent = cleanContent.replace(/\/\/.*/g, '');
|
|
81
|
+
|
|
82
|
+
// Remove safe zones (values we should not extract)
|
|
83
|
+
const safeZones = [];
|
|
84
|
+
|
|
85
|
+
// Mark variable declarations - entire lines with $variable:
|
|
86
|
+
let match;
|
|
87
|
+
const varDeclPattern = /\$[\w-]+\s*:[^;]+;/g;
|
|
88
|
+
while ((match = varDeclPattern.exec(cleanContent)) !== null) {
|
|
89
|
+
safeZones.push({ start: match.index, end: match.index + match[0].length });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Mark other safe zones
|
|
93
|
+
const markers = [
|
|
94
|
+
PATTERNS.urlFunction,
|
|
95
|
+
PATTERNS.contentProperty,
|
|
96
|
+
PATTERNS.interpolation,
|
|
97
|
+
PATTERNS.useForward,
|
|
98
|
+
PATTERNS.materialFunctions
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
markers.forEach(pattern => {
|
|
102
|
+
pattern.lastIndex = 0;
|
|
103
|
+
while ((match = pattern.exec(cleanContent)) !== null) {
|
|
104
|
+
safeZones.push({ start: match.index, end: match.index + match[0].length });
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Helper to check if position is in safe zone
|
|
109
|
+
const isInSafeZone = (pos) => {
|
|
110
|
+
return safeZones.some(zone => pos >= zone.start && pos < zone.end);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Extract colors (hex)
|
|
114
|
+
PATTERNS.hexColor.lastIndex = 0;
|
|
115
|
+
while ((match = PATTERNS.hexColor.exec(cleanContent)) !== null) {
|
|
116
|
+
if (!isInSafeZone(match.index)) {
|
|
117
|
+
extracted.colors.push({
|
|
118
|
+
value: match[0],
|
|
119
|
+
line: getLineNumber(cleanContent, match.index),
|
|
120
|
+
file: filePath,
|
|
121
|
+
context: getContext(cleanContent, match.index)
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Extract colors (rgb/rgba)
|
|
127
|
+
PATTERNS.rgbColor.lastIndex = 0;
|
|
128
|
+
while ((match = PATTERNS.rgbColor.exec(cleanContent)) !== null) {
|
|
129
|
+
if (!isInSafeZone(match.index)) {
|
|
130
|
+
extracted.colors.push({
|
|
131
|
+
value: match[0],
|
|
132
|
+
line: getLineNumber(cleanContent, match.index),
|
|
133
|
+
file: filePath,
|
|
134
|
+
context: getContext(cleanContent, match.index)
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Extract named colors
|
|
140
|
+
PATTERNS.namedColor.lastIndex = 0;
|
|
141
|
+
while ((match = PATTERNS.namedColor.exec(cleanContent)) !== null) {
|
|
142
|
+
if (!isInSafeZone(match.index)) {
|
|
143
|
+
const ctx = getContext(cleanContent, match.index);
|
|
144
|
+
// Only extract if it looks like a color value (not in selectors)
|
|
145
|
+
if (ctx.includes(':')) {
|
|
146
|
+
extracted.colors.push({
|
|
147
|
+
value: match[0],
|
|
148
|
+
line: getLineNumber(cleanContent, match.index),
|
|
149
|
+
file: filePath,
|
|
150
|
+
context: ctx
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Extract pixel values (categorize by property)
|
|
157
|
+
PATTERNS.pixelValue.lastIndex = 0;
|
|
158
|
+
while ((match = PATTERNS.pixelValue.exec(cleanContent)) !== null) {
|
|
159
|
+
if (!isInSafeZone(match.index)) {
|
|
160
|
+
const ctx = getContext(cleanContent, match.index);
|
|
161
|
+
const property = extractPropertyAtPosition(cleanContent, match.index);
|
|
162
|
+
|
|
163
|
+
if (isFontSizeProperty(property)) {
|
|
164
|
+
extracted.fontSizes.push({
|
|
165
|
+
value: match[0],
|
|
166
|
+
line: getLineNumber(cleanContent, match.index),
|
|
167
|
+
file: filePath,
|
|
168
|
+
context: ctx,
|
|
169
|
+
property
|
|
170
|
+
});
|
|
171
|
+
} else if (isSpacingProperty(property)) {
|
|
172
|
+
extracted.spacing.push({
|
|
173
|
+
value: match[0],
|
|
174
|
+
line: getLineNumber(cleanContent, match.index),
|
|
175
|
+
file: filePath,
|
|
176
|
+
context: ctx,
|
|
177
|
+
property
|
|
178
|
+
});
|
|
179
|
+
} else if (isSizingProperty(property)) {
|
|
180
|
+
extracted.sizing.push({
|
|
181
|
+
value: match[0],
|
|
182
|
+
line: getLineNumber(cleanContent, match.index),
|
|
183
|
+
file: filePath,
|
|
184
|
+
context: ctx,
|
|
185
|
+
property
|
|
186
|
+
});
|
|
187
|
+
} else if (isBorderRadiusProperty(property)) {
|
|
188
|
+
extracted.borderRadius.push({
|
|
189
|
+
value: match[0],
|
|
190
|
+
line: getLineNumber(cleanContent, match.index),
|
|
191
|
+
file: filePath,
|
|
192
|
+
context: ctx,
|
|
193
|
+
property
|
|
194
|
+
});
|
|
195
|
+
} else if (isLineHeightProperty(property)) {
|
|
196
|
+
extracted.lineHeight.push({
|
|
197
|
+
value: match[0],
|
|
198
|
+
line: getLineNumber(cleanContent, match.index),
|
|
199
|
+
file: filePath,
|
|
200
|
+
context: ctx,
|
|
201
|
+
property
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Extract percentage values (for border-radius)
|
|
208
|
+
PATTERNS.percentValue.lastIndex = 0;
|
|
209
|
+
while ((match = PATTERNS.percentValue.exec(cleanContent)) !== null) {
|
|
210
|
+
if (!isInSafeZone(match.index)) {
|
|
211
|
+
const ctx = getContext(cleanContent, match.index);
|
|
212
|
+
const property = extractPropertyAtPosition(cleanContent, match.index);
|
|
213
|
+
|
|
214
|
+
if (isBorderRadiusProperty(property)) {
|
|
215
|
+
extracted.borderRadius.push({
|
|
216
|
+
value: match[0],
|
|
217
|
+
line: getLineNumber(cleanContent, match.index),
|
|
218
|
+
file: filePath,
|
|
219
|
+
context: ctx,
|
|
220
|
+
property
|
|
221
|
+
});
|
|
222
|
+
} else if (isOpacityProperty(property)) {
|
|
223
|
+
extracted.opacity.push({
|
|
224
|
+
value: match[0],
|
|
225
|
+
line: getLineNumber(cleanContent, match.index),
|
|
226
|
+
file: filePath,
|
|
227
|
+
context: ctx,
|
|
228
|
+
property
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Extract font weights
|
|
235
|
+
PATTERNS.fontWeight.lastIndex = 0;
|
|
236
|
+
while ((match = PATTERNS.fontWeight.exec(cleanContent)) !== null) {
|
|
237
|
+
if (!isInSafeZone(match.index)) {
|
|
238
|
+
const ctx = getContext(cleanContent, match.index);
|
|
239
|
+
const property = extractPropertyAtPosition(cleanContent, match.index);
|
|
240
|
+
|
|
241
|
+
if (property && property.includes('font-weight')) {
|
|
242
|
+
extracted.fontWeights.push({
|
|
243
|
+
value: match[0],
|
|
244
|
+
line: getLineNumber(cleanContent, match.index),
|
|
245
|
+
file: filePath,
|
|
246
|
+
context: ctx,
|
|
247
|
+
property
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Extract font families
|
|
254
|
+
PATTERNS.fontFamily.lastIndex = 0;
|
|
255
|
+
while ((match = PATTERNS.fontFamily.exec(cleanContent)) !== null) {
|
|
256
|
+
if (!isInSafeZone(match.index)) {
|
|
257
|
+
const ctx = getContext(cleanContent, match.index);
|
|
258
|
+
const property = extractPropertyAtPosition(cleanContent, match.index);
|
|
259
|
+
|
|
260
|
+
if (property && property.includes('font-family')) {
|
|
261
|
+
extracted.fontFamilies.push({
|
|
262
|
+
value: match[0],
|
|
263
|
+
line: getLineNumber(cleanContent, match.index),
|
|
264
|
+
file: filePath,
|
|
265
|
+
context: ctx,
|
|
266
|
+
property
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Extract box shadows
|
|
273
|
+
PATTERNS.boxShadow.lastIndex = 0;
|
|
274
|
+
while ((match = PATTERNS.boxShadow.exec(cleanContent)) !== null) {
|
|
275
|
+
if (!isInSafeZone(match.index)) {
|
|
276
|
+
const ctx = getContext(cleanContent, match.index);
|
|
277
|
+
const property = extractPropertyAtPosition(cleanContent, match.index);
|
|
278
|
+
|
|
279
|
+
if (property && property.includes('box-shadow')) {
|
|
280
|
+
extracted.shadows.push({
|
|
281
|
+
value: match[0],
|
|
282
|
+
line: getLineNumber(cleanContent, match.index),
|
|
283
|
+
file: filePath,
|
|
284
|
+
context: ctx,
|
|
285
|
+
property
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Extract decimal values (opacity, line-height)
|
|
292
|
+
PATTERNS.decimalValue.lastIndex = 0;
|
|
293
|
+
while ((match = PATTERNS.decimalValue.exec(cleanContent)) !== null) {
|
|
294
|
+
if (!isInSafeZone(match.index)) {
|
|
295
|
+
const ctx = getContext(cleanContent, match.index);
|
|
296
|
+
const property = extractPropertyAtPosition(cleanContent, match.index);
|
|
297
|
+
|
|
298
|
+
if (isOpacityProperty(property)) {
|
|
299
|
+
extracted.opacity.push({
|
|
300
|
+
value: match[0],
|
|
301
|
+
line: getLineNumber(cleanContent, match.index),
|
|
302
|
+
file: filePath,
|
|
303
|
+
context: ctx,
|
|
304
|
+
property
|
|
305
|
+
});
|
|
306
|
+
} else if (isLineHeightProperty(property)) {
|
|
307
|
+
extracted.lineHeight.push({
|
|
308
|
+
value: match[0],
|
|
309
|
+
line: getLineNumber(cleanContent, match.index),
|
|
310
|
+
file: filePath,
|
|
311
|
+
context: ctx,
|
|
312
|
+
property
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Extract transitions
|
|
319
|
+
PATTERNS.transition.lastIndex = 0;
|
|
320
|
+
while ((match = PATTERNS.transition.exec(cleanContent)) !== null) {
|
|
321
|
+
if (!isInSafeZone(match.index)) {
|
|
322
|
+
extracted.transitions.push({
|
|
323
|
+
value: match[0],
|
|
324
|
+
line: getLineNumber(cleanContent, match.index),
|
|
325
|
+
file: filePath,
|
|
326
|
+
context: getContext(cleanContent, match.index)
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Extract z-index values (pure integers in z-index context)
|
|
332
|
+
const zIndexRegex = /z-index\s*:\s*(\d+)/g;
|
|
333
|
+
while ((match = zIndexRegex.exec(cleanContent)) !== null) {
|
|
334
|
+
if (!isInSafeZone(match.index)) {
|
|
335
|
+
extracted.zIndex.push({
|
|
336
|
+
value: match[1],
|
|
337
|
+
line: getLineNumber(cleanContent, match.index),
|
|
338
|
+
file: filePath,
|
|
339
|
+
context: getContext(cleanContent, match.index)
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return extracted;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Gets line number for a position in text
|
|
349
|
+
*/
|
|
350
|
+
function getLineNumber(text, position) {
|
|
351
|
+
return text.substring(0, position).split('\n').length;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Gets surrounding context for a position
|
|
356
|
+
*/
|
|
357
|
+
function getContext(text, position, contextSize = 50) {
|
|
358
|
+
const start = Math.max(0, position - contextSize);
|
|
359
|
+
const end = Math.min(text.length, position + contextSize);
|
|
360
|
+
return text.substring(start, end).trim();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Extracts property name at a specific position in the content
|
|
365
|
+
* Looks backward from position to find the property name
|
|
366
|
+
*/
|
|
367
|
+
function extractPropertyAtPosition(content, position) {
|
|
368
|
+
// Find the start of the current line
|
|
369
|
+
const lineStart = content.lastIndexOf('\n', position) + 1;
|
|
370
|
+
const lineSegment = content.substring(lineStart, position);
|
|
371
|
+
|
|
372
|
+
// Look for property: pattern before the position
|
|
373
|
+
const match = lineSegment.match(/([\w-]+)\s*:\s*[^:;]*$/);
|
|
374
|
+
return match ? match[1] : null;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Extracts property name from context (legacy, for other uses)
|
|
379
|
+
*/
|
|
380
|
+
function extractProperty(context) {
|
|
381
|
+
const match = context.match(/([\w-]+)\s*:/);
|
|
382
|
+
return match ? match[1] : null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Property type checkers
|
|
387
|
+
*/
|
|
388
|
+
function isSpacingProperty(prop) {
|
|
389
|
+
if (!prop) return false;
|
|
390
|
+
return /^(padding|margin|gap|top|right|bottom|left)/.test(prop);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function isFontSizeProperty(prop) {
|
|
394
|
+
if (!prop) return false;
|
|
395
|
+
return prop === 'font-size';
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function isSizingProperty(prop) {
|
|
399
|
+
if (!prop) return false;
|
|
400
|
+
return /^(width|height|min-width|max-width|min-height|max-height)/.test(prop);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function isBorderRadiusProperty(prop) {
|
|
404
|
+
if (!prop) return false;
|
|
405
|
+
return /border-radius/.test(prop);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function isLineHeightProperty(prop) {
|
|
409
|
+
if (!prop) return false;
|
|
410
|
+
return prop === 'line-height';
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function isOpacityProperty(prop) {
|
|
414
|
+
if (!prop) return false;
|
|
415
|
+
return prop === 'opacity';
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
module.exports = {
|
|
419
|
+
parseScss,
|
|
420
|
+
PATTERNS
|
|
421
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Refactors SCSS files by replacing hardcoded values with variable references
|
|
6
|
+
* @param {Array<string>} scssFiles - Array of SCSS file paths
|
|
7
|
+
* @param {Object} analysis - Analysis results with variable mappings
|
|
8
|
+
* @param {string} variablesFilePath - Path to the generated variables file
|
|
9
|
+
* @param {Object} config - Configuration
|
|
10
|
+
*/
|
|
11
|
+
function refactorScssFiles(scssFiles, analysis, variablesFilePath, config) {
|
|
12
|
+
const refactoredFiles = [];
|
|
13
|
+
|
|
14
|
+
scssFiles.forEach(filePath => {
|
|
15
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
16
|
+
const refactored = refactorFile(content, analysis, variablesFilePath, filePath, config);
|
|
17
|
+
|
|
18
|
+
if (refactored.modified) {
|
|
19
|
+
fs.writeFileSync(filePath, refactored.content, 'utf8');
|
|
20
|
+
refactoredFiles.push({
|
|
21
|
+
path: filePath,
|
|
22
|
+
changes: refactored.changes
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return refactoredFiles;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Refactors a single SCSS file
|
|
32
|
+
*/
|
|
33
|
+
function refactorFile(content, analysis, variablesFilePath, currentFilePath, config) {
|
|
34
|
+
let newContent = content;
|
|
35
|
+
let modified = false;
|
|
36
|
+
const changes = [];
|
|
37
|
+
|
|
38
|
+
// Build value-to-variable mapping
|
|
39
|
+
const valueMap = buildValueMap(analysis);
|
|
40
|
+
|
|
41
|
+
// Check if file already has @use import for variables
|
|
42
|
+
const hasVariablesImport = content.includes('@use') && content.includes('variables');
|
|
43
|
+
|
|
44
|
+
// Replace values with variables
|
|
45
|
+
Object.entries(valueMap).forEach(([value, variableName]) => {
|
|
46
|
+
const regex = createReplacementRegex(value);
|
|
47
|
+
const matches = [];
|
|
48
|
+
|
|
49
|
+
let match;
|
|
50
|
+
while ((match = regex.exec(content)) !== null) {
|
|
51
|
+
matches.push({
|
|
52
|
+
index: match.index,
|
|
53
|
+
matched: match[0],
|
|
54
|
+
fullMatch: match
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check if replacements are in safe zones
|
|
59
|
+
matches.forEach(m => {
|
|
60
|
+
if (!isInSafeZone(content, m.index)) {
|
|
61
|
+
// Replace the value
|
|
62
|
+
const before = newContent;
|
|
63
|
+
newContent = replaceAt(newContent, m.matched, variableName, m.index);
|
|
64
|
+
|
|
65
|
+
if (before !== newContent) {
|
|
66
|
+
modified = true;
|
|
67
|
+
changes.push({
|
|
68
|
+
from: value,
|
|
69
|
+
to: variableName
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Add @use import if modified and not already present
|
|
77
|
+
if (modified && !hasVariablesImport) {
|
|
78
|
+
const relativePath = getRelativeImportPath(currentFilePath, variablesFilePath);
|
|
79
|
+
const useStatement = `@use '${relativePath}' as *;\n\n`;
|
|
80
|
+
newContent = useStatement + newContent;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
content: newContent,
|
|
85
|
+
modified,
|
|
86
|
+
changes
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Builds a map of values to variable names
|
|
92
|
+
*/
|
|
93
|
+
function buildValueMap(analysis) {
|
|
94
|
+
const valueMap = {};
|
|
95
|
+
|
|
96
|
+
Object.keys(analysis).forEach(category => {
|
|
97
|
+
analysis[category].forEach(item => {
|
|
98
|
+
// Use normalized value as key for consistent matching
|
|
99
|
+
valueMap[item.value] = item.suggestedName;
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
return valueMap;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Creates a regex for replacing a specific value
|
|
108
|
+
*/
|
|
109
|
+
function createReplacementRegex(value) {
|
|
110
|
+
// Escape special regex characters
|
|
111
|
+
const escaped = value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
112
|
+
// Match the value with word boundaries or specific contexts
|
|
113
|
+
return new RegExp(escaped, 'g');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Checks if a position in content is in a safe zone (should not be replaced)
|
|
118
|
+
*/
|
|
119
|
+
function isInSafeZone(content, position) {
|
|
120
|
+
// Check for variable declarations
|
|
121
|
+
const lineStart = content.lastIndexOf('\n', position);
|
|
122
|
+
const lineEnd = content.indexOf('\n', position);
|
|
123
|
+
const line = content.substring(lineStart, lineEnd);
|
|
124
|
+
|
|
125
|
+
if (line.includes('$') && line.includes(':')) {
|
|
126
|
+
// This is a variable declaration line
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Check for @use, @forward, @import
|
|
131
|
+
if (/@(?:use|forward|import)/.test(line)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check for url()
|
|
136
|
+
const beforeContext = content.substring(Math.max(0, position - 50), position);
|
|
137
|
+
if (/url\s*\(\s*[^)]*$/.test(beforeContext)) {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Check for content property
|
|
142
|
+
if (/content\s*:\s*[^;]*$/.test(beforeContext)) {
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check for interpolation #{}
|
|
147
|
+
if (/#\{[^}]*$/.test(beforeContext)) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Check for Material functions
|
|
152
|
+
if (/mat\.\w+\([^)]*$/.test(beforeContext)) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Check for comments
|
|
157
|
+
const commentStart = content.lastIndexOf('/*', position);
|
|
158
|
+
const commentEnd = content.lastIndexOf('*/', position);
|
|
159
|
+
if (commentStart > commentEnd) {
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const lineCommentStart = content.lastIndexOf('//', lineStart);
|
|
164
|
+
if (lineCommentStart > lineStart && lineCommentStart < position) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Replaces value at specific index
|
|
173
|
+
*/
|
|
174
|
+
function replaceAt(content, searchValue, replaceValue, startIndex) {
|
|
175
|
+
// Find the exact position and replace
|
|
176
|
+
const before = content.substring(0, startIndex);
|
|
177
|
+
const after = content.substring(startIndex);
|
|
178
|
+
|
|
179
|
+
// Replace first occurrence in 'after'
|
|
180
|
+
const replaced = after.replace(searchValue, replaceValue);
|
|
181
|
+
|
|
182
|
+
return before + replaced;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Gets relative import path from current file to variables file
|
|
187
|
+
*/
|
|
188
|
+
function getRelativeImportPath(fromFile, toFile) {
|
|
189
|
+
const fromDir = path.dirname(fromFile);
|
|
190
|
+
let relativePath = path.relative(fromDir, toFile);
|
|
191
|
+
|
|
192
|
+
// Convert to forward slashes for imports
|
|
193
|
+
relativePath = relativePath.replace(/\\/g, '/');
|
|
194
|
+
|
|
195
|
+
// Remove .scss extension
|
|
196
|
+
relativePath = relativePath.replace(/\.scss$/, '');
|
|
197
|
+
|
|
198
|
+
// Add ./ prefix if not starting with ../
|
|
199
|
+
if (!relativePath.startsWith('..')) {
|
|
200
|
+
relativePath = './' + relativePath;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return relativePath;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module.exports = {
|
|
207
|
+
refactorScssFiles,
|
|
208
|
+
refactorFile
|
|
209
|
+
};
|
package/src/scanner.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const glob = require('glob');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recursively scans for SCSS files in the given source directory
|
|
6
|
+
* @param {string} srcPath - Source directory to scan
|
|
7
|
+
* @param {Array<string>} ignorePatterns - Patterns to ignore
|
|
8
|
+
* @returns {Promise<Array<string>>} - Array of absolute file paths
|
|
9
|
+
*/
|
|
10
|
+
async function scanScssFiles(srcPath, ignorePatterns = []) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const pattern = path.join(srcPath, '**/*.scss');
|
|
13
|
+
|
|
14
|
+
glob(pattern, {
|
|
15
|
+
ignore: ignorePatterns,
|
|
16
|
+
absolute: true
|
|
17
|
+
}, (err, files) => {
|
|
18
|
+
if (err) {
|
|
19
|
+
reject(err);
|
|
20
|
+
} else {
|
|
21
|
+
resolve(files);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
scanScssFiles
|
|
29
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Auto-generated SCSS Variables
|
|
3
|
+
// Generated by scss-variable-extractor v{{version}}
|
|
4
|
+
// Generated at: {{timestamp}}
|
|
5
|
+
//
|
|
6
|
+
// DO NOT EDIT THIS FILE MANUALLY
|
|
7
|
+
// This file is auto-generated. Your changes will be overwritten.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
// Colors
|
|
11
|
+
// ────────────────────────────────────────
|
|
12
|
+
{{colors}}
|
|
13
|
+
|
|
14
|
+
// Spacing
|
|
15
|
+
// ────────────────────────────────────────
|
|
16
|
+
{{spacing}}
|
|
17
|
+
|
|
18
|
+
// Font Sizes
|
|
19
|
+
// ────────────────────────────────────────
|
|
20
|
+
{{fontSizes}}
|
|
21
|
+
|
|
22
|
+
// Font Weights
|
|
23
|
+
// ────────────────────────────────────────
|
|
24
|
+
{{fontWeights}}
|
|
25
|
+
|
|
26
|
+
// Font Families
|
|
27
|
+
// ────────────────────────────────────────
|
|
28
|
+
{{fontFamilies}}
|
|
29
|
+
|
|
30
|
+
// Border Radius
|
|
31
|
+
// ────────────────────────────────────────
|
|
32
|
+
{{borderRadius}}
|
|
33
|
+
|
|
34
|
+
// Shadows
|
|
35
|
+
// ────────────────────────────────────────
|
|
36
|
+
{{shadows}}
|
|
37
|
+
|
|
38
|
+
// Z-Index
|
|
39
|
+
// ────────────────────────────────────────
|
|
40
|
+
{{zIndex}}
|
|
41
|
+
|
|
42
|
+
// Sizing
|
|
43
|
+
// ────────────────────────────────────────
|
|
44
|
+
{{sizing}}
|
|
45
|
+
|
|
46
|
+
// Line Heights
|
|
47
|
+
// ────────────────────────────────────────
|
|
48
|
+
{{lineHeight}}
|
|
49
|
+
|
|
50
|
+
// Opacity
|
|
51
|
+
// ────────────────────────────────────────
|
|
52
|
+
{{opacity}}
|
|
53
|
+
|
|
54
|
+
// Transitions
|
|
55
|
+
// ────────────────────────────────────────
|
|
56
|
+
{{transitions}}
|