scss-variable-extractor 1.1.0 → 1.5.1

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
@@ -4,11 +4,13 @@ const { Command } = require('commander');
4
4
  const chalk = require('chalk');
5
5
  const path = require('path');
6
6
  const { loadConfig } = require('../src/config');
7
- const { scanScssFiles } = require('../src/scanner');
7
+ const { scanScssFiles, scanTemplateFiles } = require('../src/scanner');
8
8
  const { parseScss } = require('../src/parser');
9
9
  const { analyzeValues } = require('../src/analyzer');
10
10
  const { generateVariablesFile, generateReport } = require('../src/generator');
11
11
  const { refactorScssFiles } = require('../src/refactorer');
12
+ const { analyzeAngularPatterns, refactorAngularPatterns, generateAngularPatternReport } = require('../src/ng-refactorer');
13
+ const { detectBootstrap, migrateBootstrapToMaterial, generateBootstrapReport } = require('../src/bootstrap-migrator');
12
14
 
13
15
  const program = new Command();
14
16
 
@@ -21,21 +23,35 @@ program
21
23
  program
22
24
  .command('analyze')
23
25
  .description('Dry-run analysis - identifies repeated values without modifying files')
24
- .argument('<src>', 'Source directory to scan')
26
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
25
27
  .option('--threshold <number>', 'Minimum repeat count threshold', parseInt)
26
28
  .option('--format <format>', 'Report format (table, json, markdown)', 'table')
27
29
  .option('--config <path>', 'Path to config file')
