scss-variable-extractor 1.5.3 → 1.6.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/bin/cli.js CHANGED
@@ -11,6 +11,7 @@ const { generateVariablesFile, generateReport } = require('../src/generator');
11
11
  const { refactorScssFiles } = require('../src/refactorer');
12
12
  const { analyzeAngularPatterns, refactorAngularPatterns, generateAngularPatternReport } = require('../src/ng-refactorer');
13
13
  const { detectBootstrap, migrateBootstrapToMaterial, generateBootstrapReport } = require('../src/bootstrap-migrator');
14
+ const { analyzeStyleOrganization, generateOrganizationReport } = require('../src/style-organizer');
14
15
 
15
16
  const program = new Command();
16
17
 
@@ -589,4 +590,63 @@ program
589
590
  }
590
591
  });
591
592
 
593
+ // Analyze style organization command
594
+ program
595
+ .command('analyze-organization')
596
+ .description('Analyze style organization and get recommendations for best practices')
597
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
598
+ .option('--format <format>', 'Report format (table, json, markdown)', 'table')
599
+ .option('--config <path>', 'Path to config file')
600
+ .option('--angular-json <path>', 'Path to angular.json file')
601
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
602
+ .option('--no-angular', 'Disable angular.json integration')
603
+ .action(async (src, options) => {
604
+ try {
605
+ console.log(chalk.cyan.bold('\nšŸ“‹ Style Organization Analysis\n'));
606
+
607
+ const config = loadConfig(options.config, {
608
+ useAngularJson: options.angular !== false,
609
+ angularJsonPath: options.angularJson,
610
+ projectName: options.project
611
+ });
612
+ if (src) config.src = src;
613
+
614
+ // Show Angular project info if available
615
+ if (config.angular) {
616
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
617
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
618
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
619
+ }
620
+
621
+ console.log(chalk.gray(`Scanning: ${config.src}\n`));
622
+
623
+ // Scan files
624
+ const files = await scanScssFiles(config.src, config.ignore);
625
+ console.log(chalk.green(`āœ“ Found ${files.length} SCSS files\n`));
626
+
627
+ // Analyze organization
628
+ const analysis = analyzeStyleOrganization(files, config);
629
+
630
+ // Generate report
631
+ const report = generateOrganizationReport(analysis, options.format);
632
+ console.log(report);
633
+
634
+ // Provide guidance
635
+ if (analysis.recommendations.length > 0) {
636
+ console.log(chalk.yellow.bold('šŸ’” Next Steps:\n'));
637
+ console.log(chalk.gray('1. Review recommendations above'));
638
+ console.log(chalk.gray('2. Run "refactor" command to extract variables'));
639
+ console.log(chalk.gray('3. Run "modernize" to remove anti-patterns'));
640
+ console.log(chalk.gray('4. Consider restructuring based on suggested organization'));
641
+ console.log(chalk.gray('5. Use @use instead of @import for better modularity\n'));
642
+ } else {
643
+ console.log(chalk.green('āœ“ Your styles are well organized!\n'));
644
+ }
645
+
646
+ } catch (error) {
647
+ console.error(chalk.red('āœ— Error:'), error.message);
648
+ process.exit(1);
649
+ }
650
+ });
651
+
592
652
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scss-variable-extractor",
3
- "version": "1.5.3",
3
+ "version": "1.6.0",
4
4
  "description": "Analyzes Angular SCSS files and extracts repeated hardcoded values into reusable variables",
