scss-variable-extractor 1.4.0 → 1.5.2

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/README.md CHANGED
@@ -342,7 +342,8 @@ npx scss-extract migrate-bootstrap ./src --custom-utilities ./src/utilities.scss
342
342
  - `[src]` - Source directory to scan (optional, auto-detected from angular.json)
343
343
 
344
344
  **Options:**
345
- - `--custom-utilities <path>` - Path for custom utilities file (default: `./src/utilities.scss`)
345
+ - `--custom-utilities <path>` - Path for custom utilities file (default: `./src/styles/utilities.scss`)
346
+ - `--no-custom-utilities` - Skip creating custom utility classes
346
347
  - `--dry-run` - Preview changes without modifying files
347
348
  - `--angular-json <path>` - Path to angular.json file
348
349
  - `--project <name>` - Specify which project to use
@@ -408,7 +409,13 @@ npx scss-extract migrate-bootstrap --dry-run
408
409
  # 3. Apply migration
409
410
  npx scss-extract migrate-bootstrap
410
411
 
411
- # 4. Review changes and test components
412
+ # 4. Apply migration with custom utilities path
413
+ npx scss-extract migrate-bootstrap --custom-utilities ./custom/utilities.scss
414
+
415
+ # 5. Skip utilities generation
416
+ npx scss-extract migrate-bootstrap --no-custom-utilities
417
+
418
+ # 6. Review changes and test components
412
419
  # Manual review of generated utilities.scss and component templates
413
420
  ```
414
421
 
@@ -701,6 +708,55 @@ MIT License - see [LICENSE](./LICENSE) file for details
701
708
 
702
709
  ## Changelog
703
710
 
711
+ ### 1.5.2 (2026-02-12)
712
+
713
+ - **Fixed:** `migrate-bootstrap` command option renamed from `--utilities` to `--custom-utilities` to work with `--no-custom-utilities`
714
+ - **Fixed:** Default utilities path corrected to `./src/styles/utilities.scss`
715
+
716
+ ### 1.5.1 (2026-02-12)
717
+
718
+ - **Enhanced:** HTML class detection now handles spaces around `=` (e.g., `class = "py-3"`)
719
+ - **Added:** Support for backtick quotes in class attributes (e.g., `class=\`py-3\``)
720
+ - **Added:** Support for Angular property binding (e.g., `[class]="py-3"`)
721
+ - **Improved:** More robust Bootstrap class detection across different HTML/template formats
722
+
723
+ ### 1.5.0 (2026-02-12)
724
+
725
+ - **Fixed:** Bootstrap detection now scans SCSS class selectors (`.py-3`, `.mt-4`, etc.)
726
+ - **Enhanced:** Bootstrap commands now scan SCSS, HTML, and TypeScript files
727
+ - **Added:** New `scanTemplateFiles` function for comprehensive file scanning
728
+ - **Improved:** Bootstrap class detection with deduplication
729
+
730
+ ### 1.4.0 (2026-02-12)
731
+
732
+ - **Added:** Bootstrap to Angular Material migration capability
733
+ - **Added:** `detect-bootstrap` command for analyzing Bootstrap usage
734
+ - **Added:** `migrate-bootstrap` command for converting Bootstrap classes
735
+ - **Added:** Custom utilities generation for flex/grid/spacing classes
736
+ - **Added:** Material Design spacing scale (0, 4px, 8px, 16px, 24px, 48px)
737
+ - 100+ Bootstrap class mappings
738
+
739
+ ### 1.3.0 (2026-02-12)
740
+
741
+ - **Added:** Angular.json integration for automatic project configuration
742
+ - **Added:** Auto-detection of source directory and style preprocessor
743
+ - **Added:** Support for multi-project Angular workspaces
744
+ - All commands support `--angular-json`, `--project`, `--no-angular` options
745
+
746
+ ### 1.2.0 (2026-02-12)
747
+
748
+ - **Added:** Angular Material v15+ pattern refactoring
749
+ - **Added:** `analyze-patterns` command to detect anti-patterns
750
+ - **Added:** `modernize` command to remove `::ng-deep` and `!important`
751
+ - Best practice enforcement with migration suggestions
752
+
753
+ ### 1.1.0 (2026-02-12)
754
+
755
+ - **Added:** Global installation support via npm link/publish
756
+ - **Added:** Positional `[src]` argument for all commands
757
+ - **Enhanced:** Scan any folder in your project
758
+ - **Improved:** CLI help and documentation
759
+
704
760
  ### 1.0.0 (2026-02-12)