30
+ .option('--angular-json <path>', 'Path to angular.json file')
31
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
32
+ .option('--no-angular', 'Disable angular.json integration')
28
33
  .action(async (src, options) => {
29
34
  try {
30
35
  console.log(chalk.cyan.bold('\nšŸ” SCSS Variable Extraction Analysis\n'));
31
36
 
32
- const config = loadConfig(options.config);
37
+ const config = loadConfig(options.config, {
38
+ useAngularJson: options.angular !== false,
39
+ angularJsonPath: options.angularJson,
40
+ projectName: options.project
41
+ });
33
42
 
34
43
  // Override config with command-line options
35
- config.src = src;
44
+ if (src) config.src = src;
36
45
  if (options.threshold) config.threshold = options.threshold;
37
46
  if (options.format) config.reportFormat = options.format;
38
47
 
48
+ // Show Angular project info if available
49
+ if (config.angular) {
50
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
51
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
52
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
53
+ }
54
+
39
55
  console.log(chalk.gray(`Scanning: ${config.src}`));
40
56
  console.log(chalk.gray(`Threshold: ${config.threshold} occurrences\n`));
41
57
 
@@ -85,21 +101,35 @@ program
85
101
  program
86
102
  .command('generate')
87
103
  .description('Generate variables file only (does not modify existing SCSS files)')
88
- .argument('<src>', 'Source directory to scan')
104
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
89
105
  .option('--output <path>', 'Output path for variables file')
90
106
  .option('--threshold <number>', 'Minimum repeat count threshold', parseInt)
91
107
  .option('--config <path>', 'Path to config file')
108
+ .option('--angular-json <path>', 'Path to angular.json file')
109
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
110
+ .option('--no-angular', 'Disable angular.json integration')
92
111
  .action(async (src, options) => {
93
112
  try {
94
113
  console.log(chalk.cyan.bold('\nšŸ“ Generating SCSS Variables File\n'));
95
114
 
96
- const config = loadConfig(options.config);
115
+ const config = loadConfig(options.config, {
116
+ useAngularJson: options.angular !== false,
117
+ angularJsonPath: options.angularJson,
118
+ projectName: options.project
119
+ });
97
120
 
98
121
  // Override config with command-line options
99
- config.src = src;
122
+ if (src) config.src = src;
100
123
  if (options.output) config.output = options.output;
101
124
  if (options.threshold) config.threshold = options.threshold;
102
125
 
126
+ // Show Angular project info if available
127
+ if (config.angular) {
128
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
129
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
130
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
131
+ }
132
+
103
133
  console.log(chalk.gray(`Scanning: ${config.src}`));
104
134
  console.log(chalk.gray(`Output: ${config.output}\n`));
105
135
 
@@ -154,21 +184,35 @@ program
154
184
  program
155
185
  .command('refactor')
156
186
  .description('Full extraction + replacement (generates variables file and refactors SCSS files)')
157
- .argument('<src>', 'Source directory to scan')
187
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
158
188
  .option('--output <path>', 'Output path for variables file')
159
189
  .option('--threshold <number>', 'Minimum repeat count threshold', parseInt)
160
190
  .option('--config <path>', 'Path to config file')
191
+ .option('--angular-json <path>', 'Path to angular.json file')
192
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
193
+ .option('--no-angular', 'Disable angular.json integration')
161
194
  .action(async (src, options) => {
162
195
  try {
163
196
  console.log(chalk.cyan.bold('\nšŸ”§ SCSS Refactoring - Full Extraction\n'));
164
197
 
165
- const config = loadConfig(options.config);
198
+ const config = loadConfig(options.config, {
199
+ useAngularJson: options.angular !== false,
200
+ angularJsonPath: options.angularJson,
201
+ projectName: options.project
202
+ });
166
203
 
167
204
  // Override config with command-line options
168
- config.src = src;
205
+ if (src) config.src = src;
169
206
  if (options.output) config.output = options.output;
170
207
  if (options.threshold) config.threshold = options.threshold;
171
208
 
209
+ // Show Angular project info if available
210
+ if (config.angular) {
211
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
212
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
213
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
214
+ }
215
+
172
216
  console.log(chalk.gray(`Scanning: ${config.src}`));
173
217
  console.log(chalk.gray(`Output: ${config.output}\n`));
174
218
 
@@ -223,4 +267,326 @@ program
223
267
  }
224
268
  });
225
269
 
270
+ // Analyze Angular patterns command (ng-deep, !important)
271
+ program
272
+ .command('analyze-patterns')
273
+ .description('Analyze SCSS files for Angular Material v15+ anti-patterns (::ng-deep, !important)')
274
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
275
+ .option('--format <format>', 'Report format (table, json, markdown)', 'table')
276
+ .option('--config <path>', 'Path to config file')
277
+ .option('--angular-json <path>', 'Path to angular.json file')
278
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
279
+ .option('--no-angular', 'Disable angular.json integration')
280
+ .action(async (src, options) => {
281
+ try {
282
+ console.log(chalk.cyan.bold('\nšŸ” Angular Anti-Pattern Analysis\n'));
283
+
284
+ const config = loadConfig(options.config, {
285
+ useAngularJson: options.angular !== false,
286
+ angularJsonPath: options.angularJson,
287
+ projectName: options.project
288
+ });
289
+ if (src) config.src = src;
290
+
291
+ // Show Angular project info if available
292
+ if (config.angular) {
293
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
294
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
295
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
296
+ }
297
+
298
+ console.log(chalk.gray(`Scanning: ${config.src}\n`));
299
+
300
+ // Scan files
301
+ const files = await scanScssFiles(config.src, config.ignore);
302
+ console.log(chalk.green(`āœ“ Found ${files.length} SCSS files\n`));
303
+
304
+ // Analyze patterns
305
+ const analysis = analyzeAngularPatterns(files);
306
+
307
+ // Generate report
308
+ const report = generateAngularPatternReport(analysis, options.format);
309
+ console.log(report);
310
+
311
+ // Exit with error code if issues found
312
+ if (analysis.summary.ngDeepCount > 0 || analysis.summary.importantCount > 0) {
313
+ console.log(chalk.yellow('⚠ Anti-patterns detected. Run "modernize" command to fix them.\n'));
314
+ } else {
315
+ console.log(chalk.green('āœ“ No anti-patterns detected!\n'));
316
+ }
317
+
318
+ } catch (error) {
319
+ console.error(chalk.red('āœ— Error:'), error.message);
320
+ process.exit(1);
321
+ }
322
+ });
323
+
324
+ // Modernize command (refactor ng-deep and !important)
325
+ program
326
+ .command('modernize')
327
+ .description('Refactor SCSS files following Angular Material v15+ best practices')
328
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
329
+ .option('--global-styles <path>', 'Path to global styles file', './src/styles.scss')
330
+ .option('--no-ng-deep', 'Skip ::ng-deep refactoring')
331
+ .option('--no-important', 'Skip !important refactoring')
332
+ .option('--dry-run', 'Preview changes without modifying files')
333
+ .option('--config <path>', 'Path to config file')
334
+ .option('--angular-json <path>', 'Path to angular.json file')
335
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
336
+ .option('--no-angular', 'Disable angular.json integration')
337
+ .action(async (src, options) => {
338
+ try {
339
+ console.log(chalk.cyan.bold('\nšŸ”§ Modernizing SCSS Files (Angular Material v15+ Best Practices)\n'));
340
+
341
+ const config = loadConfig(options.config, {
342
+ useAngularJson: options.angular !== false,
343
+ angularJsonPath: options.angularJson,
344
+ projectName: options.project
345
+ });
346
+ if (src) config.src = src;
347
+
348
+ if (options.dryRun) {
349
+ console.log(chalk.yellow('DRY RUN MODE - No files will be modified\n'));
350
+ }
351
+
352
+ // Show Angular project info if available
353
+ if (config.angular) {
354
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
355
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
356
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
357
+ }
358
+
359
+ console.log(chalk.gray(`Scanning: ${config.src}`));
360
+ console.log(chalk.gray(`Global styles: ${options.globalStyles}\n`));
361
+
362
+ // Scan files
363
+ const files = await scanScssFiles(config.src, config.ignore);
364
+ console.log(chalk.green(`āœ“ Found ${files.length} SCSS files\n`));
365
+
366
+ // Refactor patterns
367
+ const results = refactorAngularPatterns(files, {
368
+ removeNgDeep: options.ngDeep !== false,
369
+ removeImportant: options.important !== false,
370
+ createGlobalStyles: true,
371
+ globalStylesPath: options.globalStyles,
372
+ dryRun: options.dryRun
373
+ });
374
+
375
+ // Show results
376
+ if (results.modified.length > 0) {
377
+ console.log(chalk.green(`āœ“ Modified ${results.modified.length} file(s)\n`));
378
+
379
+ results.modified.forEach(file => {
380
+ console.log(chalk.white(` ${path.basename(file.file)}:`));
381
+ file.changes.forEach(change => {
382
+ if (change.type === 'ng-deep-removed') {
383
+ console.log(chalk.yellow(` - Removed ${change.count} ::ng-deep occurrence(s)`));
384
+ } else if (change.type === 'important-removed') {
385
+ console.log(chalk.yellow(` - Removed ${change.count} !important declaration(s)`));
386
+ }
387
+ });
388
+ });
389
+ console.log();
390
+ }
391
+
392
+ if (results.globalStyles.length > 0) {
393
+ console.log(chalk.cyan(`āœ“ Extracted ${results.globalStyles.length} style(s) to ${options.globalStyles}\n`));
394
+ }
395
+
396
+ if (results.warnings.length > 0) {
397
+ console.log(chalk.yellow.bold('⚠ Warnings:\n'));
398
+ results.warnings.forEach(warning => {
399
+ console.log(chalk.yellow(` ${warning.message}`));
400
+ });
401
+ console.log();
402
+ }
403
+
404
+ if (results.modified.length === 0) {
405
+ console.log(chalk.green('āœ“ No changes needed - files already follow best practices!\n'));
406
+ } else if (!options.dryRun) {
407
+ console.log(chalk.bold.green('āœ“ Modernization complete!\n'));
408
+ console.log(chalk.gray('Next steps:'));
409
+ console.log(chalk.gray(' 1. Review the modified files'));
410
+ console.log(chalk.gray(' 2. Test your application'));
411
+ console.log(chalk.gray(' 3. Consider using Angular Material theme mixins for component theming'));
412
+ console.log(chalk.gray(' 4. Review extracted global styles and organize as needed\n'));
413
+ }
414
+
415
+ } catch (error) {
416
+ console.error(chalk.red('āœ— Error:'), error.message);
417
+ process.exit(1);
418
+ }
419
+ });
420
+
421
+ // Detect Bootstrap command
422
+ program
423
+ .command('detect-bootstrap')
424
+ .description('Detect Bootstrap classes and analyze migration to Angular Material')
425
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
426
+ .option('--format <format>', 'Report format (table, json, markdown)', 'table')
427
+ .option('--config <path>', 'Path to config file')
428
+ .option('--angular-json <path>', 'Path to angular.json file')
429
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
430
+ .option('--no-angular', 'Disable angular.json integration')
431
+ .action(async (src, options) => {
432
+ try {
433
+ console.log(chalk.cyan.bold('\nšŸ“¦ Bootstrap to Material Migration Analysis\n'));
434
+
435
+ const config = loadConfig(options.config, {
436
+ useAngularJson: options.angular !== false,
437
+ angularJsonPath: options.angularJson,
438
+ projectName: options.project
439
+ });
440
+ if (src) config.src = src;
441
+
442
+ // Show Angular project info if available
443
+ if (config.angular) {
444
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
445
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
446
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
447
+ }
448
+
449
+ console.log(chalk.gray(`Scanning: ${config.src}\n`));
450
+
451
+ // Scan files - look for SCSS, HTML, and TS files
452
+ const allFiles = await scanTemplateFiles(config.src, config.ignore);
453
+ console.log(chalk.green(`āœ“ Found ${allFiles.length} files (SCSS, HTML, TS)\n`));
454
+
455
+ // Detect Bootstrap
456
+ const detection = detectBootstrap(allFiles);
457
+
458
+ // Generate report
459
+ const report = generateBootstrapReport(detection, options.format);
460
+ console.log(report);
461
+
462
+ // Provide migration guidance
463
+ if (detection.summary.totalBootstrapClasses > 0) {
464
+ console.log(chalk.yellow('⚠ Bootstrap classes detected. Use "migrate-bootstrap" command to convert.\n'));
465
+ console.log(chalk.gray('Migration options:'));
466
+ console.log(chalk.gray(' • Components (btn, card, etc.) → Angular Material components'));
467
+ console.log(chalk.gray(' • Utilities (d-flex, spacing, etc.) → Custom utility classes'));
468
+ console.log(chalk.gray(' • Grid system → CSS Grid or Flexbox\n'));
469
+ } else {
470
+ console.log(chalk.green('āœ“ No Bootstrap classes detected!\n'));
471
+ }
472
+
473
+ } catch (error) {
474
+ console.error(chalk.red('āœ— Error:'), error.message);
475
+ process.exit(1);
476
+ }
477
+ });
478
+
479
+ // Migrate Bootstrap command
480
+ program
481
+ .command('migrate-bootstrap')
482
+ .description('Migrate Bootstrap classes to Angular Material MDC-based styles')
483
+ .argument('[src]', 'Source directory to scan (optional if using angular.json)')
484
+ .option('--utilities <path>', 'Path to custom utilities file', './src/styles/utilities.scss')
485
+ .option('--no-custom-utilities', 'Skip creating custom utility classes')
486
+ .option('--no-remove-imports', 'Keep Bootstrap imports')
487
+ .option('--dry-run', 'Preview changes without modifying files')
488
+ .option('--config <path>', 'Path to config file')
489
+ .option('--angular-json <path>', 'Path to angular.json file')
490
+ .option('--project <name>', 'Angular project name (uses default if not specified)')
491
+ .option('--no-angular', 'Disable angular.json integration')
492
+ .action(async (src, options) => {
493
+ try {
494
+ console.log(chalk.cyan.bold('\nšŸ”„ Migrating Bootstrap to Angular Material\n'));
495
+
496
+ const config = loadConfig(options.config, {
497
+ useAngularJson: options.angular !== false,
498
+ angularJsonPath: options.angularJson,
499
+ projectName: options.project
500
+ });
501
+ if (src) config.src = src;
502
+
503
+ if (options.dryRun) {
504
+ console.log(chalk.yellow('DRY RUN MODE - No files will be modified\n'));
505
+ }
506
+
507
+ // Show Angular project info if available
508
+ if (config.angular) {
509
+ console.log(chalk.cyan(`Angular Project: ${config.angular.project}`));
510
+ console.log(chalk.gray(`Prefix: ${config.angular.prefix}`));
511
+ console.log(chalk.gray(`Style: ${config.angular.stylePreprocessor}\n`));
512
+ }
513
+
514
+ console.log(chalk.gray(`Scanning: ${config.src}`));
515
+ console.log(chalk.gray(`Utilities output: ${options.utilities}\n`));
516
+
517
+ // Scan files - look for SCSS, HTML, and TS files
518
+ const allFiles = await scanTemplateFiles(config.src, config.ignore);
519
+ console.log(chalk.green(`āœ“ Found ${allFiles.length} files (SCSS, HTML, TS)\n`));
520
+
521
+ // Migrate Bootstrap
522
+ const results = migrateBootstrapToMaterial(allFiles, {
523
+ createCustomUtilities: options.customUtilities !== false,
524
+ customUtilitiesPath: options.utilities,
525
+ removeBootstrapImports: options.removeImports !== false,
526
+ dryRun: options.dryRun
527
+ });
528
+
529
+ // Show results
530
+ if (results.modified.length > 0) {
531
+ console.log(chalk.green(`āœ“ Modified ${results.modified.length} file(s)\n`));
532
+
533
+ results.modified.slice(0, 10).forEach(file => {
534
+ console.log(chalk.white(` ${path.basename(file.file)}:`));
535
+ file.changes.forEach(change => {
536
+ if (change.type === 'import-removed') {
537
+ console.log(chalk.yellow(` - ${change.description}`));
538
+ }
539
+ });
540
+ });
541
+ if (results.modified.length > 10) {
542
+ console.log(chalk.gray(` ... and ${results.modified.length - 10} more`));
543
+ }
544
+ console.log();
545
+ }
546
+
547
+ if (results.customUtilities.length > 0) {
548
+ console.log(chalk.cyan(`āœ“ Generated ${results.customUtilities.length} custom utility class(es) in ${options.utilities}\n`));
549
+ }
550
+
551
+ if (results.componentMigrations.length > 0) {
552
+ console.log(chalk.yellow(`⚠ ${results.componentMigrations.length} component(s) require manual migration\n`));
553
+ }
554
+
555
+ if (results.warnings.length > 0) {
556
+ console.log(chalk.yellow.bold('⚠ Warnings:\n'));
557
+ results.warnings.slice(0, 5).forEach(warning => {
558
+ console.log(chalk.yellow(` ${warning.message}`));
559
+ if (warning.variables) {
560
+ console.log(chalk.gray(` Variables: ${warning.variables.join(', ')}`));
561
+ }
562
+ });
563
+ if (results.warnings.length > 5) {
564
+ console.log(chalk.gray(` ... and ${results.warnings.length - 5} more warnings`));
565
+ }
566
+ console.log();
567
+ }
568
+
569
+ if (results.modified.length === 0 && results.warnings.length === 0) {
570
+ console.log(chalk.green('āœ“ No Bootstrap code detected!\n'));
571
+ } else if (!options.dryRun) {
572
+ console.log(chalk.bold.green('āœ“ Migration complete!\n'));
573
+ console.log(chalk.gray('Next steps:'));
574
+ console.log(chalk.gray(' 1. Review the modified SCSS files'));
575
+ console.log(chalk.gray(' 2. Update HTML templates to use Angular Material components'));
576
+ console.log(chalk.gray(' 3. Import custom utilities in your styles.scss'));
577
+ console.log(chalk.gray(' 4. Install @angular/material if not already installed'));
578
+ console.log(chalk.gray(' 5. Test your application thoroughly\n'));
579
+ console.log(chalk.cyan('Component Migration Guide:'));
580
+ console.log(chalk.gray(' • .btn → <button mat-button>'));
581
+ console.log(chalk.gray(' • .card → <mat-card>'));
582
+ console.log(chalk.gray(' • .form-control → <mat-form-field><input matInput></mat-form-field>'));
583
+ console.log(chalk.gray(' • See: https://material.angular.io/components\n'));
584
+ }
585
+
586
+ } catch (error) {
587
+ console.error(chalk.red('āœ— Error:'), error.message);
588
+ process.exit(1);
589
+ }
590
+ });
591
+
226
592
  program.parse();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scss-variable-extractor",
3
- "version": "1.1.0",
3
+ "version": "1.5.1",
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"
@@ -10,7 +10,11 @@
10
10
  "test": "jest",
11
11
  "analyze": "node bin/cli.js analyze .",
12
12
  "generate": "node bin/cli.js generate .",
13
- "refactor": "node bin/cli.js refactor ."
13
+ "refactor": "node bin/cli.js refactor .",
14
+ "analyze-patterns": "node bin/cli.js analyze-patterns .",
15
+ "modernize": "node bin/cli.js modernize .",
16
+ "detect-bootstrap": "node bin/cli.js detect-bootstrap .",
17
+ "migrate-bootstrap": "node bin/cli.js migrate-bootstrap ."
14
18
  },
15
19
  "keywords": [
16
20
  "angular",