@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/LICENSE +6 -0
- package/README.md +263 -0
- package/dist/analyze.d.ts +31 -0
- package/dist/analyze.d.ts.map +1 -0
- package/dist/analyze.js +65 -0
- package/dist/config.d.ts +41 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +123 -0
- package/dist/crawl.d.ts +51 -0
- package/dist/crawl.d.ts.map +1 -0
- package/dist/crawl.js +153 -0
- package/dist/extract-usage.d.ts +38 -0
- package/dist/extract-usage.d.ts.map +1 -0
- package/dist/extract-usage.js +118 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/parse-css.d.ts +35 -0
- package/dist/parse-css.d.ts.map +1 -0
- package/dist/parse-css.js +186 -0
- package/dist/report.d.ts +55 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +137 -0
- package/dist/scan.d.ts +48 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +72 -0
- package/package.json +69 -0
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
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
package/dist/report.d.ts
ADDED
|
@@ -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"}
|