scss-variable-extractor 1.6.2 → 1.6.4
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/package.json +1 -1
- package/src/refactorer.js +95 -13
- package/src/style-organizer.js +5 -0
- package/test/refactorer-edge-cases.test.js +353 -0
- package/test/refactorer.test.js +87 -0
package/package.json
CHANGED
package/src/refactorer.js
CHANGED
|
@@ -42,12 +42,17 @@ function refactorFile(content, analysis, variablesFilePath, currentFilePath, con
|
|
|
42
42
|
const hasVariablesImport = content.includes('@use') && content.includes('variables');
|
|
43
43
|
|
|
44
44
|
// Replace values with variables
|
|
45
|
-
|
|
45
|
+
// Sort by value length (longest first) to avoid partial replacements
|
|
46
|
+
// E.g., replace "0.5" before "0" to prevent "0.5" becoming "$var.5"
|
|
47
|
+
const sortedEntries = Object.entries(valueMap).sort((a, b) => b[0].length - a[0].length);
|
|
48
|
+
|
|
49
|
+
sortedEntries.forEach(([value, variableName]) => {
|
|
46
50
|
const regex = createReplacementRegex(value);
|
|
47
51
|
const matches = [];
|
|
48
52
|
|
|
49
53
|
let match;
|
|
50
|
-
|
|
54
|
+
// Find matches in the current state of newContent (after previous replacements)
|
|
55
|
+
while ((match = regex.exec(newContent)) !== null) {
|
|
51
56
|
matches.push({
|
|
52
57
|
index: match.index,
|
|
53
58
|
matched: match[0],
|
|
@@ -56,8 +61,11 @@ function refactorFile(content, analysis, variablesFilePath, currentFilePath, con
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
// Check if replacements are in safe zones
|
|
59
|
-
matches
|
|
60
|
-
|
|
64
|
+
// Process matches in reverse order (highest index first) to avoid position shifts
|
|
65
|
+
matches.reverse().forEach(m => {
|
|
66
|
+
const inSafeZone = isInSafeZone(newContent, m.index, m.matched);
|
|
67
|
+
|
|
68
|
+
if (!inSafeZone) {
|
|
61
69
|
// Replace the value
|
|
62
70
|
const before = newContent;
|
|
63
71
|
newContent = replaceAt(newContent, m.matched, variableName, m.index);
|
|
@@ -119,8 +127,10 @@ function createReplacementRegex(value) {
|
|
|
119
127
|
// Values with CSS units - ensure not part of larger number
|
|
120
128
|
return new RegExp(`(?<!\\d)${escaped}\\b`, 'g');
|
|
121
129
|
} else if (/^-?\d+(?:\.\d+)?$/.test(value)) {
|
|
122
|
-
// Pure numbers (
|
|
123
|
-
|
|
130
|
+
// Pure numbers/decimals (unitless) - ensure not part of larger number or before units
|
|
131
|
+
// Negative lookbehind: not preceded by digit
|
|
132
|
+
// Negative lookahead: not followed by digit, unit letters, or % (for keyframe selectors)
|
|
133
|
+
return new RegExp(`(?<!\\d)${escaped}(?!\\d|[a-z%])`, 'g');
|
|
124
134
|
} else if (/^rgba?\(/.test(value)) {
|
|
125
135
|
// RGB/RGBA colors - match complete function
|
|
126
136
|
return new RegExp(escaped, 'g');
|
|
@@ -134,13 +144,19 @@ function createReplacementRegex(value) {
|
|
|
134
144
|
* Checks if a position in content is in a safe zone (should not be replaced)
|
|
135
145
|
*/
|
|
136
146
|
function isInSafeZone(content, position, matchedValue) {
|
|
147
|
+
const DEBUG = false; // Enable for debugging
|
|
148
|
+
|
|
137
149
|
// Check for variable declarations
|
|
138
150
|
const lineStart = content.lastIndexOf('\n', position);
|
|
139
151
|
const lineEnd = content.indexOf('\n', position);
|
|
140
152
|
const line = content.substring(lineStart, lineEnd);
|
|
141
153
|
|
|
142
|
-
if
|
|
143
|
-
|
|
154
|
+
// Check if this line is a variable declaration: $variable-name: value;
|
|
155
|
+
// Match pattern: optional whitespace, $, identifier, optional whitespace, colon
|
|
156
|
+
const varDeclMatch = line.match(/^\s*\$([\w-]+)\s*:/);
|
|
157
|
+
if (varDeclMatch) {
|
|
158
|
+
// This line IS a variable declaration
|
|
159
|
+
if (DEBUG) console.log(` → SAFE: Variable declaration line`);
|
|
144
160
|
return true;
|
|
145
161
|
}
|
|
146
162
|
|
|
@@ -150,11 +166,28 @@ function isInSafeZone(content, position, matchedValue) {
|
|
|
150
166
|
|
|
151
167
|
// Prevent replacement if surrounded by digits (partial number match)
|
|
152
168
|
if (/\d/.test(charBefore) || (/^\d+$/.test(matchedValue) && /\d/.test(charAfter))) {
|
|
169
|
+
if (DEBUG) console.log(` → SAFE: Surrounded by digits`);
|
|
153
170
|
return true;
|
|
154
171
|
}
|
|
155
172
|
|
|
173
|
+
// Prevent replacement within hex colors
|
|
174
|
+
if (/[0-9a-fA-F]/.test(charBefore) || /[0-9a-fA-F]/.test(charAfter)) {
|
|
175
|
+
// Look back further to check if we're in a hex color
|
|
176
|
+
const lookBehind = content.substring(Math.max(0, position - 10), position);
|
|
177
|
+
if (/#[0-9a-fA-F]*$/.test(lookBehind)) {
|
|
178
|
+
if (DEBUG) console.log(` → SAFE: Inside hex color`);
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
156
183
|
// Prevent replacement if it would create invalid variable concatenation
|
|
157
184
|
if (charBefore === '$' || charAfter === '$') {
|
|
185
|
+
if (DEBUG) console.log(` → SAFE: Near $ symbol`);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Prevent replacement for negative values (e.g., -16px should not become -$spacing-md)
|
|
190
|
+
if (charBefore === '-' && /^\d/.test(matchedValue)) {
|
|
158
191
|
return true;
|
|
159
192
|
}
|
|
160
193
|
|
|
@@ -163,8 +196,19 @@ function isInSafeZone(content, position, matchedValue) {
|
|
|
163
196
|
return true;
|
|
164
197
|
}
|
|
165
198
|
|
|
166
|
-
// Check for url()
|
|
167
|
-
const beforeContext = content.substring(Math.max(0, position -
|
|
199
|
+
// Check for url(), calc(), transform functions, and other contexts
|
|
200
|
+
const beforeContext = content.substring(Math.max(0, position - 100), position);
|
|
201
|
+
|
|
202
|
+
// Prevent replacement inside calc() expressions
|
|
203
|
+
if (/calc\s*\([^)]*$/.test(beforeContext)) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Prevent replacement inside transform functions
|
|
208
|
+
if (/(?:translate|rotate|scale|skew|matrix|perspective)\s*\([^)]*$/.test(beforeContext)) {
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
|
|
168
212
|
if (/url\s*\(\s*[^)]*$/.test(beforeContext)) {
|
|
169
213
|
return true;
|
|
170
214
|
}
|
|
@@ -184,18 +228,56 @@ function isInSafeZone(content, position, matchedValue) {
|
|
|
184
228
|
return true;
|
|
185
229
|
}
|
|
186
230
|
|
|
231
|
+
// Prevent replacement inside attribute selectors
|
|
232
|
+
if (/\[[^\]]*$/.test(beforeContext) &&
|
|
233
|
+
/^[^\[]*\]/.test(content.substring(position, position + 50))) {
|
|
234
|
+
if (DEBUG) console.log(` → SAFE: Inside attribute selector`);
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Prevent replacement in keyframe percentages (e.g., 0%, 50%, 100% in @keyframes)
|
|
239
|
+
// Check if we're in a @keyframes block and this position is before the opening brace
|
|
240
|
+
if (/@keyframes/.test(beforeContext)) {
|
|
241
|
+
// Check if we're in the selector part (before {) of a keyframe rule
|
|
242
|
+
// Get text from line start to current position and after
|
|
243
|
+
const lineBeforePos = content.substring(lineStart, position + matchedValue.length);
|
|
244
|
+
const lineAfterPos = content.substring(position + matchedValue.length, lineEnd);
|
|
245
|
+
|
|
246
|
+
// If the line has a { after this position and before that { there's a %,
|
|
247
|
+
// and we haven't passed the { yet, we're in the selector
|
|
248
|
+
if (/%/.test(lineBeforePos) && /^\s*%/.test(content.substring(position + matchedValue.length, position + matchedValue.length + 5))) {
|
|
249
|
+
// We're right before the % in a keyframe selector
|
|
250
|
+
if (DEBUG) console.log(` → SAFE: Keyframe percentage selector`);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
if (/%[^{]*$/.test(lineBeforePos) && !lineBeforePos.includes('{')) {
|
|
254
|
+
// We're somewhere in a percentage before the {
|
|
255
|
+
if (DEBUG) console.log(` → SAFE: Keyframe percentage line`);
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
187
260
|
// Check for comments
|
|
188
261
|
const commentStart = content.lastIndexOf('/*', position);
|
|
189
262
|
const commentEnd = content.lastIndexOf('*/', position);
|
|
190
263
|
if (commentStart > commentEnd) {
|
|
264
|
+
if (DEBUG) console.log(` → SAFE: Inside block comment`);
|
|
191
265
|
return true;
|
|
192
266
|
}
|
|
193
267
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
268
|
+
// Check for inline comments (//)
|
|
269
|
+
const lineContent = content.substring(lineStart, position + matchedValue.length);
|
|
270
|
+
const inlineCommentPos = lineContent.lastIndexOf('//');
|
|
271
|
+
if (inlineCommentPos !== -1) {
|
|
272
|
+
// Check if our position is after the // on this line
|
|
273
|
+
const posInLine = position - lineStart;
|
|
274
|
+
if (posInLine >= inlineCommentPos) {
|
|
275
|
+
if (DEBUG) console.log(` → SAFE: Inside inline comment`);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
197
278
|
}
|
|
198
279
|
|
|
280
|
+
if (DEBUG) console.log(` → NOT SAFE: Will replace`);
|
|
199
281
|
return false;
|
|
200
282
|
}
|
|
201
283
|
|
package/src/style-organizer.js
CHANGED
|
@@ -41,6 +41,11 @@ function analyzeStyleOrganization(scssFiles, config = {}) {
|
|
|
41
41
|
const importMap = new Map(); // Track import statements
|
|
42
42
|
|
|
43
43
|
scssFiles.forEach(filePath => {
|
|
44
|
+
// Skip if file doesn't exist (e.g., temp test files)
|
|
45
|
+
if (!fs.existsSync(filePath)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
44
49
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
45
50
|
const fileName = path.basename(filePath);
|
|
46
51
|
const fileInfo = analyzeFile(content, filePath);
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
const { refactorFile } = require('../src/refactorer');
|
|
2
|
+
|
|
3
|
+
describe('Refactorer Edge Cases', () => {
|
|
4
|
+
test('should handle rgba() color functions without partial replacements', () => {
|
|
5
|
+
const scss = `
|
|
6
|
+
.component {
|
|
7
|
+
background: rgba(255, 100, 50, 0.5);
|
|
8
|
+
color: rgba(100, 100, 100, 1);
|
|
9
|
+
}
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
const analysis = {
|
|
13
|
+
colors: [
|
|
14
|
+
{ value: 'rgba(255, 100, 50, 0.5)', suggestedName: '$color-primary-light' }
|
|
15
|
+
],
|
|
16
|
+
spacing: [
|
|
17
|
+
{ value: '100px', suggestedName: '$spacing-xxl' }
|
|
18
|
+
],
|
|
19
|
+
fontSizes: [],
|
|
20
|
+
fontWeights: [],
|
|
21
|
+
fontFamilies: [],
|
|
22
|
+
borderRadius: [],
|
|
23
|
+
shadows: [],
|
|
24
|
+
zIndex: [],
|
|
25
|
+
sizing: [],
|
|
26
|
+
lineHeight: [],
|
|
27
|
+
opacity: [
|
|
28
|
+
{ value: '0.5', suggestedName: '$opacity-half' }
|
|
29
|
+
],
|
|
30
|
+
transitions: []
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const result = refactorFile(scss, analysis, '/libs/styles/_variables.scss', '/app/test.scss', {});
|
|
34
|
+
|
|
35
|
+
// Should replace complete rgba function
|
|
36
|
+
expect(result.content).toContain('background: $color-primary-light');
|
|
37
|
+
|
|
38
|
+
// Should NOT do partial replacements inside rgba()
|
|
39
|
+
expect(result.content).toContain('rgba(100, 100, 100, 1)'); // Not rgba($spacing-xxl, $spacing-xxl, $spacing-xxl, 1)
|
|
40
|
+
expect(result.content).not.toContain('rgba($spacing-xxl');
|
|
41
|
+
expect(result.content).not.toContain('rgba(255, $spacing-xxl');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('should handle calc() expressions without partial replacements', () => {
|
|
45
|
+
const scss = `
|
|
46
|
+
.component {
|
|
47
|
+
width: calc(100% - 16px);
|
|
48
|
+
height: calc(50vh + 8px);
|
|
49
|
+
margin: calc(16px / 2);
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const analysis = {
|
|
54
|
+
colors: [],
|
|
55
|
+
spacing: [
|
|
56
|
+
{ value: '16px', suggestedName: '$spacing-md' },
|
|
57
|
+
{ value: '8px', suggestedName: '$spacing-sm' }
|
|
58
|
+
],
|
|
59
|
+
fontSizes: [],
|
|
60
|
+
fontWeights: [],
|
|
61
|
+
fontFamilies: [],
|
|
62
|
+
borderRadius: [],
|
|
63
|
+
shadows: [],
|
|
64
|
+
zIndex: [],
|
|
65
|
+
sizing: [],
|
|
66
|
+
lineHeight: [],
|
|
67
|
+
opacity: [],
|
|
68
|
+
transitions: []
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = refactorFile(scss, analysis, '/libs/styles/_variables.scss', '/app/test.scss', {});
|
|
72
|
+
|
|
73
|
+
// calc() values should be protected from replacement (safe zone)
|
|
74
|
+
const content = result.content;
|
|
75
|
+
|
|
76
|
+
// Values inside calc() should NOT be replaced
|
|
77
|
+
expect(content).toContain('calc(100% - 16px)');
|
|
78
|
+
expect(content).toContain('calc(50vh + 8px)');
|
|
79
|
+
expect(content).toContain('calc(16px / 2)');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('should handle negative values correctly', () => {
|
|
83
|
+
const scss = `
|
|
84
|
+
.component {
|
|
85
|
+
margin: -16px;
|
|
86
|
+
top: -8px;
|
|
87
|
+
z-index: -1;
|
|
88
|
+
}
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
const analysis = {
|
|
92
|
+
colors: [],
|
|
93
|
+
spacing: [
|
|
94
|
+
{ value: '16px', suggestedName: '$spacing-md' },
|
|
95
|
+
{ value: '8px', suggestedName: '$spacing-sm' }
|
|
96
|
+
],
|
|
97
|
+
fontSizes: [],
|
|
98
|
+
fontWeights: [],
|
|
99
|
+
fontFamilies: [],
|
|
100
|
+
borderRadius: [],
|
|
101
|
+
shadows: [],
|
|
102
|
+
zIndex: [
|
|
103
|
+
{ value: '1', suggestedName: '$z-index-1' }
|
|
104
|
+
],
|
|
105
|
+
sizing: [],
|
|
106
|
+
lineHeight: [],
|
|
107
|
+
opacity: [],
|
|
108
|
+
transitions: []
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const result = refactorFile(scss, analysis, '/libs/styles/_variables.scss', '/app/test.scss', {});
|
|
112
|
+
|
|
113
|
+
// Negative values should be protected (safe zone) to prevent -$variable syntax
|
|
114
|
+
expect(result.content).toContain('margin: -16px');
|
|
115
|
+
expect(result.content).toContain('top: -8px');
|
|
116
|
+
expect(result.content).toContain('z-index: -1');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('should handle multiple values in shorthand properties', () => {
|
|
120
|
+
const scss = `
|
|
121
|
+
.component {
|
|
122
|
+
margin: 16px 8px;
|
|
123
|
+
padding: 16px 8px 16px 8px;
|
|
124
|
+
border-radius: 4px 2px;
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
const analysis = {
|
|
129
|
+
colors: [],
|
|
130
|
+
spacing: [
|
|
131
|
+
{ value: '16px', suggestedName: '$spacing-md' },
|
|
132
|
+
{ value: '8px', suggestedName: '$spacing-sm' }
|
|
133
|
+
],
|
|
134
|
+
fontSizes: [],
|
|
135
|
+
fontWeights: [],
|
|
136
|
+
fontFamilies: [],
|
|
137
|
+
borderRadius: [
|
|
138
|
+
{ value: '4px', suggestedName: '$border-radius-sm' },
|
|
139
|
+
{ value: '2px', suggestedName: '$border-radius-xs' }
|
|
140
|
+
],
|
|
141
|
+
shadows: [],
|
|
142
|
+
zIndex: [],
|
|
143
|
+
sizing: [],
|
|
144
|
+
lineHeight: [],
|
|
145
|
+
opacity: [],
|
|
146
|
+
transitions: []
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const result = refactorFile(scss, analysis, '/libs/styles/_variables.scss', '/app/test.scss', {});
|
|
150
|
+
|
|
151
|
+
// Should replace individual values in shorthand
|
|
152
|
+
expect(result.content).toContain('$spacing-md');
|
|
153
|
+
expect(result.content).toContain('$spacing-sm');
|
|
154
|
+
expect(result.content).toContain('$border-radius-sm');
|
|
155
|
+
expect(result.content).toContain('$border-radius-xs');
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('should not replace values in CSS custom properties', () => {
|
|
159
|
+
const scss = `
|
|
160
|
+
:root {
|
|
161
|
+
--primary-color: #1976d2;
|
|
162
|
+
--spacing: 16px;
|
|
163
|
+
}
|
|
164
|
+
.component {
|
|
165
|
+
color: var(--primary-color);
|
|
166
|
+
padding: var(--spacing);
|
|
167
|
+
}
|
|
168
|
+
`;
|
|
169
|
+
|
|
170
|
+
const analysis = {
|
|
171
|
+
colors: [
|
|
172
|
+
{ value: '#1976d2', suggestedName: '$color-primary' }
|
|
173
|
+
],
|
|
174
|
+
spacing: [
|
|
175
|
+
{ value: '16px', suggestedName: '$spacing-md' }
|
|
176
|
+
],
|
|
177
|
+
fontSizes: [],
|
|
178
|
+
fontWeights: [],
|
|
179
|
+
fontFamilies: [],
|
|
180
|
+
borderRadius: [],
|
|
181
|
+
shadows: [],
|
|
182
|
+
zIndex: [],
|
|
183
|
+
sizing: [],
|
|
184
|
+
lineHeight: [],
|
|
185
|
+
opacity: [],
|
|
186
|
+
transitions: []
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const result = refactorFile(scss, analysis, '/libs/styles/_variables.scss', '/app/test.scss', {});
|
|
190
|
+
|
|
191
|
+
// CSS custom property declarations should be replaced (they're like SCSS variables)
|
|
192
|
+
// But var() references should be preserved
|
|
193
|
+
expect(result.content).toContain('var(--primary-color)');
|
|
194
|
+
expect(result.content).toContain('var(--spacing)');
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('should not replace values in attribute selectors', () => {
|
|
198
|
+
const scss = `
|
|
199
|
+
[data-size="16"] {
|
|
200
|
+
padding: 16px;
|
|
201
|
+
}
|
|
202
|
+
[data-color="#1976d2"] {
|
|
203
|
+
color: #1976d2;
|
|
204
|
+
}
|
|
205
|
+
`;
|
|
206
|
+
|
|
207
|
+
const analysis = {
|
|
208
|
+
colors: [
|
|
209
|
+
{ value: '#1976d2', suggestedName: '$color-primary' }
|
|
210
|
+
],
|
|
211
|
+
spacing: [
|
|
212
|
+
{ value: '16px', suggestedName: '$spacing-md' }
|
|
213
|
+
],
|
|
214
|
+
fontSizes: [],
|
|
215
|
+
fontWeights: [],
|
|
216
|
+
fontFamilies: [],
|
|
217
|
+
borderRadius: [],
|
|
218
|
+
shadows: [],
|
|
219
|
+
zIndex: [],
|
|
220
|
+
sizing: [],
|
|
221
|
+
lineHeight: [],
|
|
222
|
+
opacity: [],
|
|
223
|
+
transitions: []
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const result = refactorFile(scss, analysis, '/libs/styles/_variables.scss', '/app/test.scss', {});
|
|
227
|
+
|
|
228
|
+
// Should replace in property values (outside attribute selectors)
|
|
229
|
+
expect(result.content).toContain('padding: $spacing-md');
|
|
230
|
+
expect(result.content).toContain('color: $color-primary');
|
|
231
|
+
|
|
232
|
+
// Should NOT replace in attribute selector values (safe zone)
|
|
233
|
+
expect(result.content).toContain('[data-size="16"]');
|
|
234
|
+
expect(result.content).toContain('[data-color="#1976d2"]');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test('should handle transform functions', () => {
|
|
238
|
+
const scss = `
|
|
239
|
+
.component {
|
|
240
|
+
transform: translate(10px, 20px);
|
|
241
|
+
transform: rotate(45deg);
|
|
242
|
+
transform: scale(2);
|
|
243
|
+
}
|
|
244
|
+
`;
|
|
245
|
+
|
|
246
|
+
const analysis = {
|
|
247
|
+
colors: [],
|
|
248
|
+
spacing: [
|
|
249
|
+
{ value: '10px', suggestedName: '$spacing-xs' },
|
|
250
|
+
{ value: '20px', suggestedName: '$spacing-sm' }
|
|
251
|
+
],
|
|
252
|
+
fontSizes: [],
|
|
253
|
+
fontWeights: [],
|
|
254
|
+
fontFamilies: [],
|
|
255
|
+
borderRadius: [],
|
|
256
|
+
shadows: [],
|
|
257
|
+
zIndex: [
|
|
258
|
+
{ value: '2', suggestedName: '$z-index-2' }
|
|
259
|
+
],
|
|
260
|
+
sizing: [],
|
|
261
|
+
lineHeight: [],
|
|
262
|
+
opacity: [],
|
|
263
|
+
transitions: []
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const result = refactorFile(scss, analysis, '/libs/styles/_variables.scss', '/app/test.scss', {});
|
|
267
|
+
|
|
268
|
+
// Transform function arguments should be protected (safe zone)
|
|
269
|
+
const content = result.content;
|
|
270
|
+
expect(content).toContain('translate(10px, 20px)');
|
|
271
|
+
expect(content).toContain('scale(2)');
|
|
272
|
+
expect(content).toContain('rotate(45deg)');
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('should handle keyframe percentages', () => {
|
|
276
|
+
const scss = `
|
|
277
|
+
@keyframes fade {
|
|
278
|
+
0% { opacity: 0; }
|
|
279
|
+
50% { opacity: 0.5; }
|
|
280
|
+
100% { opacity: 1; }
|
|
281
|
+
}
|
|
282
|
+
`;
|
|
283
|
+
|
|
284
|
+
const analysis = {
|
|
285
|
+
colors: [],
|
|
286
|
+
spacing: [],
|
|
287
|
+
fontSizes: [],
|
|
288
|
+
fontWeights: [],
|
|
289
|
+
fontFamilies: [],
|
|
290
|
+
borderRadius: [],
|
|
291
|
+
shadows: [],
|
|
292
|
+
zIndex: [],
|
|
293
|
+
sizing: [],
|
|
294
|
+
lineHeight: [],
|
|
295
|
+
opacity: [
|
|
296
|
+
{ value: '0', suggestedName: '$opacity-transparent' },
|
|
297
|
+
{ value: '0.5', suggestedName: '$opacity-half' },
|
|
298
|
+
{ value: '1', suggestedName: '$opacity-full' }
|
|
299
|
+
],
|
|
300
|
+
transitions: []
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const result = refactorFile(scss, analysis, '/libs/styles/_variables.scss', '/app/test.scss', {});
|
|
304
|
+
|
|
305
|
+
// Keyframe selectors (0%, 50%, 100%) should not be replaced
|
|
306
|
+
expect(result.content).toContain('0%');
|
|
307
|
+
expect(result.content).toContain('50%');
|
|
308
|
+
expect(result.content).toContain('100%');
|
|
309
|
+
|
|
310
|
+
// But opacity values should be replaced
|
|
311
|
+
expect(result.content).toContain('opacity: $opacity-transparent');
|
|
312
|
+
expect(result.content).toContain('opacity: $opacity-half');
|
|
313
|
+
expect(result.content).toContain('opacity: $opacity-full');
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test('should handle comments containing values', () => {
|
|
317
|
+
const scss = `
|
|
318
|
+
/* Primary color is #1976d2 */
|
|
319
|
+
.component {
|
|
320
|
+
color: #1976d2; // Use #1976d2 for brand
|
|
321
|
+
padding: 16px; /* 16px is our standard spacing */
|
|
322
|
+
}
|
|
323
|
+
`;
|
|
324
|
+
|
|
325
|
+
const analysis = {
|
|
326
|
+
colors: [
|
|
327
|
+
{ value: '#1976d2', suggestedName: '$color-primary' }
|
|
328
|
+
],
|
|
329
|
+
spacing: [
|
|
330
|
+
{ value: '16px', suggestedName: '$spacing-md' }
|
|
331
|
+
],
|
|
332
|
+
fontSizes: [],
|
|
333
|
+
fontWeights: [],
|
|
334
|
+
fontFamilies: [],
|
|
335
|
+
borderRadius: [],
|
|
336
|
+
shadows: [],
|
|
337
|
+
zIndex: [],
|
|
338
|
+
sizing: [],
|
|
339
|
+
lineHeight: [],
|
|
340
|
+
opacity: [],
|
|
341
|
+
transitions: []
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
const result = refactorFile(scss, analysis, '/libs/styles/_variables.scss', '/app/test.scss', {});
|
|
345
|
+
|
|
346
|
+
// Should replace in actual property values
|
|
347
|
+
expect(result.content).toContain('color: $color-primary');
|
|
348
|
+
expect(result.content).toContain('padding: $spacing-md');
|
|
349
|
+
|
|
350
|
+
// Comments will likely be replaced too (current behavior)
|
|
351
|
+
// This might be acceptable or we could add comment detection to safe zones
|
|
352
|
+
});
|
|
353
|
+
});
|
package/test/refactorer.test.js
CHANGED
|
@@ -167,4 +167,91 @@ describe('Refactorer', () => {
|
|
|
167
167
|
expect(result.content).not.toContain('76$spacing-sm');
|
|
168
168
|
expect(result.content).not.toContain('$z-index23');
|
|
169
169
|
});
|
|
170
|
+
|
|
171
|
+
test('should not replace characters within hex colors', () => {
|
|
172
|
+
const scss = `
|
|
173
|
+
.component {
|
|
174
|
+
background: #a2dab1;
|
|
175
|
+
border-color: #ff2244;
|
|
176
|
+
color: #333;
|
|
177
|
+
}
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
const analysis = {
|
|
181
|
+
colors: [
|
|
182
|
+
{ value: '#a2dab1', suggestedName: '$color-success' },
|
|
183
|
+
{ value: '#333', suggestedName: '$color-text' }
|
|
184
|
+
],
|
|
185
|
+
spacing: [],
|
|
186
|
+
fontSizes: [],
|
|
187
|
+
fontWeights: [],
|
|
188
|
+
fontFamilies: [],
|
|
189
|
+
borderRadius: [],
|
|
190
|
+
shadows: [],
|
|
191
|
+
zIndex: [
|
|
192
|
+
{ value: '2', suggestedName: '$z-index-2' }
|
|
193
|
+
],
|
|
194
|
+
sizing: [],
|
|
195
|
+
lineHeight: [],
|
|
196
|
+
opacity: [],
|
|
197
|
+
transitions: []
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const result = refactorFile(scss, analysis, '/libs/styles/_variables.scss', '/app/test.scss', {});
|
|
201
|
+
|
|
202
|
+
// Should replace complete hex colors
|
|
203
|
+
expect(result.content).toContain('background: $color-success');
|
|
204
|
+
expect(result.content).toContain('color: $color-text');
|
|
205
|
+
|
|
206
|
+
// Should NOT replace digits within hex colors
|
|
207
|
+
expect(result.content).toContain('#ff2244'); // Not #ff$z-index-2$z-index-244
|
|
208
|
+
expect(result.content).not.toContain('#a$z-index-2dab1');
|
|
209
|
+
expect(result.content).not.toContain('#ff$z-index-2');
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test('should not replace unitless values before CSS units', () => {
|
|
213
|
+
const scss = `
|
|
214
|
+
.component {
|
|
215
|
+
opacity: 0.5;
|
|
216
|
+
line-height: 0.5em;
|
|
217
|
+
font-size: 2rem;
|
|
218
|
+
padding: 2px;
|
|
219
|
+
}
|
|
220
|
+
`;
|
|
221
|
+
|
|
222
|
+
const analysis = {
|
|
223
|
+
colors: [],
|
|
224
|
+
spacing: [
|
|
225
|
+
{ value: '2px', suggestedName: '$spacing-xs' }
|
|
226
|
+
],
|
|
227
|
+
fontSizes: [
|
|
228
|
+
{ value: '2rem', suggestedName: '$font-size-lg' }
|
|
229
|
+
],
|
|
230
|
+
fontWeights: [],
|
|
231
|
+
fontFamilies: [],
|
|
232
|
+
borderRadius: [],
|
|
233
|
+
shadows: [],
|
|
234
|
+
zIndex: [],
|
|
235
|
+
sizing: [],
|
|
236
|
+
lineHeight: [],
|
|
237
|
+
opacity: [
|
|
238
|
+
{ value: '0.5', suggestedName: '$opacity-half' }
|
|
239
|
+
],
|
|
240
|
+
transitions: []
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const result = refactorFile(scss, analysis, '/libs/styles/_variables.scss', '/app/test.scss', {});
|
|
244
|
+
|
|
245
|
+
// Should replace complete unitless value
|
|
246
|
+
expect(result.content).toContain('opacity: $opacity-half');
|
|
247
|
+
|
|
248
|
+
// Should replace complete values with units
|
|
249
|
+
expect(result.content).toContain('padding: $spacing-xs');
|
|
250
|
+
expect(result.content).toContain('font-size: $font-size-lg');
|
|
251
|
+
|
|
252
|
+
// Should NOT replace unitless value before units (0.5 before em, 2 before rem/px)
|
|
253
|
+
expect(result.content).toContain('line-height: 0.5em');
|
|
254
|
+
expect(result.content).not.toContain('$opacity-halfem');
|
|
255
|
+
expect(result.content).not.toContain('line-height: $opacity-half');
|
|
256
|
+
});
|
|
170
257
|
});
|