@rolandjlevy/css-ghost 0.0.0-stage → 1.0.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/README.md CHANGED
@@ -1,3 +1,123 @@
1
- # Temporary Holding Version
1
+ # 👻 css-ghost
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Find orphaned CSS class selectors in your codebase — defined in style files but never referenced in your components.
4
+
5
+ ## What it does
6
+
7
+ Scans one or more directories for style files, extracts every class selector defined, then cross-references them against usages in your component files. Anything defined but never used is reported as an orphan.
8
+
9
+ **Supported style files:**
10
+ - `.css`, `.module.css`, `.scss`, `.sass`, `.less`
11
+ - `.js` / `.ts` files using `StyleSheet.create({})` or named style objects
12
+
13
+ **Detected usage patterns:**
14
+ - `className="foo"`
15
+ - `className={styles.foo}`
16
+ - `styles['foo']`
17
+ - `cx('foo')`, `clsx('foo')`, `classnames('foo')`
18
+
19
+ **Dynamic class detection:**
20
+ Files containing `styles[variable]` or template literal class names are flagged with a ⚠️ warning — results for those files may be incomplete.
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install -g css-ghost
26
+ ```
27
+
28
+ Or run directly without installing:
29
+
30
+ ```bash
31
+ npx css-ghost ./src
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ css-ghost <directory> [directory2] ... [options]
38
+ ```
39
+
40
+ **Options:**
41
+
42
+ | Flag | Description | Default |
43
+ | --- | --- | --- |
44
+ | `--output <file>` | Output report file path | `orphan-report.md` |
45
+ | `-h, --help` | Show help | — |
46
+
47
+ ## Examples
48
+
49
+ ```bash
50
+ # Scan a single directory
51
+ css-ghost ./src
52
+
53
+ # Scan multiple directories
54
+ css-ghost ./src ./components ./pages
55
+
56
+ # Custom output path
57
+ css-ghost ./src --output ./reports/css-audit.md
58
+ ```
59
+
60
+ ## Output
61
+
62
+ Generates a markdown report (`orphan-report.md` by default):
63
+
64
+ ---
65
+
66
+ # 👻 css-ghost — Orphaned Selector Report
67
+
68
+ ## Summary
69
+ | Metric | Value |
70
+ | --- | --- |
71
+ | Style files scanned | 12 |
72
+ | Total selectors defined | 84 |
73
+ | Total orphaned selectors | 7 |
74
+ | Files with orphans | 3 |
75
+ | Overall unused rate | 8% |
76
+
77
+ ## `src/components/Button/Button.module.css`
78
+ | Defined | Orphaned | Unused % |
79
+ | --- | --- | --- |
80
+ | 6 | 2 | 33% |
81
+
82
+ **Orphaned selectors:**
83
+ - `.iconLeft`
84
+ - `.loadingSpinner`
85
+
86
+ ---
87
+
88
+ ## No dependencies
89
+
90
+ Built with Node.js built-ins only (`fs`, `path`). No npm packages required.
91
+
92
+ ## Origin — built with OpenRouter free models via Claude Code
93
+
94
+ This tool was created as a test project to explore [using OpenRouter's free model tier with Claude Code](https://www.mindstudio.ai/blog/open-router-free-models-claude-code-cost-reduction) as a way to cut AI development costs.
95
+
96
+ Claude Code normally calls Anthropic's API directly. By overriding two environment variables you can route it through OpenRouter's free tier instead — meaning you can run agentic coding sessions at no cost using models like Llama 3.3 70B or Deepseek R1.
97
+
98
+ ### Setup
99
+
100
+ Get a free API key at [openrouter.ai/keys](https://openrouter.ai/keys), then add these aliases to your `~/.zshrc`:
101
+
102
+ ```bash
103
+ # Store your key once
104
+ export OPENROUTER_API_KEY="sk-or-v1-YOUR_KEY"
105
+
106
+ # Llama 3.3 70B — fast, capable, free
107
+ alias claude-free='ANTHROPIC_BASE_URL="https://openrouter.ai/api/v1" \
108
+ ANTHROPIC_API_KEY="$OPENROUTER_API_KEY" \
109
+ claude --model "meta-llama/llama-3.3-70b-instruct:free"'
110
+
111
+ # Deepseek R1 — strongest free option for reasoning and debugging
112
+ alias claude-r1='ANTHROPIC_BASE_URL="https://openrouter.ai/api/v1" \
113
+ ANTHROPIC_API_KEY="$OPENROUTER_API_KEY" \
114
+ claude --model "deepseek/deepseek-r1:free"'
115
+ ```
116
+
117
+ Then `source ~/.zshrc`. Use `claude-free` or `claude-r1` in place of `claude` to start a free session.
118
+
119
+ > **Note:** Free tier models handle Claude Code's tool call format well in practice, but run a quick agentic task first to confirm file read/edit/bash loops work end to end in your environment before committing to it for serious work.
120
+
121
+ ## License
122
+
123
+ MIT
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve } from 'path';
4
+ import { existsSync as fsExists } from 'fs';
5
+ import { scan } from '../src/scanner.js';
6
+ import { generateReport } from '../src/reporter.js';
7
+
8
+ const args = process.argv.slice(2);
9
+
10
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
11
+ console.log(`
12
+ 👻 css-ghost — Find orphaned CSS class selectors
13
+
14
+ Usage:
15
+ css-ghost <directory> [directory2] ... [options]
16
+
17
+ Options:
18
+ --output <file> Output report file path (default: orphan-report.md)
19
+ -h, --help Show this help message
20
+
21
+ Examples:
22
+ css-ghost ./src
23
+ css-ghost ./src ./components
24
+ css-ghost ./src --output ./reports/css-audit.md
25
+ `);
26
+ process.exit(0);
27
+ }
28
+
29
+ // Parse --output flag
30
+ const outputIdx = args.indexOf('--output');
31
+ const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : 'orphan-report.md';
32
+
33
+ if (outputIdx !== -1 && !outputPath) {
34
+ console.error('❌ --output requires a file path');
35
+ process.exit(1);
36
+ }
37
+
38
+ // Remaining args are directories
39
+ const rawDirs = args.filter((a, i) => !a.startsWith('--') && (outputIdx === -1 || i !== outputIdx + 1));
40
+
41
+ if (rawDirs.length === 0) {
42
+ console.error('❌ At least one directory is required');
43
+ process.exit(1);
44
+ }
45
+
46
+ // Validate directories exist
47
+ const dirs = [];
48
+ for (const d of rawDirs) {
49
+ const resolved = resolve(d);
50
+ if (!fsExists(resolved)) {
51
+ console.error(`❌ Directory not found: ${d}`);
52
+ process.exit(1);
53
+ }
54
+ dirs.push(resolved);
55
+ }
56
+
57
+ console.log(`\n👻 css-ghost scanning ${dirs.length} director${dirs.length === 1 ? 'y' : 'ies'}...`);
58
+ dirs.forEach(d => console.log(` 📁 ${d}`));
59
+ console.log('');
60
+
61
+ const results = scan(dirs);
62
+
63
+ if (results.length === 0) {
64
+ console.log('🔍 No style files found in the specified directories.\n');
65
+ process.exit(0);
66
+ }
67
+
68
+ generateReport(results, outputPath);
package/package.json CHANGED
@@ -1,6 +1,39 @@
1
1
  {
2
2
  "name": "@rolandjlevy/css-ghost",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.1",
4
+ "description": "Find orphaned CSS class selectors in your codebase",
5
+ "type": "module",
6
+ "bin": {
7
+ "css-ghost": "bin/css-ghost.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node bin/css-ghost.js"
11
+ },
12
+ "files": [
13
+ "bin",
14
+ "src",
15
+ "README.md"
16
+ ],
17
+ "keywords": [
18
+ "css",
19
+ "orphan",
20
+ "unused",
21
+ "selectors",
22
+ "cli",
23
+ "linter",
24
+ "react",
25
+ "scss",
26
+ "stylesheet",
27
+ "audit"
28
+ ],
29
+ "author": "Roland Levy",
30
+ "license": "MIT",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/rolandjlevy/css-ghost.git"
34
+ },
35
+ "homepage": "https://github.com/rolandjlevy/css-ghost#readme",
36
+ "engines": {
37
+ "node": ">=18"
38
+ }
39
+ }
package/src/parser.js ADDED
@@ -0,0 +1,180 @@
1
+ import { readFileSync } from 'fs';
2
+ import { extname } from 'path';
3
+
4
+ // ─── CSS / SCSS selector extraction ────────────────────────────────────────
5
+
6
+ function extractCssSelectors(content) {
7
+ const selectors = new Map(); // name → { line, rule }
8
+ const lines = content.split('\n');
9
+
10
+ // Strip block comments but preserve line numbers by replacing with spaces
11
+ const stripped = content.replace(/\/\*[\s\S]*?\*\//g, m =>
12
+ m.replace(/[^\n]/g, ' ')
13
+ );
14
+
15
+ // Walk line by line to track line numbers
16
+ const strippedLines = stripped.split('\n');
17
+ let insideRule = false;
18
+ let currentRule = [];
19
+ let currentSelectors = [];
20
+ let ruleStartLine = 0;
21
+
22
+ for (let i = 0; i < strippedLines.length; i++) {
23
+ const line = strippedLines[i];
24
+ const lineNum = i + 1;
25
+
26
+ if (!insideRule) {
27
+ // Look for .className optionally followed by { on same or next line
28
+ const selectorRegex = /\.([a-zA-Z_-][a-zA-Z0-9_-]*)/g;
29
+ let m;
30
+ while ((m = selectorRegex.exec(line)) !== null) {
31
+ currentSelectors.push({ name: m[1], line: lineNum });
32
+ }
33
+
34
+ if (line.includes('{')) {
35
+ insideRule = true;
36
+ ruleStartLine = lineNum;
37
+ currentRule = [lines[i]];
38
+
39
+ if (line.includes('}')) {
40
+ // Single-line rule
41
+ const ruleText = currentRule.join('\n').trim();
42
+ currentSelectors.forEach(({ name, line: l }) => {
43
+ if (!selectors.has(name)) {
44
+ selectors.set(name, { line: l, rule: ruleText });
45
+ }
46
+ });
47
+ currentSelectors = [];
48
+ currentRule = [];
49
+ insideRule = false;
50
+ }
51
+ } else if (currentSelectors.length > 0 && line.trim() === '') {
52
+ // Empty line with no { means these weren't selectors for a rule
53
+ currentSelectors = [];
54
+ }
55
+ } else {
56
+ currentRule.push(lines[i]);
57
+ if (line.includes('}')) {
58
+ const ruleText = currentRule.join('\n').trim();
59
+ currentSelectors.forEach(({ name, line: l }) => {
60
+ if (!selectors.has(name)) {
61
+ selectors.set(name, { line: l, rule: ruleText });
62
+ }
63
+ });
64
+ currentSelectors = [];
65
+ currentRule = [];
66
+ insideRule = false;
67
+ }
68
+ }
69
+ }
70
+
71
+ return selectors;
72
+ }
73
+
74
+ // ─── JS style file detection & key extraction ───────────────────────────────
75
+
76
+ export function isJsStyleFile(content, filePath) {
77
+ return (
78
+ /StyleSheet\.create\s*\(\s*\{/.test(content) ||
79
+ (/styles?\.(js|ts)$/.test(filePath) && /(const|let|var)\s+styles?\s*=\s*\{/.test(content))
80
+ );
81
+ }
82
+
83
+ function extractJsStyleKeys(content) {
84
+ const keys = new Map(); // name → { line, rule }
85
+ const lines = content.split('\n');
86
+
87
+ const ssMatch = content.match(/StyleSheet\.create\s*\(\s*\{([\s\S]*?)\}\s*\)/);
88
+ const plainMatch = content.match(/(?:const|let|var)\s+styles?\s*=\s*\{([\s\S]*)\}/);
89
+ const objectContent = ssMatch?.[1] ?? plainMatch?.[1];
90
+
91
+ if (objectContent) {
92
+ // Find start offset in original content
93
+ const startOffset = content.indexOf(objectContent);
94
+ const preContent = content.slice(0, startOffset);
95
+ const startLine = preContent.split('\n').length;
96
+
97
+ const keyRegex = /^\s*([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/gm;
98
+ let m;
99
+ while ((m = keyRegex.exec(objectContent)) !== null) {
100
+ const keyLine = startLine + objectContent.slice(0, m.index).split('\n').length - 1;
101
+ const name = m[1];
102
+ if (!keys.has(name)) {
103
+ // Grab the value snippet for context
104
+ const lineText = lines[keyLine - 1]?.trim() ?? '';
105
+ keys.set(name, { line: keyLine, rule: lineText });
106
+ }
107
+ }
108
+ }
109
+
110
+ return keys;
111
+ }
112
+
113
+ // ─── Component file usage extraction ────────────────────────────────────────
114
+
115
+ export function extractUsages(content) {
116
+ const used = new Set();
117
+ let hasDynamic = false;
118
+
119
+ // className="foo bar" or className='foo'
120
+ const directRegex = /className\s*=\s*["']([^"']+)["']/g;
121
+ let m;
122
+ while ((m = directRegex.exec(content)) !== null) {
123
+ m[1].trim().split(/\s+/).forEach(c => c && used.add(c));
124
+ }
125
+
126
+ // styles.foo
127
+ const dotRegex = /styles?\.([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
128
+ while ((m = dotRegex.exec(content)) !== null) {
129
+ used.add(m[1]);
130
+ }
131
+
132
+ // styles['foo'] or styles["foo"]
133
+ const bracketStringRegex = /styles?\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
134
+ while ((m = bracketStringRegex.exec(content)) !== null) {
135
+ used.add(m[1]);
136
+ }
137
+
138
+ // styles[variable] — dynamic, flag it
139
+ if (/styles?\[(?!['"])/.test(content)) {
140
+ hasDynamic = true;
141
+ }
142
+
143
+ // cx('foo', 'bar') / clsx('foo') / classnames('foo')
144
+ const cxRegex = /(?:cx|clsx|classnames)\s*\(([^)]+)\)/g;
145
+ while ((m = cxRegex.exec(content)) !== null) {
146
+ const inner = m[1];
147
+ const strRegex = /['"]([a-zA-Z_-][a-zA-Z0-9_-]*)["']/g;
148
+ let sm;
149
+ while ((sm = strRegex.exec(inner)) !== null) {
150
+ used.add(sm[1]);
151
+ }
152
+ }
153
+
154
+ // Template literal dynamic class names
155
+ if (/`[^`]*\$\{[^}]+\}[^`]*`/.test(content)) {
156
+ hasDynamic = true;
157
+ }
158
+
159
+ return { used, hasDynamic };
160
+ }
161
+
162
+ // ─── Public API ─────────────────────────────────────────────────────────────
163
+
164
+ export function parseStyleFile(filePath) {
165
+ const content = readFileSync(filePath, 'utf-8');
166
+ const ext = extname(filePath);
167
+
168
+ if (['.css', '.scss', '.sass', '.less'].includes(ext)) {
169
+ return { selectors: extractCssSelectors(content), hasDynamic: false };
170
+ }
171
+
172
+ // JS/TS style file
173
+ const selectors = extractJsStyleKeys(content);
174
+ return { selectors, hasDynamic: false };
175
+ }
176
+
177
+ export function parseComponentFile(filePath) {
178
+ const content = readFileSync(filePath, 'utf-8');
179
+ return extractUsages(content);
180
+ }
@@ -0,0 +1,110 @@
1
+ import { writeFileSync } from 'fs';
2
+ import { relative } from 'path';
3
+
4
+ export function generateReport(results, outputPath = 'orphan-report.md') {
5
+ let totalDefined = 0;
6
+ let totalOrphaned = 0;
7
+ let filesWithOrphans = 0;
8
+
9
+ const sections = [];
10
+
11
+ for (const result of results) {
12
+ const { filePath, orphaned, total, hasDynamic, dynamicFiles } = result;
13
+ totalDefined += total;
14
+ totalOrphaned += orphaned.length;
15
+
16
+ const hasOrphans = orphaned.length > 0;
17
+ if (hasOrphans) filesWithOrphans++;
18
+
19
+ // Skip files with nothing to report
20
+ if (!hasOrphans && !hasDynamic) continue;
21
+
22
+ const relPath = relative(process.cwd(), filePath);
23
+ const pct = total > 0 ? Math.round((orphaned.length / total) * 100) : 0;
24
+
25
+ const lines = [
26
+ `## \`${relPath}\``,
27
+ '',
28
+ `| Defined | Orphaned | Unused % |`,
29
+ `| --- | --- | --- |`,
30
+ `| ${total} | ${orphaned.length} | ${pct}% |`,
31
+ '',
32
+ ];
33
+
34
+ if (hasDynamic) {
35
+ lines.push(`> ⚠️ **Dynamic class references detected** — results may be incomplete.`);
36
+ if (dynamicFiles?.length > 0) {
37
+ const relDynamic = dynamicFiles
38
+ .map(f => `\`${relative(process.cwd(), f)}\``)
39
+ .join(', ');
40
+ lines.push(`> Dynamic references found in: ${relDynamic}`);
41
+ }
42
+ lines.push('');
43
+ }
44
+
45
+ if (hasOrphans) {
46
+ lines.push('### 👻 Orphaned selectors\n');
47
+
48
+ orphaned.forEach(({ name, line, rule }) => {
49
+ lines.push(`#### \`.${name}\` — line ${line}`);
50
+ lines.push('');
51
+ if (rule) {
52
+ lines.push('```css');
53
+ lines.push(rule);
54
+ lines.push('```');
55
+ }
56
+ lines.push('');
57
+ });
58
+ }
59
+
60
+ lines.push('---');
61
+ lines.push('');
62
+ sections.push(lines.join('\n'));
63
+ }
64
+
65
+ const unusedPct = totalDefined > 0
66
+ ? Math.round((totalOrphaned / totalDefined) * 100)
67
+ : 0;
68
+
69
+ const summary = [
70
+ '# 👻 css-ghost — Orphaned Selector Report',
71
+ '',
72
+ `_Generated: ${new Date().toUTCString()}_`,
73
+ '',
74
+ '## Summary',
75
+ '',
76
+ `| Metric | Value |`,
77
+ `| --- | --- |`,
78
+ `| Style files scanned | ${results.length} |`,
79
+ `| Total selectors defined | ${totalDefined} |`,
80
+ `| Total orphaned selectors | ${totalOrphaned} |`,
81
+ `| Files with orphans | ${filesWithOrphans} |`,
82
+ `| Overall unused rate | ${unusedPct}% |`,
83
+ '',
84
+ '---',
85
+ '',
86
+ ].join('\n');
87
+
88
+ const content = summary + sections.join('');
89
+ writeFileSync(outputPath, content, 'utf-8');
90
+
91
+ // Console output — also print orphans inline
92
+ console.log(`✅ Report written to ${outputPath}`);
93
+ console.log(`📊 ${totalOrphaned} orphaned selector${totalOrphaned !== 1 ? 's' : ''} across ${filesWithOrphans} file${filesWithOrphans !== 1 ? 's' : ''}\n`);
94
+
95
+ for (const result of results) {
96
+ if (result.orphaned.length === 0) continue;
97
+ const relPath = relative(process.cwd(), result.filePath);
98
+ console.log(`📄 ${relPath}`);
99
+ result.orphaned.forEach(({ name, line, rule }) => {
100
+ console.log(` 👻 .${name} (line ${line})`);
101
+ if (rule) {
102
+ const preview = rule.split('\n').slice(0, 4).join('\n');
103
+ console.log(preview.split('\n').map(l => ` ${l}`).join('\n'));
104
+ }
105
+ console.log('');
106
+ });
107
+ }
108
+
109
+ console.log(`📁 ${results.length} style file${results.length !== 1 ? 's' : ''} scanned\n`);
110
+ }
package/src/scanner.js ADDED
@@ -0,0 +1,76 @@
1
+ import { readFileSync } from 'fs';
2
+ import { extname } from 'path';
3
+ import { walkDir } from './walker.js';
4
+ import { parseStyleFile, parseComponentFile, isJsStyleFile } from './parser.js';
5
+
6
+ export function scan(dirs) {
7
+ const allStyleFiles = [];
8
+ const allJsCandidates = [];
9
+
10
+ for (const dir of dirs) {
11
+ const { styleFiles, componentFiles } = walkDir(dir);
12
+ allStyleFiles.push(...styleFiles);
13
+ allJsCandidates.push(...componentFiles);
14
+ }
15
+
16
+ // Split JS/TS files into style files vs component files
17
+ const jsStyleFiles = [];
18
+ const componentFiles = [];
19
+
20
+ for (const file of allJsCandidates) {
21
+ const ext = extname(file);
22
+ if (['.js', '.ts'].includes(ext)) {
23
+ try {
24
+ const content = readFileSync(file, 'utf-8');
25
+ if (isJsStyleFile(content, file)) {
26
+ jsStyleFiles.push(file);
27
+ continue;
28
+ }
29
+ } catch {
30
+ // unreadable — treat as component
31
+ }
32
+ }
33
+ componentFiles.push(file);
34
+ }
35
+
36
+ const styleFiles = [...allStyleFiles, ...jsStyleFiles];
37
+
38
+ // Build global usage map from all component files
39
+ const globalUsed = new Set();
40
+ const dynamicComponentFiles = [];
41
+
42
+ for (const file of componentFiles) {
43
+ try {
44
+ const { used, hasDynamic } = parseComponentFile(file);
45
+ for (const cls of used) globalUsed.add(cls);
46
+ if (hasDynamic) dynamicComponentFiles.push(file);
47
+ } catch {
48
+ // skip unreadable files
49
+ }
50
+ }
51
+
52
+ // Analyse each style file
53
+ const results = [];
54
+
55
+ for (const styleFile of styleFiles) {
56
+ try {
57
+ const { selectors, hasDynamic } = parseStyleFile(styleFile);
58
+ // selectors is now a Map: name → { line, rule }
59
+ const orphaned = [...selectors.entries()]
60
+ .filter(([name]) => !globalUsed.has(name))
61
+ .map(([name, meta]) => ({ name, ...meta }));
62
+
63
+ results.push({
64
+ filePath: styleFile,
65
+ total: selectors.size,
66
+ orphaned,
67
+ hasDynamic: hasDynamic || dynamicComponentFiles.length > 0,
68
+ dynamicFiles: dynamicComponentFiles,
69
+ });
70
+ } catch (e) {
71
+ console.warn(`⚠️ Could not parse ${styleFile}: ${e.message}`);
72
+ }
73
+ }
74
+
75
+ return results;
76
+ }
package/src/walker.js ADDED
@@ -0,0 +1,42 @@
1
+ import { readdirSync } from 'fs';
2
+ import { join, extname, basename } from 'path';
3
+
4
+ const STYLE_EXTENSIONS = ['.css', '.scss', '.sass', '.less'];
5
+ const COMPONENT_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx']);
6
+ const IGNORE_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', 'out', 'coverage', '.cache']);
7
+
8
+ export function walkDir(dir) {
9
+ const styleFiles = [];
10
+ const componentFiles = [];
11
+
12
+ function walk(currentDir) {
13
+ let entries;
14
+ try {
15
+ entries = readdirSync(currentDir, { withFileTypes: true });
16
+ } catch {
17
+ return;
18
+ }
19
+
20
+ for (const entry of entries) {
21
+ if (entry.name.startsWith('.') || IGNORE_DIRS.has(entry.name)) continue;
22
+
23
+ const fullPath = join(currentDir, entry.name);
24
+
25
+ if (entry.isDirectory()) {
26
+ walk(fullPath);
27
+ } else {
28
+ const name = basename(entry.name);
29
+ const ext = extname(entry.name);
30
+
31
+ if (STYLE_EXTENSIONS.some(se => name.endsWith(se))) {
32
+ styleFiles.push(fullPath);
33
+ } else if (COMPONENT_EXTENSIONS.has(ext)) {
34
+ componentFiles.push(fullPath);
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ walk(dir);
41
+ return { styleFiles, componentFiles };
42
+ }