@ui-entropy/scanner-core 0.1.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/dist/crawl.js ADDED
@@ -0,0 +1,153 @@
1
+ import fg from 'fast-glob';
2
+ import { readFileSync } from 'fs';
3
+ // import ignore from 'ignore';
4
+ import { resolve, join } from 'path';
5
+ const DEFAULT_CSS_PATTERNS = ['**/*.css', '**/*.scss', '**/*.sass'];
6
+ const DEFAULT_SOURCE_PATTERNS = [
7
+ '**/*.html',
8
+ '**/*.tsx',
9
+ '**/*.ts',
10
+ '**/*.jsx',
11
+ '**/*.js',
12
+ '**/*.vue',
13
+ '**/*.svelte',
14
+ ];
15
+ const DEFAULT_IGNORE_PATTERNS = [
16
+ 'node_modules/**',
17
+ 'dist/**',
18
+ 'build/**',
19
+ '.git/**',
20
+ 'coverage/**',
21
+ '.next/**',
22
+ '.nuxt/**',
23
+ 'out/**',
24
+ 'public/**', // Usually static assets, not source
25
+ '**/*.min.css',
26
+ '**/*.min.js',
27
+ ];
28
+ /**
29
+ * Read and parse .gitignore file
30
+ */
31
+ function readGitignore(baseDir) {
32
+ try {
33
+ const gitignorePath = join(baseDir, '.gitignore');
34
+ const content = readFileSync(gitignorePath, 'utf-8');
35
+ return content
36
+ .split('\n')
37
+ .map(line => line.trim())
38
+ .filter(line => line && !line.startsWith('#'))
39
+ .map(line => {
40
+ // Convert gitignore patterns to glob patterns
41
+ if (line.endsWith('/')) {
42
+ return line + '**';
43
+ }
44
+ return line;
45
+ });
46
+ }
47
+ catch {
48
+ return [];
49
+ }
50
+ }
51
+ /**
52
+ * Crawl directory for CSS and source files
53
+ */
54
+ export async function crawl(options) {
55
+ const { baseDir, cssPatterns = DEFAULT_CSS_PATTERNS, sourcePatterns = DEFAULT_SOURCE_PATTERNS, ignorePatterns = DEFAULT_IGNORE_PATTERNS, respectGitignore = true, } = options;
56
+ const absoluteBaseDir = resolve(baseDir);
57
+ // Build ignore patterns
58
+ let allIgnorePatterns = [...ignorePatterns];
59
+ if (respectGitignore) {
60
+ const gitignorePatterns = readGitignore(absoluteBaseDir);
61
+ allIgnorePatterns = [...allIgnorePatterns, ...gitignorePatterns];
62
+ }
63
+ // Find CSS files
64
+ const cssPaths = await fg(cssPatterns, {
65
+ cwd: absoluteBaseDir,
66
+ ignore: allIgnorePatterns,
67
+ absolute: false,
68
+ onlyFiles: true,
69
+ });
70
+ // Find source files
71
+ const sourcePaths = await fg(sourcePatterns, {
72
+ cwd: absoluteBaseDir,
73
+ ignore: allIgnorePatterns,
74
+ absolute: false,
75
+ onlyFiles: true,
76
+ });
77
+ // Read file contents
78
+ const cssFiles = cssPaths.map(path => {
79
+ const absolutePath = join(absoluteBaseDir, path);
80
+ const content = readFileSync(absolutePath, 'utf-8');
81
+ return { path, content };
82
+ });
83
+ const sourceFiles = sourcePaths.map(path => {
84
+ const absolutePath = join(absoluteBaseDir, path);
85
+ const content = readFileSync(absolutePath, 'utf-8');
86
+ return { path, content };
87
+ });
88
+ // Calculate stats
89
+ const totalCssBytes = cssFiles.reduce((sum, f) => sum + Buffer.byteLength(f.content), 0);
90
+ const totalSourceBytes = sourceFiles.reduce((sum, f) => sum + Buffer.byteLength(f.content), 0);
91
+ return {
92
+ cssFiles,
93
+ sourceFiles,
94
+ stats: {
95
+ totalCssFiles: cssFiles.length,
96
+ totalSourceFiles: sourceFiles.length,
97
+ totalCssBytes,
98
+ totalSourceBytes,
99
+ },
100
+ };
101
+ }
102
+ /**
103
+ * Synchronous version of crawl (useful for CLI/simple use cases)
104
+ */
105
+ export function crawlSync(options) {
106
+ const { baseDir, cssPatterns = DEFAULT_CSS_PATTERNS, sourcePatterns = DEFAULT_SOURCE_PATTERNS, ignorePatterns = DEFAULT_IGNORE_PATTERNS, respectGitignore = true, } = options;
107
+ const absoluteBaseDir = resolve(baseDir);
108
+ // Build ignore patterns
109
+ let allIgnorePatterns = [...ignorePatterns];
110
+ if (respectGitignore) {
111
+ const gitignorePatterns = readGitignore(absoluteBaseDir);
112
+ allIgnorePatterns = [...allIgnorePatterns, ...gitignorePatterns];
113
+ }
114
+ // Find CSS files (sync)
115
+ const cssPaths = fg.sync(cssPatterns, {
116
+ cwd: absoluteBaseDir,
117
+ ignore: allIgnorePatterns,
118
+ absolute: false,
119
+ onlyFiles: true,
120
+ });
121
+ // Find source files (sync)
122
+ const sourcePaths = fg.sync(sourcePatterns, {
123
+ cwd: absoluteBaseDir,
124
+ ignore: allIgnorePatterns,
125
+ absolute: false,
126
+ onlyFiles: true,
127
+ });
128
+ // Read file contents
129
+ const cssFiles = cssPaths.map(path => {
130
+ const absolutePath = join(absoluteBaseDir, path);
131
+ const content = readFileSync(absolutePath, 'utf-8');
132
+ return { path, content };
133
+ });
134
+ const sourceFiles = sourcePaths.map(path => {
135
+ const absolutePath = join(absoluteBaseDir, path);
136
+ const content = readFileSync(absolutePath, 'utf-8');
137
+ return { path, content };
138
+ });
139
+ // Calculate stats
140
+ const totalCssBytes = cssFiles.reduce((sum, f) => sum + Buffer.byteLength(f.content), 0);
141
+ const totalSourceBytes = sourceFiles.reduce((sum, f) => sum + Buffer.byteLength(f.content), 0);
142
+ return {
143
+ cssFiles,
144
+ sourceFiles,
145
+ stats: {
146
+ totalCssFiles: cssFiles.length,
147
+ totalSourceFiles: sourceFiles.length,
148
+ totalCssBytes,
149
+ totalSourceBytes,
150
+ },
151
+ };
152
+ }
153
+ //# sourceMappingURL=crawl.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Extracts CSS class and ID usage from source code files.
3
+ * Supports HTML, JSX/TSX, Vue, and Angular templates.
4
+ */
5
+ export interface UsageExtractionResult {
6
+ classes: Set<string>;
7
+ ids: Set<string>;
8
+ filesScanned: number;
9
+ }
10
+ /**
11
+ * Extract used classes from a single source file
12
+ *
13
+ * @param sourceText - Contents of HTML/JSX/Vue/Angular file
14
+ * @returns Set of class names found in the source
15
+ */
16
+ export declare function extractUsedClasses(sourceText: string): Set<string>;
17
+ /**
18
+ * Extract used IDs from a single source file
19
+ *
20
+ * @param sourceText - Contents of HTML/JSX/Vue/Angular file
21
+ * @returns Set of ID names found in the source
22
+ */
23
+ export declare function extractUsedIds(sourceText: string): Set<string>;
24
+ /**
25
+ * Extract all usage from a single source file
26
+ *
27
+ * @param sourceText - Contents of source file
28
+ * @returns Classes and IDs found
29
+ */
30
+ export declare function extractUsage(sourceText: string): Omit<UsageExtractionResult, 'filesScanned'>;
31
+ /**
32
+ * Extract usage from multiple source files
33
+ *
34
+ * @param sourceFiles - Array of source file contents
35
+ * @returns Combined usage across all files
36
+ */
37
+ export declare function extractUsageFromMultiple(sourceFiles: string[]): UsageExtractionResult;
38
+ //# sourceMappingURL=extract-usage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extract-usage.d.ts","sourceRoot":"","sources":["../src/extract-usage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AA4CD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAyBlE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAiB9D;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC,qBAAqB,EAAE,cAAc,CAAC,CAK5F;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,qBAAqB,CAerF"}
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Extracts CSS class and ID usage from source code files.
3
+ * Supports HTML, JSX/TSX, Vue, and Angular templates.
4
+ */
5
+ /**
6
+ * Patterns for extracting class names from different file types
7
+ */
8
+ const CLASS_PATTERNS = [
9
+ // HTML: class="btn primary"
10
+ /class="([^"]+)"/g,
11
+ /class='([^']+)'/g,
12
+ // JSX/TSX: className="btn" or className={'btn'}
13
+ /className="([^"]+)"/g,
14
+ /className='([^']+)'/g,
15
+ /className=\{["']([^"']+)["']\}/g,
16
+ // Vue: :class="..." or class="..." (both in template and data)
17
+ /:class="([^"]+)"/g,
18
+ /:class='([^']+)'/g,
19
+ /cardClass:\s*["']([^"']+)["']/g, // Vue data properties
20
+ // Angular: [class.btn]="true" or class="btn"
21
+ /\[class\.([^\]]+)\]/g,
22
+ // Tailwind clsx/classnames patterns: clsx("btn", "primary")
23
+ /clsx\(["']([^"']+)["']/g,
24
+ /classnames\(["']([^"']+)["']/g,
25
+ /cn\(["']([^"']+)["']/g,
26
+ // Generic utility function calls with class strings
27
+ /["']([a-z][\w-]*(?:\s+[a-z][\w-]*)+)["']/g, // Multi-word class strings
28
+ ];
29
+ /**
30
+ * Patterns for extracting IDs
31
+ */
32
+ const ID_PATTERNS = [
33
+ // HTML: id="main"
34
+ /id="([^"]+)"/g,
35
+ /id='([^']+)'/g,
36
+ // JSX/TSX: id="main"
37
+ /id=\{["']([^"']+)["']\}/g,
38
+ ];
39
+ /**
40
+ * Extract used classes from a single source file
41
+ *
42
+ * @param sourceText - Contents of HTML/JSX/Vue/Angular file
43
+ * @returns Set of class names found in the source
44
+ */
45
+ export function extractUsedClasses(sourceText) {
46
+ const classes = new Set();
47
+ for (const pattern of CLASS_PATTERNS) {
48
+ const matches = sourceText.matchAll(pattern);
49
+ for (const match of matches) {
50
+ const classString = match[1];
51
+ // Split by whitespace to handle multiple classes
52
+ // "btn btn-primary" -> ["btn", "btn-primary"]
53
+ const classNames = classString.split(/\s+/).filter(Boolean);
54
+ classNames.forEach(cls => {
55
+ // Remove common dynamic indicators but keep the base class
56
+ // E.g., "btn-${variant}" -> keep as potential match for "btn-primary"
57
+ // For now, we only keep literal strings
58
+ if (!cls.includes('${') && !cls.includes('{') && !cls.includes('}')) {
59
+ classes.add(cls.trim());
60
+ }
61
+ });
62
+ }
63
+ }
64
+ return classes;
65
+ }
66
+ /**
67
+ * Extract used IDs from a single source file
68
+ *
69
+ * @param sourceText - Contents of HTML/JSX/Vue/Angular file
70
+ * @returns Set of ID names found in the source
71
+ */
72
+ export function extractUsedIds(sourceText) {
73
+ const ids = new Set();
74
+ for (const pattern of ID_PATTERNS) {
75
+ const matches = sourceText.matchAll(pattern);
76
+ for (const match of matches) {
77
+ const idName = match[1];
78
+ // Only keep literal IDs (no template strings)
79
+ if (!idName.includes('${') && !idName.includes('{') && !idName.includes('}')) {
80
+ ids.add(idName.trim());
81
+ }
82
+ }
83
+ }
84
+ return ids;
85
+ }
86
+ /**
87
+ * Extract all usage from a single source file
88
+ *
89
+ * @param sourceText - Contents of source file
90
+ * @returns Classes and IDs found
91
+ */
92
+ export function extractUsage(sourceText) {
93
+ return {
94
+ classes: extractUsedClasses(sourceText),
95
+ ids: extractUsedIds(sourceText),
96
+ };
97
+ }
98
+ /**
99
+ * Extract usage from multiple source files
100
+ *
101
+ * @param sourceFiles - Array of source file contents
102
+ * @returns Combined usage across all files
103
+ */
104
+ export function extractUsageFromMultiple(sourceFiles) {
105
+ const allClasses = new Set();
106
+ const allIds = new Set();
107
+ for (const sourceText of sourceFiles) {
108
+ const usage = extractUsage(sourceText);
109
+ usage.classes.forEach(cls => allClasses.add(cls));
110
+ usage.ids.forEach(id => allIds.add(id));
111
+ }
112
+ return {
113
+ classes: allClasses,
114
+ ids: allIds,
115
+ filesScanned: sourceFiles.length,
116
+ };
117
+ }
118
+ //# sourceMappingURL=extract-usage.js.map
@@ -0,0 +1,8 @@
1
+ export { extractSelectors, extractSelectorsFromMultiple, type ExtractedSelectors, type ParseWarning, } from './parse-css.js';
2
+ export { extractUsage, extractUsedClasses, extractUsedIds, extractUsageFromMultiple, type UsageExtractionResult, } from './extract-usage.js';
3
+ export { analyzeWaste, estimateWastedBytes, type AnalysisResult, } from './analyze.js';
4
+ export { generateReport, generateJsonReport, generateTextReport, generateSummaryReport, type ReportFormat, type JsonReport, } from './report.js';
5
+ export { crawl, crawlSync, type CrawlOptions, type CrawlResult, } from './crawl.js';
6
+ export { loadConfig, mergeConfig, getConfig, DEFAULT_CONFIG, type Config, } from './config.js';
7
+ export { scan, quickScan, type ScanOptions, type ScanResult, } from './scan.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,gBAAgB,EAChB,4BAA4B,EAC5B,KAAK,kBAAkB,EACvB,KAAK,YAAY,GAClB,MAAM,gBAAgB,CAAC;AAGxB,OAAO,EACL,YAAY,EACZ,kBAAkB,EAClB,cAAc,EACd,wBAAwB,EACxB,KAAK,qBAAqB,GAC3B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,YAAY,EACZ,mBAAmB,EACnB,KAAK,cAAc,GACpB,MAAM,cAAc,CAAC;AAGtB,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,KAAK,YAAY,EACjB,KAAK,UAAU,GAChB,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,KAAK,EACL,SAAS,EACT,KAAK,YAAY,EACjB,KAAK,WAAW,GACjB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,UAAU,EACV,WAAW,EACX,SAAS,EACT,cAAc,EACd,KAAK,MAAM,GACZ,MAAM,aAAa,CAAC;AAGrB,OAAO,EACL,IAAI,EACJ,SAAS,EACT,KAAK,WAAW,EAChB,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,15 @@
1
+ // CSS Parsing
2
+ export { extractSelectors, extractSelectorsFromMultiple, } from './parse-css.js';
3
+ // Usage Extraction
4
+ export { extractUsage, extractUsedClasses, extractUsedIds, extractUsageFromMultiple, } from './extract-usage.js';
5
+ // Analysis
6
+ export { analyzeWaste, estimateWastedBytes, } from './analyze.js';
7
+ // Reporting
8
+ export { generateReport, generateJsonReport, generateTextReport, generateSummaryReport, } from './report.js';
9
+ // File System
10
+ export { crawl, crawlSync, } from './crawl.js';
11
+ // Configuration
12
+ export { loadConfig, mergeConfig, getConfig, DEFAULT_CONFIG, } from './config.js';
13
+ // Main Scanner
14
+ export { scan, quickScan, } from './scan.js';
15
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,35 @@
1
+ export interface ParseWarning {
2
+ type: 'css-parse-error';
3
+ file?: string;
4
+ line?: number;
5
+ column?: number;
6
+ message: string;
7
+ severity: 'warning' | 'error';
8
+ }
9
+ export interface ExtractedSelectors {
10
+ classes: Set<string>;
11
+ ids: Set<string>;
12
+ totalRules: number;
13
+ totalBytes: number;
14
+ warnings: ParseWarning[];
15
+ }
16
+ /**
17
+ * Extracts class and ID selectors from CSS text.
18
+ * This is the core engine for detecting potentially unused CSS.
19
+ *
20
+ * @param cssText - Raw CSS content as a string
21
+ * @param filename - Optional filename for better error reporting
22
+ * @returns Object containing sets of classes and ids, plus metadata
23
+ */
24
+ export declare function extractSelectors(cssText: string, filename?: string): ExtractedSelectors;
25
+ /**
26
+ * Extracts selectors from multiple CSS files/strings
27
+ *
28
+ * @param cssFiles - Array of CSS content strings or file objects with path and content
29
+ * @returns Combined extracted selectors
30
+ */
31
+ export declare function extractSelectorsFromMultiple(cssFiles: string[] | Array<{
32
+ path: string;
33
+ content: string;
34
+ }>): ExtractedSelectors;
35
+ //# sourceMappingURL=parse-css.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-css.d.ts","sourceRoot":"","sources":["../src/parse-css.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,SAAS,GAAG,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AA+CD;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAiHvF;AAED;;;;;GAKG;AACH,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,MAAM,EAAE,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,GAC5D,kBAAkB,CAyBpB"}
@@ -0,0 +1,186 @@
1
+ import postcss from 'postcss';
2
+ import selectorParser from 'postcss-selector-parser';
3
+ // Common file extensions that should not be treated as CSS classes
4
+ const FILE_EXTENSIONS = new Set([
5
+ // JavaScript/TypeScript
6
+ 'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'mts', 'cts',
7
+ // Frameworks/Libraries
8
+ 'vue', 'svelte', 'astro', 'mdx',
9
+ // Markup
10
+ 'html', 'htm', 'xhtml', 'xml', 'svg',
11
+ // Stylesheets
12
+ 'css', 'scss', 'sass', 'less', 'styl', 'stylus', 'pcss',
13
+ // Data/Config
14
+ 'json', 'json5', 'jsonc', 'yaml', 'yml', 'toml', 'ini', 'env', 'config', 'conf',
15
+ // Documentation
16
+ 'md', 'mdx', 'markdown', 'txt', 'rst',
17
+ // Images
18
+ 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'svg', 'ico', 'bmp', 'tiff',
19
+ // Fonts
20
+ 'woff', 'woff2', 'ttf', 'eot', 'otf',
21
+ // Media
22
+ 'mp4', 'webm', 'ogg', 'mp3', 'wav', 'flac', 'm4a',
23
+ // Code/Scripts
24
+ 'py', 'rb', 'php', 'java', 'go', 'rs', 'sh', 'bash', 'zsh',
25
+ // Database/API
26
+ 'sql', 'graphql', 'gql', 'prisma',
27
+ // Build/Test
28
+ 'map', 'lock', 'test', 'spec', 'stories',
29
+ ]);
30
+ /**
31
+ * Check if a string looks like a file extension or other non-class pattern
32
+ */
33
+ function isValidCSSClass(value) {
34
+ // Filter out file extensions
35
+ if (FILE_EXTENSIONS.has(value.toLowerCase())) {
36
+ return false;
37
+ }
38
+ // Must start with letter, underscore, or hyphen (not a number)
39
+ if (!/^[a-zA-Z_-]/.test(value)) {
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+ /**
45
+ * Extracts class and ID selectors from CSS text.
46
+ * This is the core engine for detecting potentially unused CSS.
47
+ *
48
+ * @param cssText - Raw CSS content as a string
49
+ * @param filename - Optional filename for better error reporting
50
+ * @returns Object containing sets of classes and ids, plus metadata
51
+ */
52
+ export function extractSelectors(cssText, filename) {
53
+ const classes = new Set();
54
+ const ids = new Set();
55
+ let totalRules = 0;
56
+ const warnings = [];
57
+ try {
58
+ // Preprocess CSS to handle Tailwind directives that PostCSS doesn't understand
59
+ let processedCss = cssText;
60
+ // Remove Tailwind v3 directives (@tailwind base/components/utilities)
61
+ processedCss = processedCss.replace(/@tailwind\s+[^;]+;/g, '');
62
+ // Remove Tailwind v4 @use directive
63
+ processedCss = processedCss.replace(/@use\s+["']tailwindcss["'];?/g, '');
64
+ // Remove SCSS/SASS comments that PostCSS doesn't handle
65
+ processedCss = processedCss.replace(/\/\/[^\n]*/g, '');
66
+ const root = postcss.parse(processedCss);
67
+ root.walkRules((rule) => {
68
+ totalRules++;
69
+ // Parse each selector in the rule
70
+ try {
71
+ selectorParser((selectors) => {
72
+ selectors.each((selector) => {
73
+ // Walk through each node in the selector
74
+ selector.each((node) => {
75
+ if (node.type === 'class') {
76
+ if (isValidCSSClass(node.value)) {
77
+ classes.add(node.value);
78
+ }
79
+ }
80
+ else if (node.type === 'id') {
81
+ // Filter out hex colors (3, 4, 6, or 8 hex digits)
82
+ // Valid hex color: #fff, #ffff, #ffffff, #ffffffff
83
+ const isHexColor = /^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{4}$|^[0-9a-fA-F]{6}$|^[0-9a-fA-F]{8}$/.test(node.value);
84
+ if (!isHexColor) {
85
+ ids.add(node.value);
86
+ }
87
+ }
88
+ });
89
+ });
90
+ }).processSync(rule.selector);
91
+ }
92
+ catch (selectorError) {
93
+ // Skip invalid selectors but continue processing
94
+ // This handles edge cases in complex CSS
95
+ }
96
+ });
97
+ }
98
+ catch (error) {
99
+ // If PostCSS parsing fails completely, use regex fallback
100
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
101
+ const fileInfo = filename ? ` in file "${filename}"` : '';
102
+ // Extract line/column from PostCSS error message (format: "<css input>:LINE:COL: message")
103
+ let line;
104
+ let column;
105
+ const lineColMatch = errorMsg.match(/:?(\d+):(\d+):/);
106
+ if (lineColMatch) {
107
+ line = parseInt(lineColMatch[1], 10);
108
+ column = parseInt(lineColMatch[2], 10);
109
+ }
110
+ // Create warning object
111
+ const warning = {
112
+ type: 'css-parse-error',
113
+ file: filename,
114
+ line,
115
+ column,
116
+ message: errorMsg,
117
+ severity: 'warning',
118
+ };
119
+ warnings.push(warning);
120
+ // Also log to console for immediate feedback
121
+ console.warn(`⚠️ PostCSS parsing failed${fileInfo}, using regex fallback`);
122
+ console.warn(` Error: ${errorMsg}`);
123
+ if (filename) {
124
+ console.warn(` File: ${filename}`);
125
+ }
126
+ // Fallback: Extract class selectors using regex
127
+ // Match class names including Tailwind variants (e.g., hover\:bg-blue-500)
128
+ // Pattern: . followed by letter/underscore, then any word chars, hyphens, or escaped colons
129
+ const classMatches = cssText.matchAll(/\.([a-zA-Z_][\w-]*(?:\\:[\w-]+)*)/g);
130
+ for (const match of classMatches) {
131
+ // Unescape the class name (remove backslashes before colons)
132
+ const className = match[1].replace(/\\:/g, ':');
133
+ if (isValidCSSClass(className)) {
134
+ classes.add(className);
135
+ }
136
+ }
137
+ // Fallback: Extract ID selectors using regex
138
+ // Only match valid IDs (start with letter or underscore, not hex colors)
139
+ const idMatches = cssText.matchAll(/#([a-zA-Z_][\w-]*)/g);
140
+ for (const match of idMatches) {
141
+ // Double-check: filter out hex colors
142
+ const isHexColor = /^[0-9a-fA-F]{3}$|^[0-9a-fA-F]{4}$|^[0-9a-fA-F]{6}$|^[0-9a-fA-F]{8}$/.test(match[1]);
143
+ if (!isHexColor) {
144
+ ids.add(match[1]);
145
+ }
146
+ }
147
+ }
148
+ return {
149
+ classes,
150
+ ids,
151
+ totalRules,
152
+ totalBytes: Buffer.byteLength(cssText, 'utf8'),
153
+ warnings,
154
+ };
155
+ }
156
+ /**
157
+ * Extracts selectors from multiple CSS files/strings
158
+ *
159
+ * @param cssFiles - Array of CSS content strings or file objects with path and content
160
+ * @returns Combined extracted selectors
161
+ */
162
+ export function extractSelectorsFromMultiple(cssFiles) {
163
+ const allClasses = new Set();
164
+ const allIds = new Set();
165
+ let totalRules = 0;
166
+ let totalBytes = 0;
167
+ const allWarnings = [];
168
+ for (const file of cssFiles) {
169
+ const cssText = typeof file === 'string' ? file : file.content;
170
+ const filename = typeof file === 'string' ? undefined : file.path;
171
+ const result = extractSelectors(cssText, filename);
172
+ result.classes.forEach((cls) => allClasses.add(cls));
173
+ result.ids.forEach((id) => allIds.add(id));
174
+ totalRules += result.totalRules;
175
+ totalBytes += result.totalBytes;
176
+ allWarnings.push(...result.warnings);
177
+ }
178
+ return {
179
+ classes: allClasses,
180
+ ids: allIds,
181
+ totalRules,
182
+ totalBytes,
183
+ warnings: allWarnings,
184
+ };
185
+ }
186
+ //# sourceMappingURL=parse-css.js.map
@@ -0,0 +1,55 @@
1
+ import type { AnalysisResult } from './analyze.js';
2
+ import type { ParseWarning } from './parse-css.js';
3
+ /**
4
+ * Report format options
5
+ */
6
+ export type ReportFormat = 'json' | 'text' | 'summary';
7
+ /**
8
+ * JSON report structure (for dashboard ingestion)
9
+ */
10
+ export interface JsonReport {
11
+ version: string;
12
+ timestamp: string;
13
+ summary: {
14
+ totalCssBytes: number;
15
+ totalRules: number;
16
+ totalSelectors: number;
17
+ usedSelectors: number;
18
+ unusedSelectors: number;
19
+ wastePercentage: number;
20
+ estimatedWastedBytes: number;
21
+ filesScanned: number;
22
+ };
23
+ details: {
24
+ classes: {
25
+ total: number;
26
+ used: number;
27
+ unused: number;
28
+ unusedList: string[];
29
+ };
30
+ ids: {
31
+ total: number;
32
+ used: number;
33
+ unused: number;
34
+ unusedList: string[];
35
+ };
36
+ };
37
+ warnings: ParseWarning[];
38
+ }
39
+ /**
40
+ * Generate JSON report from analysis
41
+ */
42
+ export declare function generateJsonReport(analysis: AnalysisResult, warnings?: ParseWarning[]): JsonReport;
43
+ /**
44
+ * Generate human-readable text report
45
+ */
46
+ export declare function generateTextReport(analysis: AnalysisResult): string;
47
+ /**
48
+ * Generate a brief summary report (for CLI output)
49
+ */
50
+ export declare function generateSummaryReport(analysis: AnalysisResult): string;
51
+ /**
52
+ * Generate report in specified format
53
+ */
54
+ export declare function generateReport(analysis: AnalysisResult, format?: ReportFormat, warnings?: ParseWarning[]): string;
55
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"report.d.ts","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAEvD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE;QACP,aAAa,EAAE,MAAM,CAAC;QACtB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,aAAa,EAAE,MAAM,CAAC;QACtB,eAAe,EAAE,MAAM,CAAC;QACxB,eAAe,EAAE,MAAM,CAAC;QACxB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,OAAO,EAAE;QACP,OAAO,EAAE;YACP,KAAK,EAAE,MAAM,CAAC;YACd,IAAI,EAAE,MAAM,CAAC;YACb,MAAM,EAAE,MAAM,CAAC;YACf,UAAU,EAAE,MAAM,EAAE,CAAC;SACtB,CAAC;QACF,GAAG,EAAE;YACH,KAAK,EAAE,MAAM,CAAC;YACd,IAAI,EAAE,MAAM,CAAC;YACb,MAAM,EAAE,MAAM,CAAC;YACf,UAAU,EAAE,MAAM,EAAE,CAAC;SACtB,CAAC;KACH,CAAC;IACF,QAAQ,EAAE,YAAY,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,cAAc,EAAE,QAAQ,GAAE,YAAY,EAAO,GAAG,UAAU,CAkCtG;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CA8EnE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,cAAc,GAAG,MAAM,CAStE;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,cAAc,EACxB,MAAM,GAAE,YAAqB,EAC7B,QAAQ,GAAE,YAAY,EAAO,GAC5B,MAAM,CAUR"}