scss-variable-extractor 1.6.4 → 1.6.6

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.
@@ -0,0 +1,432 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Generates SCSS theme structure for Angular Material applications
6
+ * Supports dark and light themes with proper variable organization
7
+ * @param {Object} options - Configuration options
8
+ * @returns {Object} - Theme files content and recommendations
9
+ */
10
+ function generateThemeStructure(options = {}) {
11
+ const {
12
+ outputDir = './src/styles',
13
+ includeComponents = true,
14
+ themePalettes = {
15
+ primary: { light: '#1976d2', dark: '#90caf9' },
16
+ accent: { light: '#ff4081', dark: '#ff4081' },
17
+ warn: { light: '#f44336', dark: '#f44336' },
18
+ },
19
+ } = options;
20
+
21
+ const themeFiles = {
22
+ base: generateBaseTheme(),
23
+ lightTheme: generateLightTheme(themePalettes),
24
+ darkTheme: generateDarkTheme(themePalettes),
25
+ themeLoader: generateThemeLoader(),
26
+ componentThemes: includeComponents ? generateComponentThemeTemplate() : null,
27
+ cssVariables: generateCSSVariables(themePalettes),
28
+ };
29
+
30
+ return {
31
+ files: themeFiles,
32
+ recommendations: getThemeRecommendations(),
33
+ structure: getRecommendedStructure(),
34
+ };
35
+ }
36
+
37
+ /**
38
+ * Generates base theme file with shared variables
39
+ */
40
+ function generateBaseTheme() {
41
+ return `// Base theme configuration
42
+ // Shared variables used across all themes
43
+
44
+ // Spacing scale (t-shirt sizing)
45
+ $spacing-xs: 4px;
46
+ $spacing-sm: 8px;
47
+ $spacing-md: 16px;
48
+ $spacing-lg: 24px;
49
+ $spacing-xl: 32px;
50
+ $spacing-xxl: 48px;
51
+
52
+ // Typography scale
53
+ $font-size-xs: 12px;
54
+ $font-size-sm: 14px;
55
+ $font-size-md: 16px;
56
+ $font-size-lg: 18px;
57
+ $font-size-xl: 24px;
58
+ $font-size-xxl: 32px;
59
+
60
+ // Font weights
61
+ $font-weight-light: 300;
62
+ $font-weight-regular: 400;
63
+ $font-weight-medium: 500;
64
+ $font-weight-semibold: 600;
65
+ $font-weight-bold: 700;
66
+
67
+ // Border radius
68
+ $border-radius-sm: 4px;
69
+ $border-radius-md: 8px;
70
+ $border-radius-lg: 16px;
71
+ $border-radius-round: 50%;
72
+
73
+ // Z-index layers
74
+ $z-index-dropdown: 1000;
75
+ $z-index-sticky: 1020;
76
+ $z-index-fixed: 1030;
77
+ $z-index-modal-backdrop: 1040;
78
+ $z-index-modal: 1050;
79
+ $z-index-popover: 1060;
80
+ $z-index-tooltip: 1070;
81
+
82
+ // Transitions
83
+ $transition-fast: 150ms ease-in-out;
84
+ $transition-normal: 300ms ease-in-out;
85
+ $transition-slow: 500ms ease-in-out;
86
+
87
+ // Box shadows
88
+ $shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
89
+ $shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
90
+ $shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
91
+ $shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
92
+ `;
93
+ }
94
+
95
+ /**
96
+ * Generates light theme variables
97
+ */
98
+ function generateLightTheme(palettes) {
99
+ return `// Light theme colors
100
+ // These colors are optimized for light backgrounds
101
+
102
+ // Primary colors
103
+ $primary-light: ${palettes.primary.light};
104
+ $primary-light-contrast: #ffffff;
105
+
106
+ // Accent colors
107
+ $accent-light: ${palettes.accent.light};
108
+ $accent-light-contrast: #ffffff;
109
+
110
+ // Warn colors
111
+ $warn-light: ${palettes.warn.light};
112
+ $warn-light-contrast: #ffffff;
113
+
114
+ // Background colors
115
+ $background-light: #fafafa;
116
+ $surface-light: #ffffff;
117
+ $card-light: #ffffff;
118
+
119
+ // Text colors
120
+ $text-primary-light: rgba(0, 0, 0, 0.87);
121
+ $text-secondary-light: rgba(0, 0, 0, 0.60);
122
+ $text-disabled-light: rgba(0, 0, 0, 0.38);
123
+ $text-hint-light: rgba(0, 0, 0, 0.38);
124
+
125
+ // Divider colors
126
+ $divider-light: rgba(0, 0, 0, 0.12);
127
+ $border-light: rgba(0, 0, 0, 0.12);
128
+
129
+ // State colors
130
+ $hover-light: rgba(0, 0, 0, 0.04);
131
+ $selected-light: rgba(0, 0, 0, 0.08);
132
+ $focused-light: rgba(0, 0, 0, 0.12);
133
+ `;
134
+ }
135
+
136
+ /**
137
+ * Generates dark theme variables
138
+ */
139
+ function generateDarkTheme(palettes) {
140
+ return `// Dark theme colors
141
+ // These colors are optimized for dark backgrounds
142
+
143
+ // Primary colors
144
+ $primary-dark: ${palettes.primary.dark};
145
+ $primary-dark-contrast: rgba(0, 0, 0, 0.87);
146
+
147
+ // Accent colors
148
+ $accent-dark: ${palettes.accent.dark};
149
+ $accent-dark-contrast: rgba(0, 0, 0, 0.87);
150
+
151
+ // Warn colors
152
+ $warn-dark: ${palettes.warn.dark};
153
+ $warn-dark-contrast: #ffffff;
154
+
155
+ // Background colors
156
+ $background-dark: #303030;
157
+ $surface-dark: #424242;
158
+ $card-dark: #424242;
159
+
160
+ // Text colors
161
+ $text-primary-dark: rgba(255, 255, 255, 0.87);
162
+ $text-secondary-dark: rgba(255, 255, 255, 0.70);
163
+ $text-disabled-dark: rgba(255, 255, 255, 0.50);
164
+ $text-hint-dark: rgba(255, 255, 255, 0.50);
165
+
166
+ // Divider colors
167
+ $divider-dark: rgba(255, 255, 255, 0.12);
168
+ $border-dark: rgba(255, 255, 255, 0.12);
169
+
170
+ // State colors
171
+ $hover-dark: rgba(255, 255, 255, 0.04);
172
+ $selected-dark: rgba(255, 255, 255, 0.08);
173
+ $focused-dark: rgba(255, 255, 255, 0.12);
174
+ `;
175
+ }
176
+
177
+ /**
178
+ * Generates theme loader that switches between light and dark
179
+ */
180
+ function generateThemeLoader() {
181
+ return `@use '@angular/material' as mat;
182
+ @use './theme-base' as base;
183
+ @use './theme-light' as light;
184
+ @use './theme-dark' as dark;
185
+
186
+ // Include the common styles for Angular Material
187
+ @include mat.core();
188
+
189
+ // Define the light theme
190
+ $light-theme: mat.define-light-theme((
191
+ color: (
192
+ primary: mat.define-palette(mat.$indigo-palette),
193
+ accent: mat.define-palette(mat.$pink-palette),
194
+ warn: mat.define-palette(mat.$red-palette),
195
+ ),
196
+ typography: mat.define-typography-config(),
197
+ density: 0,
198
+ ));
199
+
200
+ // Define the dark theme
201
+ $dark-theme: mat.define-dark-theme((
202
+ color: (
203
+ primary: mat.define-palette(mat.$blue-palette),
204
+ accent: mat.define-palette(mat.$pink-palette),
205
+ warn: mat.define-palette(mat.$red-palette),
206
+ ),
207
+ typography: mat.define-typography-config(),
208
+ density: 0,
209
+ ));
210
+
211
+ // Apply the light theme by default
212
+ @include mat.all-component-themes($light-theme);
213
+
214
+ // Apply the dark theme when the 'dark-theme' class is present
215
+ .dark-theme {
216
+ @include mat.all-component-colors($dark-theme);
217
+
218
+ // Custom dark theme variables
219
+ --background-color: #{dark.$background-dark};
220
+ --surface-color: #{dark.$surface-dark};
221
+ --text-primary: #{dark.$text-primary-dark};
222
+ --text-secondary: #{dark.$text-secondary-dark};
223
+ --divider-color: #{dark.$divider-dark};
224
+ }
225
+
226
+ // Light theme custom variables (default)
227
+ :root {
228
+ --background-color: #{light.$background-light};
229
+ --surface-color: #{light.$surface-light};
230
+ --text-primary: #{light.$text-primary-light};
231
+ --text-secondary: #{light.$text-secondary-light};
232
+ --divider-color: #{light.$divider-light};
233
+ }
234
+ `;
235
+ }
236
+
237
+ /**
238
+ * Generates component theme template
239
+ */
240
+ function generateComponentThemeTemplate() {
241
+ return `// Component-specific theme mixin
242
+ // Use this pattern for components that need theme-aware styles
243
+
244
+ @mixin component-theme($theme) {
245
+ $color-config: mat.get-color-config($theme);
246
+
247
+ @if $color-config != null {
248
+ $primary: map.get($color-config, 'primary');
249
+ $accent: map.get($color-config, 'accent');
250
+ $warn: map.get($color-config, 'warn');
251
+ $foreground: map.get($color-config, 'foreground');
252
+ $background: map.get($color-config, 'background');
253
+
254
+ .my-component {
255
+ background-color: mat.get-color-from-palette($background, 'card');
256
+ color: mat.get-color-from-palette($foreground, 'text');
257
+
258
+ &__header {
259
+ background-color: mat.get-color-from-palette($primary);
260
+ color: mat.get-color-from-palette($primary, 'default-contrast');
261
+ }
262
+
263
+ &__accent {
264
+ color: mat.get-color-from-palette($accent);
265
+ }
266
+ }
267
+ }
268
+ }
269
+
270
+ // Include this mixin in your theme files:
271
+ // @include component-theme($light-theme);
272
+ // .dark-theme { @include component-theme($dark-theme); }
273
+ `;
274
+ }
275
+
276
+ /**
277
+ * Generates CSS custom properties for runtime theme switching
278
+ */
279
+ function generateCSSVariables(palettes) {
280
+ return `// CSS Custom Properties for runtime theme switching
281
+ // These can be changed dynamically with JavaScript
282
+
283
+ :root {
284
+ // Colors
285
+ --color-primary: ${palettes.primary.light};
286
+ --color-accent: ${palettes.accent.light};
287
+ --color-warn: ${palettes.warn.light};
288
+
289
+ // Usage in components:
290
+ // color: var(--color-primary);
291
+ // background-color: var(--color-accent);
292
+ }
293
+
294
+ .dark-theme {
295
+ --color-primary: ${palettes.primary.dark};
296
+ --color-accent: ${palettes.accent.dark};
297
+ --color-warn: ${palettes.warn.dark};
298
+ }
299
+ `;
300
+ }
301
+
302
+ /**
303
+ * Provides recommendations for theme organization
304
+ */
305
+ function getThemeRecommendations() {
306
+ return [
307
+ {
308
+ category: 'Structure',
309
+ recommendation:
310
+ 'Organize themes in src/styles/ directory with separate files for base, light, and dark themes',
311
+ priority: 'high',
312
+ },
313
+ {
314
+ category: 'Variables',
315
+ recommendation:
316
+ 'Use SCSS variables for build-time values and CSS custom properties for runtime theme switching',
317
+ priority: 'high',
318
+ },
319
+ {
320
+ category: 'Component Styles',
321
+ recommendation:
322
+ 'Create theme mixins for components that need theme-aware styles instead of using ::ng-deep',
323
+ priority: 'high',
324
+ },
325
+ {
326
+ category: 'Global Styles',
327
+ recommendation:
328
+ 'Move cross-cutting styles that affect multiple components to global theme files',
329
+ priority: 'medium',
330
+ },
331
+ {
332
+ category: 'Encapsulation',
333
+ recommendation:
334
+ 'Avoid ViewEncapsulation.None unless absolutely necessary; use theme mixins instead',
335
+ priority: 'medium',
336
+ },
337
+ {
338
+ category: 'Performance',
339
+ recommendation:
340
+ 'Use CSS custom properties sparingly; SCSS variables are optimized at build time',
341
+ priority: 'low',
342
+ },
343
+ ];
344
+ }
345
+
346
+ /**
347
+ * Returns recommended directory structure
348
+ */
349
+ function getRecommendedStructure() {
350
+ return {
351
+ 'src/styles/': {
352
+ '_theme-base.scss': 'Shared variables (spacing, typography, shadows, etc.)',
353
+ '_theme-light.scss': 'Light theme color variables',
354
+ '_theme-dark.scss': 'Dark theme color variables',
355
+ 'themes.scss': 'Theme loader and Material theme configuration',
356
+ '_variables.scss': 'Extracted hardcoded values (generated)',
357
+ 'global-overrides.scss': 'Global styles extracted from ::ng-deep usage',
358
+ 'utilities.scss': 'Utility classes for common patterns',
359
+ },
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Analyzes existing styles and suggests theme organization
365
+ */
366
+ function analyzeThemeReadiness(scssFiles) {
367
+ const analysis = {
368
+ hardcodedColors: [],
369
+ themeableComponents: [],
370
+ colorUsage: new Map(),
371
+ recommendations: [],
372
+ };
373
+
374
+ scssFiles.forEach(filePath => {
375
+ const content = fs.readFileSync(filePath, 'utf8');
376
+
377
+ // Find hardcoded colors
378
+ const colorMatches = content.matchAll(/#[0-9a-fA-F]{3,6}\b|rgba?\([^)]+\)/g);
379
+ for (const match of colorMatches) {
380
+ const color = match[0];
381
+ const count = analysis.colorUsage.get(color) || 0;
382
+ analysis.colorUsage.set(color, count + 1);
383
+
384
+ analysis.hardcodedColors.push({
385
+ file: filePath,
386
+ color,
387
+ line: getLineNumber(content, match.index),
388
+ });
389
+ }
390
+
391
+ // Find components that might benefit from theme mixins
392
+ if (content.includes('::ng-deep') || content.includes('!important')) {
393
+ analysis.themeableComponents.push(filePath);
394
+ }
395
+ });
396
+
397
+ // Generate recommendations
398
+ analysis.colorUsage.forEach((count, color) => {
399
+ if (count > 3) {
400
+ analysis.recommendations.push({
401
+ type: 'extract-color',
402
+ message: `Color ${color} used ${count} times - consider extracting to theme variable`,
403
+ priority: 'high',
404
+ });
405
+ }
406
+ });
407
+
408
+ if (analysis.themeableComponents.length > 0) {
409
+ analysis.recommendations.push({
410
+ type: 'create-theme-mixins',
411
+ message: `${analysis.themeableComponents.length} component(s) could benefit from theme mixins`,
412
+ priority: 'medium',
413
+ files: analysis.themeableComponents,
414
+ });
415
+ }
416
+
417
+ return analysis;
418
+ }
419
+
420
+ /**
421
+ * Helper function to get line number from content and index
422
+ */
423
+ function getLineNumber(content, index) {
424
+ return content.substring(0, index).split('\n').length;
425
+ }
426
+
427
+ module.exports = {
428
+ generateThemeStructure,
429
+ analyzeThemeReadiness,
430
+ getThemeRecommendations,
431
+ getRecommendedStructure,
432
+ };
@@ -1,107 +1,107 @@
1
- const { analyzeValues, generateVariableName } = require('../src/analyzer');
2
- const { DEFAULT_CONFIG } = require('../src/config');
3
-
4
- describe('Analyzer', () => {
5
- test('should count repeated values', () => {
6
- const extracted = {
7
- colors: [
8
- { value: '#1976d2', file: 'a.scss', line: 1 },
9
- { value: '#1976d2', file: 'b.scss', line: 1 },
10
- { value: '#fff', file: 'a.scss', line: 2 }
11
- ],
12
- spacing: [],
13
- fontSizes: [],
14
- fontWeights: [],
15
- fontFamilies: [],
16
- borderRadius: [],
17
- shadows: [],
18
- zIndex: [],
19
- sizing: [],
20
- lineHeight: [],
21
- opacity: [],
22
- transitions: []
23
- };
24
-
25
- const analysis = analyzeValues(extracted, DEFAULT_CONFIG);
26
-
27
- expect(analysis.colors.length).toBeGreaterThan(0);
28
- const primaryColor = analysis.colors.find(c => c.value === '#1976d2');
29
- expect(primaryColor.count).toBe(2);
30
- });
31
-
32
- test('should filter by threshold', () => {
33
- const extracted = {
34
- colors: [
35
- { value: '#1976d2', file: 'a.scss', line: 1 },
36
- { value: '#fff', file: 'a.scss', line: 2 }
37
- ],
38
- spacing: [],
39
- fontSizes: [],
40
- fontWeights: [],
41
- fontFamilies: [],
42
- borderRadius: [],
43
- shadows: [],
44
- zIndex: [],
45
- sizing: [],
46
- lineHeight: [],
47
- opacity: [],
48
- transitions: []
49
- };
50
-
51
- const config = { ...DEFAULT_CONFIG, threshold: 2 };
52
- const analysis = analyzeValues(extracted, config);
53
-
54
- // No colors should pass threshold of 2
55
- expect(analysis.colors.length).toBe(0);
56
- });
57
-
58
- test('should generate spacing variable names with t-shirt sizing', () => {
59
- const name = generateVariableName('spacing', '16px', [], DEFAULT_CONFIG);
60
-
61
- expect(name).toBe('$spacing-md');
62
- });
63
-
64
- test('should generate color variable names', () => {
65
- const name = generateVariableName('colors', '#1976d2', []);
66
-
67
- expect(name).toContain('$color-');
68
- });
69
-
70
- test('should generate font-size variable names', () => {
71
- const name = generateVariableName('fontSizes', '14px', []);
72
-
73
- expect(name).toBe('$font-size-sm');
74
- });
75
-
76
- test('should generate font-weight variable names', () => {
77
- const name = generateVariableName('fontWeights', '500', []);
78
-
79
- expect(name).toBe('$font-weight-medium');
80
- });
81
-
82
- test('should count unique files', () => {
83
- const extracted = {
84
- colors: [
85
- { value: '#1976d2', file: 'a.scss', line: 1 },
86
- { value: '#1976d2', file: 'a.scss', line: 5 },
87
- { value: '#1976d2', file: 'b.scss', line: 1 }
88
- ],
89
- spacing: [],
90
- fontSizes: [],
91
- fontWeights: [],
92
- fontFamilies: [],
93
- borderRadius: [],
94
- shadows: [],
95
- zIndex: [],
96
- sizing: [],
97
- lineHeight: [],
98
- opacity: [],
99
- transitions: []
100
- };
101
-
102
- const analysis = analyzeValues(extracted, DEFAULT_CONFIG);
103
-
104
- const primaryColor = analysis.colors.find(c => c.value === '#1976d2');
105
- expect(primaryColor.fileCount).toBe(2);
106
- });
107
- });
1
+ const { analyzeValues, generateVariableName } = require('../src/analyzer');
2
+ const { DEFAULT_CONFIG } = require('../src/config');
3
+
4
+ describe('Analyzer', () => {
5
+ test('should count repeated values', () => {
6
+ const extracted = {
7
+ colors: [
8
+ { value: '#1976d2', file: 'a.scss', line: 1 },
9
+ { value: '#1976d2', file: 'b.scss', line: 1 },
10
+ { value: '#fff', file: 'a.scss', line: 2 },
11
+ ],
12
+ spacing: [],
13
+ fontSizes: [],
14
+ fontWeights: [],
15
+ fontFamilies: [],
16
+ borderRadius: [],
17
+ shadows: [],
18
+ zIndex: [],
19
+ sizing: [],
20
+ lineHeight: [],
21
+ opacity: [],
22
+ transitions: [],
23
+ };
24
+
25
+ const analysis = analyzeValues(extracted, DEFAULT_CONFIG);
26
+
27
+ expect(analysis.colors.length).toBeGreaterThan(0);
28
+ const primaryColor = analysis.colors.find(c => c.value === '#1976d2');
29
+ expect(primaryColor.count).toBe(2);
30
+ });
31
+
32
+ test('should filter by threshold', () => {
33
+ const extracted = {
34
+ colors: [
35
+ { value: '#1976d2', file: 'a.scss', line: 1 },
36
+ { value: '#fff', file: 'a.scss', line: 2 },
37
+ ],
38
+ spacing: [],
39
+ fontSizes: [],
40
+ fontWeights: [],
41
+ fontFamilies: [],
42
+ borderRadius: [],
43
+ shadows: [],
44
+ zIndex: [],
45
+ sizing: [],
46
+ lineHeight: [],
47
+ opacity: [],
48
+ transitions: [],
49
+ };
50
+
51
+ const config = { ...DEFAULT_CONFIG, threshold: 2 };
52
+ const analysis = analyzeValues(extracted, config);
53
+
54
+ // No colors should pass threshold of 2
55
+ expect(analysis.colors.length).toBe(0);
56
+ });
57
+
58
+ test('should generate spacing variable names with t-shirt sizing', () => {
59
+ const name = generateVariableName('spacing', '16px', [], DEFAULT_CONFIG);
60
+
61
+ expect(name).toBe('$spacing-md');
62
+ });
63
+
64
+ test('should generate color variable names', () => {
65
+ const name = generateVariableName('colors', '#1976d2', []);
66
+
67
+ expect(name).toContain('$color-');
68
+ });
69
+
70
+ test('should generate font-size variable names', () => {
71
+ const name = generateVariableName('fontSizes', '14px', []);
72
+
73
+ expect(name).toBe('$font-size-sm');
74
+ });
75
+
76
+ test('should generate font-weight variable names', () => {
77
+ const name = generateVariableName('fontWeights', '500', []);
78
+
79
+ expect(name).toBe('$font-weight-medium');
80
+ });
81
+
82
+ test('should count unique files', () => {
83
+ const extracted = {
84
+ colors: [
85
+ { value: '#1976d2', file: 'a.scss', line: 1 },
86
+ { value: '#1976d2', file: 'a.scss', line: 5 },
87
+ { value: '#1976d2', file: 'b.scss', line: 1 },
88
+ ],
89
+ spacing: [],
90
+ fontSizes: [],
91
+ fontWeights: [],
92
+ fontFamilies: [],
93
+ borderRadius: [],
94
+ shadows: [],
95
+ zIndex: [],
96
+ sizing: [],
97
+ lineHeight: [],
98
+ opacity: [],
99
+ transitions: [],
100
+ };
101
+
102
+ const analysis = analyzeValues(extracted, DEFAULT_CONFIG);
103
+
104
+ const primaryColor = analysis.colors.find(c => c.value === '#1976d2');
105
+ expect(primaryColor.fileCount).toBe(2);
106
+ });
107
+ });