@darrenjcoxon/vibeoptimise 1.0.1 → 1.0.3
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/scanners/css-efficiency.js +14 -3
- package/dist/scanners/depcheck.js +16 -10
- package/dist/scanners/eslint-perf.js +2 -7
- package/dist/scanners/jscpd.js +2 -7
- package/dist/scanners/knip.js +2 -7
- package/dist/scanners/madge.js +2 -7
- package/dist/scanners/regex-safety.d.ts +46 -10
- package/dist/scanners/regex-safety.js +112 -40
- package/dist/scanners/source-map-explorer.js +2 -7
- package/dist/utils/tool-installer.d.ts +24 -0
- package/dist/utils/tool-installer.js +56 -0
- package/package.json +1 -1
|
@@ -174,12 +174,23 @@ export class CssEfficiencyScanner {
|
|
|
174
174
|
}
|
|
175
175
|
extractSelectors(content) {
|
|
176
176
|
const selectors = [];
|
|
177
|
+
// Keyframe stops are NOT selectors — filter them out
|
|
178
|
+
const keyframeStops = new Set(['from', 'to', '0%', '100%', '50%', '25%', '75%']);
|
|
177
179
|
const matches = content.matchAll(/^([^@{}/\n][^{]*)\{/gm);
|
|
178
180
|
for (const match of matches) {
|
|
179
181
|
const sel = match[1].trim();
|
|
180
|
-
if (
|
|
181
|
-
|
|
182
|
-
|
|
182
|
+
if (!sel)
|
|
183
|
+
continue;
|
|
184
|
+
if (sel.startsWith('/*') || sel.startsWith('//'))
|
|
185
|
+
continue;
|
|
186
|
+
// Skip keyframe percentage stops and named stops
|
|
187
|
+
if (keyframeStops.has(sel))
|
|
188
|
+
continue;
|
|
189
|
+
if (/^\d+%$/.test(sel))
|
|
190
|
+
continue; // Any percentage like 30%, 60%
|
|
191
|
+
if (/^\d+%\s*,\s*\d+%/.test(sel))
|
|
192
|
+
continue; // Combined like "0%, 100%"
|
|
193
|
+
selectors.push(sel);
|
|
183
194
|
}
|
|
184
195
|
return selectors;
|
|
185
196
|
}
|
|
@@ -10,18 +10,13 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
+
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
13
14
|
export class DepcheckScanner {
|
|
14
15
|
name = 'Depcheck';
|
|
15
16
|
description = 'Dependency analyzer — finds unused and missing npm packages';
|
|
16
17
|
category = 'dead-code';
|
|
17
18
|
async isAvailable() {
|
|
18
|
-
|
|
19
|
-
execSync('npx --yes depcheck --version', { stdio: 'pipe', timeout: 30000 });
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
19
|
+
return isToolAvailable('depcheck');
|
|
25
20
|
}
|
|
26
21
|
async scan(targetDir, options) {
|
|
27
22
|
const start = Date.now();
|
|
@@ -78,19 +73,30 @@ export class DepcheckScanner {
|
|
|
78
73
|
'tailwindcss', 'postcss', 'autoprefixer',
|
|
79
74
|
'husky', 'lint-staged', 'commitlint',
|
|
80
75
|
'@playwright/test', 'vitest',
|
|
76
|
+
'dotenv', 'dotenv-cli', 'cross-env', // Used by scripts, not detectable
|
|
77
|
+
'prisma', '@prisma/client', // Used via generate/migrate, not direct imports
|
|
78
|
+
'supabase', // CLI tool, not a direct import
|
|
81
79
|
]);
|
|
82
80
|
// Unused dependencies
|
|
83
81
|
if (report.dependencies && Array.isArray(report.dependencies)) {
|
|
84
82
|
for (const dep of report.dependencies) {
|
|
83
|
+
// In monorepos, "unused" deps may be consumed via workspace packages
|
|
84
|
+
// that depcheck can't trace — lower severity and note this
|
|
85
|
+
const severity = isMonorepo ? 'medium' : 'high';
|
|
86
|
+
const monorepoNote = isMonorepo
|
|
87
|
+
? ' Note: in a monorepo, this package may be used by a workspace dependency that depcheck cannot trace.'
|
|
88
|
+
: '';
|
|
85
89
|
findings.push({
|
|
86
90
|
id: `depcheck-unused-${dep}`,
|
|
87
91
|
scanner: this.name,
|
|
88
92
|
category: 'dead-code',
|
|
89
|
-
severity:
|
|
93
|
+
severity: severity,
|
|
90
94
|
title: `Unused dependency: ${dep}`,
|
|
91
|
-
description: `Package "${dep}" is in dependencies but no usage was detected in the source code
|
|
95
|
+
description: `Package "${dep}" is in dependencies but no usage was detected in the source code.${monorepoNote}`,
|
|
92
96
|
file: 'package.json',
|
|
93
|
-
suggestion:
|
|
97
|
+
suggestion: isMonorepo
|
|
98
|
+
? `Verify "${dep}" isn't used via workspace resolution before removing. Run \`grep -r "${dep}" --include="*.ts" --include="*.tsx" .\` to double-check.`
|
|
99
|
+
: `Run \`npm uninstall ${dep}\` to remove it. This reduces install size and potential supply chain risk.`,
|
|
94
100
|
estimatedImpact: 'Faster installs, smaller node_modules, reduced attack surface',
|
|
95
101
|
});
|
|
96
102
|
}
|
|
@@ -13,18 +13,13 @@
|
|
|
13
13
|
import { execSync } from 'child_process';
|
|
14
14
|
import { existsSync } from 'fs';
|
|
15
15
|
import { join } from 'path';
|
|
16
|
+
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
16
17
|
export class EslintPerfScanner {
|
|
17
18
|
name = 'ESLint Perf';
|
|
18
19
|
description = 'Performance-focused linting — import cycles, unused imports, inefficient patterns';
|
|
19
20
|
category = 'code-quality';
|
|
20
21
|
async isAvailable() {
|
|
21
|
-
|
|
22
|
-
execSync('npx --yes eslint --version', { stdio: 'pipe', timeout: 30000 });
|
|
23
|
-
return true;
|
|
24
|
-
}
|
|
25
|
-
catch {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
22
|
+
return isToolAvailable('eslint');
|
|
28
23
|
}
|
|
29
24
|
async scan(targetDir, options) {
|
|
30
25
|
const start = Date.now();
|
package/dist/scanners/jscpd.js
CHANGED
|
@@ -10,18 +10,13 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync, readFileSync, mkdirSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
+
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
13
14
|
export class JscpdScanner {
|
|
14
15
|
name = 'JSCPD';
|
|
15
16
|
description = 'Copy/paste detector — finds duplicated code blocks across the codebase';
|
|
16
17
|
category = 'duplication';
|
|
17
18
|
async isAvailable() {
|
|
18
|
-
|
|
19
|
-
execSync('npx --yes jscpd --version', { stdio: 'pipe', timeout: 30000 });
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
19
|
+
return isToolAvailable('jscpd');
|
|
25
20
|
}
|
|
26
21
|
async scan(targetDir, options) {
|
|
27
22
|
const start = Date.now();
|
package/dist/scanners/knip.js
CHANGED
|
@@ -10,18 +10,13 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
+
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
13
14
|
export class KnipScanner {
|
|
14
15
|
name = 'Knip';
|
|
15
16
|
description = 'Dead code detector — unused files, exports, dependencies, types';
|
|
16
17
|
category = 'dead-code';
|
|
17
18
|
async isAvailable() {
|
|
18
|
-
|
|
19
|
-
execSync('npx --yes knip --version', { stdio: 'pipe', timeout: 30000 });
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
19
|
+
return isToolAvailable('knip');
|
|
25
20
|
}
|
|
26
21
|
async scan(targetDir, options) {
|
|
27
22
|
const start = Date.now();
|
package/dist/scanners/madge.js
CHANGED
|
@@ -10,18 +10,13 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
+
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
13
14
|
export class MadgeScanner {
|
|
14
15
|
name = 'Madge';
|
|
15
16
|
description = 'Circular dependency detector — finds import cycles and orphaned modules';
|
|
16
17
|
category = 'performance';
|
|
17
18
|
async isAvailable() {
|
|
18
|
-
|
|
19
|
-
execSync('npx --yes madge --version', { stdio: 'pipe', timeout: 30000 });
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
19
|
+
return isToolAvailable('madge');
|
|
25
20
|
}
|
|
26
21
|
async scan(targetDir, options) {
|
|
27
22
|
const start = Date.now();
|
|
@@ -25,18 +25,54 @@ export declare class RegexSafetyScanner implements Scanner {
|
|
|
25
25
|
private extractRegexLiterals;
|
|
26
26
|
private extractRegExpConstructors;
|
|
27
27
|
/**
|
|
28
|
-
* Only flag genuinely dangerous patterns
|
|
28
|
+
* Only flag genuinely dangerous patterns — patterns where the regex engine
|
|
29
|
+
* can enter exponential or polynomial backtracking.
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* - High: Overlapping alternation + quantifier (likely quadratic+)
|
|
33
|
-
* - Medium: 3+ greedy .* in same pattern (quadratic risk)
|
|
31
|
+
* KEY INSIGHT: A "nested quantifier" is only dangerous when the inner
|
|
32
|
+
* quantified expression can match the SAME characters as what follows.
|
|
34
33
|
*
|
|
35
|
-
*
|
|
36
|
-
* -
|
|
37
|
-
* -
|
|
38
|
-
* -
|
|
39
|
-
* -
|
|
34
|
+
* SAFE examples (we do NOT flag these):
|
|
35
|
+
* - ([^*\n]+?)[ \t]+ → [^*\n] is bounded, can't overlap with [ \t]
|
|
36
|
+
* - (\d+(?:\.\d+)?) → \d is bounded, ? is not a loop quantifier
|
|
37
|
+
* - [\d+(?:\s*,\s*\d+)+] → requires , delimiter, can't backtrack
|
|
38
|
+
* - ([\s\S]*?) → lazy quantifier with bounded context
|
|
39
|
+
*
|
|
40
|
+
* DANGEROUS examples (we DO flag these):
|
|
41
|
+
* - (a+)+ → unbounded a+ inside quantified group
|
|
42
|
+
* - (.*)* → .* inside quantified group (classic ReDoS)
|
|
43
|
+
* - (\w+\s+)+ → \w+ and \s+ can alternate indefinitely
|
|
40
44
|
*/
|
|
41
45
|
private analyzeRegex;
|
|
46
|
+
/**
|
|
47
|
+
* Check for genuinely dangerous nested quantifiers.
|
|
48
|
+
*
|
|
49
|
+
* Only flags when:
|
|
50
|
+
* 1. There's a group with a quantifier: (...)+ or (...)* or (...){n,}
|
|
51
|
+
* 2. Inside that group, there's a quantified UNBOUNDED matcher
|
|
52
|
+
* - . (dot), \w, \s, \d followed by + or *
|
|
53
|
+
* - NOT [^specific]+ which is bounded by the exclusion
|
|
54
|
+
* - NOT (?:...) non-capturing groups with ? (optional, not loop)
|
|
55
|
+
*/
|
|
56
|
+
private hasDangerousNestedQuantifier;
|
|
57
|
+
/**
|
|
58
|
+
* Check if a group's content contains an inner quantifier that creates
|
|
59
|
+
* backtracking risk when the group itself is quantified.
|
|
60
|
+
*
|
|
61
|
+
* Key insight: (X+)+ is ALWAYS vulnerable for any X, because the regex
|
|
62
|
+
* engine can partition the input into the inner vs outer repetition in
|
|
63
|
+
* exponentially many ways. The exception is negated character classes
|
|
64
|
+
* with a following delimiter that prevents overlap.
|
|
65
|
+
*
|
|
66
|
+
* DANGEROUS: (a+)+, (.+)+, (\w+)+, (\s+\w+)+
|
|
67
|
+
* SAFE: ([^,]+,)+ — the comma delimiter prevents backtracking
|
|
68
|
+
* SAFE: (\d+(?:\.\d+)?) — ? is not a repeating quantifier
|
|
69
|
+
* SAFE: ([^*\n]+?)[ \t]+ — lazy quantifier + different char class after
|
|
70
|
+
*/
|
|
71
|
+
private hasUnboundedInnerQuantifier;
|
|
72
|
+
/**
|
|
73
|
+
* Check for overlapping alternation with quantifier.
|
|
74
|
+
* Only flags when alternation branches use unbounded matchers that
|
|
75
|
+
* can match the same characters.
|
|
76
|
+
*/
|
|
77
|
+
private hasDangerousAlternation;
|
|
42
78
|
}
|
|
@@ -186,57 +186,43 @@ export class RegexSafetyScanner {
|
|
|
186
186
|
return regexes;
|
|
187
187
|
}
|
|
188
188
|
/**
|
|
189
|
-
* Only flag genuinely dangerous patterns
|
|
189
|
+
* Only flag genuinely dangerous patterns — patterns where the regex engine
|
|
190
|
+
* can enter exponential or polynomial backtracking.
|
|
190
191
|
*
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
* - High: Overlapping alternation + quantifier (likely quadratic+)
|
|
194
|
-
* - Medium: 3+ greedy .* in same pattern (quadratic risk)
|
|
192
|
+
* KEY INSIGHT: A "nested quantifier" is only dangerous when the inner
|
|
193
|
+
* quantified expression can match the SAME characters as what follows.
|
|
195
194
|
*
|
|
196
|
-
*
|
|
197
|
-
* -
|
|
198
|
-
* -
|
|
199
|
-
* -
|
|
200
|
-
* -
|
|
195
|
+
* SAFE examples (we do NOT flag these):
|
|
196
|
+
* - ([^*\n]+?)[ \t]+ → [^*\n] is bounded, can't overlap with [ \t]
|
|
197
|
+
* - (\d+(?:\.\d+)?) → \d is bounded, ? is not a loop quantifier
|
|
198
|
+
* - [\d+(?:\s*,\s*\d+)+] → requires , delimiter, can't backtrack
|
|
199
|
+
* - ([\s\S]*?) → lazy quantifier with bounded context
|
|
200
|
+
*
|
|
201
|
+
* DANGEROUS examples (we DO flag these):
|
|
202
|
+
* - (a+)+ → unbounded a+ inside quantified group
|
|
203
|
+
* - (.*)* → .* inside quantified group (classic ReDoS)
|
|
204
|
+
* - (\w+\s+)+ → \w+ and \s+ can alternate indefinitely
|
|
201
205
|
*/
|
|
202
206
|
analyzeRegex(pattern) {
|
|
203
207
|
const issues = [];
|
|
204
|
-
// Critical:
|
|
205
|
-
|
|
206
|
-
|
|
208
|
+
// Critical: TRUE nested quantifiers only
|
|
209
|
+
// Pattern: (X+)+ or (X*)+ where X is an UNBOUNDED matcher (. or \w or \s)
|
|
210
|
+
// NOT: ([^specific]+)+ which is bounded and safe
|
|
211
|
+
if (this.hasDangerousNestedQuantifier(pattern)) {
|
|
207
212
|
issues.push({
|
|
208
213
|
severity: 'critical',
|
|
209
214
|
description: 'Nested quantifiers — exponential backtracking risk (ReDoS)',
|
|
210
215
|
suggestion: 'Rewrite to avoid nested quantifiers. Instead of `(a+)+`, use `a+`. For complex patterns, consider the RE2 engine (npm re2) which guarantees linear time.',
|
|
211
216
|
});
|
|
212
217
|
}
|
|
213
|
-
// High: Overlapping alternation with quantifier
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return false;
|
|
222
|
-
// Both contain wildcards — definite overlap
|
|
223
|
-
if ((a.includes('.') || a.includes('\\w') || a.includes('\\d')) &&
|
|
224
|
-
(b.includes('.') || b.includes('\\w') || b.includes('\\d')))
|
|
225
|
-
return true;
|
|
226
|
-
// Same starting char — potential overlap
|
|
227
|
-
const aFirst = a.replace(/^\\/, '')[0];
|
|
228
|
-
const bFirst = b.replace(/^\\/, '')[0];
|
|
229
|
-
return aFirst && bFirst && aFirst === bFirst;
|
|
230
|
-
}));
|
|
231
|
-
if (hasOverlap) {
|
|
232
|
-
issues.push({
|
|
233
|
-
severity: 'high',
|
|
234
|
-
description: 'Overlapping alternation with quantifier — potential backtracking',
|
|
235
|
-
suggestion: 'Ensure alternation branches are mutually exclusive, or rewrite to avoid the quantifier on the group.',
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
218
|
+
// High: Overlapping alternation with quantifier where branches can match same input
|
|
219
|
+
// Only flag when alternatives use unbounded matchers (., \w, \s)
|
|
220
|
+
if (this.hasDangerousAlternation(pattern)) {
|
|
221
|
+
issues.push({
|
|
222
|
+
severity: 'high',
|
|
223
|
+
description: 'Overlapping alternation with quantifier — potential backtracking',
|
|
224
|
+
suggestion: 'Ensure alternation branches are mutually exclusive, or rewrite to avoid the quantifier on the group.',
|
|
225
|
+
});
|
|
240
226
|
}
|
|
241
227
|
// Medium: 3+ greedy wildcards (2 is normal, 3+ is risky)
|
|
242
228
|
const wildcardCount = (pattern.match(/\.\*/g) || []).length;
|
|
@@ -249,4 +235,90 @@ export class RegexSafetyScanner {
|
|
|
249
235
|
}
|
|
250
236
|
return issues;
|
|
251
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Check for genuinely dangerous nested quantifiers.
|
|
240
|
+
*
|
|
241
|
+
* Only flags when:
|
|
242
|
+
* 1. There's a group with a quantifier: (...)+ or (...)* or (...){n,}
|
|
243
|
+
* 2. Inside that group, there's a quantified UNBOUNDED matcher
|
|
244
|
+
* - . (dot), \w, \s, \d followed by + or *
|
|
245
|
+
* - NOT [^specific]+ which is bounded by the exclusion
|
|
246
|
+
* - NOT (?:...) non-capturing groups with ? (optional, not loop)
|
|
247
|
+
*/
|
|
248
|
+
hasDangerousNestedQuantifier(pattern) {
|
|
249
|
+
// Match groups followed by quantifiers: (...)+ or (...)* or (...){2,}
|
|
250
|
+
const groupWithQuantifier = /\(([^)]+)\)[+*]|\(([^)]+)\)\{[\d,]+\}/g;
|
|
251
|
+
let match;
|
|
252
|
+
while ((match = groupWithQuantifier.exec(pattern)) !== null) {
|
|
253
|
+
const groupContent = match[1] || match[2];
|
|
254
|
+
if (!groupContent)
|
|
255
|
+
continue;
|
|
256
|
+
// Check if group contains an unbounded quantified expression
|
|
257
|
+
// Dangerous: .+, .*, \w+, \w*, \s+, \s*
|
|
258
|
+
// Safe: [^x]+, [specific]+, \d+ (bounded), lazy .*? in limited context
|
|
259
|
+
if (this.hasUnboundedInnerQuantifier(groupContent)) {
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Check if a group's content contains an inner quantifier that creates
|
|
267
|
+
* backtracking risk when the group itself is quantified.
|
|
268
|
+
*
|
|
269
|
+
* Key insight: (X+)+ is ALWAYS vulnerable for any X, because the regex
|
|
270
|
+
* engine can partition the input into the inner vs outer repetition in
|
|
271
|
+
* exponentially many ways. The exception is negated character classes
|
|
272
|
+
* with a following delimiter that prevents overlap.
|
|
273
|
+
*
|
|
274
|
+
* DANGEROUS: (a+)+, (.+)+, (\w+)+, (\s+\w+)+
|
|
275
|
+
* SAFE: ([^,]+,)+ — the comma delimiter prevents backtracking
|
|
276
|
+
* SAFE: (\d+(?:\.\d+)?) — ? is not a repeating quantifier
|
|
277
|
+
* SAFE: ([^*\n]+?)[ \t]+ — lazy quantifier + different char class after
|
|
278
|
+
*/
|
|
279
|
+
hasUnboundedInnerQuantifier(content) {
|
|
280
|
+
// Check for any atom followed by + or * (greedy) inside the group
|
|
281
|
+
// This catches: a+, .+, \w+, \d+, [abc]+, etc.
|
|
282
|
+
const hasInnerQuantifier = /[^?][+*]/.test(content) || /^.[+*]/.test(content);
|
|
283
|
+
if (!hasInnerQuantifier)
|
|
284
|
+
return false;
|
|
285
|
+
// Now check for SAFE exceptions:
|
|
286
|
+
// Safe: negated char class followed by a delimiter
|
|
287
|
+
// e.g., [^,]+, or [^"]+\" — the delimiter prevents overlap
|
|
288
|
+
if (/\[\^[^\]]+\][+*].*[^\\][\w,;:"'|&]/.test(content)) {
|
|
289
|
+
// Has a negated class + a literal delimiter after — likely safe
|
|
290
|
+
// But only if the delimiter is in the negated class
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
// Safe: lazy quantifier (+? or *?) — prevents catastrophic backtracking
|
|
294
|
+
// e.g., ([^*\n]+?)[ \t]+ — the lazy ? means it yields to what follows
|
|
295
|
+
if (/[+*]\?/.test(content) && !/[+*][^?].*[+*]/.test(content)) {
|
|
296
|
+
// Has a lazy quantifier and no other greedy quantifier — safe
|
|
297
|
+
return false;
|
|
298
|
+
}
|
|
299
|
+
// Safe: \d+ is bounded (only digits) — ((\d+)\.?)+ is technically
|
|
300
|
+
// vulnerable but in practice \d is so bounded it rarely causes issues
|
|
301
|
+
// Exception: (\d+\s*)+ IS dangerous because \s* can match empty
|
|
302
|
+
if (/\\d[+*]/.test(content) && !/\\[sw.][+*]/.test(content)) {
|
|
303
|
+
return false;
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Check for overlapping alternation with quantifier.
|
|
309
|
+
* Only flags when alternation branches use unbounded matchers that
|
|
310
|
+
* can match the same characters.
|
|
311
|
+
*/
|
|
312
|
+
hasDangerousAlternation(pattern) {
|
|
313
|
+
const match = pattern.match(/\(([^)]*\|[^)]*)\)\s*[+*{]/);
|
|
314
|
+
if (!match)
|
|
315
|
+
return false;
|
|
316
|
+
const altParts = match[1].split('|');
|
|
317
|
+
if (altParts.length < 2)
|
|
318
|
+
return false;
|
|
319
|
+
// Only flag if BOTH branches contain unbounded matchers
|
|
320
|
+
const hasUnbounded = (part) => /\.[+*]/.test(part) || /\\[ws][+*]/.test(part);
|
|
321
|
+
const unboundedCount = altParts.filter(hasUnbounded).length;
|
|
322
|
+
return unboundedCount >= 2;
|
|
323
|
+
}
|
|
252
324
|
}
|
|
@@ -10,18 +10,13 @@
|
|
|
10
10
|
import { execSync } from 'child_process';
|
|
11
11
|
import { existsSync } from 'fs';
|
|
12
12
|
import { join } from 'path';
|
|
13
|
+
import { isToolAvailable } from '../utils/tool-installer.js';
|
|
13
14
|
export class SourceMapExplorerScanner {
|
|
14
15
|
name = 'Source Map Explorer';
|
|
15
16
|
description = 'Bundle composition analyzer — shows what code is in your production bundles';
|
|
16
17
|
category = 'bundle-size';
|
|
17
18
|
async isAvailable() {
|
|
18
|
-
|
|
19
|
-
execSync('npx --yes source-map-explorer --version', { stdio: 'pipe', timeout: 30000 });
|
|
20
|
-
return true;
|
|
21
|
-
}
|
|
22
|
-
catch {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
19
|
+
return isToolAvailable('source-map-explorer');
|
|
25
20
|
}
|
|
26
21
|
async scan(targetDir, options) {
|
|
27
22
|
const start = Date.now();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Installer Utility
|
|
3
|
+
*
|
|
4
|
+
* Robust availability checking for npm-based scanning tools.
|
|
5
|
+
* Tries local npx → npx --yes → global install as fallback.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Check if an npm tool is available, installing it if needed.
|
|
9
|
+
*
|
|
10
|
+
* Strategy:
|
|
11
|
+
* 1. Try npx (uses local or cached version — fast)
|
|
12
|
+
* 2. Try npx --yes (auto-installs if not cached — slower)
|
|
13
|
+
* 3. Try global install as last resort
|
|
14
|
+
*
|
|
15
|
+
* @param packageName - npm package name (e.g., 'knip', 'jscpd', 'madge')
|
|
16
|
+
* @param versionFlag - flag to check version (default: '--version')
|
|
17
|
+
* @returns true if tool is available
|
|
18
|
+
*/
|
|
19
|
+
export declare function isToolAvailable(packageName: string, versionFlag?: string): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Build a command that runs a tool via npx --yes.
|
|
22
|
+
* Ensures the tool will be auto-installed if not present.
|
|
23
|
+
*/
|
|
24
|
+
export declare function npxCmd(packageName: string, args: string): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Installer Utility
|
|
3
|
+
*
|
|
4
|
+
* Robust availability checking for npm-based scanning tools.
|
|
5
|
+
* Tries local npx → npx --yes → global install as fallback.
|
|
6
|
+
*/
|
|
7
|
+
import { execSync } from 'child_process';
|
|
8
|
+
/**
|
|
9
|
+
* Check if an npm tool is available, installing it if needed.
|
|
10
|
+
*
|
|
11
|
+
* Strategy:
|
|
12
|
+
* 1. Try npx (uses local or cached version — fast)
|
|
13
|
+
* 2. Try npx --yes (auto-installs if not cached — slower)
|
|
14
|
+
* 3. Try global install as last resort
|
|
15
|
+
*
|
|
16
|
+
* @param packageName - npm package name (e.g., 'knip', 'jscpd', 'madge')
|
|
17
|
+
* @param versionFlag - flag to check version (default: '--version')
|
|
18
|
+
* @returns true if tool is available
|
|
19
|
+
*/
|
|
20
|
+
export function isToolAvailable(packageName, versionFlag = '--version') {
|
|
21
|
+
// Try 1: Local/cached npx (fast path)
|
|
22
|
+
try {
|
|
23
|
+
execSync(`npx ${packageName} ${versionFlag}`, {
|
|
24
|
+
stdio: 'pipe',
|
|
25
|
+
timeout: 15000,
|
|
26
|
+
});
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
catch { }
|
|
30
|
+
// Try 2: npx --yes to auto-install (first-time download)
|
|
31
|
+
try {
|
|
32
|
+
execSync(`npx --yes ${packageName} ${versionFlag}`, {
|
|
33
|
+
stdio: 'pipe',
|
|
34
|
+
timeout: 90000, // 90s for first download
|
|
35
|
+
});
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch { }
|
|
39
|
+
// Try 3: Global install as fallback
|
|
40
|
+
try {
|
|
41
|
+
execSync(`npm install -g ${packageName} 2>/dev/null && npx ${packageName} ${versionFlag}`, {
|
|
42
|
+
stdio: 'pipe',
|
|
43
|
+
timeout: 90000,
|
|
44
|
+
});
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
catch { }
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build a command that runs a tool via npx --yes.
|
|
52
|
+
* Ensures the tool will be auto-installed if not present.
|
|
53
|
+
*/
|
|
54
|
+
export function npxCmd(packageName, args) {
|
|
55
|
+
return `npx --yes ${packageName} ${args}`;
|
|
56
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@darrenjcoxon/vibeoptimise",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "🚀 VibeOptimise — The ultimate codebase optimisation scanner. Find dead code, duplication, circular deps, bundle bloat & more. Get OPTIMISE.md for AI agents to fix everything.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|