5
5
  "bin": {
6
6
  "scss-extract": "./bin/cli.js"
package/src/index.js CHANGED
@@ -7,6 +7,7 @@ const { refactorScssFiles } = require('./refactorer');
7
7
  const { analyzeAngularPatterns, refactorAngularPatterns, generateAngularPatternReport } = require('./ng-refactorer');
8
8
  const angularParser = require('./angular-parser');
9
9
  const { detectBootstrap, migrateBootstrapToMaterial, generateBootstrapReport } = require('./bootstrap-migrator');
10
+ const { analyzeStyleOrganization, generateOrganizationReport } = require('./style-organizer');
10
11
 
11
12
  module.exports = {
12
13
  loadConfig,
@@ -22,5 +23,7 @@ module.exports = {
22
23
  angularParser,
23
24
  detectBootstrap,
24
25
  migrateBootstrapToMaterial,
25
- generateBootstrapReport
26
+ generateBootstrapReport,
27
+ analyzeStyleOrganization,
28
+ generateOrganizationReport
26
29
  };
@@ -0,0 +1,499 @@
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
+ const content = fs.readFileSync(filePath, 'utf8');
45
+ const fileName = path.basename(filePath);
46
+ const fileInfo = analyzeFile(content, filePath);
47
+
48
+ // Categorize file type
49
+ if (fileName.includes('theme') || fileName.includes('_variables')) {
50
+ analysis.summary.themeFiles++;
51
+ } else if (fileName.includes('mixin') || fileName.includes('_mixins')) {
52
+ analysis.summary.mixinFiles++;
53
+ } else if (fileName.includes('util') || fileName.includes('helper')) {
54
+ analysis.summary.utilityFiles++;
55
+ } else if (filePath.includes('styles.scss') || filePath.includes('global')) {
56
+ analysis.summary.globalStyles++;
57
+ } else if (fileName.includes('.component.scss')) {
58
+ analysis.summary.componentStyles++;
59
+ }
60
+
61
+ // Track selectors
62
+ fileInfo.selectors.forEach(selector => {
63
+ if (!selectorMap.has(selector)) {
64
+ selectorMap.set(selector, []);
65
+ }
66
+ selectorMap.get(selector).push({ file: filePath, content: fileInfo.selectorContent[selector] });
67
+ });
68
+
69
+ // Track rules
70
+ fileInfo.rules.forEach(rule => {
71
+ const ruleKey = rule.replace(/\s+/g, ' ').trim();
72
+ if (!ruleMap.has(ruleKey)) {
73
+ ruleMap.set(ruleKey, []);
74
+ }
75
+ ruleMap.get(ruleKey).push(filePath);
76
+ });
77
+
78
+ // Track mixins
79
+ fileInfo.mixins.forEach(mixin => {
80
+ if (!mixinMap.has(mixin)) {
81
+ mixinMap.set(mixin, []);
82
+ }
83
+ mixinMap.get(mixin).push(filePath);
84
+ });
85
+
86
+ // Track imports
87
+ fileInfo.imports.forEach(importPath => {
88
+ if (!importMap.has(importPath)) {
89
+ importMap.set(importPath, []);
90
+ }
91
+ importMap.get(importPath).push(filePath);
92
+ });
93
+ });
94
+
95
+ // Analyze duplicates
96
+ selectorMap.forEach((files, selector) => {
97
+ if (files.length > 1) {
98
+ analysis.duplicates.selectors.push({
99
+ selector,
100
+ count: files.length,
101
+ files: files.map(f => f.file)
102
+ });
103
+ }
104
+ });
105
+
106
+ ruleMap.forEach((files, rule) => {
107
+ if (files.length > 2 && rule.length > 50) { // Only report significant duplicates
108
+ analysis.duplicates.rules.push({
109
+ rule: rule.substring(0, 100) + '...',
110
+ count: files.length,
111
+ files
112
+ });
113
+ }
114
+ });
115
+
116
+ mixinMap.forEach((files, mixin) => {
117
+ if (files.length > 1) {
118
+ analysis.duplicates.mixins.push({
119
+ mixin,
120
+ count: files.length,
121
+ files
122
+ });
123
+ }
124
+ });
125
+
126
+ // Analyze imports
127
+ analyzeImportPatterns(importMap, analysis, scssFiles);
128
+
129
+ // Generate recommendations
130
+ generateRecommendations(analysis, config);
131
+
132
+ // Suggest structure
133
+ suggestStructure(analysis, config);
134
+
135
+ return analysis;
136
+ }
137
+
138
+ /**
139
+ * Analyzes a single SCSS file
140
+ * @param {string} content - File content
141
+ * @param {string} filePath - File path
142
+ * @returns {Object} - File analysis
143
+ */
144
+ function analyzeFile(content, filePath) {
145
+ const selectors = [];
146
+ const selectorContent = {};
147
+ const rules = [];
148
+ const mixins = [];
149
+ const imports = [];
150
+
151
+ // Extract selectors
152
+ const selectorRegex = /([.#]?[\w-]+(?:\s*[>+~]\s*[\w-]+)*)\s*\{([^}]*)\}/g;
153
+ let match;
154
+ while ((match = selectorRegex.exec(content)) !== null) {
155
+ const selector = match[1].trim();
156
+ const ruleContent = match[2].trim();
157
+ selectors.push(selector);
158
+ selectorContent[selector] = ruleContent;
159
+ if (ruleContent.length > 20) {
160
+ rules.push(match[0]);
161
+ }
162
+ }
163
+
164
+ // Extract mixins
165
+ const mixinRegex = /@mixin\s+([\w-]+)/g;
166
+ while ((match = mixinRegex.exec(content)) !== null) {
167
+ mixins.push(match[1]);
168
+ }
169
+
170
+ // Extract imports
171
+ const importRegex = /@(?:import|use|forward)\s+['"]([^'"]+)['"]/g;
172
+ while ((match = importRegex.exec(content)) !== null) {
173
+ imports.push(match[1]);
174
+ }
175
+
176
+ return {
177
+ selectors,
178
+ selectorContent,
179
+ rules,
180
+ mixins,
181
+ imports
182
+ };
183
+ }
184
+
185
+ /**
186
+ * Analyzes import patterns
187
+ * @param {Map} importMap - Map of imports
188
+ * @param {Object} analysis - Analysis object
189
+ * @param {Array} scssFiles - All SCSS files
190
+ */
191
+ function analyzeImportPatterns(importMap, analysis, scssFiles) {
192
+ const fileSet = new Set(scssFiles.map(f => path.basename(f, '.scss')));
193
+
194
+ importMap.forEach((importingFiles, importPath) => {
195
+ const isGlobal = importPath.includes('variables') ||
196
+ importPath.includes('theme') ||
197
+ importPath.includes('mixins');
198
+
199
+ if (isGlobal && importingFiles.length > 5) {
200
+ analysis.imports.global.push({
201
+ path: importPath,
202
+ usedIn: importingFiles.length,
203
+ suggestion: 'Consider adding to global styles.scss'
204
+ });
205
+ } else if (importingFiles.length === 1) {
206
+ analysis.imports.unused.push({
207
+ path: importPath,
208
+ file: importingFiles[0],
209
+ suggestion: 'Potentially unused or should be in component file'
210
+ });
211
+ }
212
+
213
+ // Check for component-level imports
214
+ importingFiles.forEach(file => {
215
+ if (file.includes('.component.scss')) {
216
+ analysis.imports.component.push({
217
+ path: importPath,
218
+ component: path.basename(file)
219
+ });
220
+ }
221
+ });
222
+ });
223
+ }
224
+
225
+ /**
226
+ * Generates organization recommendations
227
+ * @param {Object} analysis - Analysis object
228
+ * @param {Object} config - Configuration
229
+ */
230
+ function generateRecommendations(analysis, config) {
231
+ const recs = analysis.recommendations;
232
+
233
+ // Duplicate selectors
234
+ if (analysis.duplicates.selectors.length > 0) {
235
+ recs.push({
236
+ priority: 'high',
237
+ category: 'duplication',
238
+ title: 'Duplicate selectors detected',
239
+ description: `Found ${analysis.duplicates.selectors.length} selectors used in multiple files`,
240
+ action: 'Extract common selectors to shared utility files or use mixins',
241
+ files: analysis.duplicates.selectors.slice(0, 5)
242
+ });
243
+ }
244
+
245
+ // Duplicate rules
246
+ if (analysis.duplicates.rules.length > 0) {
247
+ recs.push({
248
+ priority: 'high',
249
+ category: 'duplication',
250
+ title: 'Duplicate CSS rules detected',
251
+ description: `Found ${analysis.duplicates.rules.length} identical rule blocks`,
252
+ action: 'Create reusable mixins or utility classes',
253
+ count: analysis.duplicates.rules.length
254
+ });
255
+ }
256
+
257
+ // Component vs global styles
258
+ if (analysis.summary.componentStyles > 10 && analysis.summary.globalStyles === 0) {
259
+ recs.push({
260
+ priority: 'medium',
261
+ category: 'organization',
262
+ title: 'No global styles file detected',
263
+ description: 'Consider creating a global styles.scss for shared utilities and resets',
264
+ action: 'Create src/styles.scss with @use imports'
265
+ });
266
+ }
267
+
268
+ // Import optimization
269
+ if (analysis.imports.global.length > 3) {
270
+ recs.push({
271
+ priority: 'medium',
272
+ category: 'imports',
273
+ title: 'Optimize global imports',
274
+ description: `${analysis.imports.global.length} files are imported globally across many components`,
275
+ action: 'Consolidate into a single theme file or add to styles.scss',
276
+ files: analysis.imports.global.map(i => i.path)
277
+ });
278
+ }
279
+
280
+ // Theme organization
281
+ if (analysis.summary.themeFiles === 0 && analysis.summary.componentStyles > 5) {
282
+ recs.push({
283
+ priority: 'medium',
284
+ category: 'theming',
285
+ title: 'No theme structure detected',
286
+ description: 'Consider organizing styles following Angular Material theming patterns',
287
+ action: 'Create theme files: _variables.scss, _typography.scss, _theme.scss'
288
+ });
289
+ }
290
+
291
+ // Mixin organization
292
+ if (analysis.duplicates.mixins.length > 0) {
293
+ recs.push({
294
+ priority: 'low',
295
+ category: 'mixins',
296
+ title: 'Duplicate mixin definitions',
297
+ description: `${analysis.duplicates.mixins.length} mixins defined in multiple files`,
298
+ action: 'Consolidate mixins into a shared _mixins.scss file'
299
+ });
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Suggests ideal folder structure
305
+ * @param {Object} analysis - Analysis object
306
+ * @param {Object} config - Configuration
307
+ */
308
+ function suggestStructure(analysis, config) {
309
+ const angularConfigured = config.angular !== undefined;
310
+
311
+ analysis.structure.current = {
312
+ componentStyles: analysis.summary.componentStyles,
313
+ globalStyles: analysis.summary.globalStyles,
314
+ themeFiles: analysis.summary.themeFiles,
315
+ utilityFiles: analysis.summary.utilityFiles,
316
+ mixinFiles: analysis.summary.mixinFiles
317
+ };
318
+
319
+ analysis.structure.suggested = {
320
+ description: 'Recommended Angular Material theme structure',
321
+ folders: [
322
+ {
323
+ path: 'src/styles/',
324
+ files: [
325
+ '_variables.scss - Design tokens (colors, spacing, typography)',
326
+ '_mixins.scss - Reusable SCSS mixins',
327
+ '_utilities.scss - Utility classes (flex, spacing, etc.)',
328
+ '_theme.scss - Angular Material theme configuration',
329
+ '_typography.scss - Typography styles',
330
+ 'styles.scss - Global entry point'
331
+ ]
332
+ },
333
+ {
334
+ path: 'src/app/components/',
335
+ files: [
336
+ '*.component.scss - Component-scoped styles only',
337
+ 'Use @use for variables/mixins instead of @import'
338
+ ]
339
+ }
340
+ ],
341
+ imports: {
342
+ global: [
343
+ '@use "@angular/material" as mat;',
344
+ '@use "./styles/variables" as vars;',
345
+ '@use "./styles/mixins";',
346
+ '@use "./styles/utilities";'
347
+ ],
348
+ component: [
349
+ '@use "../../../styles/variables" as vars;',
350
+ '@use "../../../styles/mixins";'
351
+ ]
352
+ }
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Generates organization report
358
+ * @param {Object} analysis - Analysis results
359
+ * @param {string} format - Report format
360
+ * @returns {string} - Formatted report
361
+ */
362
+ function generateOrganizationReport(analysis, format = 'table') {
363
+ if (format === 'json') {
364
+ return JSON.stringify(analysis, null, 2);
365
+ }
366
+
367
+ if (format === 'markdown') {
368
+ return generateMarkdownOrganizationReport(analysis);
369
+ }
370
+
371
+ // Table format
372
+ const chalk = require('chalk');
373
+ const Table = require('cli-table3');
374
+
375
+ let report = chalk.cyan.bold('\nšŸ“‹ Style Organization Analysis\n\n');
376
+
377
+ // Summary
378
+ report += chalk.white.bold('Summary:\n');
379
+ const summaryTable = new Table({
380
+ head: ['Metric', 'Count'],
381
+ style: { head: ['cyan'] }
382
+ });
383
+
384
+ summaryTable.push(
385
+ ['Total SCSS Files', analysis.summary.totalFiles],
386
+ ['Component Styles', analysis.summary.componentStyles],
387
+ ['Global Styles', analysis.summary.globalStyles],
388
+ ['Theme Files', analysis.summary.themeFiles],
389
+ ['Utility Files', analysis.summary.utilityFiles],
390
+ ['Mixin Files', analysis.summary.mixinFiles]
391
+ );
392
+
393
+ report += summaryTable.toString() + '\n\n';
394
+
395
+ // Duplicates
396
+ if (analysis.duplicates.selectors.length > 0 ||
397
+ analysis.duplicates.rules.length > 0) {
398
+ report += chalk.yellow.bold('Duplication Issues:\n');
399
+ const dupTable = new Table({
400
+ head: ['Type', 'Count', 'Example'],
401
+ style: { head: ['yellow'] }
402
+ });
403
+
404
+ if (analysis.duplicates.selectors.length > 0) {
405
+ const example = analysis.duplicates.selectors[0];
406
+ dupTable.push([
407
+ 'Duplicate Selectors',
408
+ analysis.duplicates.selectors.length,
409
+ `${example.selector} (${example.count} files)`
410
+ ]);
411
+ }
412
+
413
+ if (analysis.duplicates.rules.length > 0) {
414
+ dupTable.push([
415
+ 'Duplicate Rules',
416
+ analysis.duplicates.rules.length,
417
+ 'Identical CSS blocks'
418
+ ]);
419
+ }
420
+
421
+ if (analysis.duplicates.mixins.length > 0) {
422
+ dupTable.push([
423
+ 'Duplicate Mixins',
424
+ analysis.duplicates.mixins.length,
425
+ analysis.duplicates.mixins[0].mixin
426
+ ]);
427
+ }
428
+
429
+ report += dupTable.toString() + '\n\n';
430
+ }
431
+
432
+ // Recommendations
433
+ if (analysis.recommendations.length > 0) {
434
+ report += chalk.green.bold('Recommendations:\n\n');
435
+ analysis.recommendations.forEach((rec, i) => {
436
+ const priorityColor = rec.priority === 'high' ? 'red' :
437
+ rec.priority === 'medium' ? 'yellow' : 'gray';
438
+ report += chalk[priorityColor](`${i + 1}. [${rec.priority.toUpperCase()}] ${rec.title}\n`);
439
+ report += chalk.gray(` ${rec.description}\n`);
440
+ report += chalk.white(` → ${rec.action}\n\n`);
441
+ });
442
+ }
443
+
444
+ // Suggested structure
445
+ report += chalk.blue.bold('Suggested Structure:\n\n');
446
+ analysis.structure.suggested.folders.forEach(folder => {
447
+ report += chalk.cyan(`${folder.path}\n`);
448
+ folder.files.forEach(file => {
449
+ report += chalk.gray(` • ${file}\n`);
450
+ });
451
+ report += '\n';
452
+ });
453
+
454
+ return report;
455
+ }
456
+
457
+ /**
458
+ * Generates markdown report
459
+ * @param {Object} analysis - Analysis results
460
+ * @returns {string} - Markdown formatted report
461
+ */
462
+ function generateMarkdownOrganizationReport(analysis) {
463
+ let md = '# Style Organization Analysis\n\n';
464
+
465
+ md += '## Summary\n\n';
466
+ md += '| Metric | Count |\n';
467
+ md += '|--------|-------|\n';
468
+ md += `| Total SCSS Files | ${analysis.summary.totalFiles} |\n`;
469
+ md += `| Component Styles | ${analysis.summary.componentStyles} |\n`;
470
+ md += `| Global Styles | ${analysis.summary.globalStyles} |\n`;
471
+ md += `| Theme Files | ${analysis.summary.themeFiles} |\n`;
472
+ md += `| Utility Files | ${analysis.summary.utilityFiles} |\n`;
473
+ md += `| Mixin Files | ${analysis.summary.mixinFiles} |\n\n`;
474
+
475
+ if (analysis.recommendations.length > 0) {
476
+ md += '## Recommendations\n\n';
477
+ analysis.recommendations.forEach((rec, i) => {
478
+ md += `### ${i + 1}. ${rec.title} (${rec.priority})\n\n`;
479
+ md += `**Description:** ${rec.description}\n\n`;
480
+ md += `**Action:** ${rec.action}\n\n`;
481
+ });
482
+ }
483
+
484
+ md += '## Suggested Structure\n\n';
485
+ analysis.structure.suggested.folders.forEach(folder => {
486
+ md += `### ${folder.path}\n\n`;
487
+ folder.files.forEach(file => {
488
+ md += `- ${file}\n`;
489
+ });
490
+ md += '\n';
491
+ });
492
+
493
+ return md;
494
+ }
495
+
496
+ module.exports = {
497
+ analyzeStyleOrganization,
498
+ generateOrganizationReport
499
+ };
@@ -0,0 +1,106 @@
1
+ const { analyzeStyleOrganization, generateOrganizationReport } = require('../src/style-organizer');
2
+ const path = require('path');
3
+ const fs = require('fs');
4
+
5
+ describe('Style Organizer', () => {
6
+ const fixturesPath = path.join(__dirname, 'fixtures/apps/subapp/src');
7
+
8
+ describe('analyzeStyleOrganization', () => {
9
+ test('should analyze style organization', async () => {
10
+ const { scanScssFiles } = require('../src/scanner');
11
+ const files = await scanScssFiles(fixturesPath);
12
+
13
+ const analysis = analyzeStyleOrganization(files);
14
+
15
+ expect(analysis).toHaveProperty('summary');
16
+ expect(analysis).toHaveProperty('duplicates');
17
+ expect(analysis).toHaveProperty('imports');
18
+ expect(analysis).toHaveProperty('recommendations');
19
+ expect(analysis).toHaveProperty('structure');
20
+
21
+ expect(analysis.summary.totalFiles).toBeGreaterThan(0);
22
+ });
23
+
24
+ test('should categorize file types', async () => {
25
+ const { scanScssFiles } = require('../src/scanner');
26
+ const files = await scanScssFiles(fixturesPath);
27
+
28
+ const analysis = analyzeStyleOrganization(files);
29
+
30
+ expect(analysis.summary).toHaveProperty('componentStyles');
31
+ expect(analysis.summary).toHaveProperty('globalStyles');
32
+ expect(analysis.summary).toHaveProperty('themeFiles');
33
+ expect(analysis.summary).toHaveProperty('utilityFiles');
34
+ });
35
+
36
+ test('should detect duplicate selectors', () => {
37
+ const testFiles = [
38
+ path.join(__dirname, 'fixtures/apps/subapp/src/app/component-a/component-a.component.scss'),
39
+ path.join(__dirname, 'fixtures/apps/subapp/src/app/component-b/component-b.component.scss')
40
+ ];
41
+
42
+ const analysis = analyzeStyleOrganization(testFiles);
43
+
44
+ expect(analysis.duplicates).toHaveProperty('selectors');
45
+ expect(Array.isArray(analysis.duplicates.selectors)).toBe(true);
46
+ });
47
+
48
+ test('should generate recommendations', async () => {
49
+ const { scanScssFiles } = require('../src/scanner');
50
+ const files = await scanScssFiles(fixturesPath);
51
+
52
+ const analysis = analyzeStyleOrganization(files);
53
+
54
+ expect(Array.isArray(analysis.recommendations)).toBe(true);
55
+ });
56
+
57
+ test('should suggest structure', async () => {
58
+ const { scanScssFiles } = require('../src/scanner');
59
+ const files = await scanScssFiles(fixturesPath);
60
+
61
+ const analysis = analyzeStyleOrganization(files);
62
+
63
+ expect(analysis.structure).toHaveProperty('current');
64
+ expect(analysis.structure).toHaveProperty('suggested');
65
+ expect(analysis.structure.suggested).toHaveProperty('folders');
66
+ });
67
+ });
68
+
69
+ describe('generateOrganizationReport', () => {
70
+ test('should generate table report', async () => {
71
+ const { scanScssFiles } = require('../src/scanner');
72
+ const files = await scanScssFiles(fixturesPath);
73
+ const analysis = analyzeStyleOrganization(files);
74
+
75
+ const report = generateOrganizationReport(analysis, 'table');
76
+
77
+ expect(report).toContain('Style Organization Analysis');
78
+ expect(report).toContain('Summary');
79
+ expect(typeof report).toBe('string');
80
+ });
81
+
82
+ test('should generate JSON report', async () => {
83
+ const { scanScssFiles } = require('../src/scanner');
84
+ const files = await scanScssFiles(fixturesPath);
85
+ const analysis = analyzeStyleOrganization(files);
86
+
87
+ const report = generateOrganizationReport(analysis, 'json');
88
+
89
+ const parsed = JSON.parse(report);
90
+ expect(parsed).toHaveProperty('summary');
91
+ expect(parsed).toHaveProperty('recommendations');
92
+ });
93
+
94
+ test('should generate Markdown report', async () => {
95
+ const { scanScssFiles } = require('../src/scanner');
96
+ const files = await scanScssFiles(fixturesPath);
97
+ const analysis = analyzeStyleOrganization(files);
98
+
99
+ const report = generateOrganizationReport(analysis, 'markdown');
100
+
101
+ expect(report).toContain('# Style Organization Analysis');
102
+ expect(report).toContain('## Summary');
103
+ expect(report).toContain('## Suggested Structure');
104
+ });
105
+ });
106
+ });