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/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "scss-variable-extractor",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Analyzes Angular SCSS files and extracts repeated hardcoded values into reusable variables",
|
|
5
|
+
"bin": {
|
|
6
|
+
"scss-extract": "./bin/cli.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "jest",
|
|
11
|
+
"analyze": "node bin/cli.js analyze",
|
|
12
|
+
"generate": "node bin/cli.js generate",
|
|
13
|
+
"refactor": "node bin/cli.js refactor"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"angular",
|
|
17
|
+
"scss",
|
|
18
|
+
"sass",
|
|
19
|
+
"css",
|
|
20
|
+
"variables",
|
|
21
|
+
"refactoring",
|
|
22
|
+
"cli",
|
|
23
|
+
"angular-material"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=14.0.0"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"commander": "^9.0.0",
|
|
31
|
+
"glob": "^8.0.0",
|
|
32
|
+
"chalk": "^4.1.2",
|
|
33
|
+
"cli-table3": "^0.6.3"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"jest": "^29.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/analyzer.js
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Analyzes extracted values and identifies repeated ones
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Analyzes all extracted values from multiple files
|
|
7
|
+
* @param {Array<Object>} allExtracted - Array of extracted values from all files
|
|
8
|
+
* @param {Object} config - Configuration object
|
|
9
|
+
* @returns {Object} - Analysis results with repeated values
|
|
10
|
+
*/
|
|
11
|
+
function analyzeValues(allExtracted, config) {
|
|
12
|
+
const analysis = {
|
|
13
|
+
colors: {},
|
|
14
|
+
spacing: {},
|
|
15
|
+
fontSizes: {},
|
|
16
|
+
fontWeights: {},
|
|
17
|
+
fontFamilies: {},
|
|
18
|
+
borderRadius: {},
|
|
19
|
+
shadows: {},
|
|
20
|
+
zIndex: {},
|
|
21
|
+
sizing: {},
|
|
22
|
+
lineHeight: {},
|
|
23
|
+
opacity: {},
|
|
24
|
+
transitions: {}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Count occurrences of each value
|
|
28
|
+
Object.keys(allExtracted).forEach(category => {
|
|
29
|
+
if (!config.categories[category]) return;
|
|
30
|
+
|
|
31
|
+
allExtracted[category].forEach(item => {
|
|
32
|
+
const normalizedValue = normalizeValue(item.value, category);
|
|
33
|
+
|
|
34
|
+
if (!analysis[category][normalizedValue]) {
|
|
35
|
+
analysis[category][normalizedValue] = {
|
|
36
|
+
value: item.value,
|
|
37
|
+
count: 0,
|
|
38
|
+
files: new Set(),
|
|
39
|
+
occurrences: []
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
analysis[category][normalizedValue].count++;
|
|
44
|
+
analysis[category][normalizedValue].files.add(item.file);
|
|
45
|
+
analysis[category][normalizedValue].occurrences.push({
|
|
46
|
+
file: item.file,
|
|
47
|
+
line: item.line,
|
|
48
|
+
context: item.context,
|
|
49
|
+
property: item.property
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Filter by threshold and generate variable names
|
|
55
|
+
const results = {};
|
|
56
|
+
|
|
57
|
+
Object.keys(analysis).forEach(category => {
|
|
58
|
+
results[category] = [];
|
|
59
|
+
|
|
60
|
+
Object.keys(analysis[category]).forEach(normalizedValue => {
|
|
61
|
+
const data = analysis[category][normalizedValue];
|
|
62
|
+
|
|
63
|
+
if (data.count >= config.threshold) {
|
|
64
|
+
results[category].push({
|
|
65
|
+
value: data.value,
|
|
66
|
+
normalizedValue,
|
|
67
|
+
count: data.count,
|
|
68
|
+
fileCount: data.files.size,
|
|
69
|
+
files: Array.from(data.files),
|
|
70
|
+
occurrences: data.occurrences,
|
|
71
|
+
suggestedName: generateVariableName(category, data.value, data.occurrences, config)
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Sort by count (descending)
|
|
77
|
+
results[category].sort((a, b) => b.count - a.count);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return results;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Normalizes value for comparison
|
|
85
|
+
*/
|
|
86
|
+
function normalizeValue(value, category) {
|
|
87
|
+
if (category === 'colors') {
|
|
88
|
+
return normalizeColor(value);
|
|
89
|
+
}
|
|
90
|
+
return value.toLowerCase().trim();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Normalizes color values for comparison
|
|
95
|
+
*/
|
|
96
|
+
function normalizeColor(color) {
|
|
97
|
+
color = color.toLowerCase().trim();
|
|
98
|
+
|
|
99
|
+
// Convert 3-digit hex to 6-digit
|
|
100
|
+
if (/^#[0-9a-f]{3}$/i.test(color)) {
|
|
101
|
+
color = '#' + color[1] + color[1] + color[2] + color[2] + color[3] + color[3];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Normalize rgb/rgba spacing
|
|
105
|
+
color = color.replace(/\s+/g, '');
|
|
106
|
+
|
|
107
|
+
return color;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Generates a variable name based on category and value
|
|
112
|
+
*/
|
|
113
|
+
function generateVariableName(category, value, occurrences, config) {
|
|
114
|
+
const prefix = getCategoryPrefix(category);
|
|
115
|
+
|
|
116
|
+
if (category === 'spacing') {
|
|
117
|
+
return generateSpacingVariableName(value, config);
|
|
118
|
+
} else if (category === 'colors') {
|
|
119
|
+
return generateColorVariableName(value, occurrences);
|
|
120
|
+
} else if (category === 'fontSizes') {
|
|
121
|
+
return generateFontSizeVariableName(value);
|
|
122
|
+
} else if (category === 'fontWeights') {
|
|
123
|
+
return generateFontWeightVariableName(value);
|
|
124
|
+
} else if (category === 'fontFamilies') {
|
|
125
|
+
return prefix + sanitizeName(value);
|
|
126
|
+
} else if (category === 'borderRadius') {
|
|
127
|
+
return generateBorderRadiusVariableName(value);
|
|
128
|
+
} else if (category === 'shadows') {
|
|
129
|
+
return prefix + 'default';
|
|
130
|
+
} else if (category === 'zIndex') {
|
|
131
|
+
return prefix + value;
|
|
132
|
+
} else if (category === 'sizing') {
|
|
133
|
+
return prefix + value.replace('px', '');
|
|
134
|
+
} else if (category === 'lineHeight') {
|
|
135
|
+
return prefix + sanitizeName(value);
|
|
136
|
+
} else if (category === 'opacity') {
|
|
137
|
+
return prefix + sanitizeName(value);
|
|
138
|
+
} else if (category === 'transitions') {
|
|
139
|
+
return prefix + 'default';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return prefix + 'value';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Gets prefix for a category
|
|
147
|
+
*/
|
|
148
|
+
function getCategoryPrefix(category) {
|
|
149
|
+
const prefixes = {
|
|
150
|
+
colors: '$color-',
|
|
151
|
+
spacing: '$spacing-',
|
|
152
|
+
fontSizes: '$font-size-',
|
|
153
|
+
fontWeights: '$font-weight-',
|
|
154
|
+
fontFamilies: '$font-family-',
|
|
155
|
+
borderRadius: '$border-radius-',
|
|
156
|
+
shadows: '$shadow-',
|
|
157
|
+
zIndex: '$z-index-',
|
|
158
|
+
sizing: '$size-',
|
|
159
|
+
lineHeight: '$line-height-',
|
|
160
|
+
opacity: '$opacity-',
|
|
161
|
+
transitions: '$transition-'
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
return prefixes[category] || '$';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generates spacing variable name using t-shirt sizing
|
|
169
|
+
*/
|
|
170
|
+
function generateSpacingVariableName(value, config) {
|
|
171
|
+
const spacingScale = config.spacingScale;
|
|
172
|
+
|
|
173
|
+
// Try to match to spacing scale
|
|
174
|
+
for (const [size, scaleValue] of Object.entries(spacingScale)) {
|
|
175
|
+
if (scaleValue === value) {
|
|
176
|
+
return `$spacing-${size}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Fallback to pixel value
|
|
181
|
+
return `$spacing-${value.replace('px', '')}`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Generates color variable name
|
|
186
|
+
*/
|
|
187
|
+
function generateColorVariableName(value, occurrences) {
|
|
188
|
+
// Try to detect common color patterns
|
|
189
|
+
const normalizedColor = normalizeColor(value);
|
|
190
|
+
|
|
191
|
+
// Check for common Material colors
|
|
192
|
+
if (normalizedColor === '#1976d2' || normalizedColor === 'rgb(25,118,210)') {
|
|
193
|
+
return '$color-brand-primary';
|
|
194
|
+
}
|
|
195
|
+
if (normalizedColor === 'rgba(0,0,0,0.87)') {
|
|
196
|
+
return '$color-text-primary';
|
|
197
|
+
}
|
|
198
|
+
if (normalizedColor === 'rgba(0,0,0,0.54)') {
|
|
199
|
+
return '$color-text-secondary';
|
|
200
|
+
}
|
|
201
|
+
if (normalizedColor === '#ffffff' || normalizedColor === 'white') {
|
|
202
|
+
return '$color-white';
|
|
203
|
+
}
|
|
204
|
+
if (normalizedColor === '#000000' || normalizedColor === 'black') {
|
|
205
|
+
return '$color-black';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Try to derive from context
|
|
209
|
+
if (occurrences && occurrences.length > 0) {
|
|
210
|
+
const context = occurrences[0].context || '';
|
|
211
|
+
if (context.includes('background')) return '$color-background';
|
|
212
|
+
if (context.includes('border')) return '$color-border';
|
|
213
|
+
if (context.includes('text') || context.includes('color:')) return '$color-text';
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Fallback to hex value
|
|
217
|
+
return '$color-' + sanitizeName(value);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Generates font size variable name
|
|
222
|
+
*/
|
|
223
|
+
function generateFontSizeVariableName(value) {
|
|
224
|
+
const sizeMap = {
|
|
225
|
+
'12px': 'xs',
|
|
226
|
+
'14px': 'sm',
|
|
227
|
+
'16px': 'md',
|
|
228
|
+
'18px': 'lg',
|
|
229
|
+
'20px': 'xl',
|
|
230
|
+
'24px': 'xxl'
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
return `$font-size-${sizeMap[value] || value.replace('px', '')}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Generates font weight variable name
|
|
238
|
+
*/
|
|
239
|
+
function generateFontWeightVariableName(value) {
|
|
240
|
+
const weightMap = {
|
|
241
|
+
'300': 'light',
|
|
242
|
+
'400': 'normal',
|
|
243
|
+
'500': 'medium',
|
|
244
|
+
'600': 'semibold',
|
|
245
|
+
'700': 'bold',
|
|
246
|
+
'bold': 'bold',
|
|
247
|
+
'normal': 'normal'
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
return `$font-weight-${weightMap[value] || value}`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Generates border radius variable name
|
|
255
|
+
*/
|
|
256
|
+
function generateBorderRadiusVariableName(value) {
|
|
257
|
+
if (value === '50%') return '$border-radius-circle';
|
|
258
|
+
|
|
259
|
+
const radiusMap = {
|
|
260
|
+
'2px': 'xs',
|
|
261
|
+
'4px': 'sm',
|
|
262
|
+
'8px': 'md',
|
|
263
|
+
'12px': 'lg',
|
|
264
|
+
'16px': 'xl'
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return `$border-radius-${radiusMap[value] || value.replace(/px|%/, '')}`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Sanitizes a value to be a valid variable name
|
|
272
|
+
*/
|
|
273
|
+
function sanitizeName(value) {
|
|
274
|
+
return value
|
|
275
|
+
.replace(/[^a-zA-Z0-9-]/g, '-')
|
|
276
|
+
.replace(/-+/g, '-')
|
|
277
|
+
.replace(/^-|-$/g, '')
|
|
278
|
+
.toLowerCase();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
module.exports = {
|
|
282
|
+
analyzeValues,
|
|
283
|
+
normalizeValue,
|
|
284
|
+
generateVariableName
|
|
285
|
+
};
|
package/src/config.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const DEFAULT_CONFIG = {
|
|
5
|
+
src: './apps/subapp/src',
|
|
6
|
+
output: './libs/styles/_variables.scss',
|
|
7
|
+
threshold: 2,
|
|
8
|
+
categories: {
|
|
9
|
+
colors: true,
|
|
10
|
+
spacing: true,
|
|
11
|
+
fontSizes: true,
|
|
12
|
+
fontWeights: true,
|
|
13
|
+
fontFamilies: true,
|
|
14
|
+
borderRadius: true,
|
|
15
|
+
shadows: true,
|
|
16
|
+
zIndex: true,
|
|
17
|
+
sizing: true,
|
|
18
|
+
lineHeight: true,
|
|
19
|
+
opacity: true,
|
|
20
|
+
transitions: true
|
|
21
|
+
},
|
|
22
|
+
spacingScale: {
|
|
23
|
+
xxs: '2px',
|
|
24
|
+
xs: '4px',
|
|
25
|
+
sm: '8px',
|
|
26
|
+
md: '16px',
|
|
27
|
+
lg: '24px',
|
|
28
|
+
xl: '32px',
|
|
29
|
+
xxl: '48px'
|
|
30
|
+
},
|
|
31
|
+
ignore: [
|
|
32
|
+
'**/node_modules/**',
|
|
33
|
+
'**/dist/**',
|
|
34
|
+
'**/_variables.scss'
|
|
35
|
+
],
|
|
36
|
+
importStyle: 'use',
|
|
37
|
+
reportFormat: 'table'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
function loadConfig(configPath = null) {
|
|
41
|
+
let userConfig = {};
|
|
42
|
+
|
|
43
|
+
// Try to find config file
|
|
44
|
+
const possiblePaths = [
|
|
45
|
+
configPath,
|
|
46
|
+
'.scssextractrc.json',
|
|
47
|
+
path.join(process.cwd(), '.scssextractrc.json')
|
|
48
|
+
].filter(Boolean);
|
|
49
|
+
|
|
50
|
+
for (const configFile of possiblePaths) {
|
|
51
|
+
if (fs.existsSync(configFile)) {
|
|
52
|
+
try {
|
|
53
|
+
const content = fs.readFileSync(configFile, 'utf8');
|
|
54
|
+
userConfig = JSON.parse(content);
|
|
55
|
+
console.log(`Loaded config from: ${configFile}`);
|
|
56
|
+
break;
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn(`Warning: Could not parse config file ${configFile}:`, error.message);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Merge with defaults
|
|
64
|
+
return {
|
|
65
|
+
...DEFAULT_CONFIG,
|
|
66
|
+
...userConfig,
|
|
67
|
+
categories: {
|
|
68
|
+
...DEFAULT_CONFIG.categories,
|
|
69
|
+
...(userConfig.categories || {})
|
|
70
|
+
},
|
|
71
|
+
spacingScale: {
|
|
72
|
+
...DEFAULT_CONFIG.spacingScale,
|
|
73
|
+
...(userConfig.spacingScale || {})
|
|
74
|
+
},
|
|
75
|
+
ignore: userConfig.ignore || DEFAULT_CONFIG.ignore
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = {
|
|
80
|
+
DEFAULT_CONFIG,
|
|
81
|
+
loadConfig
|
|
82
|
+
};
|
package/src/generator.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Generates a _variables.scss file from analyzed values
|
|
6
|
+
* @param {Object} analysis - Analysis results
|
|
7
|
+
* @param {string} outputPath - Output file path
|
|
8
|
+
* @param {Object} config - Configuration
|
|
9
|
+
*/
|
|
10
|
+
function generateVariablesFile(analysis, outputPath, config) {
|
|
11
|
+
const version = require('../package.json').version;
|
|
12
|
+
const timestamp = new Date().toISOString();
|
|
13
|
+
|
|
14
|
+
let content = '';
|
|
15
|
+
|
|
16
|
+
// Header
|
|
17
|
+
content += `//\n`;
|
|
18
|
+
content += `// Auto-generated SCSS Variables\n`;
|
|
19
|
+
content += `// Generated by scss-variable-extractor v${version}\n`;
|
|
20
|
+
content += `// Generated at: ${timestamp}\n`;
|
|
21
|
+
content += `//\n`;
|
|
22
|
+
content += `// DO NOT EDIT THIS FILE MANUALLY\n`;
|
|
23
|
+
content += `// This file is auto-generated. Your changes will be overwritten.\n`;
|
|
24
|
+
content += `//\n\n`;
|
|
25
|
+
|
|
26
|
+
// Categories
|
|
27
|
+
const categories = [
|
|
28
|
+
{ key: 'colors', title: 'Colors' },
|
|
29
|
+
{ key: 'spacing', title: 'Spacing' },
|
|
30
|
+
{ key: 'fontSizes', title: 'Font Sizes' },
|
|
31
|
+
{ key: 'fontWeights', title: 'Font Weights' },
|
|
32
|
+
{ key: 'fontFamilies', title: 'Font Families' },
|
|
33
|
+
{ key: 'borderRadius', title: 'Border Radius' },
|
|
34
|
+
{ key: 'shadows', title: 'Shadows' },
|
|
35
|
+
{ key: 'zIndex', title: 'Z-Index' },
|
|
36
|
+
{ key: 'sizing', title: 'Sizing' },
|
|
37
|
+
{ key: 'lineHeight', title: 'Line Heights' },
|
|
38
|
+
{ key: 'opacity', title: 'Opacity' },
|
|
39
|
+
{ key: 'transitions', title: 'Transitions' }
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
categories.forEach(({ key, title }) => {
|
|
43
|
+
if (analysis[key] && analysis[key].length > 0) {
|
|
44
|
+
content += `// ${title}\n`;
|
|
45
|
+
content += `// ────────────────────────────────────────\n`;
|
|
46
|
+
|
|
47
|
+
// Group variables with same name
|
|
48
|
+
const variableGroups = {};
|
|
49
|
+
analysis[key].forEach(item => {
|
|
50
|
+
const baseName = item.suggestedName;
|
|
51
|
+
if (!variableGroups[baseName]) {
|
|
52
|
+
variableGroups[baseName] = [];
|
|
53
|
+
}
|
|
54
|
+
variableGroups[baseName].push(item);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Generate unique variable names
|
|
58
|
+
Object.keys(variableGroups).forEach(baseName => {
|
|
59
|
+
const items = variableGroups[baseName];
|
|
60
|
+
|
|
61
|
+
if (items.length === 1) {
|
|
62
|
+
const item = items[0];
|
|
63
|
+
content += `${item.suggestedName}: ${item.value};\n`;
|
|
64
|
+
} else {
|
|
65
|
+
// Multiple values with same suggested name, add suffixes
|
|
66
|
+
items.forEach((item, index) => {
|
|
67
|
+
const uniqueName = index === 0 ? item.suggestedName : `${item.suggestedName}-${index + 1}`;
|
|
68
|
+
content += `${uniqueName}: ${item.value};\n`;
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
content += '\n';
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Ensure output directory exists
|
|
78
|
+
const outputDir = path.dirname(outputPath);
|
|
79
|
+
if (!fs.existsSync(outputDir)) {
|
|
80
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Write file
|
|
84
|
+
fs.writeFileSync(outputPath, content, 'utf8');
|
|
85
|
+
|
|
86
|
+
return content;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Generates a report in the specified format
|
|
91
|
+
* @param {Object} analysis - Analysis results
|
|
92
|
+
* @param {string} format - Report format (table, json, markdown)
|
|
93
|
+
* @param {Object} config - Configuration
|
|
94
|
+
* @returns {string} - Formatted report
|
|
95
|
+
*/
|
|
96
|
+
function generateReport(analysis, format = 'table', config) {
|
|
97
|
+
if (format === 'json') {
|
|
98
|
+
return generateJsonReport(analysis);
|
|
99
|
+
} else if (format === 'markdown') {
|
|
100
|
+
return generateMarkdownReport(analysis);
|
|
101
|
+
} else {
|
|
102
|
+
return generateTableReport(analysis);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Generates a console table report
|
|
108
|
+
*/
|
|
109
|
+
function generateTableReport(analysis) {
|
|
110
|
+
const Table = require('cli-table3');
|
|
111
|
+
const chalk = require('chalk');
|
|
112
|
+
|
|
113
|
+
let output = '\n';
|
|
114
|
+
output += chalk.bold.cyan('SCSS Variable Extraction Analysis\n');
|
|
115
|
+
output += chalk.gray('═'.repeat(80)) + '\n\n';
|
|
116
|
+
|
|
117
|
+
const categories = [
|
|
118
|
+
{ key: 'colors', title: 'Colors' },
|
|
119
|
+
{ key: 'spacing', title: 'Spacing' },
|
|
120
|
+
{ key: 'fontSizes', title: 'Font Sizes' },
|
|
121
|
+
{ key: 'fontWeights', title: 'Font Weights' },
|
|
122
|
+
{ key: 'fontFamilies', title: 'Font Families' },
|
|
123
|
+
{ key: 'borderRadius', title: 'Border Radius' },
|
|
124
|
+
{ key: 'shadows', title: 'Shadows' },
|
|
125
|
+
{ key: 'zIndex', title: 'Z-Index' },
|
|
126
|
+
{ key: 'sizing', title: 'Sizing' },
|
|
127
|
+
{ key: 'lineHeight', title: 'Line Heights' },
|
|
128
|
+
{ key: 'opacity', title: 'Opacity' },
|
|
129
|
+
{ key: 'transitions', title: 'Transitions' }
|
|
130
|
+
];
|
|
131
|
+
|
|
132
|
+
categories.forEach(({ key, title }) => {
|
|
133
|
+
if (analysis[key] && analysis[key].length > 0) {
|
|
134
|
+
output += chalk.bold.yellow(`${title}\n`);
|
|
135
|
+
|
|
136
|
+
const table = new Table({
|
|
137
|
+
head: [
|
|
138
|
+
chalk.white('Value'),
|
|
139
|
+
chalk.white('Count'),
|
|
140
|
+
chalk.white('Files'),
|
|
141
|
+
chalk.white('Suggested Variable')
|
|
142
|
+
],
|
|
143
|
+
colWidths: [30, 10, 10, 40]
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
analysis[key].forEach(item => {
|
|
147
|
+
table.push([
|
|
148
|
+
item.value,
|
|
149
|
+
chalk.green(item.count),
|
|
150
|
+
chalk.blue(item.fileCount),
|
|
151
|
+
chalk.cyan(item.suggestedName)
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
output += table.toString() + '\n\n';
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Summary
|
|
160
|
+
const totalExtracted = Object.values(analysis).reduce((sum, arr) => sum + arr.length, 0);
|
|
161
|
+
output += chalk.gray('─'.repeat(80)) + '\n';
|
|
162
|
+
output += chalk.bold(`Total extractable values: ${totalExtracted}\n`);
|
|
163
|
+
|
|
164
|
+
return output;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Generates a JSON report
|
|
169
|
+
*/
|
|
170
|
+
function generateJsonReport(analysis) {
|
|
171
|
+
return JSON.stringify(analysis, null, 2);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Generates a Markdown report
|
|
176
|
+
*/
|
|
177
|
+
function generateMarkdownReport(analysis) {
|
|
178
|
+
let output = '# SCSS Variable Extraction Analysis\n\n';
|
|
179
|
+
|
|
180
|
+
const categories = [
|
|
181
|
+
{ key: 'colors', title: 'Colors' },
|
|
182
|
+
{ key: 'spacing', title: 'Spacing' },
|
|
183
|
+
{ key: 'fontSizes', title: 'Font Sizes' },
|
|
184
|
+
{ key: 'fontWeights', title: 'Font Weights' },
|
|
185
|
+
{ key: 'fontFamilies', title: 'Font Families' },
|
|
186
|
+
{ key: 'borderRadius', title: 'Border Radius' },
|
|
187
|
+
{ key: 'shadows', title: 'Shadows' },
|
|
188
|
+
{ key: 'zIndex', title: 'Z-Index' },
|
|
189
|
+
{ key: 'sizing', title: 'Sizing' },
|
|
190
|
+
{ key: 'lineHeight', title: 'Line Heights' },
|
|
191
|
+
{ key: 'opacity', title: 'Opacity' },
|
|
192
|
+
{ key: 'transitions', title: 'Transitions' }
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
categories.forEach(({ key, title }) => {
|
|
196
|
+
if (analysis[key] && analysis[key].length > 0) {
|
|
197
|
+
output += `## ${title}\n\n`;
|
|
198
|
+
output += '| Value | Count | Files | Suggested Variable |\n';
|
|
199
|
+
output += '|-------|-------|-------|-------------------|\n';
|
|
200
|
+
|
|
201
|
+
analysis[key].forEach(item => {
|
|
202
|
+
output += `| ${item.value} | ${item.count} | ${item.fileCount} | \`${item.suggestedName}\` |\n`;
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
output += '\n';
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
// Summary
|
|
210
|
+
const totalExtracted = Object.values(analysis).reduce((sum, arr) => sum + arr.length, 0);
|
|
211
|
+
output += `---\n\n**Total extractable values:** ${totalExtracted}\n`;
|
|
212
|
+
|
|
213
|
+
return output;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
generateVariablesFile,
|
|
218
|
+
generateReport
|
|
219
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { loadConfig } = require('./config');
|
|
2
|
+
const { scanScssFiles } = require('./scanner');
|
|
3
|
+
const { parseScss } = require('./parser');
|
|
4
|
+
const { analyzeValues } = require('./analyzer');
|
|
5
|
+
const { generateVariablesFile, generateReport } = require('./generator');
|
|
6
|
+
const { refactorScssFiles } = require('./refactorer');
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
loadConfig,
|
|
10
|
+
scanScssFiles,
|
|
11
|
+
parseScss,
|
|
12
|
+
analyzeValues,
|
|
13
|
+
generateVariablesFile,
|
|
14
|
+
generateReport,
|
|
15
|
+
refactorScssFiles
|
|
16
|
+
};
|