@darrenjcoxon/vibeoptimise 1.0.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.
@@ -0,0 +1,209 @@
1
+ /**
2
+ * ESLint Performance Scanner — Import Hygiene & Inefficient Patterns
3
+ *
4
+ * Runs ESLint with performance-focused rules to detect:
5
+ * - Import cycles (eslint-plugin-import)
6
+ * - Unused imports
7
+ * - Barrel file anti-patterns
8
+ * - Inefficient array/object patterns
9
+ *
10
+ * Tool: eslint (npm)
11
+ * Output: JSON formatter
12
+ */
13
+ import { execSync } from 'child_process';
14
+ import { existsSync } from 'fs';
15
+ import { join } from 'path';
16
+ export class EslintPerfScanner {
17
+ name = 'ESLint Perf';
18
+ description = 'Performance-focused linting — import cycles, unused imports, inefficient patterns';
19
+ category = 'code-quality';
20
+ async isAvailable() {
21
+ try {
22
+ execSync('npx eslint --version', { stdio: 'pipe', timeout: 15000 });
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ async scan(targetDir, options) {
30
+ const start = Date.now();
31
+ const findings = [];
32
+ try {
33
+ // Check if eslint is available in the project
34
+ const hasEslint = existsSync(join(targetDir, 'node_modules', '.bin', 'eslint'))
35
+ || existsSync(join(targetDir, 'node_modules', 'eslint'));
36
+ if (!hasEslint) {
37
+ // Try with npx instead
38
+ return this.scanWithCustomRules(targetDir, options, start);
39
+ }
40
+ // Run eslint with JSON output
41
+ const extensions = '--ext .js,.jsx,.ts,.tsx,.mjs';
42
+ const ignorePattern = '--ignore-pattern "node_modules" --ignore-pattern "dist" --ignore-pattern "build" --ignore-pattern ".next"';
43
+ try {
44
+ const cmd = `npx eslint "${targetDir}" ${extensions} ${ignorePattern} -f json --no-error-on-unmatched-pattern 2>/dev/null`;
45
+ const output = execSync(cmd, {
46
+ cwd: targetDir,
47
+ stdio: 'pipe',
48
+ timeout: 120000,
49
+ }).toString();
50
+ if (output.trim()) {
51
+ const results = JSON.parse(output);
52
+ this.processEslintResults(results, findings);
53
+ }
54
+ }
55
+ catch (err) {
56
+ // ESLint exits non-zero when it finds errors
57
+ const output = err.stdout?.toString() || '';
58
+ if (output.trim()) {
59
+ try {
60
+ const results = JSON.parse(output);
61
+ this.processEslintResults(results, findings);
62
+ }
63
+ catch {
64
+ // JSON parse failed — ESLint might have errored
65
+ }
66
+ }
67
+ }
68
+ // Additionally, scan for common anti-patterns
69
+ this.scanForAntiPatterns(targetDir, findings);
70
+ return {
71
+ scanner: this.name,
72
+ findings,
73
+ summary: findings.length === 0
74
+ ? '✅ No performance-impacting lint issues found'
75
+ : `Found ${findings.length} performance-related lint issues`,
76
+ duration: Date.now() - start,
77
+ };
78
+ }
79
+ catch (err) {
80
+ return {
81
+ scanner: this.name,
82
+ findings: [],
83
+ summary: `Error: ${err.message}`,
84
+ duration: Date.now() - start,
85
+ error: err.message,
86
+ };
87
+ }
88
+ }
89
+ async scanWithCustomRules(targetDir, options, start) {
90
+ const findings = [];
91
+ // Even without project ESLint, scan for anti-patterns
92
+ this.scanForAntiPatterns(targetDir, findings);
93
+ return {
94
+ scanner: this.name,
95
+ findings,
96
+ summary: findings.length === 0
97
+ ? '✅ No performance anti-patterns detected'
98
+ : `Found ${findings.length} performance anti-patterns`,
99
+ duration: Date.now() - start,
100
+ };
101
+ }
102
+ processEslintResults(results, findings) {
103
+ if (!Array.isArray(results))
104
+ return;
105
+ // Performance-relevant rule IDs
106
+ const perfRules = new Set([
107
+ 'import/no-cycle',
108
+ 'import/no-duplicates',
109
+ 'import/no-unused-modules',
110
+ 'no-unused-vars',
111
+ '@typescript-eslint/no-unused-vars',
112
+ 'unused-imports/no-unused-imports',
113
+ 'no-console',
114
+ 'no-debugger',
115
+ 'prefer-const',
116
+ 'no-var',
117
+ ]);
118
+ for (const fileResult of results) {
119
+ if (!fileResult.messages || !Array.isArray(fileResult.messages))
120
+ continue;
121
+ for (const msg of fileResult.messages) {
122
+ if (!msg.ruleId)
123
+ continue;
124
+ // Only include performance-relevant rules
125
+ const isPerf = perfRules.has(msg.ruleId) ||
126
+ msg.ruleId.startsWith('import/') ||
127
+ msg.ruleId.includes('unused');
128
+ if (!isPerf)
129
+ continue;
130
+ let severity = 'low';
131
+ if (msg.ruleId === 'import/no-cycle')
132
+ severity = 'high';
133
+ else if (msg.ruleId.includes('unused'))
134
+ severity = 'medium';
135
+ findings.push({
136
+ id: `eslint-${msg.ruleId}-${fileResult.filePath}-${msg.line}`,
137
+ scanner: this.name,
138
+ category: msg.ruleId.startsWith('import/') ? 'import-hygiene' : 'code-quality',
139
+ severity,
140
+ title: `${msg.ruleId}: ${msg.message}`,
141
+ description: msg.message,
142
+ file: fileResult.filePath,
143
+ line: msg.line,
144
+ column: msg.column,
145
+ suggestion: msg.fix
146
+ ? 'Auto-fixable: run `eslint --fix` to resolve this automatically.'
147
+ : `Address the ESLint rule "${msg.ruleId}" violation.`,
148
+ });
149
+ }
150
+ }
151
+ }
152
+ scanForAntiPatterns(targetDir, findings) {
153
+ try {
154
+ // Check for barrel files that re-export everything (causes bundle bloat)
155
+ const barrelCheck = execSync(`find "${targetDir}" -name "index.ts" -o -name "index.js" | head -20`, { stdio: 'pipe', timeout: 10000 }).toString().trim();
156
+ if (barrelCheck) {
157
+ const barrelFiles = barrelCheck.split('\n').filter(f => !f.includes('node_modules') && !f.includes('dist') && !f.includes('.next'));
158
+ for (const barrelFile of barrelFiles) {
159
+ try {
160
+ const content = execSync(`cat "${barrelFile}"`, { stdio: 'pipe' }).toString();
161
+ const exportAllCount = (content.match(/export \* from/g) || []).length;
162
+ if (exportAllCount > 5) {
163
+ findings.push({
164
+ id: `eslint-barrel-${barrelFile}`,
165
+ scanner: this.name,
166
+ category: 'bundle-size',
167
+ severity: 'medium',
168
+ title: `Barrel file with ${exportAllCount} wildcard re-exports`,
169
+ description: `"${barrelFile}" uses ${exportAllCount} \`export * from\` statements. This can defeat tree-shaking and pull in unused code.`,
170
+ file: barrelFile,
171
+ suggestion: `Replace \`export * from\` with named exports to enable tree-shaking:\n\`export { SpecificThing } from './module'\`\n\nOnly re-export what consumers actually need.`,
172
+ estimatedImpact: 'Better tree-shaking, smaller bundles',
173
+ });
174
+ }
175
+ }
176
+ catch {
177
+ // Skip files we can't read
178
+ }
179
+ }
180
+ }
181
+ // Check for console.log statements in source (not test) files
182
+ try {
183
+ const consoleLogs = execSync(`grep -rn "console\\.log" "${targetDir}" --include="*.ts" --include="*.tsx" --include="*.js" --include="*.jsx" | grep -v node_modules | grep -v dist | grep -v ".test." | grep -v ".spec." | grep -v "__tests__" | head -20`, { stdio: 'pipe', timeout: 10000 }).toString().trim();
184
+ if (consoleLogs) {
185
+ const logLines = consoleLogs.split('\n').filter(Boolean);
186
+ if (logLines.length > 3) {
187
+ findings.push({
188
+ id: `eslint-console-logs`,
189
+ scanner: this.name,
190
+ category: 'code-quality',
191
+ severity: 'low',
192
+ title: `${logLines.length}+ console.log statements in production code`,
193
+ description: `Found ${logLines.length}+ console.log statements outside test files. These add unnecessary overhead and noise.`,
194
+ file: logLines[0]?.split(':')[0] || 'multiple files',
195
+ suggestion: `Remove console.log statements from production code. Use a proper logger (like pino or winston) that can be configured per environment, or use console.log only in development with a build-time strip.`,
196
+ estimatedImpact: 'Cleaner output, slight performance improvement',
197
+ });
198
+ }
199
+ }
200
+ }
201
+ catch {
202
+ // grep found nothing — that's good
203
+ }
204
+ }
205
+ catch {
206
+ // Anti-pattern scanning is best-effort
207
+ }
208
+ }
209
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Scanner Registry
3
+ *
4
+ * All 10 VibeOptimise scanners, registered in priority order.
5
+ */
6
+ export { KnipScanner } from './knip.js';
7
+ export { JscpdScanner } from './jscpd.js';
8
+ export { MadgeScanner } from './madge.js';
9
+ export { DepcheckScanner } from './depcheck.js';
10
+ export { BundlePhobiaScanner } from './bundlephobia.js';
11
+ export { EslintPerfScanner } from './eslint-perf.js';
12
+ export { SourceMapExplorerScanner } from './source-map-explorer.js';
13
+ export { RegexSafetyScanner } from './regex-safety.js';
14
+ export { CssEfficiencyScanner } from './css-efficiency.js';
15
+ export { QueryEfficiencyScanner } from './query-efficiency.js';
16
+ import { Scanner } from '../types.js';
17
+ /**
18
+ * Get all scanners in priority order:
19
+ * 1. Performance-critical (circular deps, regex safety)
20
+ * 2. Dead code removal (knip, depcheck)
21
+ * 3. Duplication (jscpd)
22
+ * 4. Bundle & dependency analysis
23
+ * 5. Code quality & CSS
24
+ * 6. Query efficiency
25
+ */
26
+ export declare function getAllScanners(): Scanner[];
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Scanner Registry
3
+ *
4
+ * All 10 VibeOptimise scanners, registered in priority order.
5
+ */
6
+ export { KnipScanner } from './knip.js';
7
+ export { JscpdScanner } from './jscpd.js';
8
+ export { MadgeScanner } from './madge.js';
9
+ export { DepcheckScanner } from './depcheck.js';
10
+ export { BundlePhobiaScanner } from './bundlephobia.js';
11
+ export { EslintPerfScanner } from './eslint-perf.js';
12
+ export { SourceMapExplorerScanner } from './source-map-explorer.js';
13
+ export { RegexSafetyScanner } from './regex-safety.js';
14
+ export { CssEfficiencyScanner } from './css-efficiency.js';
15
+ export { QueryEfficiencyScanner } from './query-efficiency.js';
16
+ import { KnipScanner } from './knip.js';
17
+ import { JscpdScanner } from './jscpd.js';
18
+ import { MadgeScanner } from './madge.js';
19
+ import { DepcheckScanner } from './depcheck.js';
20
+ import { BundlePhobiaScanner } from './bundlephobia.js';
21
+ import { EslintPerfScanner } from './eslint-perf.js';
22
+ import { SourceMapExplorerScanner } from './source-map-explorer.js';
23
+ import { RegexSafetyScanner } from './regex-safety.js';
24
+ import { CssEfficiencyScanner } from './css-efficiency.js';
25
+ import { QueryEfficiencyScanner } from './query-efficiency.js';
26
+ /**
27
+ * Get all scanners in priority order:
28
+ * 1. Performance-critical (circular deps, regex safety)
29
+ * 2. Dead code removal (knip, depcheck)
30
+ * 3. Duplication (jscpd)
31
+ * 4. Bundle & dependency analysis
32
+ * 5. Code quality & CSS
33
+ * 6. Query efficiency
34
+ */
35
+ export function getAllScanners() {
36
+ return [
37
+ // Tier 1: Performance-critical
38
+ new MadgeScanner(),
39
+ new RegexSafetyScanner(),
40
+ // Tier 2: Dead weight removal
41
+ new KnipScanner(),
42
+ new DepcheckScanner(),
43
+ // Tier 3: Duplication
44
+ new JscpdScanner(),
45
+ // Tier 4: Bundle & dependency health
46
+ new BundlePhobiaScanner(),
47
+ new SourceMapExplorerScanner(),
48
+ new EslintPerfScanner(),
49
+ // Tier 5: Asset & query efficiency
50
+ new CssEfficiencyScanner(),
51
+ new QueryEfficiencyScanner(),
52
+ ];
53
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * JSCPD Scanner — Code Duplication Detection
3
+ *
4
+ * Finds copy/pasted code blocks across the entire codebase.
5
+ * Uses Rabin-Karp algorithm, supports 150+ languages.
6
+ *
7
+ * Tool: jscpd (npm)
8
+ * Output: JSON reporter
9
+ */
10
+ import { Scanner, ScannerResult, ScanOptions } from '../types.js';
11
+ export declare class JscpdScanner implements Scanner {
12
+ name: string;
13
+ description: string;
14
+ category: "duplication";
15
+ isAvailable(): Promise<boolean>;
16
+ scan(targetDir: string, options: ScanOptions): Promise<ScannerResult>;
17
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * JSCPD Scanner — Code Duplication Detection
3
+ *
4
+ * Finds copy/pasted code blocks across the entire codebase.
5
+ * Uses Rabin-Karp algorithm, supports 150+ languages.
6
+ *
7
+ * Tool: jscpd (npm)
8
+ * Output: JSON reporter
9
+ */
10
+ import { execSync } from 'child_process';
11
+ import { existsSync, readFileSync, mkdirSync } from 'fs';
12
+ import { join } from 'path';
13
+ export class JscpdScanner {
14
+ name = 'JSCPD';
15
+ description = 'Copy/paste detector — finds duplicated code blocks across the codebase';
16
+ category = 'duplication';
17
+ async isAvailable() {
18
+ try {
19
+ execSync('npx jscpd --version', { stdio: 'pipe', timeout: 15000 });
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ async scan(targetDir, options) {
27
+ const start = Date.now();
28
+ const findings = [];
29
+ try {
30
+ const reportDir = join(targetDir, '.vibeoptimise-tmp');
31
+ mkdirSync(reportDir, { recursive: true });
32
+ const excludePatterns = [
33
+ '**/node_modules/**',
34
+ '**/dist/**',
35
+ '**/build/**',
36
+ '**/.next/**',
37
+ '**/coverage/**',
38
+ '**/*.min.js',
39
+ '**/*.map',
40
+ '**/package-lock.json',
41
+ '**/yarn.lock',
42
+ '**/pnpm-lock.yaml',
43
+ ...(options.exclude || []),
44
+ ].map(p => `"${p}"`).join(',');
45
+ const cmd = [
46
+ 'npx jscpd',
47
+ `"${targetDir}"`,
48
+ '--min-tokens 50',
49
+ '--min-lines 5',
50
+ '--reporters json',
51
+ `--output "${reportDir}"`,
52
+ `--ignore ${excludePatterns}`,
53
+ '--silent',
54
+ '--formats "javascript,typescript,jsx,tsx,css,scss"',
55
+ ].join(' ');
56
+ try {
57
+ execSync(cmd, {
58
+ cwd: targetDir,
59
+ stdio: 'pipe',
60
+ timeout: 120000,
61
+ });
62
+ }
63
+ catch {
64
+ // jscpd may exit non-zero when duplication exceeds threshold
65
+ }
66
+ // Read the JSON report
67
+ const reportPath = join(reportDir, 'jscpd-report.json');
68
+ if (!existsSync(reportPath)) {
69
+ // Clean up
70
+ try {
71
+ execSync(`rm -rf "${reportDir}"`, { stdio: 'pipe' });
72
+ }
73
+ catch { }
74
+ return {
75
+ scanner: this.name,
76
+ findings: [],
77
+ summary: '✅ No code duplication detected',
78
+ duration: Date.now() - start,
79
+ };
80
+ }
81
+ const report = JSON.parse(readFileSync(reportPath, 'utf-8'));
82
+ // Clean up temp directory
83
+ try {
84
+ execSync(`rm -rf "${reportDir}"`, { stdio: 'pipe' });
85
+ }
86
+ catch { }
87
+ if (report.duplicates && Array.isArray(report.duplicates)) {
88
+ for (const clone of report.duplicates) {
89
+ const firstFile = clone.firstFile?.name || 'unknown';
90
+ const secondFile = clone.secondFile?.name || 'unknown';
91
+ const lines = clone.lines || 0;
92
+ const tokens = clone.tokens || 0;
93
+ const firstStart = clone.firstFile?.startLoc?.line || 0;
94
+ const firstEnd = clone.firstFile?.endLoc?.line || 0;
95
+ const secondStart = clone.secondFile?.startLoc?.line || 0;
96
+ const secondEnd = clone.secondFile?.endLoc?.line || 0;
97
+ // Determine severity based on duplication size
98
+ let severity = 'low';
99
+ if (lines > 50)
100
+ severity = 'high';
101
+ else if (lines > 20)
102
+ severity = 'medium';
103
+ else if (lines > 10)
104
+ severity = 'low';
105
+ const sameFile = firstFile === secondFile;
106
+ findings.push({
107
+ id: `jscpd-${firstFile}-${firstStart}-${secondFile}-${secondStart}`,
108
+ scanner: this.name,
109
+ category: 'duplication',
110
+ severity,
111
+ title: `${lines} duplicated lines (${tokens} tokens)`,
112
+ description: sameFile
113
+ ? `Duplicated code block within "${firstFile}" — lines ${firstStart}-${firstEnd} and ${secondStart}-${secondEnd} are identical.`
114
+ : `Duplicated code between "${firstFile}" (lines ${firstStart}-${firstEnd}) and "${secondFile}" (lines ${secondStart}-${secondEnd}).`,
115
+ file: firstFile,
116
+ line: firstStart,
117
+ endLine: firstEnd,
118
+ suggestion: sameFile
119
+ ? `Extract the duplicated code (lines ${firstStart}-${firstEnd} and ${secondStart}-${secondEnd}) into a shared function within this file.`
120
+ : `Extract the shared logic into a common utility module and import it from both "${firstFile}" and "${secondFile}".`,
121
+ estimatedImpact: `~${lines} lines can be consolidated`,
122
+ relatedFiles: sameFile ? undefined : [secondFile],
123
+ metadata: { tokens, secondFileStart: secondStart, secondFileEnd: secondEnd },
124
+ });
125
+ }
126
+ }
127
+ // Build summary from statistics
128
+ const stats = report.statistics;
129
+ const totalDuplication = stats?.total?.percentage ? `${stats.total.percentage.toFixed(1)}%` : 'unknown';
130
+ const totalClones = findings.length;
131
+ return {
132
+ scanner: this.name,
133
+ findings,
134
+ summary: totalClones === 0
135
+ ? '✅ No significant code duplication found'
136
+ : `Found ${totalClones} duplicated blocks (${totalDuplication} total duplication)`,
137
+ duration: Date.now() - start,
138
+ };
139
+ }
140
+ catch (err) {
141
+ return {
142
+ scanner: this.name,
143
+ findings: [],
144
+ summary: `Error: ${err.message}`,
145
+ duration: Date.now() - start,
146
+ error: err.message,
147
+ };
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Knip Scanner — Dead Code Detection
3
+ *
4
+ * Finds unused files, dependencies, exports, types, and class members.
5
+ * The most comprehensive dead code scanner for JS/TS projects.
6
+ *
7
+ * Tool: knip (npm)
8
+ * Output: JSON via --reporter json
9
+ */
10
+ import { Scanner, ScannerResult, ScanOptions } from '../types.js';
11
+ export declare class KnipScanner implements Scanner {
12
+ name: string;
13
+ description: string;
14
+ category: "dead-code";
15
+ isAvailable(): Promise<boolean>;
16
+ scan(targetDir: string, options: ScanOptions): Promise<ScannerResult>;
17
+ }