@vibecodeqa/cli 0.42.0 → 0.43.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/README.md +130 -165
- package/dist/check-meta.js +59 -6
- package/dist/cli.js +268 -761
- package/dist/commands/explain.d.ts +2 -0
- package/dist/commands/explain.js +33 -0
- package/dist/commands/fix.d.ts +6 -0
- package/dist/commands/fix.js +131 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +96 -0
- package/dist/commands/shared.d.ts +4 -0
- package/dist/commands/shared.js +80 -0
- package/dist/core.js +10 -0
- package/dist/runners/accessibility.js +4 -1
- package/dist/runners/design-consistency.d.ts +12 -0
- package/dist/runners/design-consistency.js +125 -0
- package/dist/runners/error-handling.js +18 -2
- package/dist/runners/file-cohesion.d.ts +17 -0
- package/dist/runners/file-cohesion.js +177 -0
- package/dist/runners/frontend-health.d.ts +14 -0
- package/dist/runners/frontend-health.js +206 -0
- package/dist/runners/html-quality.d.ts +8 -0
- package/dist/runners/html-quality.js +203 -0
- package/dist/runners/react.js +1 -0
- package/dist/runners/secrets.js +7 -2
- package/dist/runners/security.js +7 -1
- package/dist/runners/standards.js +29 -9
- package/dist/runners/styling.d.ts +15 -0
- package/dist/runners/styling.js +280 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Code standards check — naming conventions, anti-patterns, config hygiene. */
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import { basename, extname, join } from "node:path";
|
|
4
4
|
import { getProductionFiles, readDeps } from "../fs-utils.js";
|
|
5
5
|
import { gradeFromScore } from "../types.js";
|
|
@@ -45,6 +45,16 @@ const CODE_SMELLS = [
|
|
|
45
45
|
export function runStandards(cwd, stack) {
|
|
46
46
|
const start = Date.now();
|
|
47
47
|
const issues = [];
|
|
48
|
+
// Detect CLI projects — console.log is intentional in CLI tools
|
|
49
|
+
let isCLI = false;
|
|
50
|
+
try {
|
|
51
|
+
const pkgPath = join(cwd, "package.json");
|
|
52
|
+
if (existsSync(pkgPath)) {
|
|
53
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
54
|
+
isCLI = !!pkg.bin;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch { /* ignore */ }
|
|
48
58
|
// Collect source files
|
|
49
59
|
const files = getProductionFiles(cwd);
|
|
50
60
|
// ── File naming conventions ──
|
|
@@ -78,16 +88,25 @@ export function runStandards(cwd, stack) {
|
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
90
|
}
|
|
81
|
-
// ── Large files ──
|
|
91
|
+
// ── Large files (exponential penalty) ──
|
|
92
|
+
// Penalty grows with the square of excess lines. A 300-line file is a nudge.
|
|
93
|
+
// A 600-line file is a wall. A 1000-line file tanks the check.
|
|
82
94
|
let largeFiles = 0;
|
|
95
|
+
let fileSizePenalty = 0;
|
|
96
|
+
const SOFT_LIMIT = 300;
|
|
83
97
|
for (const f of files) {
|
|
84
98
|
const lines = f.content.split("\n").length;
|
|
85
|
-
if (lines >
|
|
99
|
+
if (lines > SOFT_LIMIT * 2) {
|
|
86
100
|
largeFiles++;
|
|
87
|
-
|
|
101
|
+
const excess = lines - SOFT_LIMIT;
|
|
102
|
+
fileSizePenalty += (excess / 100) ** 2;
|
|
103
|
+
issues.push({ severity: "error", message: `${lines} lines — split this file (exponential penalty above ${SOFT_LIMIT})`, file: f.path, rule: "large-file" });
|
|
88
104
|
}
|
|
89
|
-
else if (lines >
|
|
90
|
-
|
|
105
|
+
else if (lines > SOFT_LIMIT) {
|
|
106
|
+
largeFiles++;
|
|
107
|
+
const excess = lines - SOFT_LIMIT;
|
|
108
|
+
fileSizePenalty += (excess / 100) ** 2;
|
|
109
|
+
issues.push({ severity: "warning", message: `${lines} lines — consider splitting (penalty grows exponentially above ${SOFT_LIMIT})`, file: f.path, rule: "large-file" });
|
|
91
110
|
}
|
|
92
111
|
}
|
|
93
112
|
// ── Code smell patterns ──
|
|
@@ -105,8 +124,8 @@ export function runStandards(cwd, stack) {
|
|
|
105
124
|
if (/^\s*["'`].*["'`][,;]?\s*$/.test(line))
|
|
106
125
|
continue;
|
|
107
126
|
for (const check of CODE_SMELLS) {
|
|
108
|
-
// Skip console.log in CLI
|
|
109
|
-
if (check.name === "console.log" &&
|
|
127
|
+
// Skip console.log in CLI projects (intentional terminal output)
|
|
128
|
+
if (check.name === "console.log" && isCLI)
|
|
110
129
|
continue;
|
|
111
130
|
if (check.pattern.test(line)) {
|
|
112
131
|
if (check.exclude?.test(line))
|
|
@@ -192,7 +211,8 @@ export function runStandards(cwd, stack) {
|
|
|
192
211
|
const totalFiles = files.length || 1;
|
|
193
212
|
const errorPenalty = Math.min(40, (errors / totalFiles) * 150);
|
|
194
213
|
const warningPenalty = Math.min(30, (warnings / totalFiles) * 80);
|
|
195
|
-
|
|
214
|
+
// fileSizePenalty is already exponential — normalize by file count, cap at 95
|
|
215
|
+
const largePenalty = Math.min(95, (fileSizePenalty / totalFiles) * 5);
|
|
196
216
|
const score = Math.max(0, Math.min(100, Math.round(100 - errorPenalty - warningPenalty - largePenalty)));
|
|
197
217
|
return {
|
|
198
218
|
name: "standards",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Styling consistency — delegates to Stylelint when available, adds cross-file analysis.
|
|
2
|
+
*
|
|
3
|
+
* Tool delegation (same pattern as lint → biome/eslint, secrets → gitleaks):
|
|
4
|
+
* - Stylelint installed → run it for CSS/SCSS linting (170+ rules)
|
|
5
|
+
* - Always: cross-file analysis that no CSS linter covers:
|
|
6
|
+
* 1. Mixed styling approaches (Tailwind + CSS modules + styled-components + inline)
|
|
7
|
+
* 2. Hardcoded colors in JSX (not CSS — Stylelint handles CSS)
|
|
8
|
+
* 3. Magic numbers in spacing (cross-file consistency)
|
|
9
|
+
* 4. Inline style ratio
|
|
10
|
+
* 5. !important abuse
|
|
11
|
+
* 6. Duplicate Tailwind class strings across components
|
|
12
|
+
* 7. Inconsistent spacing values across components
|
|
13
|
+
*/
|
|
14
|
+
import type { CheckResult } from "../types.js";
|
|
15
|
+
export declare function runStyling(cwd: string): CheckResult;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/** Styling consistency — delegates to Stylelint when available, adds cross-file analysis.
|
|
2
|
+
*
|
|
3
|
+
* Tool delegation (same pattern as lint → biome/eslint, secrets → gitleaks):
|
|
4
|
+
* - Stylelint installed → run it for CSS/SCSS linting (170+ rules)
|
|
5
|
+
* - Always: cross-file analysis that no CSS linter covers:
|
|
6
|
+
* 1. Mixed styling approaches (Tailwind + CSS modules + styled-components + inline)
|
|
7
|
+
* 2. Hardcoded colors in JSX (not CSS — Stylelint handles CSS)
|
|
8
|
+
* 3. Magic numbers in spacing (cross-file consistency)
|
|
9
|
+
* 4. Inline style ratio
|
|
10
|
+
* 5. !important abuse
|
|
11
|
+
* 6. Duplicate Tailwind class strings across components
|
|
12
|
+
* 7. Inconsistent spacing values across components
|
|
13
|
+
*/
|
|
14
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { getProductionFiles, readDeps } from "../fs-utils.js";
|
|
17
|
+
import { gradeFromScore } from "../types.js";
|
|
18
|
+
import { runJSON } from "./exec.js";
|
|
19
|
+
function tryStylelint(cwd) {
|
|
20
|
+
const deps = readDeps(cwd);
|
|
21
|
+
if (!deps.stylelint)
|
|
22
|
+
return null;
|
|
23
|
+
// Check for config
|
|
24
|
+
const hasConfig = existsSync(join(cwd, ".stylelintrc.json")) ||
|
|
25
|
+
existsSync(join(cwd, ".stylelintrc.js")) ||
|
|
26
|
+
existsSync(join(cwd, ".stylelintrc.yml")) ||
|
|
27
|
+
existsSync(join(cwd, "stylelint.config.js")) ||
|
|
28
|
+
existsSync(join(cwd, "stylelint.config.mjs")) ||
|
|
29
|
+
existsSync(join(cwd, "stylelint.config.cjs"));
|
|
30
|
+
if (!hasConfig) {
|
|
31
|
+
// No config — try with standard config if available
|
|
32
|
+
const hasStandard = deps["stylelint-config-standard"] || deps["stylelint-config-recommended"];
|
|
33
|
+
if (!hasStandard)
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const results = runJSON("npx stylelint --formatter json \"src/**/*.{css,scss}\" 2>/dev/null || true", cwd, 30_000);
|
|
37
|
+
if (!results || !Array.isArray(results))
|
|
38
|
+
return null;
|
|
39
|
+
const issues = [];
|
|
40
|
+
for (const file of results) {
|
|
41
|
+
const relPath = file.source.replace(cwd + "/", "").replace(cwd + "\\", "");
|
|
42
|
+
for (const w of file.warnings) {
|
|
43
|
+
issues.push({
|
|
44
|
+
severity: w.severity === "error" ? "error" : "warning",
|
|
45
|
+
message: `${w.text} (${w.rule})`,
|
|
46
|
+
file: relPath,
|
|
47
|
+
line: w.line,
|
|
48
|
+
rule: w.rule,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return issues;
|
|
53
|
+
}
|
|
54
|
+
// ── Cross-file analysis (our unique value — no linter covers this) ──
|
|
55
|
+
const TAILWIND_CLASS = /className\s*=\s*["'`][^"'`]*(?:flex|grid|p-|m-|text-|bg-|rounded|border|shadow|w-|h-)/;
|
|
56
|
+
const CSS_MODULE = /styles\.\w+|\.module\.css|\.module\.scss/;
|
|
57
|
+
const STYLED_COMPONENT = /styled\.\w+|styled\(|css`/;
|
|
58
|
+
const INLINE_STYLE = /style\s*=\s*\{\s*\{|style\s*=\s*\{[^}]/;
|
|
59
|
+
const EMOTION_CSS = /@emotion|css\s*\(/;
|
|
60
|
+
const HARDCODED_COLOR_JSX = /(?:color|backgroundColor|borderColor|fill|stroke)\s*:\s*['"]#[0-9a-fA-F]{3,8}/;
|
|
61
|
+
const SPACING_PROP = /(?:margin|padding|gap|top|bottom|left|right|width|height|inset)\s*:\s*/;
|
|
62
|
+
const MAGIC_PX = /(\d+)px/g;
|
|
63
|
+
export function runStyling(cwd) {
|
|
64
|
+
const start = Date.now();
|
|
65
|
+
const issues = [];
|
|
66
|
+
const files = getProductionFiles(cwd);
|
|
67
|
+
const deps = readDeps(cwd);
|
|
68
|
+
const componentFiles = files.filter((f) => !f.isTest && /\.(tsx|jsx|vue|svelte)$/.test(f.path));
|
|
69
|
+
if (componentFiles.length === 0) {
|
|
70
|
+
return {
|
|
71
|
+
name: "styling",
|
|
72
|
+
score: 0,
|
|
73
|
+
grade: "F",
|
|
74
|
+
details: { skipped: true, reason: "no component files found" },
|
|
75
|
+
issues: [],
|
|
76
|
+
duration: Date.now() - start,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
// ── Phase 1: Delegate to Stylelint ──
|
|
80
|
+
let tool = "built-in";
|
|
81
|
+
const stylelintIssues = tryStylelint(cwd);
|
|
82
|
+
if (stylelintIssues) {
|
|
83
|
+
tool = "stylelint";
|
|
84
|
+
issues.push(...stylelintIssues);
|
|
85
|
+
}
|
|
86
|
+
// ── Phase 2: Cross-file analysis (always runs — Stylelint can't do this) ──
|
|
87
|
+
const approaches = new Map();
|
|
88
|
+
const hasTailwind = existsSync(join(cwd, "tailwind.config.js")) ||
|
|
89
|
+
existsSync(join(cwd, "tailwind.config.ts")) ||
|
|
90
|
+
existsSync(join(cwd, "tailwind.config.mjs")) ||
|
|
91
|
+
!!deps.tailwindcss;
|
|
92
|
+
let inlineStyleCount = 0;
|
|
93
|
+
let classNameCount = 0;
|
|
94
|
+
let hardcodedColorCount = 0;
|
|
95
|
+
let importantCount = 0;
|
|
96
|
+
const spacingValues = new Map();
|
|
97
|
+
const tailwindStrings = new Map();
|
|
98
|
+
// Scan CSS files for !important (only if Stylelint didn't already flag them)
|
|
99
|
+
if (!stylelintIssues) {
|
|
100
|
+
scanCssFiles(cwd, "src", (content) => {
|
|
101
|
+
importantCount += (content.match(/!important/g) || []).length;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
for (const f of componentFiles) {
|
|
105
|
+
const lines = f.content.split("\n");
|
|
106
|
+
for (let i = 0; i < lines.length; i++) {
|
|
107
|
+
const line = lines[i];
|
|
108
|
+
const trimmed = line.trim();
|
|
109
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("*"))
|
|
110
|
+
continue;
|
|
111
|
+
// Detect styling approaches
|
|
112
|
+
if (TAILWIND_CLASS.test(line))
|
|
113
|
+
approaches.set("tailwind", (approaches.get("tailwind") || 0) + 1);
|
|
114
|
+
if (CSS_MODULE.test(line))
|
|
115
|
+
approaches.set("css-modules", (approaches.get("css-modules") || 0) + 1);
|
|
116
|
+
if (STYLED_COMPONENT.test(line))
|
|
117
|
+
approaches.set("styled-components", (approaches.get("styled-components") || 0) + 1);
|
|
118
|
+
if (EMOTION_CSS.test(line))
|
|
119
|
+
approaches.set("emotion", (approaches.get("emotion") || 0) + 1);
|
|
120
|
+
if (INLINE_STYLE.test(line)) {
|
|
121
|
+
approaches.set("inline", (approaches.get("inline") || 0) + 1);
|
|
122
|
+
inlineStyleCount++;
|
|
123
|
+
}
|
|
124
|
+
if (/className/.test(line))
|
|
125
|
+
classNameCount++;
|
|
126
|
+
// Hardcoded colors in JSX (Stylelint only catches CSS files)
|
|
127
|
+
if (HARDCODED_COLOR_JSX.test(line)) {
|
|
128
|
+
if (f.path.includes("tailwind.config") || f.path.includes("theme") || f.path.includes("tokens"))
|
|
129
|
+
continue;
|
|
130
|
+
hardcodedColorCount++;
|
|
131
|
+
if (hardcodedColorCount <= 5) {
|
|
132
|
+
const match = line.match(/#[0-9a-fA-F]{3,8}/);
|
|
133
|
+
issues.push({
|
|
134
|
+
severity: "warning",
|
|
135
|
+
message: `Hardcoded color ${match?.[0] || ""} in JSX — use a CSS variable or design token`,
|
|
136
|
+
file: f.path,
|
|
137
|
+
line: i + 1,
|
|
138
|
+
rule: "hardcoded-color",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Spacing values (cross-file consistency)
|
|
143
|
+
if (SPACING_PROP.test(line)) {
|
|
144
|
+
let match;
|
|
145
|
+
MAGIC_PX.lastIndex = 0;
|
|
146
|
+
while ((match = MAGIC_PX.exec(line)) !== null) {
|
|
147
|
+
const px = parseInt(match[1], 10);
|
|
148
|
+
if (px > 2)
|
|
149
|
+
spacingValues.set(px, (spacingValues.get(px) || 0) + 1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Duplicate Tailwind class strings
|
|
153
|
+
if (hasTailwind) {
|
|
154
|
+
const classMatch = line.match(/className\s*=\s*["'`]([^"'`]{20,})["'`]/);
|
|
155
|
+
if (classMatch) {
|
|
156
|
+
const classes = classMatch[1].trim();
|
|
157
|
+
const existing = tailwindStrings.get(classes) || [];
|
|
158
|
+
existing.push(f.path);
|
|
159
|
+
tailwindStrings.set(classes, existing);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ── Aggregate findings ──
|
|
165
|
+
// Mixed approaches
|
|
166
|
+
const activeApproaches = [...approaches.entries()].filter(([, count]) => count >= 3);
|
|
167
|
+
if (activeApproaches.length > 1) {
|
|
168
|
+
issues.push({
|
|
169
|
+
severity: "warning",
|
|
170
|
+
message: `Mixed styling approaches: ${activeApproaches.map(([n, c]) => `${n} (${c})`).join(", ")} — pick one`,
|
|
171
|
+
rule: "mixed-styling",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
// Inline style ratio
|
|
175
|
+
if (componentFiles.length > 3 && inlineStyleCount > 0) {
|
|
176
|
+
const ratio = inlineStyleCount / (inlineStyleCount + classNameCount);
|
|
177
|
+
if (ratio > 0.3) {
|
|
178
|
+
issues.push({
|
|
179
|
+
severity: "warning",
|
|
180
|
+
message: `${Math.round(ratio * 100)}% inline styles — extract to CSS classes or Tailwind`,
|
|
181
|
+
rule: "inline-style-ratio",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// Hardcoded colors summary
|
|
186
|
+
if (hardcodedColorCount > 5) {
|
|
187
|
+
issues.push({ severity: "warning", message: `${hardcodedColorCount} hardcoded colors in JSX — define a color palette`, rule: "hardcoded-color" });
|
|
188
|
+
}
|
|
189
|
+
// !important (only if we counted, not Stylelint)
|
|
190
|
+
if (!stylelintIssues && importantCount > 3) {
|
|
191
|
+
issues.push({ severity: "warning", message: `${importantCount} uses of !important — indicates specificity wars`, rule: "important-abuse" });
|
|
192
|
+
}
|
|
193
|
+
// Inconsistent spacing
|
|
194
|
+
const values = [...spacingValues.keys()].sort((a, b) => a - b);
|
|
195
|
+
const notOnScale = values.filter((v) => v % 4 !== 0 && v !== 1 && v !== 2);
|
|
196
|
+
if (notOnScale.length > 3) {
|
|
197
|
+
issues.push({ severity: "warning", message: `Inconsistent spacing: ${notOnScale.slice(0, 6).join(", ")}px — use a 4px/8px scale`, rule: "inconsistent-spacing" });
|
|
198
|
+
}
|
|
199
|
+
// Duplicate Tailwind strings
|
|
200
|
+
if (hasTailwind) {
|
|
201
|
+
let dupeCount = 0;
|
|
202
|
+
for (const [classes, usedIn] of tailwindStrings) {
|
|
203
|
+
if (usedIn.length >= 3) {
|
|
204
|
+
dupeCount++;
|
|
205
|
+
if (dupeCount <= 3) {
|
|
206
|
+
issues.push({
|
|
207
|
+
severity: "info",
|
|
208
|
+
message: `Tailwind classes duplicated in ${usedIn.length} files — extract component: "${classes.slice(0, 60)}${classes.length > 60 ? "..." : ""}"`,
|
|
209
|
+
rule: "duplicate-tailwind",
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (dupeCount > 3) {
|
|
215
|
+
issues.push({ severity: "warning", message: `${dupeCount} duplicated Tailwind class strings — extract shared components`, rule: "duplicate-tailwind" });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// Tailwind theme check
|
|
219
|
+
if (hasTailwind && componentFiles.length > 5) {
|
|
220
|
+
let hasThemeExtend = false;
|
|
221
|
+
for (const cfg of ["tailwind.config.js", "tailwind.config.ts", "tailwind.config.mjs"]) {
|
|
222
|
+
if (existsSync(join(cwd, cfg))) {
|
|
223
|
+
try {
|
|
224
|
+
if (readFileSync(join(cwd, cfg), "utf-8").includes("extend"))
|
|
225
|
+
hasThemeExtend = true;
|
|
226
|
+
}
|
|
227
|
+
catch { /* ignore */ }
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (!hasThemeExtend) {
|
|
232
|
+
issues.push({ severity: "info", message: "No theme extension in tailwind.config — consider defining custom colors/spacing", rule: "tailwind-no-theme" });
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const errorCount = issues.filter((i) => i.severity === "error").length;
|
|
236
|
+
const warnCount = issues.filter((i) => i.severity === "warning").length;
|
|
237
|
+
const score = Math.max(0, 100 - errorCount * 20 - warnCount * 12);
|
|
238
|
+
return {
|
|
239
|
+
name: "styling",
|
|
240
|
+
score,
|
|
241
|
+
grade: gradeFromScore(score),
|
|
242
|
+
details: {
|
|
243
|
+
tool,
|
|
244
|
+
totalComponentFiles: componentFiles.length,
|
|
245
|
+
approaches: Object.fromEntries(approaches),
|
|
246
|
+
hasTailwind,
|
|
247
|
+
inlineStyleCount,
|
|
248
|
+
hardcodedColorCount,
|
|
249
|
+
importantCount,
|
|
250
|
+
spacingValues: spacingValues.size,
|
|
251
|
+
stylelintIssues: stylelintIssues?.length ?? 0,
|
|
252
|
+
suggestion: !stylelintIssues ? "Install Stylelint for deeper CSS analysis (170+ rules): pnpm add -D stylelint stylelint-config-standard" : undefined,
|
|
253
|
+
},
|
|
254
|
+
issues,
|
|
255
|
+
duration: Date.now() - start,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
/** Recursively scan for CSS/SCSS files. */
|
|
259
|
+
function scanCssFiles(cwd, subdir, fn) {
|
|
260
|
+
const { readdirSync, statSync } = require("node:fs");
|
|
261
|
+
const dir = join(cwd, subdir);
|
|
262
|
+
if (!existsSync(dir))
|
|
263
|
+
return;
|
|
264
|
+
try {
|
|
265
|
+
for (const entry of readdirSync(dir)) {
|
|
266
|
+
if (entry === "node_modules" || entry === ".git" || entry === ".vibe-check")
|
|
267
|
+
continue;
|
|
268
|
+
const full = join(dir, entry);
|
|
269
|
+
try {
|
|
270
|
+
const stat = statSync(full);
|
|
271
|
+
if (stat.isDirectory())
|
|
272
|
+
scanCssFiles(cwd, join(subdir, entry), fn);
|
|
273
|
+
else if (/\.(css|scss)$/.test(entry))
|
|
274
|
+
fn(readFileSync(full, "utf-8"));
|
|
275
|
+
}
|
|
276
|
+
catch { /* skip */ }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
catch { /* skip */ }
|
|
280
|
+
}
|