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.
@@ -1,504 +1,500 @@
1
- const fs = require('fs');
2
- const path = require('path');
3
-
4
- /**
5
- * Analyzes style organization across the project
6
- * @param {Array<string>} scssFiles - Array of SCSS file paths
7
- * @param {Object} config - Configuration options
8
- * @returns {Object} - Organization analysis results
9
- */
10
- function analyzeStyleOrganization(scssFiles, config = {}) {
11
- const analysis = {
12
- summary: {
13
- totalFiles: scssFiles.length,
14
- componentStyles: 0,
15
- globalStyles: 0,
16
- utilityFiles: 0,
17
- themeFiles: 0,
18
- mixinFiles: 0
19
- },
20
- duplicates: {
21
- selectors: [],
22
- rules: [],
23
- mixins: []
24
- },
25
- imports: {
26
- global: [],
27
- component: [],
28
- unused: [],
29
- circular: []
30
- },
31
- recommendations: [],
32
- structure: {
33
- current: {},
34
- suggested: {}
35
- }
36
- };
37
-
38
- const selectorMap = new Map(); // Track selector usage across files
39
- const ruleMap = new Map(); // Track identical rule blocks
40
- const mixinMap = new Map(); // Track mixin definitions
41
- const importMap = new Map(); // Track import statements
42
-
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
-
49
- const content = fs.readFileSync(filePath, 'utf8');
50
- const fileName = path.basename(filePath);
51
- const fileInfo = analyzeFile(content, filePath);
52
-
53
- // Categorize file type
54
- if (fileName.includes('theme') || fileName.includes('_variables')) {
55
- analysis.summary.themeFiles++;
56
- } else if (fileName.includes('mixin') || fileName.includes('_mixins')) {
57
- analysis.summary.mixinFiles++;
58
- } else if (fileName.includes('util') || fileName.includes('helper')) {
59
- analysis.summary.utilityFiles++;
60
- } else if (filePath.includes('styles.scss') || filePath.includes('global')) {
61
- analysis.summary.globalStyles++;
62
- } else if (fileName.includes('.component.scss')) {
63
- analysis.summary.componentStyles++;
64
- }
65
-
66
- // Track selectors
67
- fileInfo.selectors.forEach(selector => {
68
- if (!selectorMap.has(selector)) {
69
- selectorMap.set(selector, []);
70
- }
71
- selectorMap.get(selector).push({ file: filePath, content: fileInfo.selectorContent[selector] });
72
- });
73
-
74
- // Track rules
75
- fileInfo.rules.forEach(rule => {
76
- const ruleKey = rule.replace(/\s+/g, ' ').trim();
77
- if (!ruleMap.has(ruleKey)) {
78
- ruleMap.set(ruleKey, []);
79
- }
80
- ruleMap.get(ruleKey).push(filePath);
81
- });
82
-
83
- // Track mixins
84
- fileInfo.mixins.forEach(mixin => {
85
- if (!mixinMap.has(mixin)) {
86
- mixinMap.set(mixin, []);
87
- }
88
- mixinMap.get(mixin).push(filePath);
89
- });
90
-
91
- // Track imports
92
- fileInfo.imports.forEach(importPath => {
93
- if (!importMap.has(importPath)) {
94
- importMap.set(importPath, []);
95
- }
96
- importMap.get(importPath).push(filePath);
97
- });
98
- });
99
-
100
- // Analyze duplicates
101
- selectorMap.forEach((files, selector) => {
102
- if (files.length > 1) {
103
- analysis.duplicates.selectors.push({
104
- selector,
105
- count: files.length,
106
- files: files.map(f => f.file)
107
- });
108
- }
109
- });
110
-
111
- ruleMap.forEach((files, rule) => {
112
- if (files.length > 2 && rule.length > 50) { // Only report significant duplicates
113
- analysis.duplicates.rules.push({
114
- rule: rule.substring(0, 100) + '...',
115
- count: files.length,
116
- files
117
- });
118
- }
119
- });
120
-
121
- mixinMap.forEach((files, mixin) => {
122
- if (files.length > 1) {
123
- analysis.duplicates.mixins.push({
124
- mixin,
125
- count: files.length,
126
- files
127
- });
128
- }
129
- });
130
-
131
- // Analyze imports
132
- analyzeImportPatterns(importMap, analysis, scssFiles);
133
-
134
- // Generate recommendations
135
- generateRecommendations(analysis, config);
136
-
137
- // Suggest structure
138
- suggestStructure(analysis, config);
139
-
140
- return analysis;
141
- }
142
-
143
- /**
144
- * Analyzes a single SCSS file
145
- * @param {string} content - File content
146
- * @param {string} filePath - File path
147
- * @returns {Object} - File analysis
148
- */
149
- function analyzeFile(content, filePath) {
150
- const selectors = [];
151
- const selectorContent = {};
152
- const rules = [];
153
- const mixins = [];
154
- const imports = [];
155
-
156
- // Extract selectors
157
- const selectorRegex = /([.#]?[\w-]+(?:\s*[>+~]\s*[\w-]+)*)\s*\{([^}]*)\}/g;
158
- let match;
159
- while ((match = selectorRegex.exec(content)) !== null) {
160
- const selector = match[1].trim();
161
- const ruleContent = match[2].trim();
162
- selectors.push(selector);
163
- selectorContent[selector] = ruleContent;
164
- if (ruleContent.length > 20) {
165
- rules.push(match[0]);
166
- }
167
- }
168
-
169
- // Extract mixins
170
- const mixinRegex = /@mixin\s+([\w-]+)/g;
171
- while ((match = mixinRegex.exec(content)) !== null) {
172
- mixins.push(match[1]);
173
- }
174
-
175
- // Extract imports
176
- const importRegex = /@(?:import|use|forward)\s+['"]([^'"]+)['"]/g;
177
- while ((match = importRegex.exec(content)) !== null) {
178
- imports.push(match[1]);
179
- }
180
-
181
- return {
182
- selectors,
183
- selectorContent,
184
- rules,
185
- mixins,
186
- imports
187
- };
188
- }
189
-
190
- /**
191
- * Analyzes import patterns
192
- * @param {Map} importMap - Map of imports
193
- * @param {Object} analysis - Analysis object
194
- * @param {Array} scssFiles - All SCSS files
195
- */
196
- function analyzeImportPatterns(importMap, analysis, scssFiles) {
197
- const fileSet = new Set(scssFiles.map(f => path.basename(f, '.scss')));
198
-
199
- importMap.forEach((importingFiles, importPath) => {
200
- const isGlobal = importPath.includes('variables') ||
201
- importPath.includes('theme') ||
202
- importPath.includes('mixins');
203
-
204
- if (isGlobal && importingFiles.length > 5) {
205
- analysis.imports.global.push({
206
- path: importPath,
207
- usedIn: importingFiles.length,
208
- suggestion: 'Consider adding to global styles.scss'
209
- });
210
- } else if (importingFiles.length === 1) {
211
- analysis.imports.unused.push({
212
- path: importPath,
213
- file: importingFiles[0],
214
- suggestion: 'Potentially unused or should be in component file'
215
- });
216
- }
217
-
218
- // Check for component-level imports
219
- importingFiles.forEach(file => {
220
- if (file.includes('.component.scss')) {
221
- analysis.imports.component.push({
222
- path: importPath,
223
- component: path.basename(file)
224
- });
225
- }
226
- });
227
- });
228
- }
229
-
230
- /**
231
- * Generates organization recommendations
232
- * @param {Object} analysis - Analysis object
233
- * @param {Object} config - Configuration
234
- */
235
- function generateRecommendations(analysis, config) {
236
- const recs = analysis.recommendations;
237
-
238
- // Duplicate selectors
239
- if (analysis.duplicates.selectors.length > 0) {
240
- recs.push({
241
- priority: 'high',
242
- category: 'duplication',
243
- title: 'Duplicate selectors detected',
244
- description: `Found ${analysis.duplicates.selectors.length} selectors used in multiple files`,
245
- action: 'Extract common selectors to shared utility files or use mixins',
246
- files: analysis.duplicates.selectors.slice(0, 5)
247
- });
248
- }
249
-
250
- // Duplicate rules
251
- if (analysis.duplicates.rules.length > 0) {
252
- recs.push({
253
- priority: 'high',
254
- category: 'duplication',
255
- title: 'Duplicate CSS rules detected',
256
- description: `Found ${analysis.duplicates.rules.length} identical rule blocks`,
257
- action: 'Create reusable mixins or utility classes',
258
- count: analysis.duplicates.rules.length
259
- });
260
- }
261
-
262
- // Component vs global styles
263
- if (analysis.summary.componentStyles > 10 && analysis.summary.globalStyles === 0) {
264
- recs.push({
265
- priority: 'medium',
266
- category: 'organization',
267
- title: 'No global styles file detected',
268
- description: 'Consider creating a global styles.scss for shared utilities and resets',
269
- action: 'Create src/styles.scss with @use imports'
270
- });
271
- }
272
-
273
- // Import optimization
274
- if (analysis.imports.global.length > 3) {
275
- recs.push({
276
- priority: 'medium',
277
- category: 'imports',
278
- title: 'Optimize global imports',
279
- description: `${analysis.imports.global.length} files are imported globally across many components`,
280
- action: 'Consolidate into a single theme file or add to styles.scss',
281
- files: analysis.imports.global.map(i => i.path)
282
- });
283
- }
284
-
285
- // Theme organization
286
- if (analysis.summary.themeFiles === 0 && analysis.summary.componentStyles > 5) {
287
- recs.push({
288
- priority: 'medium',
289
- category: 'theming',
290
- title: 'No theme structure detected',
291
- description: 'Consider organizing styles following Angular Material theming patterns',
292
- action: 'Create theme files: _variables.scss, _typography.scss, _theme.scss'
293
- });
294
- }
295
-
296
- // Mixin organization
297
- if (analysis.duplicates.mixins.length > 0) {
298
- recs.push({
299
- priority: 'low',
300
- category: 'mixins',
301
- title: 'Duplicate mixin definitions',
302
- description: `${analysis.duplicates.mixins.length} mixins defined in multiple files`,
303
- action: 'Consolidate mixins into a shared _mixins.scss file'
304
- });
305
- }
306
- }
307
-
308
- /**
309
- * Suggests ideal folder structure
310
- * @param {Object} analysis - Analysis object
311
- * @param {Object} config - Configuration
312
- */
313
- function suggestStructure(analysis, config) {
314
- const angularConfigured = config.angular !== undefined;
315
-
316
- analysis.structure.current = {
317
- componentStyles: analysis.summary.componentStyles,
318
- globalStyles: analysis.summary.globalStyles,
319
- themeFiles: analysis.summary.themeFiles,
320
- utilityFiles: analysis.summary.utilityFiles,
321
- mixinFiles: analysis.summary.mixinFiles
322
- };
323
-
324
- analysis.structure.suggested = {
325
- description: 'Recommended Angular Material theme structure',
326
- folders: [
327
- {
328
- path: 'src/styles/',
329
- files: [
330
- '_variables.scss - Design tokens (colors, spacing, typography)',
331
- '_mixins.scss - Reusable SCSS mixins',
332
- '_utilities.scss - Utility classes (flex, spacing, etc.)',
333
- '_theme.scss - Angular Material theme configuration',
334
- '_typography.scss - Typography styles',
335
- 'styles.scss - Global entry point'
336
- ]
337
- },
338
- {
339
- path: 'src/app/components/',
340
- files: [
341
- '*.component.scss - Component-scoped styles only',
342
- 'Use @use for variables/mixins instead of @import'
343
- ]
344
- }
345
- ],
346
- imports: {
347
- global: [
348
- '@use "@angular/material" as mat;',
349
- '@use "./styles/variables" as vars;',
350
- '@use "./styles/mixins";',
351
- '@use "./styles/utilities";'
352
- ],
353
- component: [
354
- '@use "../../../styles/variables" as vars;',
355
- '@use "../../../styles/mixins";'
356
- ]
357
- }
358
- };
359
- }
360
-
361
- /**
362
- * Generates organization report
363
- * @param {Object} analysis - Analysis results
364
- * @param {string} format - Report format
365
- * @returns {string} - Formatted report
366
- */
367
- function generateOrganizationReport(analysis, format = 'table') {
368
- if (format === 'json') {
369
- return JSON.stringify(analysis, null, 2);
370
- }
371
-
372
- if (format === 'markdown') {
373
- return generateMarkdownOrganizationReport(analysis);
374
- }
375
-
376
- // Table format
377
- const chalk = require('chalk');
378
- const Table = require('cli-table3');
379
-
380
- let report = chalk.cyan.bold('\nšŸ“‹ Style Organization Analysis\n\n');
381
-
382
- // Summary
383
- report += chalk.white.bold('Summary:\n');
384
- const summaryTable = new Table({
385
- head: ['Metric', 'Count'],
386
- style: { head: ['cyan'] }
387
- });
388
-
389
- summaryTable.push(
390
- ['Total SCSS Files', analysis.summary.totalFiles],
391
- ['Component Styles', analysis.summary.componentStyles],
392
- ['Global Styles', analysis.summary.globalStyles],
393
- ['Theme Files', analysis.summary.themeFiles],
394
- ['Utility Files', analysis.summary.utilityFiles],
395
- ['Mixin Files', analysis.summary.mixinFiles]
396
- );
397
-
398
- report += summaryTable.toString() + '\n\n';
399
-
400
- // Duplicates
401
- if (analysis.duplicates.selectors.length > 0 ||
402
- analysis.duplicates.rules.length > 0) {
403
- report += chalk.yellow.bold('Duplication Issues:\n');
404
- const dupTable = new Table({
405
- head: ['Type', 'Count', 'Example'],
406
- style: { head: ['yellow'] }
407
- });
408
-
409
- if (analysis.duplicates.selectors.length > 0) {
410
- const example = analysis.duplicates.selectors[0];
411
- dupTable.push([
412
- 'Duplicate Selectors',
413
- analysis.duplicates.selectors.length,
414
- `${example.selector} (${example.count} files)`
415
- ]);
416
- }
417
-
418
- if (analysis.duplicates.rules.length > 0) {
419
- dupTable.push([
420
- 'Duplicate Rules',
421
- analysis.duplicates.rules.length,
422
- 'Identical CSS blocks'
423
- ]);
424
- }
425
-
426
- if (analysis.duplicates.mixins.length > 0) {
427
- dupTable.push([
428
- 'Duplicate Mixins',
429
- analysis.duplicates.mixins.length,
430
- analysis.duplicates.mixins[0].mixin
431
- ]);
432
- }
433
-
434
- report += dupTable.toString() + '\n\n';
435
- }
436
-
437
- // Recommendations
438
- if (analysis.recommendations.length > 0) {
439
- report += chalk.green.bold('Recommendations:\n\n');
440
- analysis.recommendations.forEach((rec, i) => {
441
- const priorityColor = rec.priority === 'high' ? 'red' :
442
- rec.priority === 'medium' ? 'yellow' : 'gray';
443
- report += chalk[priorityColor](`${i + 1}. [${rec.priority.toUpperCase()}] ${rec.title}\n`);
444
- report += chalk.gray(` ${rec.description}\n`);
445
- report += chalk.white(` → ${rec.action}\n\n`);
446
- });
447
- }
448
-
449
- // Suggested structure
450
- report += chalk.blue.bold('Suggested Structure:\n\n');
451
- analysis.structure.suggested.folders.forEach(folder => {
452
- report += chalk.cyan(`${folder.path}\n`);
453
- folder.files.forEach(file => {
454
- report += chalk.gray(` • ${file}\n`);
455
- });
456
- report += '\n';
457
- });
458
-
459
- return report;
460
- }
461
-
462
- /**
463
- * Generates markdown report
464
- * @param {Object} analysis - Analysis results
465
- * @returns {string} - Markdown formatted report
466
- */
467
- function generateMarkdownOrganizationReport(analysis) {
468
- let md = '# Style Organization Analysis\n\n';
469
-
470
- md += '## Summary\n\n';
471
- md += '| Metric | Count |\n';
472
- md += '|--------|-------|\n';
473
- md += `| Total SCSS Files | ${analysis.summary.totalFiles} |\n`;
474
- md += `| Component Styles | ${analysis.summary.componentStyles} |\n`;
475
- md += `| Global Styles | ${analysis.summary.globalStyles} |\n`;
476
- md += `| Theme Files | ${analysis.summary.themeFiles} |\n`;
477
- md += `| Utility Files | ${analysis.summary.utilityFiles} |\n`;
478
- md += `| Mixin Files | ${analysis.summary.mixinFiles} |\n\n`;
479
-
480
- if (analysis.recommendations.length > 0) {
481
- md += '## Recommendations\n\n';
482
- analysis.recommendations.forEach((rec, i) => {
483
- md += `### ${i + 1}. ${rec.title} (${rec.priority})\n\n`;
484
- md += `**Description:** ${rec.description}\n\n`;
485
- md += `**Action:** ${rec.action}\n\n`;
486
- });
487
- }
488
-
489
- md += '## Suggested Structure\n\n';
490
- analysis.structure.suggested.folders.forEach(folder => {
491
- md += `### ${folder.path}\n\n`;
492
- folder.files.forEach(file => {
493
- md += `- ${file}\n`;
494
- });
495
- md += '\n';
496
- });
497
-
498
- return md;
499
- }
500
-
501
- module.exports = {
502
- analyzeStyleOrganization,
503
- generateOrganizationReport
504
- };
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Analyzes style organization across the project
6
+ * @param {Array<string>} scssFiles - Array of SCSS file paths
7
+ * @param {Object} config - Configuration options
8
+ * @returns {Object} - Organization analysis results
9
+ */
10
+ function analyzeStyleOrganization(scssFiles, config = {}) {
11
+ const analysis = {
12
+ summary: {
13
+ totalFiles: scssFiles.length,
14
+ componentStyles: 0,
15
+ globalStyles: 0,
16
+ utilityFiles: 0,
17
+ themeFiles: 0,
18
+ mixinFiles: 0,
19
+ },
20
+ duplicates: {
21
+ selectors: [],
22
+ rules: [],
23
+ mixins: [],
24
+ },
25
+ imports: {
26
+ global: [],
27
+ component: [],
28
+ unused: [],
29
+ circular: [],
30
+ },
31
+ recommendations: [],
32
+ structure: {
33
+ current: {},
34
+ suggested: {},
35
+ },
36
+ };
37
+
38
+ const selectorMap = new Map(); // Track selector usage across files
39
+ const ruleMap = new Map(); // Track identical rule blocks
40
+ const mixinMap = new Map(); // Track mixin definitions
41
+ const importMap = new Map(); // Track import statements
42
+
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
+
49
+ const content = fs.readFileSync(filePath, 'utf8');
50
+ const fileName = path.basename(filePath);
51
+ const fileInfo = analyzeFile(content, filePath);
52
+
53
+ // Categorize file type
54
+ if (fileName.includes('theme') || fileName.includes('_variables')) {
55
+ analysis.summary.themeFiles++;
56
+ } else if (fileName.includes('mixin') || fileName.includes('_mixins')) {
57
+ analysis.summary.mixinFiles++;
58
+ } else if (fileName.includes('util') || fileName.includes('helper')) {
59
+ analysis.summary.utilityFiles++;
60
+ } else if (filePath.includes('styles.scss') || filePath.includes('global')) {
61
+ analysis.summary.globalStyles++;
62
+ } else if (fileName.includes('.component.scss')) {
63
+ analysis.summary.componentStyles++;
64
+ }
65
+
66
+ // Track selectors
67
+ fileInfo.selectors.forEach(selector => {
68
+ if (!selectorMap.has(selector)) {
69
+ selectorMap.set(selector, []);
70
+ }
71
+ selectorMap
72
+ .get(selector)
73
+ .push({ file: filePath, content: fileInfo.selectorContent[selector] });
74
+ });
75
+
76
+ // Track rules
77
+ fileInfo.rules.forEach(rule => {
78
+ const ruleKey = rule.replace(/\s+/g, ' ').trim();
79
+ if (!ruleMap.has(ruleKey)) {
80
+ ruleMap.set(ruleKey, []);
81
+ }
82
+ ruleMap.get(ruleKey).push(filePath);
83
+ });
84
+
85
+ // Track mixins
86
+ fileInfo.mixins.forEach(mixin => {
87
+ if (!mixinMap.has(mixin)) {
88
+ mixinMap.set(mixin, []);
89
+ }
90
+ mixinMap.get(mixin).push(filePath);
91
+ });
92
+
93
+ // Track imports
94
+ fileInfo.imports.forEach(importPath => {
95
+ if (!importMap.has(importPath)) {
96
+ importMap.set(importPath, []);
97
+ }
98
+ importMap.get(importPath).push(filePath);
99
+ });
100
+ });
101
+
102
+ // Analyze duplicates
103
+ selectorMap.forEach((files, selector) => {
104
+ if (files.length > 1) {
105
+ analysis.duplicates.selectors.push({
106
+ selector,
107
+ count: files.length,
108
+ files: files.map(f => f.file),
109
+ });
110
+ }
111
+ });
112
+
113
+ ruleMap.forEach((files, rule) => {
114
+ if (files.length > 2 && rule.length > 50) {
115
+ // Only report significant duplicates
116
+ analysis.duplicates.rules.push({
117
+ rule: rule.substring(0, 100) + '...',
118
+ count: files.length,
119
+ files,
120
+ });
121
+ }
122
+ });
123
+
124
+ mixinMap.forEach((files, mixin) => {
125
+ if (files.length > 1) {
126
+ analysis.duplicates.mixins.push({
127
+ mixin,
128
+ count: files.length,
129
+ files,
130
+ });
131
+ }
132
+ });
133
+
134
+ // Analyze imports
135
+ analyzeImportPatterns(importMap, analysis, scssFiles);
136
+
137
+ // Generate recommendations
138
+ generateRecommendations(analysis, config);
139
+
140
+ // Suggest structure
141
+ suggestStructure(analysis, config);
142
+
143
+ return analysis;
144
+ }
145
+
146
+ /**
147
+ * Analyzes a single SCSS file
148
+ * @param {string} content - File content
149
+ * @param {string} filePath - File path
150
+ * @returns {Object} - File analysis
151
+ */
152
+ function analyzeFile(content, filePath) {
153
+ const selectors = [];
154
+ const selectorContent = {};
155
+ const rules = [];
156
+ const mixins = [];
157
+ const imports = [];
158
+
159
+ // Extract selectors
160
+ const selectorRegex = /([.#]?[\w-]+(?:\s*[>+~]\s*[\w-]+)*)\s*\{([^}]*)\}/g;
161
+ let match;
162
+ while ((match = selectorRegex.exec(content)) !== null) {
163
+ const selector = match[1].trim();
164
+ const ruleContent = match[2].trim();
165
+ selectors.push(selector);
166
+ selectorContent[selector] = ruleContent;
167
+ if (ruleContent.length > 20) {
168
+ rules.push(match[0]);
169
+ }
170
+ }
171
+
172
+ // Extract mixins
173
+ const mixinRegex = /@mixin\s+([\w-]+)/g;
174
+ while ((match = mixinRegex.exec(content)) !== null) {
175
+ mixins.push(match[1]);
176
+ }
177
+
178
+ // Extract imports
179
+ const importRegex = /@(?:import|use|forward)\s+['"]([^'"]+)['"]/g;
180
+ while ((match = importRegex.exec(content)) !== null) {
181
+ imports.push(match[1]);
182
+ }
183
+
184
+ return {
185
+ selectors,
186
+ selectorContent,
187
+ rules,
188
+ mixins,
189
+ imports,
190
+ };
191
+ }
192
+
193
+ /**
194
+ * Analyzes import patterns
195
+ * @param {Map} importMap - Map of imports
196
+ * @param {Object} analysis - Analysis object
197
+ * @param {Array} scssFiles - All SCSS files
198
+ */
199
+ function analyzeImportPatterns(importMap, analysis, scssFiles) {
200
+ const fileSet = new Set(scssFiles.map(f => path.basename(f, '.scss')));
201
+
202
+ importMap.forEach((importingFiles, importPath) => {
203
+ const isGlobal =
204
+ importPath.includes('variables') ||
205
+ importPath.includes('theme') ||
206
+ importPath.includes('mixins');
207
+
208
+ if (isGlobal && importingFiles.length > 5) {
209
+ analysis.imports.global.push({
210
+ path: importPath,
211
+ usedIn: importingFiles.length,
212
+ suggestion: 'Consider adding to global styles.scss',
213
+ });
214
+ } else if (importingFiles.length === 1) {
215
+ analysis.imports.unused.push({
216
+ path: importPath,
217
+ file: importingFiles[0],
218
+ suggestion: 'Potentially unused or should be in component file',
219
+ });
220
+ }
221
+
222
+ // Check for component-level imports
223
+ importingFiles.forEach(file => {
224
+ if (file.includes('.component.scss')) {
225
+ analysis.imports.component.push({
226
+ path: importPath,
227
+ component: path.basename(file),
228
+ });
229
+ }
230
+ });
231
+ });
232
+ }
233
+
234
+ /**
235
+ * Generates organization recommendations
236
+ * @param {Object} analysis - Analysis object
237
+ * @param {Object} config - Configuration
238
+ */
239
+ function generateRecommendations(analysis, config) {
240
+ const recs = analysis.recommendations;
241
+
242
+ // Duplicate selectors
243
+ if (analysis.duplicates.selectors.length > 0) {
244
+ recs.push({
245
+ priority: 'high',
246
+ category: 'duplication',
247
+ title: 'Duplicate selectors detected',
248
+ description: `Found ${analysis.duplicates.selectors.length} selectors used in multiple files`,
249
+ action: 'Extract common selectors to shared utility files or use mixins',
250
+ files: analysis.duplicates.selectors.slice(0, 5),
251
+ });
252
+ }
253
+
254
+ // Duplicate rules
255
+ if (analysis.duplicates.rules.length > 0) {
256
+ recs.push({
257
+ priority: 'high',
258
+ category: 'duplication',
259
+ title: 'Duplicate CSS rules detected',
260
+ description: `Found ${analysis.duplicates.rules.length} identical rule blocks`,
261
+ action: 'Create reusable mixins or utility classes',
262
+ count: analysis.duplicates.rules.length,
263
+ });
264
+ }
265
+
266
+ // Component vs global styles
267
+ if (analysis.summary.componentStyles > 10 && analysis.summary.globalStyles === 0) {
268
+ recs.push({
269
+ priority: 'medium',
270
+ category: 'organization',
271
+ title: 'No global styles file detected',
272
+ description: 'Consider creating a global styles.scss for shared utilities and resets',
273
+ action: 'Create src/styles.scss with @use imports',
274
+ });
275
+ }
276
+
277
+ // Import optimization
278
+ if (analysis.imports.global.length > 3) {
279
+ recs.push({
280
+ priority: 'medium',
281
+ category: 'imports',
282
+ title: 'Optimize global imports',
283
+ description: `${analysis.imports.global.length} files are imported globally across many components`,
284
+ action: 'Consolidate into a single theme file or add to styles.scss',
285
+ files: analysis.imports.global.map(i => i.path),
286
+ });
287
+ }
288
+
289
+ // Theme organization
290
+ if (analysis.summary.themeFiles === 0 && analysis.summary.componentStyles > 5) {
291
+ recs.push({
292
+ priority: 'medium',
293
+ category: 'theming',
294
+ title: 'No theme structure detected',
295
+ description: 'Consider organizing styles following Angular Material theming patterns',
296
+ action: 'Create theme files: _variables.scss, _typography.scss, _theme.scss',
297
+ });
298
+ }
299
+
300
+ // Mixin organization
301
+ if (analysis.duplicates.mixins.length > 0) {
302
+ recs.push({
303
+ priority: 'low',
304
+ category: 'mixins',
305
+ title: 'Duplicate mixin definitions',
306
+ description: `${analysis.duplicates.mixins.length} mixins defined in multiple files`,
307
+ action: 'Consolidate mixins into a shared _mixins.scss file',
308
+ });
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Suggests ideal folder structure
314
+ * @param {Object} analysis - Analysis object
315
+ * @param {Object} config - Configuration
316
+ */
317
+ function suggestStructure(analysis, config) {
318
+ const angularConfigured = config.angular !== undefined;
319
+
320
+ analysis.structure.current = {
321
+ componentStyles: analysis.summary.componentStyles,
322
+ globalStyles: analysis.summary.globalStyles,
323
+ themeFiles: analysis.summary.themeFiles,
324
+ utilityFiles: analysis.summary.utilityFiles,
325
+ mixinFiles: analysis.summary.mixinFiles,
326
+ };
327
+
328
+ analysis.structure.suggested = {
329
+ description: 'Recommended Angular Material theme structure',
330
+ folders: [
331
+ {
332
+ path: 'src/styles/',
333
+ files: [
334
+ '_variables.scss - Design tokens (colors, spacing, typography)',
335
+ '_mixins.scss - Reusable SCSS mixins',
336
+ '_utilities.scss - Utility classes (flex, spacing, etc.)',
337
+ '_theme.scss - Angular Material theme configuration',
338
+ '_typography.scss - Typography styles',
339
+ 'styles.scss - Global entry point',
340
+ ],
341
+ },
342
+ {
343
+ path: 'src/app/components/',
344
+ files: [
345
+ '*.component.scss - Component-scoped styles only',
346
+ 'Use @use for variables/mixins instead of @import',
347
+ ],
348
+ },
349
+ ],
350
+ imports: {
351
+ global: [
352
+ '@use "@angular/material" as mat;',
353
+ '@use "./styles/variables" as vars;',
354
+ '@use "./styles/mixins";',
355
+ '@use "./styles/utilities";',
356
+ ],
357
+ component: ['@use "../../../styles/variables" as vars;', '@use "../../../styles/mixins";'],
358
+ },
359
+ };
360
+ }
361
+
362
+ /**
363
+ * Generates organization report
364
+ * @param {Object} analysis - Analysis results
365
+ * @param {string} format - Report format
366
+ * @returns {string} - Formatted report
367
+ */
368
+ function generateOrganizationReport(analysis, format = 'table') {
369
+ if (format === 'json') {
370
+ return JSON.stringify(analysis, null, 2);
371
+ }
372
+
373
+ if (format === 'markdown') {
374
+ return generateMarkdownOrganizationReport(analysis);
375
+ }
376
+
377
+ // Table format
378
+ const chalk = require('chalk');
379
+ const Table = require('cli-table3');
380
+
381
+ let report = chalk.cyan.bold('\nšŸ“‹ Style Organization Analysis\n\n');
382
+
383
+ // Summary
384
+ report += chalk.white.bold('Summary:\n');
385
+ const summaryTable = new Table({
386
+ head: ['Metric', 'Count'],
387
+ style: { head: ['cyan'] },
388
+ });
389
+
390
+ summaryTable.push(
391
+ ['Total SCSS Files', analysis.summary.totalFiles],
392
+ ['Component Styles', analysis.summary.componentStyles],
393
+ ['Global Styles', analysis.summary.globalStyles],
394
+ ['Theme Files', analysis.summary.themeFiles],
395
+ ['Utility Files', analysis.summary.utilityFiles],
396
+ ['Mixin Files', analysis.summary.mixinFiles]
397
+ );
398
+
399
+ report += summaryTable.toString() + '\n\n';
400
+
401
+ // Duplicates
402
+ if (analysis.duplicates.selectors.length > 0 || analysis.duplicates.rules.length > 0) {
403
+ report += chalk.yellow.bold('Duplication Issues:\n');
404
+ const dupTable = new Table({
405
+ head: ['Type', 'Count', 'Example'],
406
+ style: { head: ['yellow'] },
407
+ });
408
+
409
+ if (analysis.duplicates.selectors.length > 0) {
410
+ const example = analysis.duplicates.selectors[0];
411
+ dupTable.push([
412
+ 'Duplicate Selectors',
413
+ analysis.duplicates.selectors.length,
414
+ `${example.selector} (${example.count} files)`,
415
+ ]);
416
+ }
417
+
418
+ if (analysis.duplicates.rules.length > 0) {
419
+ dupTable.push(['Duplicate Rules', analysis.duplicates.rules.length, 'Identical CSS blocks']);
420
+ }
421
+
422
+ if (analysis.duplicates.mixins.length > 0) {
423
+ dupTable.push([
424
+ 'Duplicate Mixins',
425
+ analysis.duplicates.mixins.length,
426
+ analysis.duplicates.mixins[0].mixin,
427
+ ]);
428
+ }
429
+
430
+ report += dupTable.toString() + '\n\n';
431
+ }
432
+
433
+ // Recommendations
434
+ if (analysis.recommendations.length > 0) {
435
+ report += chalk.green.bold('Recommendations:\n\n');
436
+ analysis.recommendations.forEach((rec, i) => {
437
+ const priorityColor =
438
+ rec.priority === 'high' ? 'red' : rec.priority === 'medium' ? 'yellow' : 'gray';
439
+ report += chalk[priorityColor](`${i + 1}. [${rec.priority.toUpperCase()}] ${rec.title}\n`);
440
+ report += chalk.gray(` ${rec.description}\n`);
441
+ report += chalk.white(` → ${rec.action}\n\n`);
442
+ });
443
+ }
444
+
445
+ // Suggested structure
446
+ report += chalk.blue.bold('Suggested Structure:\n\n');
447
+ analysis.structure.suggested.folders.forEach(folder => {
448
+ report += chalk.cyan(`${folder.path}\n`);
449
+ folder.files.forEach(file => {
450
+ report += chalk.gray(` • ${file}\n`);
451
+ });
452
+ report += '\n';
453
+ });
454
+
455
+ return report;
456
+ }
457
+
458
+ /**
459
+ * Generates markdown report
460
+ * @param {Object} analysis - Analysis results
461
+ * @returns {string} - Markdown formatted report
462
+ */
463
+ function generateMarkdownOrganizationReport(analysis) {
464
+ let md = '# Style Organization Analysis\n\n';
465
+
466
+ md += '## Summary\n\n';
467
+ md += '| Metric | Count |\n';
468
+ md += '|--------|-------|\n';
469
+ md += `| Total SCSS Files | ${analysis.summary.totalFiles} |\n`;
470
+ md += `| Component Styles | ${analysis.summary.componentStyles} |\n`;
471
+ md += `| Global Styles | ${analysis.summary.globalStyles} |\n`;
472
+ md += `| Theme Files | ${analysis.summary.themeFiles} |\n`;
473
+ md += `| Utility Files | ${analysis.summary.utilityFiles} |\n`;
474
+ md += `| Mixin Files | ${analysis.summary.mixinFiles} |\n\n`;
475
+
476
+ if (analysis.recommendations.length > 0) {
477
+ md += '## Recommendations\n\n';
478
+ analysis.recommendations.forEach((rec, i) => {
479
+ md += `### ${i + 1}. ${rec.title} (${rec.priority})\n\n`;
480
+ md += `**Description:** ${rec.description}\n\n`;
481
+ md += `**Action:** ${rec.action}\n\n`;
482
+ });
483
+ }
484
+
485
+ md += '## Suggested Structure\n\n';
486
+ analysis.structure.suggested.folders.forEach(folder => {
487
+ md += `### ${folder.path}\n\n`;
488
+ folder.files.forEach(file => {
489
+ md += `- ${file}\n`;
490
+ });
491
+ md += '\n';
492
+ });
493
+
494
+ return md;
495
+ }
496
+
497
+ module.exports = {
498
+ analyzeStyleOrganization,
499
+ generateOrganizationReport,
500
+ };