705
761
 
706
762
  - Initial release
package/bin/cli.js CHANGED
@@ -4,7 +4,7 @@ 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');
@@ -448,19 +448,9 @@ program
448
448
 
449
449
  console.log(chalk.gray(`Scanning: ${config.src}\n`));
450
450
 
451
- // Scan files - look for HTML and SCSS files
452
- const scssFiles = await scanScssFiles(config.src, config.ignore);
453
- const htmlPattern = path.join(config.src, '**/*.html').replace(/\\/g, '/');
454
- const glob = require('glob');
455
- const htmlFiles = await new Promise((resolve, reject) => {
456
- glob(htmlPattern, { ignore: config.ignore }, (err, files) => {
457
- if (err) reject(err);
458
- else resolve(files.map(f => path.resolve(f)));
459
- });
460
- });
461
-
462
- const allFiles = [...scssFiles, ...htmlFiles];
463
- console.log(chalk.green(`✓ Found ${scssFiles.length} SCSS and ${htmlFiles.length} HTML files\n`));
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`));
464
454
 
465
455
  // Detect Bootstrap
466
456
  const detection = detectBootstrap(allFiles);
@@ -491,7 +481,7 @@ program
491
481
  .command('migrate-bootstrap')
492
482
  .description('Migrate Bootstrap classes to Angular Material MDC-based styles')
493
483
  .argument('[src]', 'Source directory to scan (optional if using angular.json)')
494
- .option('--utilities <path>', 'Path to custom utilities file', './src/styles/utilities.scss')
484
+ .option('--custom-utilities <path>', 'Path to custom utilities file', './src/styles/utilities.scss')
495
485
  .option('--no-custom-utilities', 'Skip creating custom utility classes')
496
486
  .option('--no-remove-imports', 'Keep Bootstrap imports')
497
487
  .option('--dry-run', 'Preview changes without modifying files')
@@ -522,26 +512,16 @@ program
522
512
  }
523
513
 
524
514
  console.log(chalk.gray(`Scanning: ${config.src}`));
525
- console.log(chalk.gray(`Utilities output: ${options.utilities}\n`));
526
-
527
- // Scan files
528
- const scssFiles = await scanScssFiles(config.src, config.ignore);
529
- const htmlPattern = path.join(config.src, '**/*.html').replace(/\\/g, '/');
530
- const glob = require('glob');
531
- const htmlFiles = await new Promise((resolve, reject) => {
532
- glob(htmlPattern, { ignore: config.ignore }, (err, files) => {
533
- if (err) reject(err);
534
- else resolve(files.map(f => path.resolve(f)));
535
- });
536
- });
515
+ console.log(chalk.gray(`Utilities output: ${options.customUtilities || './src/styles/utilities.scss'}\n`));
537
516
 
538
- const allFiles = [...scssFiles, ...htmlFiles];
539
- console.log(chalk.green(`✓ Found ${scssFiles.length} SCSS and ${htmlFiles.length} HTML files\n`));
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`));
540
520
 
541
521
  // Migrate Bootstrap
542
522
  const results = migrateBootstrapToMaterial(allFiles, {
543
523
  createCustomUtilities: options.customUtilities !== false,
544
- customUtilitiesPath: options.utilities,
524
+ customUtilitiesPath: options.customUtilities || './src/styles/utilities.scss',
545
525
  removeBootstrapImports: options.removeImports !== false,
546
526
  dryRun: options.dryRun
547
527
  });
@@ -565,7 +545,7 @@ program
565
545
  }
