@vibecheckai/cli 3.5.1 → 3.5.2
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/bin/registry.js +406 -154
- package/bin/runners/context/analyzer.js +52 -1
- package/bin/runners/context/generators/mcp.js +15 -13
- package/bin/runners/context/git-context.js +3 -1
- package/bin/runners/context/proof-context.js +248 -1
- package/bin/runners/context/team-conventions.js +33 -7
- package/bin/runners/lib/agent-firewall/ai/false-positive-analyzer.js +474 -0
- package/bin/runners/lib/agent-firewall/change-packet/builder.js +488 -0
- package/bin/runners/lib/agent-firewall/change-packet/schema.json +228 -0
- package/bin/runners/lib/agent-firewall/change-packet/store.js +200 -0
- package/bin/runners/lib/agent-firewall/claims/claim-types.js +21 -0
- package/bin/runners/lib/agent-firewall/claims/extractor.js +303 -0
- package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -0
- package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
- package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
- package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
- package/bin/runners/lib/agent-firewall/evidence/auth-evidence.js +88 -0
- package/bin/runners/lib/agent-firewall/evidence/contract-evidence.js +75 -0
- package/bin/runners/lib/agent-firewall/evidence/env-evidence.js +127 -0
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +213 -0
- package/bin/runners/lib/agent-firewall/evidence/side-effect-evidence.js +145 -0
- package/bin/runners/lib/agent-firewall/fs-hook/daemon.js +19 -0
- package/bin/runners/lib/agent-firewall/fs-hook/installer.js +87 -0
- package/bin/runners/lib/agent-firewall/fs-hook/watcher.js +184 -0
- package/bin/runners/lib/agent-firewall/git-hook/pre-commit.js +163 -0
- package/bin/runners/lib/agent-firewall/ide-extension/cursor.js +107 -0
- package/bin/runners/lib/agent-firewall/ide-extension/vscode.js +68 -0
- package/bin/runners/lib/agent-firewall/ide-extension/windsurf.js +66 -0
- package/bin/runners/lib/agent-firewall/interceptor/base.js +304 -0
- package/bin/runners/lib/agent-firewall/interceptor/cursor.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/vscode.js +35 -0
- package/bin/runners/lib/agent-firewall/interceptor/windsurf.js +34 -0
- package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
- package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
- package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
- package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
- package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
- package/bin/runners/lib/agent-firewall/logger.js +141 -0
- package/bin/runners/lib/agent-firewall/policy/default-policy.json +90 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +103 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +451 -0
- package/bin/runners/lib/agent-firewall/policy/rules/auth-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/contract-drift.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/fake-success.js +86 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +162 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +189 -0
- package/bin/runners/lib/agent-firewall/policy/rules/scope.js +93 -0
- package/bin/runners/lib/agent-firewall/policy/rules/unsafe-side-effect.js +57 -0
- package/bin/runners/lib/agent-firewall/policy/schema.json +183 -0
- package/bin/runners/lib/agent-firewall/policy/verdict.js +54 -0
- package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
- package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
- package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
- package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
- package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
- package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
- package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
- package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
- package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
- package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
- package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
- package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
- package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
- package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
- package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
- package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
- package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
- package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
- package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
- package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
- package/bin/runners/lib/agent-firewall/truthpack/index.js +67 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +137 -0
- package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
- package/bin/runners/lib/agent-firewall/utils/ignore-checker.js +118 -0
- package/bin/runners/lib/analysis-core.js +220 -182
- package/bin/runners/lib/analyzers.js +2145 -224
- package/bin/runners/lib/api-client.js +269 -0
- package/bin/runners/lib/authority-badge.js +425 -0
- package/bin/runners/lib/cli-output.js +242 -210
- package/bin/runners/lib/default-config.js +127 -0
- package/bin/runners/lib/detectors-v2.js +547 -785
- package/bin/runners/lib/doctor/modules/security.js +3 -1
- package/bin/runners/lib/engine/ast-cache.js +210 -0
- package/bin/runners/lib/engine/auth-extractor.js +211 -0
- package/bin/runners/lib/engine/billing-extractor.js +112 -0
- package/bin/runners/lib/engine/enforcement-extractor.js +100 -0
- package/bin/runners/lib/engine/env-extractor.js +207 -0
- package/bin/runners/lib/engine/express-extractor.js +208 -0
- package/bin/runners/lib/engine/extractors.js +849 -0
- package/bin/runners/lib/engine/index.js +207 -0
- package/bin/runners/lib/engine/repo-index.js +514 -0
- package/bin/runners/lib/engine/types.js +124 -0
- package/bin/runners/lib/engines/accessibility-engine.js +190 -0
- package/bin/runners/lib/engines/api-consistency-engine.js +162 -0
- package/bin/runners/lib/engines/ast-cache.js +99 -0
- package/bin/runners/lib/engines/code-quality-engine.js +255 -0
- package/bin/runners/lib/engines/console-logs-engine.js +115 -0
- package/bin/runners/lib/engines/cross-file-analysis-engine.js +268 -0
- package/bin/runners/lib/engines/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/deprecated-api-engine.js +226 -0
- package/bin/runners/lib/engines/empty-catch-engine.js +150 -0
- package/bin/runners/lib/engines/file-filter.js +131 -0
- package/bin/runners/lib/engines/hardcoded-secrets-engine.js +251 -0
- package/bin/runners/lib/engines/mock-data-engine.js +272 -0
- package/bin/runners/lib/engines/parallel-processor.js +71 -0
- package/bin/runners/lib/engines/performance-issues-engine.js +265 -0
- package/bin/runners/lib/engines/security-vulnerabilities-engine.js +243 -0
- package/bin/runners/lib/engines/todo-fixme-engine.js +115 -0
- package/bin/runners/lib/engines/type-aware-engine.js +152 -0
- package/bin/runners/lib/engines/unsafe-regex-engine.js +225 -0
- package/bin/runners/lib/engines/vibecheck-engines/README.md +53 -0
- package/bin/runners/lib/engines/vibecheck-engines/index.js +15 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/ast-cache.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/code-quality-engine.js +291 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/console-logs-engine.js +83 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/dead-code-engine.js +198 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/deprecated-api-engine.js +275 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/empty-catch-engine.js +167 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/file-filter.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/hardcoded-secrets-engine.js +139 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/mock-data-engine.js +140 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/parallel-processor.js +164 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/performance-issues-engine.js +234 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/type-aware-engine.js +217 -0
- package/bin/runners/lib/engines/vibecheck-engines/lib/unsafe-regex-engine.js +78 -0
- package/bin/runners/lib/engines/vibecheck-engines/package.json +13 -0
- package/bin/runners/lib/entitlements-v2.js +152 -446
- package/bin/runners/lib/error-handler.js +60 -12
- package/bin/runners/lib/error-messages.js +289 -0
- package/bin/runners/lib/evidence-pack.js +7 -1
- package/bin/runners/lib/exit-codes.js +275 -0
- package/bin/runners/lib/finding-id.js +69 -0
- package/bin/runners/lib/finding-sorter.js +89 -0
- package/bin/runners/lib/fingerprint.js +377 -0
- package/bin/runners/lib/global-flags.js +37 -0
- package/bin/runners/lib/help-formatter.js +413 -0
- package/bin/runners/lib/logger.js +38 -0
- package/bin/runners/lib/next-action.js +560 -0
- package/bin/runners/lib/prerequisites.js +149 -0
- package/bin/runners/lib/route-detection.js +137 -68
- package/bin/runners/lib/route-truth.js +1167 -322
- package/bin/runners/lib/scan-output.js +504 -463
- package/bin/runners/lib/scan-runner.js +135 -0
- package/bin/runners/lib/schemas/ajv-validator.js +464 -0
- package/bin/runners/lib/schemas/error-envelope.schema.json +105 -0
- package/bin/runners/lib/schemas/finding-v3.schema.json +151 -0
- package/bin/runners/lib/schemas/report-artifact.schema.json +120 -0
- package/bin/runners/lib/schemas/run-request.schema.json +108 -0
- package/bin/runners/lib/schemas/validator.js +27 -0
- package/bin/runners/lib/schemas/verdict.schema.json +140 -0
- package/bin/runners/lib/ship-output-enterprise.js +239 -0
- package/bin/runners/lib/ship-output.js +328 -31
- package/bin/runners/lib/terminal-ui.js +234 -731
- package/bin/runners/lib/truth.js +1332 -308
- package/bin/runners/lib/unified-cli-output.js +604 -0
- package/bin/runners/lib/unified-output.js +163 -155
- package/bin/runners/lib/upsell.js +104 -204
- package/bin/runners/runAgent.d.ts +5 -0
- package/bin/runners/runAgent.js +161 -0
- package/bin/runners/runAllowlist.js +166 -101
- package/bin/runners/runApprove.js +1200 -0
- package/bin/runners/runAuth.js +373 -95
- package/bin/runners/runCheckpoint.js +59 -21
- package/bin/runners/runClassify.js +926 -0
- package/bin/runners/runContext.d.ts +4 -0
- package/bin/runners/runContext.js +136 -24
- package/bin/runners/runDoctor.js +115 -67
- package/bin/runners/runEvidencePack.js +239 -96
- package/bin/runners/runFirewall.d.ts +5 -0
- package/bin/runners/runFirewall.js +134 -0
- package/bin/runners/runFirewallHook.d.ts +5 -0
- package/bin/runners/runFirewallHook.js +56 -0
- package/bin/runners/runFix.js +6 -5
- package/bin/runners/runGuard.js +212 -118
- package/bin/runners/runInit.js +66 -21
- package/bin/runners/runLabs.js +204 -121
- package/bin/runners/runMcp.js +131 -60
- package/bin/runners/runPolish.d.ts +4 -0
- package/bin/runners/runPolish.js +43 -20
- package/bin/runners/runProof.zip +0 -0
- package/bin/runners/runProve.js +15 -5
- package/bin/runners/runQuickstart.js +531 -0
- package/bin/runners/runReality.js +14 -0
- package/bin/runners/runReport.js +36 -4
- package/bin/runners/runScan.js +689 -91
- package/bin/runners/runShip.js +96 -40
- package/bin/runners/runTruth.d.ts +5 -0
- package/bin/runners/runTruth.js +101 -0
- package/bin/runners/runValidate.js +21 -4
- package/bin/runners/runWatch.js +118 -54
- package/bin/scan.js +6 -1
- package/bin/vibecheck.js +297 -52
- package/mcp-server/HARDENING_SUMMARY.md +299 -0
- package/mcp-server/agent-firewall-interceptor.js +500 -0
- package/mcp-server/authority-tools.js +569 -0
- package/mcp-server/conductor/conflict-resolver.js +588 -0
- package/mcp-server/conductor/execution-planner.js +544 -0
- package/mcp-server/conductor/index.js +377 -0
- package/mcp-server/conductor/lock-manager.js +615 -0
- package/mcp-server/conductor/request-queue.js +550 -0
- package/mcp-server/conductor/session-manager.js +500 -0
- package/mcp-server/conductor/tools.js +510 -0
- package/mcp-server/deprecation-middleware.js +282 -0
- package/mcp-server/handlers/index.ts +15 -0
- package/mcp-server/handlers/tool-handler.ts +474 -591
- package/mcp-server/index.js +1748 -1099
- package/mcp-server/lib/api-client.cjs +13 -0
- package/mcp-server/lib/cache-wrapper.cjs +383 -0
- package/mcp-server/lib/error-envelope.js +138 -0
- package/mcp-server/lib/executor.ts +428 -721
- package/mcp-server/lib/index.ts +19 -0
- package/mcp-server/lib/logger.cjs +30 -0
- package/mcp-server/lib/rate-limiter.js +166 -0
- package/mcp-server/lib/sandbox.test.ts +519 -0
- package/mcp-server/lib/sandbox.ts +342 -284
- package/mcp-server/lib/types.ts +267 -0
- package/mcp-server/logger.js +173 -0
- package/mcp-server/package.json +11 -27
- package/mcp-server/premium-tools.js +2 -2
- package/mcp-server/registry/tool-registry.js +794 -0
- package/mcp-server/registry/tools.json +507 -378
- package/mcp-server/registry.test.ts +334 -0
- package/mcp-server/tests/tier-gating.test.js +297 -0
- package/mcp-server/tier-auth.js +492 -347
- package/mcp-server/tools-v3.js +950 -0
- package/mcp-server/truth-context.js +131 -90
- package/mcp-server/truth-firewall-tools.js +1612 -1001
- package/mcp-server/tsconfig.json +8 -5
- package/mcp-server/vibecheck-2.0-tools.js +14 -1
- package/mcp-server/vibecheck-mcp-server-3.2.0.tgz +0 -0
- package/mcp-server/vibecheck-tools.js +2 -2
- package/package.json +4 -3
- package/bin/runners/runInstall.js +0 -281
- package/mcp-server/ARCHITECTURE.md +0 -339
- package/mcp-server/__tests__/cache.test.ts +0 -313
- package/mcp-server/__tests__/executor.test.ts +0 -239
- package/mcp-server/__tests__/fixtures/exclusion-test/.cache/webpack/cache.pack +0 -1
- package/mcp-server/__tests__/fixtures/exclusion-test/.next/server/chunk.js +0 -3
- package/mcp-server/__tests__/fixtures/exclusion-test/.turbo/cache.json +0 -3
- package/mcp-server/__tests__/fixtures/exclusion-test/.venv/lib/env.py +0 -3
- package/mcp-server/__tests__/fixtures/exclusion-test/dist/bundle.js +0 -3
- package/mcp-server/__tests__/fixtures/exclusion-test/package.json +0 -5
- package/mcp-server/__tests__/fixtures/exclusion-test/src/app.ts +0 -5
- package/mcp-server/__tests__/fixtures/exclusion-test/venv/lib/config.py +0 -4
- package/mcp-server/__tests__/ids.test.ts +0 -345
- package/mcp-server/__tests__/integration/tools.test.ts +0 -410
- package/mcp-server/__tests__/registry.test.ts +0 -365
- package/mcp-server/__tests__/sandbox.test.ts +0 -323
- package/mcp-server/__tests__/schemas.test.ts +0 -372
- package/mcp-server/benchmarks/run-benchmarks.ts +0 -304
- package/mcp-server/examples/doctor.request.json +0 -14
- package/mcp-server/examples/doctor.response.json +0 -53
- package/mcp-server/examples/error.response.json +0 -15
- package/mcp-server/examples/scan.request.json +0 -14
- package/mcp-server/examples/scan.response.json +0 -108
- package/mcp-server/index-v3.ts +0 -293
- package/mcp-server/index.old.js +0 -4137
- package/mcp-server/lib/cache.ts +0 -341
- package/mcp-server/lib/errors.ts +0 -346
- package/mcp-server/lib/ids.ts +0 -238
- package/mcp-server/lib/logger.ts +0 -368
- package/mcp-server/lib/metrics.ts +0 -365
- package/mcp-server/lib/validator.ts +0 -229
- package/mcp-server/package-lock.json +0 -165
- package/mcp-server/schemas/error-envelope.schema.json +0 -125
- package/mcp-server/schemas/finding.schema.json +0 -167
- package/mcp-server/schemas/report-artifact.schema.json +0 -88
- package/mcp-server/schemas/run-request.schema.json +0 -75
- package/mcp-server/schemas/verdict.schema.json +0 -168
- package/mcp-server/tier-auth.d.ts +0 -71
- package/mcp-server/vitest.config.ts +0 -16
package/bin/runners/lib/truth.js
CHANGED
|
@@ -18,6 +18,19 @@ const { buildEnforcementTruth } = require("./enforcement");
|
|
|
18
18
|
// Multi-framework route detection v2
|
|
19
19
|
const { resolveAllRoutes, detectFrameworks } = require("./route-detection");
|
|
20
20
|
|
|
21
|
+
// ---------- constants ----------
|
|
22
|
+
const IGNORE_GLOBS = [
|
|
23
|
+
"**/node_modules/**",
|
|
24
|
+
"**/.next/**",
|
|
25
|
+
"**/dist/**",
|
|
26
|
+
"**/build/**",
|
|
27
|
+
"**/.turbo/**",
|
|
28
|
+
"**/.git/**",
|
|
29
|
+
"**/.vibecheck/**",
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const CODE_FILE_GLOBS = ["**/*.{ts,tsx,js,jsx}"];
|
|
33
|
+
|
|
21
34
|
// ---------- helpers ----------
|
|
22
35
|
function sha256(text) {
|
|
23
36
|
return "sha256:" + crypto.createHash("sha256").update(text).digest("hex");
|
|
@@ -29,15 +42,43 @@ function canonicalizeMethod(m) {
|
|
|
29
42
|
return u;
|
|
30
43
|
}
|
|
31
44
|
|
|
45
|
+
function stripQueryHash(s) {
|
|
46
|
+
const v = String(s || "");
|
|
47
|
+
const q = v.indexOf("?");
|
|
48
|
+
const h = v.indexOf("#");
|
|
49
|
+
const cut = (q === -1 ? h : (h === -1 ? q : Math.min(q, h)));
|
|
50
|
+
return cut === -1 ? v : v.slice(0, cut);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Canonical path rules:
|
|
55
|
+
* - ensure leading slash
|
|
56
|
+
* - collapse multiple slashes
|
|
57
|
+
* - strip query/hash
|
|
58
|
+
* - normalize Next dynamic segments:
|
|
59
|
+
* [[...slug]] -> *slug?
|
|
60
|
+
* [...slug] -> *slug
|
|
61
|
+
* [id] -> :id
|
|
62
|
+
*/
|
|
32
63
|
function canonicalizePath(p) {
|
|
33
|
-
let s = String(p || "").trim();
|
|
64
|
+
let s = stripQueryHash(String(p || "").trim());
|
|
65
|
+
|
|
66
|
+
// If someone passed a full URL, only keep pathname-like portion if possible.
|
|
67
|
+
// (We still require local routes to start with "/" for client refs.)
|
|
68
|
+
const protoIdx = s.indexOf("://");
|
|
69
|
+
if (protoIdx !== -1) {
|
|
70
|
+
// attempt to strip scheme+host
|
|
71
|
+
const slashAfterHost = s.indexOf("/", protoIdx + 3);
|
|
72
|
+
s = slashAfterHost === -1 ? "/" : s.slice(slashAfterHost);
|
|
73
|
+
}
|
|
74
|
+
|
|
34
75
|
if (!s.startsWith("/")) s = "/" + s;
|
|
35
76
|
s = s.replace(/\/+/g, "/");
|
|
36
77
|
|
|
37
|
-
// Next dynamic segments
|
|
78
|
+
// Next dynamic segments (filesystem style)
|
|
38
79
|
s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, "*$1?"); // [[...slug]] -> *slug?
|
|
39
|
-
s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1");
|
|
40
|
-
s = s.replace(/\[([^\]]+)\]/g, ":$1");
|
|
80
|
+
s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1"); // [...slug] -> *slug
|
|
81
|
+
s = s.replace(/\[([^\]]+)\]/g, ":$1"); // [id] -> :id
|
|
41
82
|
|
|
42
83
|
if (s.length > 1) s = s.replace(/\/$/, "");
|
|
43
84
|
return s;
|
|
@@ -51,12 +92,46 @@ function joinPaths(prefix, p) {
|
|
|
51
92
|
return canonicalizePath(a + "/" + b);
|
|
52
93
|
}
|
|
53
94
|
|
|
54
|
-
function parseFile(code) {
|
|
55
|
-
|
|
95
|
+
function parseFile(code, fileAbsForErrors) {
|
|
96
|
+
// Be permissive: production repos contain decorators, top-level await, etc.
|
|
97
|
+
return parser.parse(code, {
|
|
98
|
+
sourceType: "unambiguous",
|
|
99
|
+
errorRecovery: true,
|
|
100
|
+
allowImportExportEverywhere: true,
|
|
101
|
+
plugins: [
|
|
102
|
+
"typescript",
|
|
103
|
+
"jsx",
|
|
104
|
+
"dynamicImport",
|
|
105
|
+
"importMeta",
|
|
106
|
+
"topLevelAwait",
|
|
107
|
+
"classProperties",
|
|
108
|
+
"classPrivateProperties",
|
|
109
|
+
"classPrivateMethods",
|
|
110
|
+
"optionalChaining",
|
|
111
|
+
"nullishCoalescingOperator",
|
|
112
|
+
"decorators-legacy",
|
|
113
|
+
],
|
|
114
|
+
sourceFilename: fileAbsForErrors || undefined,
|
|
115
|
+
});
|
|
56
116
|
}
|
|
57
117
|
|
|
118
|
+
// File cache for performance (avoids reading the same file multiple times)
|
|
119
|
+
const _FILE_CACHE = new Map();
|
|
120
|
+
|
|
58
121
|
function safeRead(fileAbs) {
|
|
59
|
-
return
|
|
122
|
+
if (_FILE_CACHE.has(fileAbs)) return _FILE_CACHE.get(fileAbs);
|
|
123
|
+
try {
|
|
124
|
+
const content = fs.readFileSync(fileAbs, "utf8");
|
|
125
|
+
_FILE_CACHE.set(fileAbs, content);
|
|
126
|
+
return content;
|
|
127
|
+
} catch {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Clear cache to free memory after a scan (important for long-running processes)
|
|
133
|
+
function clearCache() {
|
|
134
|
+
_FILE_CACHE.clear();
|
|
60
135
|
}
|
|
61
136
|
|
|
62
137
|
function ensureDir(p) {
|
|
@@ -65,68 +140,154 @@ function ensureDir(p) {
|
|
|
65
140
|
|
|
66
141
|
function evidenceFromLoc({ fileAbs, fileRel, loc, reason }) {
|
|
67
142
|
if (!loc) return null;
|
|
68
|
-
const
|
|
143
|
+
const code = safeRead(fileAbs);
|
|
144
|
+
if (!code) return null;
|
|
145
|
+
|
|
146
|
+
const lines = code.split(/\r?\n/);
|
|
69
147
|
const start = Math.max(1, loc.start?.line || 1);
|
|
70
148
|
const end = Math.max(start, loc.end?.line || start);
|
|
71
149
|
const snippet = lines.slice(start - 1, end).join("\n");
|
|
150
|
+
|
|
72
151
|
return {
|
|
73
152
|
id: `ev_${crypto.randomBytes(4).toString("hex")}`,
|
|
74
153
|
file: fileRel,
|
|
75
154
|
lines: `${start}-${end}`,
|
|
76
155
|
snippetHash: sha256(snippet),
|
|
77
|
-
reason
|
|
156
|
+
reason,
|
|
78
157
|
};
|
|
79
158
|
}
|
|
80
159
|
|
|
160
|
+
function normalizeRel(repoRoot, fileAbs) {
|
|
161
|
+
return path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function scoreConfidence(c) {
|
|
165
|
+
if (c === "high") return 3;
|
|
166
|
+
if (c === "med") return 2;
|
|
167
|
+
if (c === "low") return 1;
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isRouteGroupSegment(seg) {
|
|
172
|
+
// Next route group: (group)
|
|
173
|
+
return seg.startsWith("(") && seg.endsWith(")");
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isParallelSegment(seg) {
|
|
177
|
+
// Next parallel routes: @slot
|
|
178
|
+
return seg.startsWith("@");
|
|
179
|
+
}
|
|
180
|
+
|
|
81
181
|
// ---------- Next: app router API ----------
|
|
82
|
-
const HTTP_EXPORTS = new Set(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]);
|
|
182
|
+
const HTTP_EXPORTS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]);
|
|
183
|
+
|
|
184
|
+
function nextAppApiPathFromRel(fileRel) {
|
|
185
|
+
const idx = fileRel.indexOf("app/api/");
|
|
186
|
+
if (idx === -1) return null;
|
|
187
|
+
|
|
188
|
+
let sub = fileRel.slice(idx + "app/api/".length);
|
|
189
|
+
|
|
190
|
+
// route.ts / route.js / route.tsx / route.jsx
|
|
191
|
+
sub = sub.replace(/\/route\.(ts|tsx|js|jsx)$/, "");
|
|
83
192
|
|
|
84
|
-
|
|
85
|
-
const
|
|
193
|
+
// remove route groups + parallel segments from the filesystem path
|
|
194
|
+
const parts = sub.split("/").filter(Boolean).filter((seg) => !isRouteGroupSegment(seg) && !isParallelSegment(seg));
|
|
195
|
+
sub = parts.join("/");
|
|
196
|
+
|
|
197
|
+
return canonicalizePath("/api/" + sub);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function resolveNextAppApiRoutes(repoRoot, stats) {
|
|
201
|
+
const files = await fg(["**/app/api/**/route.@(ts|tsx|js|jsx)"], {
|
|
86
202
|
cwd: repoRoot,
|
|
87
203
|
absolute: true,
|
|
88
|
-
ignore:
|
|
204
|
+
ignore: IGNORE_GLOBS,
|
|
89
205
|
});
|
|
90
206
|
|
|
91
207
|
const out = [];
|
|
92
208
|
|
|
93
209
|
for (const fileAbs of files) {
|
|
94
|
-
const fileRel =
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
const routePath = canonicalizePath("/api/" + sub);
|
|
210
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
211
|
+
const routePath = nextAppApiPathFromRel(fileRel);
|
|
212
|
+
if (!routePath) continue;
|
|
98
213
|
|
|
99
214
|
const code = safeRead(fileAbs);
|
|
215
|
+
if (!code) continue;
|
|
216
|
+
|
|
100
217
|
let ast;
|
|
101
|
-
try {
|
|
218
|
+
try {
|
|
219
|
+
ast = parseFile(code, fileAbs);
|
|
220
|
+
} catch {
|
|
221
|
+
stats.parseErrors++;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
102
224
|
|
|
103
225
|
const methods = [];
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
226
|
+
|
|
227
|
+
try {
|
|
228
|
+
traverse(ast, {
|
|
229
|
+
// export async function GET() {}
|
|
230
|
+
ExportNamedDeclaration(p) {
|
|
231
|
+
const decl = p.node.declaration;
|
|
232
|
+
|
|
233
|
+
if (t.isFunctionDeclaration(decl) && decl.id?.name) {
|
|
234
|
+
const n = decl.id.name.toUpperCase();
|
|
235
|
+
if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: decl.loc, why: "export function" });
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// export const GET = async () => {}
|
|
239
|
+
if (t.isVariableDeclaration(decl)) {
|
|
240
|
+
for (const d of decl.declarations) {
|
|
241
|
+
if (!t.isVariableDeclarator(d)) continue;
|
|
242
|
+
if (!t.isIdentifier(d.id)) continue;
|
|
243
|
+
const n = d.id.name.toUpperCase();
|
|
244
|
+
if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: d.loc || decl.loc, why: "export const" });
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// export { GET } from "./handler"
|
|
249
|
+
for (const s of p.node.specifiers || []) {
|
|
250
|
+
if (!t.isExportSpecifier(s)) continue;
|
|
251
|
+
if (!t.isIdentifier(s.exported)) continue;
|
|
252
|
+
const n = s.exported.name.toUpperCase();
|
|
253
|
+
if (HTTP_EXPORTS.has(n)) methods.push({ method: n, loc: s.loc || p.node.loc, why: "export re-export" });
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
} catch {
|
|
258
|
+
// Babel traverse can fail on some edge-case files; skip them
|
|
259
|
+
stats.parseErrors++;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
113
262
|
|
|
114
263
|
if (methods.length === 0) {
|
|
115
|
-
|
|
264
|
+
// Still include route.ts, but with "*" and low confidence to avoid missing-route spam.
|
|
265
|
+
out.push({
|
|
266
|
+
method: "*",
|
|
267
|
+
path: routePath,
|
|
268
|
+
handler: fileRel,
|
|
269
|
+
confidence: "low",
|
|
270
|
+
framework: "next",
|
|
271
|
+
evidence: [],
|
|
272
|
+
});
|
|
116
273
|
continue;
|
|
117
274
|
}
|
|
118
275
|
|
|
119
276
|
for (const m of methods) {
|
|
120
277
|
const ev = evidenceFromLoc({
|
|
121
|
-
fileAbs,
|
|
122
|
-
|
|
278
|
+
fileAbs,
|
|
279
|
+
fileRel,
|
|
280
|
+
loc: m.loc,
|
|
281
|
+
reason: `Next app router ${m.method} (${m.why})`,
|
|
123
282
|
});
|
|
283
|
+
|
|
124
284
|
out.push({
|
|
125
285
|
method: m.method,
|
|
126
286
|
path: routePath,
|
|
127
287
|
handler: fileRel,
|
|
128
|
-
confidence: "high",
|
|
129
|
-
|
|
288
|
+
confidence: m.why === "export re-export" ? "med" : "high",
|
|
289
|
+
framework: "next",
|
|
290
|
+
evidence: ev ? [ev] : [],
|
|
130
291
|
});
|
|
131
292
|
}
|
|
132
293
|
}
|
|
@@ -135,51 +296,133 @@ async function resolveNextAppApiRoutes(repoRoot) {
|
|
|
135
296
|
}
|
|
136
297
|
|
|
137
298
|
// ---------- Next: pages router API ----------
|
|
138
|
-
|
|
139
|
-
const
|
|
299
|
+
function nextPagesApiPathFromRel(fileRel) {
|
|
300
|
+
const idx = fileRel.indexOf("pages/api/");
|
|
301
|
+
if (idx === -1) return null;
|
|
302
|
+
|
|
303
|
+
let sub = fileRel.slice(idx + "pages/api/".length);
|
|
304
|
+
sub = sub.replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
305
|
+
|
|
306
|
+
// pages/api/foo/index.ts -> /api/foo
|
|
307
|
+
if (sub === "index") sub = "";
|
|
308
|
+
sub = sub.replace(/\/index$/, "");
|
|
309
|
+
|
|
310
|
+
return canonicalizePath("/api/" + sub);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function resolveNextPagesApiRoutes(repoRoot, stats) {
|
|
314
|
+
const files = await fg(["**/pages/api/**/*.@(ts|tsx|js|jsx)"], {
|
|
140
315
|
cwd: repoRoot,
|
|
141
316
|
absolute: true,
|
|
142
|
-
ignore:
|
|
317
|
+
ignore: IGNORE_GLOBS,
|
|
143
318
|
});
|
|
144
319
|
|
|
145
320
|
const out = [];
|
|
321
|
+
|
|
146
322
|
for (const fileAbs of files) {
|
|
147
|
-
const fileRel =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
323
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
324
|
+
|
|
325
|
+
// Skip Next.js special files that aren't API routes (_app, _document, _utils, etc.)
|
|
326
|
+
if (fileRel.includes("/_") && !fileRel.includes("/_next")) continue;
|
|
327
|
+
|
|
328
|
+
const routePath = nextPagesApiPathFromRel(fileRel);
|
|
329
|
+
if (!routePath) continue;
|
|
330
|
+
|
|
331
|
+
const code = safeRead(fileAbs);
|
|
332
|
+
if (!code) continue;
|
|
333
|
+
|
|
334
|
+
// Parse to verify it's actually a route (has export default)
|
|
335
|
+
let ast;
|
|
336
|
+
try {
|
|
337
|
+
ast = parseFile(code, fileAbs);
|
|
338
|
+
} catch {
|
|
339
|
+
stats.parseErrors++;
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Check for 'export default' (Required for Pages Router API routes)
|
|
344
|
+
// Files without default export are helper files (db.ts, types.ts, utils.ts)
|
|
345
|
+
let hasDefaultExport = false;
|
|
346
|
+
try {
|
|
347
|
+
traverse(ast, {
|
|
348
|
+
ExportDefaultDeclaration(p) {
|
|
349
|
+
hasDefaultExport = true;
|
|
350
|
+
p.stop(); // Found it, stop traversing
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
} catch {
|
|
354
|
+
// Traverse failed, skip this file
|
|
355
|
+
stats.parseErrors++;
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (!hasDefaultExport) continue; // It's a helper file, not an API route
|
|
151
360
|
|
|
152
361
|
out.push({
|
|
153
|
-
method: "*",
|
|
362
|
+
method: "*", // Pages router handles all methods in one function
|
|
154
363
|
path: routePath,
|
|
155
364
|
handler: fileRel,
|
|
156
365
|
confidence: "med",
|
|
157
|
-
|
|
366
|
+
framework: "next",
|
|
367
|
+
evidence: [],
|
|
158
368
|
});
|
|
159
369
|
}
|
|
370
|
+
|
|
160
371
|
return out;
|
|
161
372
|
}
|
|
162
373
|
|
|
163
374
|
// ---------- minimal relative module resolver ----------
|
|
164
375
|
function exists(p) {
|
|
165
|
-
try {
|
|
376
|
+
try {
|
|
377
|
+
return fs.statSync(p).isFile();
|
|
378
|
+
} catch {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
166
381
|
}
|
|
382
|
+
|
|
167
383
|
function resolveRelativeModule(fromFileAbs, spec) {
|
|
168
384
|
if (!spec || (!spec.startsWith("./") && !spec.startsWith("../"))) return null;
|
|
385
|
+
|
|
169
386
|
const base = path.resolve(path.dirname(fromFileAbs), spec);
|
|
170
387
|
const candidates = [
|
|
171
388
|
base,
|
|
172
389
|
base + ".ts",
|
|
390
|
+
base + ".tsx",
|
|
173
391
|
base + ".js",
|
|
392
|
+
base + ".jsx",
|
|
174
393
|
path.join(base, "index.ts"),
|
|
175
|
-
path.join(base, "index.
|
|
394
|
+
path.join(base, "index.tsx"),
|
|
395
|
+
path.join(base, "index.js"),
|
|
396
|
+
path.join(base, "index.jsx"),
|
|
176
397
|
];
|
|
398
|
+
|
|
177
399
|
for (const c of candidates) if (exists(c)) return c;
|
|
178
400
|
return null;
|
|
179
401
|
}
|
|
180
402
|
|
|
403
|
+
function extractRequireOrImportSpec(node) {
|
|
404
|
+
// require("./x")
|
|
405
|
+
if (t.isCallExpression(node) && t.isIdentifier(node.callee, { name: "require" })) {
|
|
406
|
+
const a0 = node.arguments && node.arguments[0];
|
|
407
|
+
if (t.isStringLiteral(a0)) return a0.value;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// import("./x")
|
|
411
|
+
if (t.isCallExpression(node) && node.callee && node.callee.type === "Import") {
|
|
412
|
+
const a0 = node.arguments && node.arguments[0];
|
|
413
|
+
if (t.isStringLiteral(a0)) return a0.value;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// await import("./x")
|
|
417
|
+
if (t.isAwaitExpression(node)) {
|
|
418
|
+
return extractRequireOrImportSpec(node.argument);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
return null;
|
|
422
|
+
}
|
|
423
|
+
|
|
181
424
|
// ---------- Fastify route extraction ----------
|
|
182
|
-
const FASTIFY_METHODS = new Set(["get","post","put","patch","delete","options","head","all"]);
|
|
425
|
+
const FASTIFY_METHODS = new Set(["get", "post", "put", "patch", "delete", "options", "head", "all"]);
|
|
183
426
|
|
|
184
427
|
function isFastifyMethod(name) {
|
|
185
428
|
return FASTIFY_METHODS.has(name);
|
|
@@ -222,18 +465,18 @@ function extractRouteObject(objExpr) {
|
|
|
222
465
|
if (key === "method") {
|
|
223
466
|
if (t.isStringLiteral(p.value)) methods = [p.value.value];
|
|
224
467
|
if (t.isArrayExpression(p.value)) {
|
|
225
|
-
methods = p.value.elements.filter(e => t.isStringLiteral(e)).map(e => e.value);
|
|
468
|
+
methods = p.value.elements.filter((e) => t.isStringLiteral(e)).map((e) => e.value);
|
|
226
469
|
}
|
|
227
470
|
}
|
|
228
471
|
|
|
229
472
|
if (key === "handler") hasHandler = true;
|
|
230
|
-
if (["preHandler","onRequest","preValidation","preSerialization"].includes(key)) hooks.push(key);
|
|
473
|
+
if (["preHandler", "onRequest", "preValidation", "preSerialization"].includes(key)) hooks.push(key);
|
|
231
474
|
}
|
|
232
475
|
|
|
233
476
|
return { url, methods, hasHandler, hooks };
|
|
234
477
|
}
|
|
235
478
|
|
|
236
|
-
function resolveFastifyRoutes(repoRoot, entryAbs) {
|
|
479
|
+
function resolveFastifyRoutes(repoRoot, entryAbs, stats) {
|
|
237
480
|
const seen = new Set();
|
|
238
481
|
const routes = [];
|
|
239
482
|
const gaps = [];
|
|
@@ -242,217 +485,521 @@ function resolveFastifyRoutes(repoRoot, entryAbs) {
|
|
|
242
485
|
if (!fileAbs || seen.has(fileAbs)) return;
|
|
243
486
|
seen.add(fileAbs);
|
|
244
487
|
|
|
245
|
-
const fileRel =
|
|
488
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
246
489
|
const code = safeRead(fileAbs);
|
|
490
|
+
if (!code) return;
|
|
247
491
|
|
|
248
492
|
let ast;
|
|
249
|
-
try {
|
|
493
|
+
try {
|
|
494
|
+
ast = parseFile(code, fileAbs);
|
|
495
|
+
} catch {
|
|
496
|
+
stats.parseErrors++;
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
250
499
|
|
|
251
500
|
// best-effort: fastify instance identifiers
|
|
252
501
|
const fastifyNames = new Set(["fastify"]);
|
|
253
502
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
503
|
+
// Static string + local-plugin harvesting (cheap, big impact on false positives)
|
|
504
|
+
const _rawConstInits = new Map(); // name -> init node (evaluated lazily)
|
|
505
|
+
const _constStrings = new Map(); // name -> resolved string
|
|
506
|
+
const _localPlugins = new Map(); // name -> Function node
|
|
507
|
+
|
|
508
|
+
function evalStaticString(node, depth = 0) {
|
|
509
|
+
if (!node || depth > 6) return null;
|
|
510
|
+
|
|
511
|
+
if (t.isStringLiteral(node)) return node.value;
|
|
512
|
+
|
|
513
|
+
if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
|
|
514
|
+
return node.quasis.map((q) => q.value.cooked || "").join("");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (t.isIdentifier(node)) {
|
|
518
|
+
const name = node.name;
|
|
519
|
+
if (_constStrings.has(name)) return _constStrings.get(name);
|
|
520
|
+
const init = _rawConstInits.get(name);
|
|
521
|
+
if (!init) return null;
|
|
522
|
+
const v = evalStaticString(init, depth + 1);
|
|
523
|
+
if (typeof v === "string") {
|
|
524
|
+
// cap to avoid pathological blowups
|
|
525
|
+
if (v.length <= 4096) _constStrings.set(name, v);
|
|
526
|
+
return v;
|
|
263
527
|
}
|
|
528
|
+
return null;
|
|
264
529
|
}
|
|
265
|
-
});
|
|
266
530
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
531
|
+
if (t.isBinaryExpression(node, { operator: "+" })) {
|
|
532
|
+
const l = evalStaticString(node.left, depth + 1);
|
|
533
|
+
const r = evalStaticString(node.right, depth + 1);
|
|
534
|
+
if (typeof l !== "string" || typeof r !== "string") return null;
|
|
535
|
+
const out = l + r;
|
|
536
|
+
return out.length <= 4096 ? out : null;
|
|
537
|
+
}
|
|
270
538
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function extractPrefixFromOptsV3(node) {
|
|
543
|
+
if (!t.isObjectExpression(node)) return null;
|
|
544
|
+
for (const p of node.properties) {
|
|
545
|
+
if (!t.isObjectProperty(p)) continue;
|
|
546
|
+
const key =
|
|
547
|
+
t.isIdentifier(p.key) ? p.key.name :
|
|
548
|
+
t.isStringLiteral(p.key) ? p.key.value :
|
|
549
|
+
null;
|
|
550
|
+
if (key !== "prefix") continue;
|
|
551
|
+
return evalStaticString(p.value);
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function extractRouteObjectV3(objExpr) {
|
|
557
|
+
let url = null;
|
|
558
|
+
let methods = [];
|
|
559
|
+
let hasHandler = false;
|
|
560
|
+
const hooks = [];
|
|
561
|
+
|
|
562
|
+
for (const p of objExpr.properties) {
|
|
563
|
+
if (!t.isObjectProperty(p)) continue;
|
|
564
|
+
|
|
565
|
+
const key =
|
|
566
|
+
t.isIdentifier(p.key) ? p.key.name :
|
|
567
|
+
t.isStringLiteral(p.key) ? p.key.value :
|
|
568
|
+
null;
|
|
569
|
+
if (!key) continue;
|
|
570
|
+
|
|
571
|
+
if (key === "url") {
|
|
572
|
+
const u = evalStaticString(p.value);
|
|
573
|
+
if (typeof u === "string") url = u;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (key === "method") {
|
|
577
|
+
if (t.isStringLiteral(p.value)) methods = [p.value.value];
|
|
578
|
+
if (t.isArrayExpression(p.value)) {
|
|
579
|
+
methods = p.value.elements.filter((e) => t.isStringLiteral(e)).map((e) => e.value);
|
|
277
580
|
}
|
|
278
|
-
},
|
|
279
|
-
VariableDeclarator(vp) {
|
|
280
|
-
if (!t.isIdentifier(vp.node.id) || vp.node.id.name !== localName) return;
|
|
281
|
-
const init = vp.node.init;
|
|
282
|
-
if (!t.isCallExpression(init)) return;
|
|
283
|
-
if (!t.isIdentifier(init.callee) || init.callee.name !== "require") return;
|
|
284
|
-
const a0 = init.arguments[0];
|
|
285
|
-
if (t.isStringLiteral(a0)) spec = a0.value;
|
|
286
581
|
}
|
|
287
|
-
});
|
|
288
582
|
|
|
289
|
-
|
|
583
|
+
if (key === "handler") hasHandler = true;
|
|
584
|
+
if (["preHandler", "onRequest", "preValidation", "preSerialization"].includes(key)) hooks.push(key);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
return { url, methods, hasHandler, hooks };
|
|
290
588
|
}
|
|
291
589
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
590
|
+
function unwrapPluginArg(node) {
|
|
591
|
+
// fastify-plugin wrappers are common: fastify.register(fp(plugin))
|
|
592
|
+
// We unwrap 1 layer, best-effort.
|
|
593
|
+
if (!node) return node;
|
|
594
|
+
if (!t.isCallExpression(node)) return node;
|
|
297
595
|
|
|
298
|
-
|
|
299
|
-
|
|
596
|
+
const calleeName =
|
|
597
|
+
t.isIdentifier(node.callee) ? node.callee.name :
|
|
598
|
+
t.isMemberExpression(node.callee) && t.isIdentifier(node.callee.property) ? node.callee.property.name :
|
|
599
|
+
null;
|
|
300
600
|
|
|
301
|
-
|
|
601
|
+
if (!calleeName) return node;
|
|
602
|
+
if (!/^(fp|fastifyPlugin|plugin|fastifyPluginify)$/i.test(calleeName)) return node;
|
|
302
603
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if (!routeStr) return;
|
|
604
|
+
const a0 = node.arguments && node.arguments[0];
|
|
605
|
+
return a0 || node;
|
|
606
|
+
}
|
|
307
607
|
|
|
308
|
-
|
|
309
|
-
|
|
608
|
+
try {
|
|
609
|
+
traverse(ast, {
|
|
610
|
+
FunctionDeclaration(p) {
|
|
611
|
+
if (t.isIdentifier(p.node.id)) {
|
|
612
|
+
_localPlugins.set(p.node.id.name, p.node);
|
|
613
|
+
}
|
|
614
|
+
},
|
|
615
|
+
VariableDeclarator(p) {
|
|
616
|
+
if (!t.isIdentifier(p.node.id)) return;
|
|
617
|
+
const id = p.node.id.name;
|
|
618
|
+
const init = p.node.init;
|
|
619
|
+
if (!init) return;
|
|
310
620
|
|
|
311
|
-
|
|
312
|
-
fileAbs, fileRel, loc: p.node.loc,
|
|
313
|
-
reason: `Fastify ${prop.toUpperCase()}("${routeStr}")`
|
|
314
|
-
});
|
|
621
|
+
_rawConstInits.set(id, init);
|
|
315
622
|
|
|
316
|
-
routes
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
confidence: "med",
|
|
321
|
-
evidence: ev ? [ev] : []
|
|
322
|
-
});
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
623
|
+
// local plugin: const routes = async (fastify) => { ... }
|
|
624
|
+
if (t.isFunctionExpression(init) || t.isArrowFunctionExpression(init)) {
|
|
625
|
+
_localPlugins.set(id, init);
|
|
626
|
+
}
|
|
325
627
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
628
|
+
// const app = Fastify()
|
|
629
|
+
if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
|
|
630
|
+
const cal = init.callee.name;
|
|
631
|
+
if (cal === "Fastify" || cal === "fastify") fastifyNames.add(id);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// const app = require("fastify")()
|
|
635
|
+
if (t.isCallExpression(init) && t.isCallExpression(init.callee)) {
|
|
636
|
+
const inner = init.callee;
|
|
637
|
+
if (t.isIdentifier(inner.callee, { name: "require" }) && t.isStringLiteral(inner.arguments?.[0], { value: "fastify" })) {
|
|
638
|
+
fastifyNames.add(id);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
} catch {
|
|
644
|
+
// Babel traverse can fail on some edge-case files; skip this step
|
|
645
|
+
}
|
|
330
646
|
|
|
331
|
-
|
|
332
|
-
|
|
647
|
+
// helper: resolve imports for register(pluginIdent,...)
|
|
648
|
+
function resolveImportSpecForLocal(localName) {
|
|
649
|
+
let spec = null;
|
|
333
650
|
|
|
334
|
-
|
|
335
|
-
|
|
651
|
+
try {
|
|
652
|
+
traverse(ast, {
|
|
653
|
+
ImportDeclaration(ip) {
|
|
654
|
+
for (const s of ip.node.specifiers) {
|
|
655
|
+
if ((t.isImportDefaultSpecifier(s) || t.isImportSpecifier(s)) && s.local.name === localName) {
|
|
656
|
+
spec = ip.node.source.value;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
},
|
|
660
|
+
VariableDeclarator(vp) {
|
|
661
|
+
if (!t.isIdentifier(vp.node.id) || vp.node.id.name !== localName) return;
|
|
662
|
+
const init = vp.node.init;
|
|
663
|
+
if (!t.isCallExpression(init)) return;
|
|
664
|
+
if (!t.isIdentifier(init.callee) || init.callee.name !== "require") return;
|
|
665
|
+
const a0 = init.arguments[0];
|
|
666
|
+
if (t.isStringLiteral(a0)) spec = a0.value;
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
} catch {
|
|
670
|
+
// Babel traverse can fail; ignore
|
|
671
|
+
}
|
|
336
672
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
673
|
+
return spec;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
traverse(ast, {
|
|
678
|
+
CallExpression(p) {
|
|
679
|
+
const callee = p.node.callee;
|
|
680
|
+
if (!t.isMemberExpression(callee)) return;
|
|
681
|
+
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
682
|
+
|
|
683
|
+
const obj = callee.object.name;
|
|
684
|
+
const prop = callee.property.name;
|
|
685
|
+
|
|
686
|
+
if (!fastifyNames.has(obj)) return;
|
|
687
|
+
|
|
688
|
+
// fastify.get('/x', ...)
|
|
689
|
+
if (isFastifyMethod(prop)) {
|
|
690
|
+
const routeStr = evalStaticString(p.node.arguments[0]);
|
|
691
|
+
if (!routeStr) return;
|
|
692
|
+
|
|
693
|
+
const fullPath = joinPaths(prefix, routeStr);
|
|
694
|
+
const method = canonicalizeMethod(prop);
|
|
695
|
+
|
|
696
|
+
const ev = evidenceFromLoc({
|
|
697
|
+
fileAbs,
|
|
698
|
+
fileRel,
|
|
699
|
+
loc: p.node.loc,
|
|
700
|
+
reason: `Fastify ${prop.toUpperCase()}("${routeStr}")`,
|
|
701
|
+
});
|
|
341
702
|
|
|
342
|
-
for (const m of ms) {
|
|
343
703
|
routes.push({
|
|
344
|
-
method
|
|
704
|
+
method,
|
|
345
705
|
path: fullPath,
|
|
346
706
|
handler: fileRel,
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
evidence: ev ? [ev] : []
|
|
707
|
+
confidence: "med",
|
|
708
|
+
framework: "fastify",
|
|
709
|
+
evidence: ev ? [ev] : [],
|
|
350
710
|
});
|
|
711
|
+
return;
|
|
351
712
|
}
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
713
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
const childPrefixRaw = extractPrefixFromOpts(optsArg);
|
|
360
|
-
const childPrefix = childPrefixRaw ? joinPaths(prefix, childPrefixRaw) : prefix;
|
|
361
|
-
|
|
362
|
-
// inline plugin
|
|
363
|
-
if (t.isFunctionExpression(pluginArg) || t.isArrowFunctionExpression(pluginArg)) {
|
|
364
|
-
const param0 = pluginArg.params[0];
|
|
365
|
-
const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
|
|
366
|
-
|
|
367
|
-
// traverse just the plugin body (best effort)
|
|
368
|
-
traverse(pluginArg.body, {
|
|
369
|
-
CallExpression(pp) {
|
|
370
|
-
const c = pp.node.callee;
|
|
371
|
-
if (!t.isMemberExpression(c)) return;
|
|
372
|
-
if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
|
|
373
|
-
if (c.object.name !== innerName) return;
|
|
374
|
-
|
|
375
|
-
const pr = c.property.name;
|
|
376
|
-
|
|
377
|
-
if (isFastifyMethod(pr)) {
|
|
378
|
-
const rs = extractStringLiteral(pp.node.arguments[0]);
|
|
379
|
-
if (!rs) return;
|
|
380
|
-
const fullPath = joinPaths(childPrefix, rs);
|
|
381
|
-
const method = canonicalizeMethod(pr);
|
|
382
|
-
|
|
383
|
-
const ev = evidenceFromLoc({
|
|
384
|
-
fileAbs, fileRel, loc: pp.node.loc,
|
|
385
|
-
reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
routes.push({ method, path: fullPath, handler: fileRel, confidence: "med", evidence: ev ? [ev] : [] });
|
|
389
|
-
}
|
|
714
|
+
// fastify.route({ method, url, handler })
|
|
715
|
+
if (prop === "route") {
|
|
716
|
+
const arg0 = p.node.arguments[0];
|
|
717
|
+
if (!t.isObjectExpression(arg0)) return;
|
|
390
718
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
if (!t.isObjectExpression(a0)) return;
|
|
394
|
-
const r = extractRouteObject(a0);
|
|
395
|
-
if (!r.url) return;
|
|
396
|
-
const fullPath = joinPaths(childPrefix, r.url);
|
|
397
|
-
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
719
|
+
const r = extractRouteObjectV3(arg0);
|
|
720
|
+
if (!r.url) return;
|
|
398
721
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`
|
|
402
|
-
});
|
|
722
|
+
const fullPath = joinPaths(prefix, r.url);
|
|
723
|
+
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
403
724
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
725
|
+
const ev = evidenceFromLoc({
|
|
726
|
+
fileAbs,
|
|
727
|
+
fileRel,
|
|
728
|
+
loc: p.node.loc,
|
|
729
|
+
reason: `Fastify.route({ url: "${r.url}" })`,
|
|
730
|
+
});
|
|
408
731
|
|
|
732
|
+
for (const m of ms) {
|
|
733
|
+
routes.push({
|
|
734
|
+
method: m,
|
|
735
|
+
path: fullPath,
|
|
736
|
+
handler: fileRel,
|
|
737
|
+
hooks: r.hooks,
|
|
738
|
+
confidence: r.hasHandler ? "med" : "low",
|
|
739
|
+
framework: "fastify",
|
|
740
|
+
evidence: ev ? [ev] : [],
|
|
741
|
+
});
|
|
742
|
+
}
|
|
409
743
|
return;
|
|
410
744
|
}
|
|
411
745
|
|
|
412
|
-
//
|
|
413
|
-
if (
|
|
414
|
-
const
|
|
415
|
-
const
|
|
746
|
+
// fastify.register(plugin, { prefix })
|
|
747
|
+
if (prop === "register") {
|
|
748
|
+
const pluginArgRaw = unwrapPluginArg(p.node.arguments[0]);
|
|
749
|
+
const optsArg = p.node.arguments[1];
|
|
750
|
+
|
|
751
|
+
const childPrefixRaw = extractPrefixFromOptsV3(optsArg);
|
|
752
|
+
const childPrefix = childPrefixRaw ? joinPaths(prefix, childPrefixRaw) : prefix;
|
|
753
|
+
|
|
754
|
+
// inline plugin OR local plugin identifier (common in real Fastify codebases)
|
|
755
|
+
let pluginFn = null;
|
|
756
|
+
const localIdentName = t.isIdentifier(pluginArgRaw) ? pluginArgRaw.name : null;
|
|
757
|
+
if (t.isFunctionExpression(pluginArgRaw) || t.isArrowFunctionExpression(pluginArgRaw)) pluginFn = pluginArgRaw;
|
|
758
|
+
if (!pluginFn && localIdentName) pluginFn = _localPlugins.get(localIdentName) || null;
|
|
759
|
+
|
|
760
|
+
if (pluginFn) {
|
|
761
|
+
const param0 = pluginFn.params && pluginFn.params[0];
|
|
762
|
+
const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
|
|
763
|
+
const bodyNode = pluginFn.body;
|
|
764
|
+
if (!t.isBlockStatement(bodyNode)) {
|
|
765
|
+
// We only handle block bodies. Expression-bodied arrows are rare for route plugins.
|
|
766
|
+
if (localIdentName) return;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// traverse just the plugin body (best effort)
|
|
770
|
+
try {
|
|
771
|
+
traverse(
|
|
772
|
+
bodyNode,
|
|
773
|
+
{
|
|
774
|
+
CallExpression(pp) {
|
|
775
|
+
const c = pp.node.callee;
|
|
776
|
+
if (!t.isMemberExpression(c)) return;
|
|
777
|
+
if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
|
|
778
|
+
if (c.object.name !== innerName) return;
|
|
779
|
+
|
|
780
|
+
const pr = c.property.name;
|
|
781
|
+
|
|
782
|
+
if (isFastifyMethod(pr)) {
|
|
783
|
+
const rs = evalStaticString(pp.node.arguments[0]);
|
|
784
|
+
if (!rs) return;
|
|
785
|
+
|
|
786
|
+
const fullPath = joinPaths(childPrefix, rs);
|
|
787
|
+
const method = canonicalizeMethod(pr);
|
|
788
|
+
|
|
789
|
+
const ev = evidenceFromLoc({
|
|
790
|
+
fileAbs,
|
|
791
|
+
fileRel,
|
|
792
|
+
loc: pp.node.loc,
|
|
793
|
+
reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`,
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
routes.push({
|
|
797
|
+
method,
|
|
798
|
+
path: fullPath,
|
|
799
|
+
handler: fileRel,
|
|
800
|
+
confidence: "med",
|
|
801
|
+
framework: "fastify",
|
|
802
|
+
evidence: ev ? [ev] : [],
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (pr === "route") {
|
|
807
|
+
const a0 = pp.node.arguments[0];
|
|
808
|
+
if (!t.isObjectExpression(a0)) return;
|
|
809
|
+
|
|
810
|
+
const r = extractRouteObjectV3(a0);
|
|
811
|
+
if (!r.url) return;
|
|
812
|
+
|
|
813
|
+
const fullPath = joinPaths(childPrefix, r.url);
|
|
814
|
+
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
815
|
+
|
|
816
|
+
const ev = evidenceFromLoc({
|
|
817
|
+
fileAbs,
|
|
818
|
+
fileRel,
|
|
819
|
+
loc: pp.node.loc,
|
|
820
|
+
reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`,
|
|
821
|
+
});
|
|
822
|
+
|
|
823
|
+
for (const m of ms) {
|
|
824
|
+
routes.push({
|
|
825
|
+
method: m,
|
|
826
|
+
path: fullPath,
|
|
827
|
+
handler: fileRel,
|
|
828
|
+
confidence: "med",
|
|
829
|
+
framework: "fastify",
|
|
830
|
+
evidence: ev ? [ev] : [],
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
},
|
|
836
|
+
p.scope,
|
|
837
|
+
p
|
|
838
|
+
);
|
|
839
|
+
} catch {
|
|
840
|
+
// Inner traverse can fail; skip this plugin body
|
|
841
|
+
}
|
|
416
842
|
|
|
417
|
-
|
|
418
|
-
|
|
843
|
+
// If this was a local plugin identifier, we've already extracted its routes.
|
|
844
|
+
if (localIdentName) return;
|
|
419
845
|
return;
|
|
420
846
|
}
|
|
421
847
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
848
|
+
// Resolve dynamic require/import spec directly (fastify.register(require("./x")) / import("./x"))
|
|
849
|
+
const dynSpec = extractRequireOrImportSpec(pluginArgRaw);
|
|
850
|
+
if (dynSpec) {
|
|
851
|
+
const resolved = resolveRelativeModule(fileAbs, dynSpec);
|
|
852
|
+
if (!resolved) {
|
|
853
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec: dynSpec });
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
scanFile(resolved, childPrefix);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// imported plugin identifier
|
|
861
|
+
if (t.isIdentifier(pluginArgRaw)) {
|
|
862
|
+
const localName = pluginArgRaw.name;
|
|
863
|
+
const spec = resolveImportSpecForLocal(localName);
|
|
864
|
+
|
|
865
|
+
if (!spec) {
|
|
866
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, name: localName });
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const resolved = resolveRelativeModule(fileAbs, spec);
|
|
871
|
+
if (!resolved) {
|
|
872
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec });
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
scanFile(resolved, childPrefix);
|
|
425
877
|
return;
|
|
426
878
|
}
|
|
427
879
|
|
|
428
|
-
|
|
880
|
+
// Anything else: unknown plugin shape. Mark a gap so analyzers can be lenient.
|
|
881
|
+
gaps.push({
|
|
882
|
+
kind: "fastify_plugin_unresolved",
|
|
883
|
+
file: fileRel,
|
|
884
|
+
note: "register() plugin not statically resolvable",
|
|
885
|
+
});
|
|
429
886
|
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
}
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
} catch {
|
|
890
|
+
// Babel traverse can fail on some edge-case files; skip
|
|
891
|
+
stats.parseErrors++;
|
|
892
|
+
}
|
|
433
893
|
}
|
|
434
894
|
|
|
435
895
|
scanFile(entryAbs, "/");
|
|
436
896
|
return { routes, gaps };
|
|
437
897
|
}
|
|
438
898
|
|
|
439
|
-
// ---------- client refs (fetch + axios
|
|
899
|
+
// ---------- client refs (fetch + axios + template literals best-effort) ----------
|
|
440
900
|
function isAxiosMember(node) {
|
|
441
|
-
return
|
|
901
|
+
return (
|
|
902
|
+
t.isMemberExpression(node) &&
|
|
442
903
|
t.isIdentifier(node.object) &&
|
|
443
904
|
t.isIdentifier(node.property) &&
|
|
444
|
-
["get","post","put","patch","delete"].includes(node.property.name)
|
|
905
|
+
["get", "post", "put", "patch", "delete"].includes(node.property.name)
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function isAxiosCallee(node) {
|
|
910
|
+
return t.isIdentifier(node, { name: "axios" }) || isAxiosMember(node);
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function extractUrlLike(node) {
|
|
914
|
+
// "literal"
|
|
915
|
+
if (t.isStringLiteral(node)) return { url: node.value, confidence: "high", note: "string" };
|
|
916
|
+
|
|
917
|
+
// `literal`
|
|
918
|
+
if (t.isTemplateLiteral(node) && node.expressions.length === 0) {
|
|
919
|
+
return { url: node.quasis.map((q) => q.value.cooked || "").join(""), confidence: "high", note: "template_static" };
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// `/api/x/${id}` -> "/api/x/:id" (med confidence)
|
|
923
|
+
if (t.isTemplateLiteral(node) && node.quasis.length >= 1) {
|
|
924
|
+
const start = node.quasis[0]?.value?.cooked || "";
|
|
925
|
+
if (!start.startsWith("/")) return null;
|
|
926
|
+
|
|
927
|
+
let built = "";
|
|
928
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
929
|
+
built += node.quasis[i].value.cooked || "";
|
|
930
|
+
if (i < node.expressions.length) {
|
|
931
|
+
const expr = node.expressions[i];
|
|
932
|
+
if (t.isIdentifier(expr)) built += `:${expr.name}`;
|
|
933
|
+
else built += "*";
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return { url: built, confidence: "med", note: "template_dynamic" };
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
// "/api/x" + "/y" or "/api/x" + id (low confidence)
|
|
940
|
+
if (t.isBinaryExpression(node, { operator: "+" })) {
|
|
941
|
+
if (t.isStringLiteral(node.left) && node.left.value.startsWith("/")) {
|
|
942
|
+
const left = node.left.value;
|
|
943
|
+
let right = "";
|
|
944
|
+
if (t.isStringLiteral(node.right)) right = node.right.value;
|
|
945
|
+
else if (t.isIdentifier(node.right)) right = `:${node.right.name}`;
|
|
946
|
+
else right = "*";
|
|
947
|
+
return { url: left + right, confidence: "low", note: "concat" };
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function extractFetchMethodFromOptions(node) {
|
|
955
|
+
if (!t.isObjectExpression(node)) return "*";
|
|
956
|
+
for (const prop of node.properties) {
|
|
957
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
958
|
+
const key =
|
|
959
|
+
t.isIdentifier(prop.key) ? prop.key.name :
|
|
960
|
+
t.isStringLiteral(prop.key) ? prop.key.value :
|
|
961
|
+
null;
|
|
962
|
+
if (key === "method" && t.isStringLiteral(prop.value)) return canonicalizeMethod(prop.value.value);
|
|
963
|
+
}
|
|
964
|
+
return "*";
|
|
445
965
|
}
|
|
446
966
|
|
|
447
|
-
|
|
448
|
-
|
|
967
|
+
function extractAxiosConfig(node) {
|
|
968
|
+
// axios({ url: "/api/x", method: "post" })
|
|
969
|
+
if (!t.isObjectExpression(node)) return null;
|
|
970
|
+
|
|
971
|
+
let urlNode = null;
|
|
972
|
+
let methodNode = null;
|
|
973
|
+
|
|
974
|
+
for (const prop of node.properties) {
|
|
975
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
976
|
+
const key =
|
|
977
|
+
t.isIdentifier(prop.key) ? prop.key.name :
|
|
978
|
+
t.isStringLiteral(prop.key) ? prop.key.value :
|
|
979
|
+
null;
|
|
980
|
+
if (!key) continue;
|
|
981
|
+
|
|
982
|
+
if (key === "url") urlNode = prop.value;
|
|
983
|
+
if (key === "method") methodNode = prop.value;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
const urlInfo = urlNode ? extractUrlLike(urlNode) : null;
|
|
987
|
+
if (!urlInfo) return null;
|
|
988
|
+
|
|
989
|
+
const method =
|
|
990
|
+
methodNode && t.isStringLiteral(methodNode)
|
|
991
|
+
? canonicalizeMethod(methodNode.value)
|
|
992
|
+
: "*";
|
|
993
|
+
|
|
994
|
+
return { method, urlInfo };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
async function resolveClientRouteRefs(repoRoot, stats) {
|
|
998
|
+
const files = await fg(CODE_FILE_GLOBS, {
|
|
449
999
|
cwd: repoRoot,
|
|
450
1000
|
absolute: true,
|
|
451
1001
|
ignore: [
|
|
452
|
-
|
|
453
|
-
"**/.next/**",
|
|
454
|
-
"**/dist/**",
|
|
455
|
-
"**/build/**",
|
|
1002
|
+
...IGNORE_GLOBS,
|
|
456
1003
|
"**/test/**",
|
|
457
1004
|
"**/tests/**",
|
|
458
1005
|
"**/__tests__/**",
|
|
@@ -462,141 +1009,372 @@ async function resolveClientRouteRefs(repoRoot) {
|
|
|
462
1009
|
"**/jest.config.*",
|
|
463
1010
|
"**/*.mock.*",
|
|
464
1011
|
"**/mocks/**",
|
|
465
|
-
"**/fixtures/**"
|
|
466
|
-
]
|
|
1012
|
+
"**/fixtures/**",
|
|
1013
|
+
],
|
|
467
1014
|
});
|
|
468
1015
|
|
|
469
1016
|
const out = [];
|
|
470
1017
|
|
|
471
1018
|
for (const fileAbs of files) {
|
|
472
|
-
const fileRel =
|
|
1019
|
+
const fileRel = normalizeRel(repoRoot, fileAbs);
|
|
473
1020
|
const code = safeRead(fileAbs);
|
|
1021
|
+
if (!code) continue;
|
|
474
1022
|
|
|
475
1023
|
let ast;
|
|
476
|
-
try {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
1024
|
+
try {
|
|
1025
|
+
ast = parseFile(code, fileAbs);
|
|
1026
|
+
} catch {
|
|
1027
|
+
stats.parseErrors++;
|
|
1028
|
+
continue;
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
try {
|
|
1032
|
+
traverse(ast, {
|
|
1033
|
+
CallExpression(p) {
|
|
1034
|
+
const callee = p.node.callee;
|
|
1035
|
+
|
|
1036
|
+
// fetch(url, opts)
|
|
1037
|
+
if (t.isIdentifier(callee, { name: "fetch" })) {
|
|
1038
|
+
const a0 = p.node.arguments[0];
|
|
1039
|
+
const a1 = p.node.arguments[1];
|
|
1040
|
+
|
|
1041
|
+
const urlInfo = extractUrlLike(a0);
|
|
1042
|
+
if (!urlInfo) return;
|
|
1043
|
+
|
|
1044
|
+
const url = urlInfo.url;
|
|
1045
|
+
if (!url.startsWith("/")) return;
|
|
1046
|
+
|
|
1047
|
+
const method = extractFetchMethodFromOptions(a1);
|
|
1048
|
+
|
|
1049
|
+
const ev = evidenceFromLoc({
|
|
1050
|
+
fileAbs,
|
|
1051
|
+
fileRel,
|
|
1052
|
+
loc: p.node.loc,
|
|
1053
|
+
reason: `Client fetch(${urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
out.push({
|
|
1057
|
+
method,
|
|
1058
|
+
path: canonicalizePath(url),
|
|
1059
|
+
source: fileRel,
|
|
1060
|
+
confidence: urlInfo.confidence,
|
|
1061
|
+
kind: "fetch",
|
|
1062
|
+
evidence: ev ? [ev] : [],
|
|
1063
|
+
});
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// axios.get("/api/x") etc
|
|
1068
|
+
if (isAxiosMember(callee)) {
|
|
1069
|
+
const verb = callee.property.name.toUpperCase();
|
|
1070
|
+
const a0 = p.node.arguments[0];
|
|
1071
|
+
|
|
1072
|
+
const urlInfo = extractUrlLike(a0);
|
|
1073
|
+
if (!urlInfo) return;
|
|
1074
|
+
|
|
1075
|
+
const url = urlInfo.url;
|
|
1076
|
+
if (!url.startsWith("/")) return;
|
|
1077
|
+
|
|
1078
|
+
const ev = evidenceFromLoc({
|
|
1079
|
+
fileAbs,
|
|
1080
|
+
fileRel,
|
|
1081
|
+
loc: p.node.loc,
|
|
1082
|
+
reason: `Client axios.${verb.toLowerCase()}(${urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
out.push({
|
|
1086
|
+
method: canonicalizeMethod(verb),
|
|
1087
|
+
path: canonicalizePath(url),
|
|
1088
|
+
source: fileRel,
|
|
1089
|
+
confidence: urlInfo.confidence,
|
|
1090
|
+
kind: "axios_member",
|
|
1091
|
+
evidence: ev ? [ev] : [],
|
|
1092
|
+
});
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// axios({ url, method })
|
|
1097
|
+
if (t.isIdentifier(callee, { name: "axios" })) {
|
|
1098
|
+
const a0 = p.node.arguments[0];
|
|
1099
|
+
const cfg = extractAxiosConfig(a0);
|
|
1100
|
+
if (!cfg) return;
|
|
1101
|
+
|
|
1102
|
+
const url = cfg.urlInfo.url;
|
|
1103
|
+
if (!url.startsWith("/")) return;
|
|
1104
|
+
|
|
1105
|
+
const ev = evidenceFromLoc({
|
|
1106
|
+
fileAbs,
|
|
1107
|
+
fileRel,
|
|
1108
|
+
loc: p.node.loc,
|
|
1109
|
+
reason: `Client axios(config:${cfg.urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
out.push({
|
|
1113
|
+
method: cfg.method,
|
|
1114
|
+
path: canonicalizePath(url),
|
|
1115
|
+
source: fileRel,
|
|
1116
|
+
confidence: cfg.urlInfo.confidence === "high" ? "high" : "med",
|
|
1117
|
+
kind: "axios_config",
|
|
1118
|
+
evidence: ev ? [ev] : [],
|
|
1119
|
+
});
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
// useSWR("/api/user", fetcher) - Modern React data fetching
|
|
1124
|
+
if (t.isIdentifier(callee, { name: "useSWR" })) {
|
|
1125
|
+
const a0 = p.node.arguments[0];
|
|
1126
|
+
|
|
1127
|
+
const urlInfo = extractUrlLike(a0);
|
|
1128
|
+
if (!urlInfo) return;
|
|
1129
|
+
|
|
1130
|
+
const url = urlInfo.url;
|
|
1131
|
+
if (!url.startsWith("/")) return;
|
|
1132
|
+
|
|
1133
|
+
const ev = evidenceFromLoc({
|
|
1134
|
+
fileAbs,
|
|
1135
|
+
fileRel,
|
|
1136
|
+
loc: p.node.loc,
|
|
1137
|
+
reason: `Client useSWR(${urlInfo.note}) "${stripQueryHash(url)}"`,
|
|
1138
|
+
});
|
|
1139
|
+
|
|
1140
|
+
out.push({
|
|
1141
|
+
method: "GET", // SWR is almost always GET
|
|
1142
|
+
path: canonicalizePath(url),
|
|
1143
|
+
source: fileRel,
|
|
1144
|
+
confidence: urlInfo.confidence,
|
|
1145
|
+
kind: "useSWR",
|
|
1146
|
+
evidence: ev ? [ev] : [],
|
|
1147
|
+
});
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// useQuery (React Query / TanStack Query) - Another popular data fetching library
|
|
1152
|
+
if (t.isIdentifier(callee, { name: "useQuery" })) {
|
|
1153
|
+
// useQuery({ queryKey: [...], queryFn: () => fetch("/api/x") })
|
|
1154
|
+
// or useQuery(["key"], () => fetch("/api/x"))
|
|
1155
|
+
const a0 = p.node.arguments[0];
|
|
1156
|
+
|
|
1157
|
+
// Try to extract URL from the arguments (often in queryFn)
|
|
1158
|
+
if (t.isObjectExpression(a0)) {
|
|
1159
|
+
for (const prop of a0.properties) {
|
|
1160
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
1161
|
+
if (!t.isIdentifier(prop.key, { name: "queryFn" })) continue;
|
|
1162
|
+
|
|
1163
|
+
// queryFn is often an arrow function with fetch inside
|
|
1164
|
+
const fn = prop.value;
|
|
1165
|
+
if (t.isArrowFunctionExpression(fn) || t.isFunctionExpression(fn)) {
|
|
1166
|
+
// Best effort: look for string literals that look like API paths
|
|
1167
|
+
const fnCode = code.slice(fn.start, fn.end);
|
|
1168
|
+
const urlMatch = fnCode.match(/["'`](\/api\/[^"'`]+)["'`]/);
|
|
1169
|
+
if (urlMatch) {
|
|
1170
|
+
const url = urlMatch[1].split("?")[0].split("#")[0];
|
|
1171
|
+
const ev = evidenceFromLoc({
|
|
1172
|
+
fileAbs,
|
|
1173
|
+
fileRel,
|
|
1174
|
+
loc: p.node.loc,
|
|
1175
|
+
reason: `Client useQuery(queryFn) "${url}"`,
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
out.push({
|
|
1179
|
+
method: "GET",
|
|
1180
|
+
path: canonicalizePath(url),
|
|
1181
|
+
source: fileRel,
|
|
1182
|
+
confidence: "low", // Less certain extraction
|
|
1183
|
+
kind: "useQuery",
|
|
1184
|
+
evidence: ev ? [ev] : [],
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
501
1188
|
}
|
|
502
1189
|
}
|
|
503
1190
|
}
|
|
1191
|
+
},
|
|
1192
|
+
});
|
|
1193
|
+
} catch {
|
|
1194
|
+
// Babel traverse can fail on some edge-case files; skip
|
|
1195
|
+
stats.parseErrors++;
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
504
1198
|
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
reason: `Client fetch("${url}")`
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
out.push({
|
|
511
|
-
method,
|
|
512
|
-
path: canonicalizePath(url),
|
|
513
|
-
source: fileRel,
|
|
514
|
-
confidence: "high",
|
|
515
|
-
evidence: ev ? [ev] : []
|
|
516
|
-
});
|
|
517
|
-
return;
|
|
518
|
-
}
|
|
1199
|
+
return out;
|
|
1200
|
+
}
|
|
519
1201
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1202
|
+
// ---------- workspace detection (best-effort, no new deps) ----------
|
|
1203
|
+
function readJsonIfExists(abs) {
|
|
1204
|
+
try {
|
|
1205
|
+
return JSON.parse(fs.readFileSync(abs, "utf8"));
|
|
1206
|
+
} catch {
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function detectWorkspaces(repoRoot) {
|
|
1212
|
+
const roots = [];
|
|
1213
|
+
|
|
1214
|
+
const pkg = readJsonIfExists(path.join(repoRoot, "package.json"));
|
|
1215
|
+
if (pkg && pkg.workspaces) {
|
|
1216
|
+
const ws = pkg.workspaces;
|
|
1217
|
+
const patterns = Array.isArray(ws) ? ws : Array.isArray(ws.packages) ? ws.packages : [];
|
|
1218
|
+
for (const pat of patterns) {
|
|
1219
|
+
if (typeof pat === "string") roots.push(pat);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// pnpm-workspace.yaml minimal parser (just handles `packages:` list)
|
|
1224
|
+
const pnpmWs = path.join(repoRoot, "pnpm-workspace.yaml");
|
|
1225
|
+
if (fs.existsSync(pnpmWs)) {
|
|
1226
|
+
const raw = safeRead(pnpmWs) || "";
|
|
1227
|
+
const lines = raw.split(/\r?\n/);
|
|
1228
|
+
let inPackages = false;
|
|
1229
|
+
for (const line of lines) {
|
|
1230
|
+
const l = line.trim();
|
|
1231
|
+
if (!l) continue;
|
|
1232
|
+
if (l.startsWith("packages:")) {
|
|
1233
|
+
inPackages = true;
|
|
1234
|
+
continue;
|
|
542
1235
|
}
|
|
543
|
-
|
|
1236
|
+
if (inPackages) {
|
|
1237
|
+
const m = l.match(/^-+\s*['"]?([^'"]+)['"]?\s*$/);
|
|
1238
|
+
if (m && m[1]) roots.push(m[1]);
|
|
1239
|
+
else if (!l.startsWith("-")) inPackages = false;
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
544
1242
|
}
|
|
545
1243
|
|
|
546
|
-
|
|
1244
|
+
// Expand to actual package.json roots
|
|
1245
|
+
const uniq = Array.from(new Set(roots)).filter(Boolean);
|
|
1246
|
+
const pkgJsonGlobs = uniq.map((p) => (p.endsWith("/") ? p : p + "/") + "package.json");
|
|
1247
|
+
|
|
1248
|
+
const found = pkgJsonGlobs.length
|
|
1249
|
+
? fg.sync(pkgJsonGlobs, { cwd: repoRoot, absolute: true, ignore: IGNORE_GLOBS })
|
|
1250
|
+
: [];
|
|
1251
|
+
|
|
1252
|
+
const workspaces = found
|
|
1253
|
+
.map((abs) => path.dirname(abs))
|
|
1254
|
+
.map((abs) => normalizeRel(repoRoot, abs))
|
|
1255
|
+
.sort();
|
|
1256
|
+
|
|
1257
|
+
return workspaces;
|
|
547
1258
|
}
|
|
548
1259
|
|
|
549
|
-
// ---------- fastify entry detection ----------
|
|
550
|
-
function
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1260
|
+
// ---------- fastify entry detection (monorepo-friendly) ----------
|
|
1261
|
+
async function detectFastifyEntries(repoRoot) {
|
|
1262
|
+
// Keep it targeted (fast), but broad enough for monorepos.
|
|
1263
|
+
const candidates = await fg(
|
|
1264
|
+
[
|
|
1265
|
+
"**/{server,app,main,index}.{ts,tsx,js,jsx}",
|
|
1266
|
+
"**/src/{server,app,main,index}.{ts,tsx,js,jsx}",
|
|
1267
|
+
"**/*fastify*.{ts,tsx,js,jsx}",
|
|
1268
|
+
"**/*api*.{ts,tsx,js,jsx}",
|
|
1269
|
+
],
|
|
1270
|
+
{
|
|
1271
|
+
cwd: repoRoot,
|
|
1272
|
+
absolute: true,
|
|
1273
|
+
ignore: IGNORE_GLOBS,
|
|
1274
|
+
}
|
|
1275
|
+
);
|
|
1276
|
+
|
|
1277
|
+
const entries = [];
|
|
1278
|
+
const fastifySignal = /\b(Fastify\s*\(|fastify\s*\(|require\(['"]fastify['"]\)|from\s+['"]fastify['"])\b/;
|
|
1279
|
+
const listenSignal = /\.\s*(listen|ready)\s*\(/;
|
|
1280
|
+
|
|
1281
|
+
for (const fileAbs of candidates) {
|
|
1282
|
+
const code = safeRead(fileAbs);
|
|
1283
|
+
if (!code) continue;
|
|
1284
|
+
// Must look like fastify + server start-ish signal (reduces noise)
|
|
1285
|
+
if (fastifySignal.test(code) && listenSignal.test(code)) {
|
|
1286
|
+
entries.push(fileAbs);
|
|
1287
|
+
}
|
|
560
1288
|
}
|
|
561
|
-
|
|
1289
|
+
|
|
1290
|
+
return Array.from(new Set(entries));
|
|
562
1291
|
}
|
|
563
1292
|
|
|
564
1293
|
// ---------- truthpack build/write ----------
|
|
565
1294
|
async function buildTruthpack({ repoRoot, fastifyEntry }) {
|
|
1295
|
+
const stats = {
|
|
1296
|
+
parseErrors: 0,
|
|
1297
|
+
fastifyEntries: 0,
|
|
1298
|
+
fastifyRoutes: 0,
|
|
1299
|
+
nextAppRoutes: 0,
|
|
1300
|
+
nextPagesRoutes: 0,
|
|
1301
|
+
clientRefs: 0,
|
|
1302
|
+
serverRoutes: 0,
|
|
1303
|
+
gaps: 0,
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
// Workspaces (for metadata + future use)
|
|
1307
|
+
const workspaces = detectWorkspaces(repoRoot);
|
|
1308
|
+
|
|
566
1309
|
// Next.js routes (App Router + Pages Router)
|
|
567
|
-
const nextApp = await resolveNextAppApiRoutes(repoRoot);
|
|
568
|
-
const nextPages = await resolveNextPagesApiRoutes(repoRoot);
|
|
1310
|
+
const nextApp = await resolveNextAppApiRoutes(repoRoot, stats);
|
|
1311
|
+
const nextPages = await resolveNextPagesApiRoutes(repoRoot, stats);
|
|
1312
|
+
|
|
1313
|
+
stats.nextAppRoutes = nextApp.length;
|
|
1314
|
+
stats.nextPagesRoutes = nextPages.length;
|
|
569
1315
|
|
|
570
|
-
// Fastify routes (
|
|
571
|
-
const entryRel = fastifyEntry || detectFastifyEntry(repoRoot);
|
|
1316
|
+
// Fastify routes (monorepo-friendly)
|
|
572
1317
|
let fastify = { routes: [], gaps: [] };
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
1318
|
+
|
|
1319
|
+
if (fastifyEntry) {
|
|
1320
|
+
const entryAbs = path.isAbsolute(fastifyEntry) ? fastifyEntry : path.join(repoRoot, fastifyEntry);
|
|
1321
|
+
if (exists(entryAbs)) {
|
|
1322
|
+
const resolved = resolveFastifyRoutes(repoRoot, entryAbs, stats);
|
|
1323
|
+
fastify.routes.push(...resolved.routes);
|
|
1324
|
+
fastify.gaps.push(...resolved.gaps);
|
|
1325
|
+
stats.fastifyEntries = 1;
|
|
1326
|
+
}
|
|
1327
|
+
} else {
|
|
1328
|
+
const entries = await detectFastifyEntries(repoRoot);
|
|
1329
|
+
stats.fastifyEntries = entries.length;
|
|
1330
|
+
|
|
1331
|
+
for (const entryAbs of entries) {
|
|
1332
|
+
const resolved = resolveFastifyRoutes(repoRoot, entryAbs, stats);
|
|
1333
|
+
fastify.routes.push(...resolved.routes);
|
|
1334
|
+
fastify.gaps.push(...resolved.gaps);
|
|
1335
|
+
}
|
|
576
1336
|
}
|
|
577
1337
|
|
|
1338
|
+
stats.fastifyRoutes = fastify.routes.length;
|
|
1339
|
+
|
|
578
1340
|
// Multi-framework route detection v2 (Express, Flask, FastAPI, Django, Hono, Koa, etc.)
|
|
579
1341
|
const multiFramework = await resolveAllRoutes(repoRoot);
|
|
580
1342
|
const detectedFrameworks = await detectFrameworks(repoRoot);
|
|
581
1343
|
|
|
582
1344
|
// Client refs (JS/TS fetch/axios + Python requests/httpx)
|
|
583
|
-
const clientRefs = await resolveClientRouteRefs(repoRoot);
|
|
584
|
-
const allClientRefs = [...clientRefs, ...multiFramework.clientRefs];
|
|
1345
|
+
const clientRefs = await resolveClientRouteRefs(repoRoot, stats);
|
|
1346
|
+
const allClientRefs = [...clientRefs, ...(multiFramework.clientRefs || [])];
|
|
1347
|
+
|
|
1348
|
+
stats.clientRefs = allClientRefs.length;
|
|
1349
|
+
|
|
1350
|
+
// Merge all server routes (dedupe with priority)
|
|
1351
|
+
const serverRoutesRaw = [...nextApp, ...nextPages, ...(fastify.routes || []), ...(multiFramework.routes || [])];
|
|
585
1352
|
|
|
586
|
-
|
|
587
|
-
const serverRoutesRaw = [...nextApp, ...nextPages, ...fastify.routes, ...multiFramework.routes];
|
|
588
|
-
const seenRoutes = new Set();
|
|
589
|
-
const server = [];
|
|
1353
|
+
const bestByKey = new Map(); // key = method:path
|
|
590
1354
|
for (const r of serverRoutesRaw) {
|
|
591
|
-
const key = `${r.method}:${r.path}`;
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
1355
|
+
const key = `${canonicalizeMethod(r.method)}:${canonicalizePath(r.path)}`;
|
|
1356
|
+
|
|
1357
|
+
const prev = bestByKey.get(key);
|
|
1358
|
+
if (!prev) {
|
|
1359
|
+
bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// Prefer higher confidence, and prefer specific method over "*"
|
|
1364
|
+
const prevScore = scoreConfidence(prev.confidence) + (prev.method === "*" ? 0 : 1);
|
|
1365
|
+
const curScore = scoreConfidence(r.confidence) + (r.method === "*" ? 0 : 1);
|
|
1366
|
+
|
|
1367
|
+
if (curScore > prevScore) {
|
|
1368
|
+
bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
|
|
595
1369
|
}
|
|
596
1370
|
}
|
|
597
1371
|
|
|
1372
|
+
const server = Array.from(bestByKey.values());
|
|
1373
|
+
stats.serverRoutes = server.length;
|
|
1374
|
+
|
|
598
1375
|
// Merge gaps
|
|
599
1376
|
const allGaps = [...(fastify.gaps || []), ...(multiFramework.gaps || [])];
|
|
1377
|
+
stats.gaps = allGaps.length;
|
|
600
1378
|
|
|
601
1379
|
// Env Truth v1
|
|
602
1380
|
const env = await buildEnvTruth(repoRoot);
|
|
@@ -611,23 +1389,32 @@ async function buildTruthpack({ repoRoot, fastifyEntry }) {
|
|
|
611
1389
|
const enforcement = buildEnforcementTruth(repoRoot, server);
|
|
612
1390
|
|
|
613
1391
|
// Determine frameworks
|
|
614
|
-
const frameworks = new Set(
|
|
615
|
-
detectedFrameworks.forEach(f => frameworks.add(f));
|
|
616
|
-
server.forEach(r => r.framework && frameworks.add(r.framework));
|
|
1392
|
+
const frameworks = new Set();
|
|
1393
|
+
detectedFrameworks.forEach((f) => frameworks.add(f));
|
|
1394
|
+
server.forEach((r) => r.framework && frameworks.add(r.framework));
|
|
1395
|
+
if (nextApp.length || nextPages.length) frameworks.add("next");
|
|
1396
|
+
if (fastify.routes.length) frameworks.add("fastify");
|
|
617
1397
|
|
|
618
1398
|
const truthpack = {
|
|
619
1399
|
meta: {
|
|
620
|
-
version: "2.
|
|
1400
|
+
version: "2.1.0",
|
|
621
1401
|
generatedAt: new Date().toISOString(),
|
|
622
1402
|
repoRoot,
|
|
623
|
-
commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" }
|
|
1403
|
+
commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" },
|
|
1404
|
+
stats,
|
|
1405
|
+
},
|
|
1406
|
+
project: {
|
|
1407
|
+
frameworks: Array.from(frameworks),
|
|
1408
|
+
workspaces,
|
|
1409
|
+
entrypoints: {
|
|
1410
|
+
fastify: fastifyEntry ? [fastifyEntry] : [], // entries auto-detected are not stored as rel here by default
|
|
1411
|
+
},
|
|
624
1412
|
},
|
|
625
|
-
project: { frameworks: Array.from(frameworks), workspaces: [], entrypoints: [] },
|
|
626
1413
|
routes: { server, clientRefs: allClientRefs, gaps: allGaps },
|
|
627
1414
|
env,
|
|
628
1415
|
auth,
|
|
629
1416
|
billing,
|
|
630
|
-
enforcement
|
|
1417
|
+
enforcement,
|
|
631
1418
|
};
|
|
632
1419
|
|
|
633
1420
|
const hash = sha256(JSON.stringify(truthpack));
|
|
@@ -639,29 +1426,266 @@ async function buildTruthpack({ repoRoot, fastifyEntry }) {
|
|
|
639
1426
|
function writeTruthpack(repoRoot, truthpack) {
|
|
640
1427
|
const dir = path.join(repoRoot, ".vibecheck");
|
|
641
1428
|
ensureDir(dir);
|
|
642
|
-
|
|
643
|
-
|
|
1429
|
+
|
|
1430
|
+
const target = path.join(dir, "truthpack.json");
|
|
1431
|
+
const tmp = path.join(dir, `truthpack.${process.pid}.${Date.now()}.tmp.json`);
|
|
1432
|
+
|
|
1433
|
+
// atomic-ish write: write tmp then rename
|
|
1434
|
+
fs.writeFileSync(tmp, JSON.stringify(truthpack, null, 2));
|
|
1435
|
+
fs.renameSync(tmp, target);
|
|
644
1436
|
}
|
|
645
1437
|
|
|
646
1438
|
function loadTruthpack(repoRoot) {
|
|
647
|
-
// Spec path: .vibecheck/truthpack.json
|
|
648
1439
|
const specPath = path.join(repoRoot, ".vibecheck", "truthpack.json");
|
|
649
|
-
// Legacy path: .vibecheck/truth/truthpack.json (backward compat)
|
|
650
1440
|
const legacyPath = path.join(repoRoot, ".vibecheck", "truth", "truthpack.json");
|
|
1441
|
+
|
|
1442
|
+
try {
|
|
1443
|
+
return JSON.parse(fs.readFileSync(specPath, "utf8"));
|
|
1444
|
+
} catch {
|
|
1445
|
+
try {
|
|
1446
|
+
return JSON.parse(fs.readFileSync(legacyPath, "utf8"));
|
|
1447
|
+
} catch {
|
|
1448
|
+
return null;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
// ---------- RepoIndex-powered build (vNext) ----------
|
|
1454
|
+
|
|
1455
|
+
/**
|
|
1456
|
+
* Build truthpack using RepoIndex for single-pass file indexing
|
|
1457
|
+
* This is the optimized path that shares file reads across all extractors.
|
|
1458
|
+
*
|
|
1459
|
+
* Enable with: VIBECHECK_ENGINE_V2=1
|
|
1460
|
+
*
|
|
1461
|
+
* @param {Object} options
|
|
1462
|
+
* @param {string} options.repoRoot
|
|
1463
|
+
* @param {string} [options.fastifyEntry]
|
|
1464
|
+
* @param {boolean} [options.verbose]
|
|
1465
|
+
* @returns {Promise<Object>}
|
|
1466
|
+
*/
|
|
1467
|
+
async function buildTruthpackV2({ repoRoot, fastifyEntry, verbose }) {
|
|
1468
|
+
const {
|
|
1469
|
+
createIndex,
|
|
1470
|
+
globalASTCache,
|
|
1471
|
+
logIndexSummary,
|
|
1472
|
+
extractNextAppRoutes,
|
|
1473
|
+
extractNextPagesRoutes,
|
|
1474
|
+
extractClientRefs,
|
|
1475
|
+
extractFastifyRoutes,
|
|
1476
|
+
detectFastifyEntries: detectFastifyEntriesV2,
|
|
1477
|
+
buildEnvTruthV2,
|
|
1478
|
+
buildBillingTruthV2,
|
|
1479
|
+
buildAuthTruthV2,
|
|
1480
|
+
buildEnforcementTruthV2,
|
|
1481
|
+
extractExpressRoutes,
|
|
1482
|
+
} = require("./engine");
|
|
1483
|
+
|
|
1484
|
+
const startTime = Date.now();
|
|
1485
|
+
|
|
1486
|
+
// Phase 0: Build RepoIndex (single glob pass)
|
|
1487
|
+
const index = await createIndex(repoRoot);
|
|
1488
|
+
|
|
1489
|
+
if (verbose || process.env.VIBECHECK_VERBOSE) {
|
|
1490
|
+
logIndexSummary(index);
|
|
1491
|
+
console.log(` AST Cache: ${globalASTCache.getSummary().hitRate} hit rate`);
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
const stats = {
|
|
1495
|
+
parseErrors: 0,
|
|
1496
|
+
fastifyEntries: 0,
|
|
1497
|
+
fastifyRoutes: 0,
|
|
1498
|
+
nextAppRoutes: 0,
|
|
1499
|
+
nextPagesRoutes: 0,
|
|
1500
|
+
clientRefs: 0,
|
|
1501
|
+
serverRoutes: 0,
|
|
1502
|
+
gaps: 0,
|
|
1503
|
+
indexTimeMs: index.stats.indexTimeMs,
|
|
1504
|
+
};
|
|
1505
|
+
|
|
1506
|
+
// Use signals from index instead of re-detecting
|
|
1507
|
+
const detectedFrameworks = Array.from(index.signals.detectedFrameworks);
|
|
1508
|
+
|
|
1509
|
+
// Workspaces
|
|
1510
|
+
const workspaces = detectWorkspaces(repoRoot);
|
|
1511
|
+
|
|
1512
|
+
// Phase 1: Extract routes using optimized extractors (use RepoIndex + AST cache)
|
|
651
1513
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1514
|
+
// Next.js routes - use optimized extractors
|
|
1515
|
+
const nextApp = extractNextAppRoutes(index, stats);
|
|
1516
|
+
const nextPages = extractNextPagesRoutes(index, stats);
|
|
1517
|
+
|
|
1518
|
+
stats.nextAppRoutes = nextApp.length;
|
|
1519
|
+
stats.nextPagesRoutes = nextPages.length;
|
|
1520
|
+
|
|
1521
|
+
// Fastify routes - use optimized extractor
|
|
1522
|
+
let fastify = { routes: [], gaps: [] };
|
|
1523
|
+
|
|
1524
|
+
if (index.hasFramework("fastify") || fastifyEntry) {
|
|
1525
|
+
if (fastifyEntry) {
|
|
1526
|
+
const entryAbs = path.isAbsolute(fastifyEntry) ? fastifyEntry : path.join(repoRoot, fastifyEntry);
|
|
1527
|
+
if (exists(entryAbs)) {
|
|
1528
|
+
const resolved = extractFastifyRoutes(index, entryAbs, stats);
|
|
1529
|
+
fastify.routes.push(...resolved.routes);
|
|
1530
|
+
fastify.gaps.push(...resolved.gaps);
|
|
1531
|
+
stats.fastifyEntries = 1;
|
|
1532
|
+
}
|
|
1533
|
+
} else {
|
|
1534
|
+
const entries = detectFastifyEntriesV2(index);
|
|
1535
|
+
stats.fastifyEntries = entries.length;
|
|
1536
|
+
|
|
1537
|
+
for (const entryAbs of entries) {
|
|
1538
|
+
const resolved = extractFastifyRoutes(index, entryAbs, stats);
|
|
1539
|
+
fastify.routes.push(...resolved.routes);
|
|
1540
|
+
fastify.gaps.push(...resolved.gaps);
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
657
1543
|
}
|
|
1544
|
+
|
|
1545
|
+
stats.fastifyRoutes = fastify.routes.length;
|
|
1546
|
+
|
|
1547
|
+
// Express routes - use optimized extractor
|
|
1548
|
+
const expressRoutes = extractExpressRoutes(index, stats);
|
|
1549
|
+
|
|
1550
|
+
// Multi-framework routes (Flask, Django, etc. - still uses old resolvers for non-JS frameworks)
|
|
1551
|
+
// Express is handled above via optimized extractor
|
|
1552
|
+
const multiFramework = await resolveAllRoutes(repoRoot, {
|
|
1553
|
+
mode: process.env.VIBECHECK_ROUTE_SCAN_MODE || "smart",
|
|
1554
|
+
verbose: verbose || !!process.env.VIBECHECK_VERBOSE_ROUTES,
|
|
1555
|
+
skipExpress: true, // Skip Express since we handle it above with optimized extractor
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
// Client refs - use optimized extractor
|
|
1559
|
+
const clientRefsOptimized = extractClientRefs(index, stats);
|
|
1560
|
+
const allClientRefs = [...clientRefsOptimized, ...(multiFramework.clientRefs || [])];
|
|
1561
|
+
|
|
1562
|
+
stats.clientRefs = allClientRefs.length;
|
|
1563
|
+
|
|
1564
|
+
// Merge server routes (including optimized Express routes)
|
|
1565
|
+
const serverRoutesRaw = [...nextApp, ...nextPages, ...(fastify.routes || []), ...expressRoutes, ...(multiFramework.routes || [])];
|
|
1566
|
+
|
|
1567
|
+
const bestByKey = new Map();
|
|
1568
|
+
for (const r of serverRoutesRaw) {
|
|
1569
|
+
const key = `${canonicalizeMethod(r.method)}:${canonicalizePath(r.path)}`;
|
|
1570
|
+
const prev = bestByKey.get(key);
|
|
1571
|
+
if (!prev) {
|
|
1572
|
+
bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
|
|
1573
|
+
continue;
|
|
1574
|
+
}
|
|
1575
|
+
const prevScore = scoreConfidence(prev.confidence) + (prev.method === "*" ? 0 : 1);
|
|
1576
|
+
const curScore = scoreConfidence(r.confidence) + (r.method === "*" ? 0 : 1);
|
|
1577
|
+
if (curScore > prevScore) {
|
|
1578
|
+
bestByKey.set(key, { ...r, method: canonicalizeMethod(r.method), path: canonicalizePath(r.path) });
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
const server = Array.from(bestByKey.values());
|
|
1583
|
+
stats.serverRoutes = server.length;
|
|
1584
|
+
|
|
1585
|
+
// Merge gaps
|
|
1586
|
+
const allGaps = [...(fastify.gaps || []), ...(multiFramework.gaps || [])];
|
|
1587
|
+
stats.gaps = allGaps.length;
|
|
1588
|
+
|
|
1589
|
+
// Phase 2: Build other truths (env, auth, billing, enforcement)
|
|
1590
|
+
// All use optimized extractors with RepoIndex
|
|
1591
|
+
const env = buildEnvTruthV2(index, stats);
|
|
1592
|
+
const auth = buildAuthTruthV2(index, server);
|
|
1593
|
+
const billing = buildBillingTruthV2(index, stats);
|
|
1594
|
+
const enforcement = buildEnforcementTruthV2(index, server);
|
|
1595
|
+
|
|
1596
|
+
// Determine frameworks
|
|
1597
|
+
const frameworks = new Set(detectedFrameworks);
|
|
1598
|
+
server.forEach((r) => r.framework && frameworks.add(r.framework));
|
|
1599
|
+
if (nextApp.length || nextPages.length) frameworks.add("next");
|
|
1600
|
+
if (fastify.routes.length) frameworks.add("fastify");
|
|
1601
|
+
|
|
1602
|
+
stats.totalTimeMs = Date.now() - startTime;
|
|
1603
|
+
|
|
1604
|
+
const truthpack = {
|
|
1605
|
+
meta: {
|
|
1606
|
+
version: "2.2.0", // Bump version for v2 engine
|
|
1607
|
+
generatedAt: new Date().toISOString(),
|
|
1608
|
+
repoRoot,
|
|
1609
|
+
commit: { sha: process.env.VIBECHECK_COMMIT_SHA || "unknown" },
|
|
1610
|
+
stats,
|
|
1611
|
+
engine: "v2", // Mark as v2 engine
|
|
1612
|
+
},
|
|
1613
|
+
project: {
|
|
1614
|
+
frameworks: Array.from(frameworks),
|
|
1615
|
+
workspaces,
|
|
1616
|
+
entrypoints: {
|
|
1617
|
+
fastify: fastifyEntry ? [fastifyEntry] : [],
|
|
1618
|
+
},
|
|
1619
|
+
},
|
|
1620
|
+
routes: { server, clientRefs: allClientRefs, gaps: allGaps },
|
|
1621
|
+
env,
|
|
1622
|
+
auth,
|
|
1623
|
+
billing,
|
|
1624
|
+
enforcement,
|
|
1625
|
+
};
|
|
1626
|
+
|
|
1627
|
+
const hash = sha256(JSON.stringify(truthpack));
|
|
1628
|
+
truthpack.index = {
|
|
1629
|
+
hashes: { truthpackHash: hash },
|
|
1630
|
+
evidenceRefs: [],
|
|
1631
|
+
repoIndex: {
|
|
1632
|
+
totalFiles: index.stats.totalFiles,
|
|
1633
|
+
totalSize: index.stats.totalSize,
|
|
1634
|
+
indexTimeMs: index.stats.indexTimeMs,
|
|
1635
|
+
},
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
// Clear caches to free memory
|
|
1639
|
+
index.clearContentCache();
|
|
1640
|
+
clearCache();
|
|
1641
|
+
|
|
1642
|
+
return truthpack;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
/**
|
|
1646
|
+
* Smart buildTruthpack - uses v2 engine by default for better performance.
|
|
1647
|
+
* Set VIBECHECK_ENGINE_V1=1 to fall back to v1 for backward compatibility.
|
|
1648
|
+
*
|
|
1649
|
+
* V2 Engine Benefits:
|
|
1650
|
+
* - Single-pass file indexing (RepoIndex)
|
|
1651
|
+
* - Shared AST cache (globalASTCache)
|
|
1652
|
+
* - Token prefiltering for faster file selection
|
|
1653
|
+
* - ~30% faster on typical projects
|
|
1654
|
+
*/
|
|
1655
|
+
async function buildTruthpackSmart(options) {
|
|
1656
|
+
if (process.env.VIBECHECK_ENGINE_V1 === "1") {
|
|
1657
|
+
return buildTruthpack(options);
|
|
1658
|
+
}
|
|
1659
|
+
// V2 is now the default - uses RepoIndex + AST cache
|
|
1660
|
+
return buildTruthpackV2(options);
|
|
658
1661
|
}
|
|
659
1662
|
|
|
660
1663
|
module.exports = {
|
|
661
1664
|
canonicalizeMethod,
|
|
662
1665
|
canonicalizePath,
|
|
663
1666
|
buildTruthpack,
|
|
1667
|
+
buildTruthpackV2,
|
|
1668
|
+
buildTruthpackSmart,
|
|
664
1669
|
writeTruthpack,
|
|
665
1670
|
loadTruthpack,
|
|
666
|
-
|
|
1671
|
+
clearCache, // Clear file cache to free memory (important for long-running processes)
|
|
1672
|
+
// kept for backward compatibility if other code imports it,
|
|
1673
|
+
// but fastifyEntry is now optional and auto-detected.
|
|
1674
|
+
detectFastifyEntry: function detectFastifyEntry(repoRoot) {
|
|
1675
|
+
const candidates = [
|
|
1676
|
+
"src/server.ts",
|
|
1677
|
+
"src/server.js",
|
|
1678
|
+
"server.ts",
|
|
1679
|
+
"server.js",
|
|
1680
|
+
"src/index.ts",
|
|
1681
|
+
"src/index.js",
|
|
1682
|
+
"index.ts",
|
|
1683
|
+
"index.js",
|
|
1684
|
+
];
|
|
1685
|
+
for (const rel of candidates) {
|
|
1686
|
+
const abs = path.join(repoRoot, rel);
|
|
1687
|
+
if (exists(abs)) return rel;
|
|
1688
|
+
}
|
|
1689
|
+
return null;
|
|
1690
|
+
},
|
|
667
1691
|
};
|