566
546
 
567
547
  if (results.customUtilities.length > 0) {
568
- console.log(chalk.cyan(`✓ Generated ${results.customUtilities.length} custom utility class(es) in ${options.utilities}\n`));
548
+ console.log(chalk.cyan(`✓ Generated ${results.customUtilities.length} custom utility class(es) in ${options.customUtilities || './src/styles/utilities.scss'}\n`));
569
549
  }
570
550
 
571
551
  if (results.componentMigrations.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scss-variable-extractor",
3
- "version": "1.4.0",
3
+ "version": "1.5.2",
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"
@@ -160,28 +160,60 @@ function detectBootstrapInFile(content, filePath) {
160
160
  utilities: 0
161
161
  };
162
162
 
163
- // Detect class attributes in templates (if included in SCSS via ::ng-deep)
164
- const classRegex = /class=["']([^"']+)["']/g;
165
- let match;
163
+ const foundClasses = new Set();
164
+
165
+ // Detect class attributes in HTML/TS templates (with optional whitespace)
166
+ // Supports: class="...", class='...', class=`...`, [class]="..."
167
+ const classPatterns = [
168
+ /class\s*=\s*["']([^"']+)["']/g,
169
+ /class\s*=\s*`([^`]+)`/g,
170
+ /\[class\]\s*=\s*["']([^"']+)["']/g
171
+ ];
166
172
 
167
- while ((match = classRegex.exec(content)) !== null) {
168
- const classes = match[1].split(/\s+/);
169
- classes.forEach(cls => {
170
- if (isBootstrapClass(cls)) {
171
- const mapping = getBootstrapMapping(cls);
172
- detected.classes.push({
173
- class: cls,
174
- line: getLineNumber(content, match.index),
175
- mapping: mapping,
176
- type: mapping.component ? 'component' : 'utility'
177
- });
178
-
179
- if (mapping.material) detected.materialMappable++;
180
- if (mapping.needsCustom) detected.needsCustomStyles++;
181
- if (mapping.component) detected.components++;
182
- else detected.utilities++;
183
- }
184
- });
173
+ classPatterns.forEach(classRegex => {
174
+ let match;
175
+ while ((match = classRegex.exec(content)) !== null) {
176
+ const classes = match[1].split(/\s+/);
177
+ classes.forEach(cls => {
178
+ if (cls && isBootstrapClass(cls) && !foundClasses.has(cls)) {
179
+ foundClasses.add(cls);
180
+ const mapping = getBootstrapMapping(cls);
181
+ detected.classes.push({
182
+ class: cls,
183
+ line: getLineNumber(content, match.index),
184
+ mapping: mapping,
185
+ type: mapping.component ? 'component' : 'utility'
186
+ });
187
+
188
+ if (mapping.material) detected.materialMappable++;
189
+ if (mapping.needsCustom) detected.needsCustomStyles++;
190
+ if (mapping.component) detected.components++;
191
+ else detected.utilities++;
192
+ }
193
+ });
194
+ }
195
+ });
196
+
197
+ // Detect CSS class selectors in SCSS files (e.g., .py-3, .mt-4)
198
+ const selectorRegex = /\.([a-z][a-z0-9-]*)/gi;
199
+ let match;
200
+ while ((match = selectorRegex.exec(content)) !== null) {
201
+ const cls = match[1];
202
+ if (cls && isBootstrapClass(cls) && !foundClasses.has(cls)) {
203
+ foundClasses.add(cls);
204
+ const mapping = getBootstrapMapping(cls);
205
+ detected.classes.push({
206
+ class: cls,
207
+ line: getLineNumber(content, match.index),
208
+ mapping: mapping,
209
+ type: mapping.component ? 'component' : 'utility'
210
+ });
211
+
212
+ if (mapping.material) detected.materialMappable++;
213
+ if (mapping.needsCustom) detected.needsCustomStyles++;
214
+ if (mapping.component) detected.components++;
215
+ else detected.utilities++;
216
+ }
185
217
  }
186
218
 
187
219
  // Detect Bootstrap variables
package/src/scanner.js CHANGED
@@ -25,6 +25,31 @@ async function scanScssFiles(srcPath, ignorePatterns = []) {
25
25
  });
26
26
  }
27
27
 
28
+ /**
29
+ * Recursively scans for SCSS and template files (HTML, TS) in the given source directory
30
+ * @param {string} srcPath - Source directory to scan
31
+ * @param {Array<string>} ignorePatterns - Patterns to ignore
32
+ * @returns {Promise<Array<string>>} - Array of absolute file paths
33
+ */
34
+ async function scanTemplateFiles(srcPath, ignorePatterns = []) {
35
+ return new Promise((resolve, reject) => {
36
+ const pattern = path.join(srcPath, '**/*.{scss,html,ts}').replace(/\\/g, '/');
37
+
38
+ glob(pattern, {
39
+ ignore: ignorePatterns
40
+ }, (err, files) => {
41
+ if (err) {
42
+ reject(err);
43
+ } else {
44
+ // Ensure absolute paths
45
+ const absoluteFiles = files.map(f => path.resolve(f));
46
+ resolve(absoluteFiles);
47
+ }
48
+ });
49
+ });
50
+ }
51
+
28
52
  module.exports = {
29
- scanScssFiles
53
+ scanScssFiles,
54
+ scanTemplateFiles
30
55
  };
@@ -77,6 +77,44 @@ describe('Bootstrap Migrator', () => {
77
77
  expect(results.summary.filesWithBootstrap).toBe(0);
78
78
  expect(results.summary.totalBootstrapClasses).toBe(0);
79
79
  });
80
+
81
+ test('should detect Bootstrap classes in SCSS selectors', () => {
82
+ // Create a temp SCSS file with Bootstrap class selectors
83
+ const tempScssPath = path.join(__dirname, 'fixtures/apps/subapp/src/app/bootstrap-component/temp-scss-test.scss');
84
+ const scssContent = `
85
+ .component {
86
+ .py-3 {
87
+ color: blue;
88
+ }
89
+
90
+ .mt-4 {
91
+ background: red;
92
+ }
93
+
94
+ .d-flex {
95
+ display: flex;
96
+ }
97
+ }
98
+ `;
99
+ fs.writeFileSync(tempScssPath, scssContent, 'utf8');
100
+
101
+ try {
102
+ const results = detectBootstrap([tempScssPath]);
103
+
104
+ expect(results.summary.filesWithBootstrap).toBe(1);
105
+ expect(results.summary.totalBootstrapClasses).toBeGreaterThan(0);
106
+
107
+ const classNames = results.files[0].classes.map(c => c.class);
108
+ expect(classNames).toContain('py-3');
109
+ expect(classNames).toContain('mt-4');
110
+ expect(classNames).toContain('d-flex');
111
+ } finally {
112
+ // Clean up
113
+ if (fs.existsSync(tempScssPath)) {
114
+ fs.unlinkSync(tempScssPath);
115
+ }
116
+ }
117
+ });
80
118
  });
81
119
 
82
120
  describe('migrateBootstrapToMaterial', () => {
@@ -1,4 +1,4 @@
1
- const { scanScssFiles } = require('../src/scanner');
1
+ const { scanScssFiles, scanTemplateFiles } = require('../src/scanner');
2
2
  const path = require('path');
3
3
 
4
4
  describe('Scanner', () => {
@@ -22,4 +22,11 @@ describe('Scanner', () => {
22
22
 
23
23
  expect(files.every(f => path.isAbsolute(f))).toBe(true);
24
24
  });
25
+
26
+ test('should find template files (SCSS, HTML, TS) in directory', async () => {
27
+ const files = await scanTemplateFiles(fixturesPath);
28
+
29
+ expect(files.length).toBeGreaterThan(0);
30
+ expect(files.every(f => f.endsWith('.scss') || f.endsWith('.html') || f.endsWith('.ts'))).toBe(true);
31
+ });
25
32
  });