@vibecheckai/cli 3.2.0 → 3.2.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/runners/lib/agent-firewall/change-packet/builder.js +214 -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 +214 -0
- package/bin/runners/lib/agent-firewall/claims/patterns.js +24 -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 +118 -0
- package/bin/runners/lib/agent-firewall/evidence/resolver.js +102 -0
- package/bin/runners/lib/agent-firewall/evidence/route-evidence.js +142 -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/policy/default-policy.json +84 -0
- package/bin/runners/lib/agent-firewall/policy/engine.js +72 -0
- package/bin/runners/lib/agent-firewall/policy/loader.js +143 -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 +61 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +50 -0
- package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +50 -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/truthpack/index.js +67 -0
- package/bin/runners/lib/agent-firewall/truthpack/loader.js +116 -0
- package/bin/runners/lib/agent-firewall/unblock/planner.js +337 -0
- package/bin/runners/lib/analysis-core.js +198 -180
- package/bin/runners/lib/analyzers.js +1119 -536
- package/bin/runners/lib/cli-output.js +236 -210
- package/bin/runners/lib/detectors-v2.js +547 -785
- package/bin/runners/lib/fingerprint.js +377 -0
- package/bin/runners/lib/route-truth.js +1167 -322
- package/bin/runners/lib/scan-output.js +144 -738
- package/bin/runners/lib/ship-output-enterprise.js +239 -0
- package/bin/runners/lib/terminal-ui.js +188 -770
- package/bin/runners/lib/truth.js +1004 -321
- package/bin/runners/lib/unified-output.js +162 -158
- package/bin/runners/runAgent.js +161 -0
- package/bin/runners/runFirewall.js +134 -0
- package/bin/runners/runFirewallHook.js +56 -0
- package/bin/runners/runScan.js +113 -10
- package/bin/runners/runShip.js +7 -8
- package/bin/runners/runTruth.js +89 -0
- package/mcp-server/agent-firewall-interceptor.js +164 -0
- package/mcp-server/index.js +347 -313
- package/mcp-server/truth-context.js +131 -90
- package/mcp-server/truth-firewall-tools.js +1412 -1045
- package/package.json +1 -1
|
@@ -1,322 +1,1155 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Route Truth v1 - JavaScript Runtime
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* - Next.js
|
|
6
|
-
* - Fastify (
|
|
7
|
-
*
|
|
8
|
-
*
|
|
2
|
+
* Route Truth v1 - JavaScript Runtime (hardened + more accurate)
|
|
3
|
+
*
|
|
4
|
+
* Upgrades vs your version:
|
|
5
|
+
* - Next.js App/Pages routes: AST-based export detection + safer path derivation
|
|
6
|
+
* - Fastify routes: AST-based extraction (get/post/route/register + inline plugins + relative import resolution)
|
|
7
|
+
* - Better canonicalization + prefix joining + safer matching (no weird edge crashes)
|
|
8
|
+
* - Gaps are real (unresolved plugins/modules) instead of silent “false”
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
const fs = require(
|
|
12
|
-
const path = require(
|
|
13
|
-
const crypto = require(
|
|
11
|
+
const fs = require("fs");
|
|
12
|
+
const path = require("path");
|
|
13
|
+
const crypto = require("crypto");
|
|
14
|
+
|
|
15
|
+
let fg = null;
|
|
16
|
+
try {
|
|
17
|
+
fg = require("fast-glob");
|
|
18
|
+
} catch { /* optional */ }
|
|
19
|
+
|
|
20
|
+
const parser = require("@babel/parser");
|
|
21
|
+
const traverse = require("@babel/traverse").default;
|
|
22
|
+
const t = require("@babel/types");
|
|
14
23
|
|
|
15
24
|
// ============================================================================
|
|
16
|
-
// CANONICALIZATION
|
|
25
|
+
// CANONICALIZATION + MATCHING
|
|
17
26
|
// ============================================================================
|
|
18
27
|
|
|
19
|
-
/**
|
|
20
|
-
* Canonicalize a path to standard format.
|
|
21
|
-
*/
|
|
22
28
|
function canonicalizePath(p) {
|
|
23
|
-
let s = p.trim();
|
|
24
|
-
if (!s
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
s = s.replace(/\[\.{3}([^\]]+)\]/g,
|
|
30
|
-
s = s.replace(/\[([^\]]+)\]/g,
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
let s = String(p || "").trim();
|
|
30
|
+
if (!s) return "/";
|
|
31
|
+
if (!s.startsWith("/")) s = "/" + s;
|
|
32
|
+
s = s.replace(/\/+/g, "/");
|
|
33
|
+
|
|
34
|
+
// Next.js dynamic segments
|
|
35
|
+
s = s.replace(/\[\[\.{3}([^\]]+)\]\]/g, "*$1?"); // [[...slug]] -> *slug?
|
|
36
|
+
s = s.replace(/\[\.{3}([^\]]+)\]/g, "*$1"); // [...slug] -> *slug
|
|
37
|
+
s = s.replace(/\[([^\]]+)\]/g, ":$1"); // [id] -> :id
|
|
38
|
+
|
|
39
|
+
if (s.length > 1) s = s.replace(/\/$/, "");
|
|
33
40
|
return s;
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
function canonicalizeMethod(m) {
|
|
37
|
-
const u = m.toUpperCase();
|
|
38
|
-
if (u ===
|
|
39
|
-
return u;
|
|
44
|
+
const u = String(m || "").toUpperCase();
|
|
45
|
+
if (u === "ALL" || u === "ANY" || u === "*" ) return "*";
|
|
46
|
+
return u || "*";
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
function joinPrefix(prefix, p) {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
50
|
+
const a = canonicalizePath(prefix || "/");
|
|
51
|
+
const b = canonicalizePath(p || "/");
|
|
52
|
+
if (a === "/") return b;
|
|
53
|
+
if (b === "/") return a;
|
|
54
|
+
return canonicalizePath(a + "/" + b);
|
|
46
55
|
}
|
|
47
56
|
|
|
48
|
-
function isParameterizedPath(
|
|
49
|
-
|
|
57
|
+
function isParameterizedPath(p) {
|
|
58
|
+
const s = canonicalizePath(p);
|
|
59
|
+
return s.includes(":") || s.includes("*");
|
|
50
60
|
}
|
|
51
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Match a pattern against a concrete path.
|
|
64
|
+
* Supported:
|
|
65
|
+
* - :id matches one segment
|
|
66
|
+
* - *slug or *slug? matches the rest of the path (0+ segments)
|
|
67
|
+
*/
|
|
52
68
|
function matchPath(pattern, concrete) {
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
|
|
69
|
+
const pat = canonicalizePath(pattern);
|
|
70
|
+
const con = canonicalizePath(concrete);
|
|
71
|
+
|
|
72
|
+
if (pat === con) return true;
|
|
73
|
+
|
|
74
|
+
const pParts = pat.split("/").filter(Boolean);
|
|
75
|
+
const cParts = con.split("/").filter(Boolean);
|
|
76
|
+
|
|
56
77
|
let pIdx = 0, cIdx = 0;
|
|
57
|
-
|
|
58
|
-
while (pIdx <
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
|
|
78
|
+
|
|
79
|
+
while (pIdx < pParts.length && cIdx < cParts.length) {
|
|
80
|
+
const pSeg = pParts[pIdx];
|
|
81
|
+
const cSeg = cParts[cIdx];
|
|
82
|
+
|
|
83
|
+
if (pSeg.startsWith("*")) {
|
|
84
|
+
// splat: match remainder (including empty remainder)
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
if (pSeg.startsWith(":")) {
|
|
88
|
+
pIdx++; cIdx++; continue;
|
|
89
|
+
}
|
|
90
|
+
if (pSeg !== cSeg) return false;
|
|
91
|
+
|
|
65
92
|
pIdx++; cIdx++;
|
|
66
93
|
}
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
|
|
95
|
+
// If pattern still has a trailing splat, it matches empty remainder
|
|
96
|
+
if (pIdx === pParts.length - 1 && pParts[pIdx]?.startsWith("*")) return true;
|
|
97
|
+
|
|
98
|
+
return pIdx === pParts.length && cIdx === cParts.length;
|
|
69
99
|
}
|
|
70
100
|
|
|
71
101
|
function matchMethod(pattern, concrete) {
|
|
72
|
-
|
|
73
|
-
|
|
102
|
+
const p = canonicalizeMethod(pattern);
|
|
103
|
+
const c = canonicalizeMethod(concrete);
|
|
104
|
+
if (p === "*") return true;
|
|
105
|
+
return p === c;
|
|
74
106
|
}
|
|
75
107
|
|
|
76
108
|
// ============================================================================
|
|
77
|
-
//
|
|
109
|
+
// COMMON HELPERS
|
|
78
110
|
// ============================================================================
|
|
79
111
|
|
|
80
|
-
const NEXT_HTTP_METHODS = [
|
|
112
|
+
const NEXT_HTTP_METHODS = new Set(["GET","POST","PUT","PATCH","DELETE","OPTIONS","HEAD"]);
|
|
81
113
|
let evidenceCounter = 0;
|
|
82
114
|
|
|
115
|
+
function sha256Short(txt) {
|
|
116
|
+
return crypto.createHash("sha256").update(String(txt || "")).digest("hex").slice(0, 16);
|
|
117
|
+
}
|
|
118
|
+
|
|
83
119
|
function createEvidence(file, lines, reason, snippet) {
|
|
84
120
|
evidenceCounter++;
|
|
85
121
|
return {
|
|
86
|
-
id: `ev_${String(evidenceCounter).padStart(4,
|
|
122
|
+
id: `ev_${String(evidenceCounter).padStart(4, "0")}`,
|
|
87
123
|
file,
|
|
88
124
|
lines,
|
|
89
|
-
snippetHash: `sha256:${
|
|
125
|
+
snippetHash: `sha256:${sha256Short(snippet || "")}`,
|
|
90
126
|
reason,
|
|
91
127
|
};
|
|
92
128
|
}
|
|
93
129
|
|
|
94
|
-
function
|
|
95
|
-
|
|
96
|
-
|
|
130
|
+
function safeRead(fileAbs) {
|
|
131
|
+
try { return fs.readFileSync(fileAbs, "utf8"); } catch { return null; }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function parseFile(code) {
|
|
135
|
+
return parser.parse(code, {
|
|
136
|
+
sourceType: "unambiguous",
|
|
137
|
+
plugins: ["typescript", "jsx"],
|
|
138
|
+
errorRecovery: true,
|
|
139
|
+
ranges: false,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function evidenceFromLoc({ fileAbs, fileRel, loc, reason }) {
|
|
144
|
+
if (!loc || !loc.start) return [];
|
|
145
|
+
const code = safeRead(fileAbs);
|
|
146
|
+
if (!code) return [];
|
|
147
|
+
const lines = code.split(/\r?\n/);
|
|
148
|
+
const start = Math.max(1, loc.start.line || 1);
|
|
149
|
+
const end = Math.max(start, loc.end?.line || start);
|
|
150
|
+
const snippet = lines.slice(start - 1, end).join("\n");
|
|
151
|
+
return [createEvidence(fileRel, `${start}-${end}`, reason, snippet)];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function findFilesFallback(dirAbs, includeRe, excludeRe) {
|
|
155
|
+
const out = [];
|
|
97
156
|
function walk(d) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
157
|
+
let entries;
|
|
158
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
159
|
+
for (const ent of entries) {
|
|
160
|
+
const full = path.join(d, ent.name);
|
|
161
|
+
if (ent.isDirectory()) {
|
|
162
|
+
if (
|
|
163
|
+
ent.name === "node_modules" ||
|
|
164
|
+
ent.name === ".next" ||
|
|
165
|
+
ent.name === "dist" ||
|
|
166
|
+
ent.name === "build" ||
|
|
167
|
+
ent.name === "coverage" ||
|
|
168
|
+
ent.name.startsWith(".")
|
|
169
|
+
) continue;
|
|
170
|
+
walk(full);
|
|
171
|
+
} else if (ent.isFile()) {
|
|
172
|
+
if (excludeRe && excludeRe.test(ent.name)) continue;
|
|
173
|
+
if (includeRe.test(ent.name)) out.push(full);
|
|
111
174
|
}
|
|
112
|
-
}
|
|
175
|
+
}
|
|
113
176
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
return files;
|
|
177
|
+
walk(dirAbs);
|
|
178
|
+
return out;
|
|
117
179
|
}
|
|
118
180
|
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
181
|
+
async function globFiles(repoRoot, patterns, ignore) {
|
|
182
|
+
if (fg) {
|
|
183
|
+
return fg(patterns, {
|
|
184
|
+
cwd: repoRoot,
|
|
185
|
+
absolute: true,
|
|
186
|
+
dot: false,
|
|
187
|
+
ignore: ignore || [
|
|
188
|
+
"**/node_modules/**",
|
|
189
|
+
"**/.next/**",
|
|
190
|
+
"**/dist/**",
|
|
191
|
+
"**/build/**",
|
|
192
|
+
"**/coverage/**",
|
|
193
|
+
],
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// fallback: only supports the specific Next patterns we use
|
|
198
|
+
const out = [];
|
|
199
|
+
for (const ptn of patterns) {
|
|
200
|
+
// minimal handling: find root dirs from patterns
|
|
201
|
+
if (ptn.includes("app/api") || ptn.includes("src/app/api")) {
|
|
202
|
+
const dir1 = path.join(repoRoot, "app", "api");
|
|
203
|
+
const dir2 = path.join(repoRoot, "src", "app", "api");
|
|
204
|
+
if (fs.existsSync(dir1)) out.push(...findFilesFallback(dir1, /route\.(ts|js)$/));
|
|
205
|
+
if (fs.existsSync(dir2)) out.push(...findFilesFallback(dir2, /route\.(ts|js)$/));
|
|
206
|
+
}
|
|
207
|
+
if (ptn.includes("pages/api") || ptn.includes("src/pages/api")) {
|
|
208
|
+
const dir1 = path.join(repoRoot, "pages", "api");
|
|
209
|
+
const dir2 = path.join(repoRoot, "src", "pages", "api");
|
|
210
|
+
if (fs.existsSync(dir1)) out.push(...findFilesFallback(dir1, /\.(ts|js)$/, /\.d\.ts$/));
|
|
211
|
+
if (fs.existsSync(dir2)) out.push(...findFilesFallback(dir2, /\.(ts|js)$/, /\.d\.ts$/));
|
|
138
212
|
}
|
|
139
213
|
}
|
|
140
|
-
|
|
214
|
+
return Array.from(new Set(out));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ============================================================================
|
|
218
|
+
// NEXT.JS RESOLVER (AST)
|
|
219
|
+
// ============================================================================
|
|
220
|
+
|
|
221
|
+
function extractNextAppRouterMethodsAST(ast) {
|
|
222
|
+
const methods = [];
|
|
223
|
+
traverse(ast, {
|
|
224
|
+
ExportNamedDeclaration(p) {
|
|
225
|
+
const decl = p.node.declaration;
|
|
226
|
+
|
|
227
|
+
// export function GET() {}
|
|
228
|
+
if (t.isFunctionDeclaration(decl) && decl.id?.name) {
|
|
229
|
+
const n = decl.id.name.toUpperCase();
|
|
230
|
+
if (NEXT_HTTP_METHODS.has(n)) {
|
|
231
|
+
methods.push({ name: n, loc: decl.loc });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// export const GET = () => {}
|
|
236
|
+
if (t.isVariableDeclaration(decl)) {
|
|
237
|
+
for (const d of decl.declarations) {
|
|
238
|
+
if (!t.isIdentifier(d.id)) continue;
|
|
239
|
+
const n = d.id.name.toUpperCase();
|
|
240
|
+
if (NEXT_HTTP_METHODS.has(n)) {
|
|
241
|
+
methods.push({ name: n, loc: d.loc || decl.loc });
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
});
|
|
141
247
|
return methods;
|
|
142
248
|
}
|
|
143
249
|
|
|
250
|
+
function deriveNextAppRoutePath(fileRel) {
|
|
251
|
+
// matches: app/api/**/route.ts|js OR src/app/api/**/route.ts|js
|
|
252
|
+
const m = fileRel.match(/(?:^|\/)(?:src\/)?app\/api\/(.+)\/route\.(ts|js)$/);
|
|
253
|
+
if (!m) return null;
|
|
254
|
+
const sub = m[1];
|
|
255
|
+
return canonicalizePath("/api/" + sub);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function deriveNextPagesRoutePath(fileRel) {
|
|
259
|
+
// matches: pages/api/**.ts|js OR src/pages/api/**.ts|js
|
|
260
|
+
const m = fileRel.match(/(?:^|\/)(?:src\/)?pages\/api\/(.+)\.(ts|js)$/);
|
|
261
|
+
if (!m) return null;
|
|
262
|
+
let sub = m[1];
|
|
263
|
+
sub = sub.replace(/\/index$/, ""); // /foo/index -> /foo
|
|
264
|
+
return canonicalizePath("/api/" + sub);
|
|
265
|
+
}
|
|
266
|
+
|
|
144
267
|
async function resolveNextRoutes(repoRoot) {
|
|
145
268
|
const routes = [];
|
|
146
|
-
|
|
147
|
-
// App Router
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
for (const m of methods) {
|
|
178
|
-
routes.push({
|
|
179
|
-
method: m.name,
|
|
180
|
-
path: routePath,
|
|
181
|
-
handler: `${relPath}:${m.line}`,
|
|
182
|
-
framework: 'next',
|
|
183
|
-
routerType: 'app',
|
|
184
|
-
confidence: 'high',
|
|
185
|
-
evidence: [createEvidence(relPath, String(m.line), `export ${m.name}`, m.snippet)],
|
|
186
|
-
});
|
|
187
|
-
}
|
|
269
|
+
|
|
270
|
+
// App Router
|
|
271
|
+
const appFiles = await globFiles(repoRoot, [
|
|
272
|
+
"**/app/api/**/route.@(ts|js)",
|
|
273
|
+
"**/src/app/api/**/route.@(ts|js)",
|
|
274
|
+
]);
|
|
275
|
+
|
|
276
|
+
for (const fileAbs of appFiles) {
|
|
277
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
278
|
+
const routePath = deriveNextAppRoutePath(fileRel);
|
|
279
|
+
if (!routePath) continue;
|
|
280
|
+
|
|
281
|
+
const code = safeRead(fileAbs);
|
|
282
|
+
if (!code) continue;
|
|
283
|
+
|
|
284
|
+
let ast;
|
|
285
|
+
try { ast = parseFile(code); } catch { continue; }
|
|
286
|
+
|
|
287
|
+
const methods = extractNextAppRouterMethodsAST(ast);
|
|
288
|
+
|
|
289
|
+
if (methods.length === 0) {
|
|
290
|
+
routes.push({
|
|
291
|
+
method: "*",
|
|
292
|
+
path: routePath,
|
|
293
|
+
handler: fileRel,
|
|
294
|
+
framework: "next",
|
|
295
|
+
routerType: "app",
|
|
296
|
+
confidence: "low",
|
|
297
|
+
evidence: [createEvidence(fileRel, "1", "route file with no method exports", code.slice(0, 140))],
|
|
298
|
+
});
|
|
299
|
+
continue;
|
|
188
300
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
// Pages Router: pages/api/**/*.ts|js
|
|
192
|
-
const pagesDirs = ['pages', 'src/pages'];
|
|
193
|
-
for (const pagesDir of pagesDirs) {
|
|
194
|
-
const apiDir = path.join(repoRoot, pagesDir, 'api');
|
|
195
|
-
if (!fs.existsSync(apiDir)) continue;
|
|
196
|
-
|
|
197
|
-
const apiFiles = findFiles(apiDir, /\.(ts|js)$/, /\.d\.ts$/);
|
|
198
|
-
|
|
199
|
-
for (const file of apiFiles) {
|
|
200
|
-
const relPath = path.relative(repoRoot, file).replace(/\\/g, '/');
|
|
201
|
-
const apiIdx = relPath.indexOf('/api/');
|
|
202
|
-
const sub = relPath
|
|
203
|
-
.slice(apiIdx + '/api/'.length)
|
|
204
|
-
.replace(/\.(ts|js)$/, '')
|
|
205
|
-
.replace(/\/index$/, '');
|
|
206
|
-
|
|
207
|
-
const routePath = canonicalizePath('/api/' + sub);
|
|
208
|
-
const code = fs.readFileSync(file, 'utf8');
|
|
209
|
-
const hasDefaultExport = /export\s+default/.test(code);
|
|
210
|
-
|
|
301
|
+
|
|
302
|
+
for (const m of methods) {
|
|
211
303
|
routes.push({
|
|
212
|
-
method:
|
|
304
|
+
method: m.name,
|
|
213
305
|
path: routePath,
|
|
214
|
-
handler:
|
|
215
|
-
framework:
|
|
216
|
-
routerType:
|
|
217
|
-
confidence:
|
|
218
|
-
evidence:
|
|
306
|
+
handler: fileRel,
|
|
307
|
+
framework: "next",
|
|
308
|
+
routerType: "app",
|
|
309
|
+
confidence: "high",
|
|
310
|
+
evidence: evidenceFromLoc({ fileAbs, fileRel, loc: m.loc, reason: `Next app router export ${m.name}` }),
|
|
219
311
|
});
|
|
220
312
|
}
|
|
221
313
|
}
|
|
222
|
-
|
|
314
|
+
|
|
315
|
+
// Pages Router
|
|
316
|
+
const pagesFiles = await globFiles(repoRoot, [
|
|
317
|
+
"**/pages/api/**/*.@(ts|js)",
|
|
318
|
+
"**/src/pages/api/**/*.@(ts|js)",
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
for (const fileAbs of pagesFiles) {
|
|
322
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
323
|
+
if (fileRel.endsWith(".d.ts")) continue;
|
|
324
|
+
|
|
325
|
+
const routePath = deriveNextPagesRoutePath(fileRel);
|
|
326
|
+
if (!routePath) continue;
|
|
327
|
+
|
|
328
|
+
const code = safeRead(fileAbs);
|
|
329
|
+
if (!code) continue;
|
|
330
|
+
|
|
331
|
+
const hasDefaultExport = /\bexport\s+default\b/.test(code);
|
|
332
|
+
|
|
333
|
+
routes.push({
|
|
334
|
+
method: "*",
|
|
335
|
+
path: routePath,
|
|
336
|
+
handler: fileRel,
|
|
337
|
+
framework: "next",
|
|
338
|
+
routerType: "pages",
|
|
339
|
+
confidence: hasDefaultExport ? "med" : "low",
|
|
340
|
+
evidence: [createEvidence(fileRel, "1", "Next pages API route", code.slice(0, 140))],
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
223
344
|
return routes;
|
|
224
345
|
}
|
|
225
346
|
|
|
226
347
|
// ============================================================================
|
|
227
|
-
// FASTIFY RESOLVER (
|
|
348
|
+
// FASTIFY RESOLVER (AST, follows register prefixes + relative plugin modules)
|
|
228
349
|
// ============================================================================
|
|
229
350
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
if (
|
|
248
|
-
|
|
351
|
+
const FASTIFY_METHODS = new Set(["get","post","put","patch","delete","options","head","all"]);
|
|
352
|
+
|
|
353
|
+
function existsFile(p) {
|
|
354
|
+
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function existsDir(p) {
|
|
358
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Find project root by walking up from a file until we find package.json
|
|
363
|
+
*/
|
|
364
|
+
function findProjectRoot(fromFileAbs) {
|
|
365
|
+
let dir = path.dirname(fromFileAbs);
|
|
366
|
+
const root = path.parse(dir).root;
|
|
367
|
+
while (dir !== root) {
|
|
368
|
+
if (existsFile(path.join(dir, "package.json"))) return dir;
|
|
369
|
+
const parent = path.dirname(dir);
|
|
370
|
+
if (parent === dir) break;
|
|
371
|
+
dir = parent;
|
|
372
|
+
}
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Load and cache tsconfig.json paths for a project
|
|
378
|
+
*/
|
|
379
|
+
const tsconfigCache = new Map();
|
|
380
|
+
|
|
381
|
+
function loadTsConfigPaths(projectRoot) {
|
|
382
|
+
if (!projectRoot) return null;
|
|
383
|
+
if (tsconfigCache.has(projectRoot)) return tsconfigCache.get(projectRoot);
|
|
384
|
+
|
|
385
|
+
const tsconfigPath = path.join(projectRoot, "tsconfig.json");
|
|
386
|
+
if (!existsFile(tsconfigPath)) {
|
|
387
|
+
tsconfigCache.set(projectRoot, null);
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const raw = fs.readFileSync(tsconfigPath, "utf8");
|
|
393
|
+
// Remove comments (// and /* */) for JSON parsing
|
|
394
|
+
const cleaned = raw
|
|
395
|
+
.replace(/\/\/.*$/gm, "")
|
|
396
|
+
.replace(/\/\*[\s\S]*?\*\//g, "");
|
|
397
|
+
const tsconfig = JSON.parse(cleaned);
|
|
398
|
+
|
|
399
|
+
const paths = tsconfig?.compilerOptions?.paths || {};
|
|
400
|
+
const baseUrl = tsconfig?.compilerOptions?.baseUrl || ".";
|
|
401
|
+
const baseDir = path.resolve(projectRoot, baseUrl);
|
|
402
|
+
|
|
403
|
+
const result = { paths, baseDir, projectRoot };
|
|
404
|
+
tsconfigCache.set(projectRoot, result);
|
|
405
|
+
return result;
|
|
406
|
+
} catch {
|
|
407
|
+
tsconfigCache.set(projectRoot, null);
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Resolve a module specifier using TypeScript path mappings
|
|
414
|
+
*/
|
|
415
|
+
function resolveWithTsConfigPaths(spec, tsConfig) {
|
|
416
|
+
if (!tsConfig || !tsConfig.paths) return null;
|
|
417
|
+
|
|
418
|
+
const { paths, baseDir } = tsConfig;
|
|
419
|
+
|
|
420
|
+
for (const [pattern, targets] of Object.entries(paths)) {
|
|
421
|
+
// Handle exact match: "@vibecheck/core" -> ["../../packages/core/dist"]
|
|
422
|
+
if (pattern === spec) {
|
|
423
|
+
for (const target of targets) {
|
|
424
|
+
const resolved = path.resolve(baseDir, target.replace(/\/\*$/, ""));
|
|
425
|
+
const candidates = [
|
|
426
|
+
resolved,
|
|
427
|
+
resolved + ".ts",
|
|
428
|
+
resolved + ".js",
|
|
429
|
+
path.join(resolved, "index.ts"),
|
|
430
|
+
path.join(resolved, "index.js"),
|
|
431
|
+
];
|
|
432
|
+
for (const c of candidates) if (existsFile(c)) return c;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Handle wildcard pattern: "@/*" -> ["./src/*"]
|
|
437
|
+
if (pattern.endsWith("/*")) {
|
|
438
|
+
const prefix = pattern.slice(0, -2);
|
|
439
|
+
if (spec.startsWith(prefix + "/")) {
|
|
440
|
+
const rest = spec.slice(prefix.length + 1);
|
|
441
|
+
for (const target of targets) {
|
|
442
|
+
const targetBase = target.replace(/\/\*$/, "");
|
|
443
|
+
const resolved = path.resolve(baseDir, targetBase, rest);
|
|
444
|
+
const candidates = [
|
|
445
|
+
resolved,
|
|
446
|
+
resolved + ".ts",
|
|
447
|
+
resolved + ".js",
|
|
448
|
+
path.join(resolved, "index.ts"),
|
|
449
|
+
path.join(resolved, "index.js"),
|
|
450
|
+
];
|
|
451
|
+
for (const c of candidates) if (existsFile(c)) return c;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
249
454
|
}
|
|
250
455
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
456
|
+
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Parse package.json and find the main entrypoint
|
|
462
|
+
*/
|
|
463
|
+
function getPackageEntrypoint(pkgJsonPath) {
|
|
464
|
+
try {
|
|
465
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
|
466
|
+
const pkgDir = path.dirname(pkgJsonPath);
|
|
467
|
+
|
|
468
|
+
// Priority: exports["."] > main > index.js
|
|
469
|
+
// Handle exports field (modern packages)
|
|
470
|
+
if (pkgJson.exports) {
|
|
471
|
+
const exp = pkgJson.exports;
|
|
472
|
+
|
|
473
|
+
// exports: "./lib/index.js" (string shorthand)
|
|
474
|
+
if (typeof exp === "string") {
|
|
475
|
+
const resolved = path.resolve(pkgDir, exp);
|
|
476
|
+
if (existsFile(resolved)) return resolved;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// exports: { ".": "./lib/index.js" } or { ".": { "require": "...", "import": "..." } }
|
|
480
|
+
if (typeof exp === "object" && exp["."]) {
|
|
481
|
+
const dotExport = exp["."];
|
|
482
|
+
|
|
483
|
+
if (typeof dotExport === "string") {
|
|
484
|
+
const resolved = path.resolve(pkgDir, dotExport);
|
|
485
|
+
if (existsFile(resolved)) return resolved;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Conditional exports - prefer require for CommonJS, then import, then default
|
|
489
|
+
if (typeof dotExport === "object") {
|
|
490
|
+
const conditions = ["require", "node", "import", "default"];
|
|
491
|
+
for (const cond of conditions) {
|
|
492
|
+
if (dotExport[cond]) {
|
|
493
|
+
const val = dotExport[cond];
|
|
494
|
+
const target = typeof val === "string" ? val : val?.default;
|
|
495
|
+
if (target) {
|
|
496
|
+
const resolved = path.resolve(pkgDir, target);
|
|
497
|
+
if (existsFile(resolved)) return resolved;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Fallback to main field
|
|
506
|
+
if (pkgJson.main) {
|
|
507
|
+
const resolved = path.resolve(pkgDir, pkgJson.main);
|
|
508
|
+
if (existsFile(resolved)) return resolved;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Final fallback: index.js
|
|
512
|
+
const indexJs = path.join(pkgDir, "index.js");
|
|
513
|
+
if (existsFile(indexJs)) return indexJs;
|
|
514
|
+
|
|
515
|
+
return null;
|
|
516
|
+
} catch {
|
|
517
|
+
return null;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Resolve a package specifier from node_modules
|
|
523
|
+
* Handles: "fastify", "@fastify/cors", "@vibecheck/core"
|
|
524
|
+
*/
|
|
525
|
+
function resolveFromNodeModules(spec, projectRoot) {
|
|
526
|
+
if (!projectRoot || !spec) return null;
|
|
527
|
+
|
|
528
|
+
// Don't resolve built-in Node.js modules
|
|
529
|
+
const builtins = new Set([
|
|
530
|
+
"fs", "path", "http", "https", "url", "crypto", "os", "util", "stream",
|
|
531
|
+
"events", "buffer", "querystring", "child_process", "cluster", "dgram",
|
|
532
|
+
"dns", "net", "readline", "tls", "tty", "zlib", "assert", "async_hooks",
|
|
533
|
+
"perf_hooks", "v8", "vm", "worker_threads", "module", "process"
|
|
534
|
+
]);
|
|
535
|
+
|
|
536
|
+
const pkgName = spec.startsWith("@")
|
|
537
|
+
? spec.split("/").slice(0, 2).join("/") // @scope/name
|
|
538
|
+
: spec.split("/")[0]; // name
|
|
539
|
+
|
|
540
|
+
if (builtins.has(pkgName)) return null;
|
|
541
|
+
|
|
542
|
+
// Walk up directory tree looking for node_modules
|
|
543
|
+
let searchDir = projectRoot;
|
|
544
|
+
const root = path.parse(searchDir).root;
|
|
545
|
+
|
|
546
|
+
while (searchDir !== root) {
|
|
547
|
+
const nodeModulesDir = path.join(searchDir, "node_modules");
|
|
548
|
+
|
|
549
|
+
if (existsDir(nodeModulesDir)) {
|
|
550
|
+
const pkgDir = path.join(nodeModulesDir, pkgName);
|
|
551
|
+
|
|
552
|
+
if (existsDir(pkgDir)) {
|
|
553
|
+
const pkgJsonPath = path.join(pkgDir, "package.json");
|
|
554
|
+
|
|
555
|
+
if (existsFile(pkgJsonPath)) {
|
|
556
|
+
// Check if spec has a subpath: "@fastify/cors/lib/foo"
|
|
557
|
+
const subpath = spec.slice(pkgName.length);
|
|
558
|
+
|
|
559
|
+
if (subpath && subpath !== "/") {
|
|
560
|
+
// Resolve subpath within the package
|
|
561
|
+
const subpathResolved = path.join(pkgDir, subpath);
|
|
562
|
+
const candidates = [
|
|
563
|
+
subpathResolved,
|
|
564
|
+
subpathResolved + ".js",
|
|
565
|
+
subpathResolved + ".ts",
|
|
566
|
+
path.join(subpathResolved, "index.js"),
|
|
567
|
+
path.join(subpathResolved, "index.ts"),
|
|
568
|
+
];
|
|
569
|
+
for (const c of candidates) if (existsFile(c)) return c;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Resolve main entrypoint
|
|
573
|
+
const entry = getPackageEntrypoint(pkgJsonPath);
|
|
574
|
+
if (entry) return entry;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const parent = path.dirname(searchDir);
|
|
580
|
+
if (parent === searchDir) break;
|
|
581
|
+
searchDir = parent;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Check if a package is a "non-route" Fastify plugin
|
|
589
|
+
* These plugins add functionality but don't define routes themselves
|
|
590
|
+
*/
|
|
591
|
+
function isNonRoutePlugin(spec) {
|
|
592
|
+
const nonRoutePlugins = new Set([
|
|
593
|
+
// @fastify/* scoped plugins
|
|
594
|
+
"@fastify/cors",
|
|
595
|
+
"@fastify/helmet",
|
|
596
|
+
"@fastify/compress",
|
|
597
|
+
"@fastify/cookie",
|
|
598
|
+
"@fastify/secure-session",
|
|
599
|
+
"@fastify/session",
|
|
600
|
+
"@fastify/rate-limit",
|
|
601
|
+
"@fastify/jwt",
|
|
602
|
+
"@fastify/auth",
|
|
603
|
+
"@fastify/bearer-auth",
|
|
604
|
+
"@fastify/basic-auth",
|
|
605
|
+
"@fastify/multipart",
|
|
606
|
+
"@fastify/formbody",
|
|
607
|
+
"@fastify/static",
|
|
608
|
+
"@fastify/view",
|
|
609
|
+
"@fastify/sensible",
|
|
610
|
+
"@fastify/env",
|
|
611
|
+
"@fastify/accepts",
|
|
612
|
+
"@fastify/caching",
|
|
613
|
+
"@fastify/etag",
|
|
614
|
+
"@fastify/circuit-breaker",
|
|
615
|
+
"@fastify/response-validation",
|
|
616
|
+
"@fastify/request-context",
|
|
617
|
+
"@fastify/under-pressure",
|
|
618
|
+
"@fastify/middie",
|
|
619
|
+
"@fastify/express",
|
|
620
|
+
"@fastify/http-proxy",
|
|
621
|
+
"@fastify/reply-from",
|
|
622
|
+
"@fastify/websocket",
|
|
623
|
+
"@fastify/type-provider-json-schema-to-ts",
|
|
624
|
+
"@fastify/type-provider-typebox",
|
|
625
|
+
"@fastify/type-provider-zod",
|
|
626
|
+
"@fastify/mongodb",
|
|
627
|
+
"@fastify/postgres",
|
|
628
|
+
"@fastify/mysql",
|
|
629
|
+
"@fastify/redis",
|
|
630
|
+
"@fastify/leveldb",
|
|
631
|
+
"@fastify/elasticsearch",
|
|
632
|
+
"@fastify/metrics",
|
|
633
|
+
"@fastify/request-id",
|
|
634
|
+
// Legacy fastify-* plugins
|
|
635
|
+
"fastify-plugin",
|
|
636
|
+
"fastify-cors",
|
|
637
|
+
"fastify-helmet",
|
|
638
|
+
"fastify-compress",
|
|
639
|
+
"fastify-cookie",
|
|
640
|
+
"fastify-session",
|
|
641
|
+
"fastify-rate-limit",
|
|
642
|
+
"fastify-jwt",
|
|
643
|
+
"fastify-auth",
|
|
644
|
+
"fastify-sensible",
|
|
645
|
+
"fastify-multipart",
|
|
646
|
+
"fastify-formbody",
|
|
647
|
+
"fastify-static",
|
|
648
|
+
"fastify-websocket",
|
|
649
|
+
]);
|
|
650
|
+
|
|
651
|
+
const pkgName = spec.startsWith("@")
|
|
652
|
+
? spec.split("/").slice(0, 2).join("/")
|
|
653
|
+
: spec.split("/")[0];
|
|
654
|
+
|
|
655
|
+
return nonRoutePlugins.has(pkgName);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Check if this is @fastify/autoload which needs special directory handling
|
|
660
|
+
*/
|
|
661
|
+
function isAutoloadPlugin(spec) {
|
|
662
|
+
return spec === "@fastify/autoload" || spec === "fastify-autoload";
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function resolveRelativeModule(fromFileAbs, spec) {
|
|
666
|
+
if (!spec || (!spec.startsWith("./") && !spec.startsWith("../"))) return null;
|
|
667
|
+
const base = path.resolve(path.dirname(fromFileAbs), spec);
|
|
668
|
+
const candidates = [
|
|
669
|
+
base,
|
|
670
|
+
base + ".ts",
|
|
671
|
+
base + ".js",
|
|
672
|
+
path.join(base, "index.ts"),
|
|
673
|
+
path.join(base, "index.js"),
|
|
261
674
|
];
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
675
|
+
for (const c of candidates) if (existsFile(c)) return c;
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Resolve any module specifier (relative, absolute, package, or TS paths)
|
|
681
|
+
* Returns: { resolved: string | null, isNonRoute: boolean, reason: string }
|
|
682
|
+
*/
|
|
683
|
+
function resolveModuleSpec(fromFileAbs, spec) {
|
|
684
|
+
if (!spec) return { resolved: null, isNonRoute: false, reason: "empty spec" };
|
|
685
|
+
|
|
686
|
+
// 1. Relative imports
|
|
687
|
+
if (spec.startsWith("./") || spec.startsWith("../")) {
|
|
688
|
+
const resolved = resolveRelativeModule(fromFileAbs, spec);
|
|
689
|
+
return {
|
|
690
|
+
resolved,
|
|
691
|
+
isNonRoute: false,
|
|
692
|
+
reason: resolved ? "relative import" : "relative module not found"
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// 2. Check if it's a non-route plugin (skip scanning)
|
|
697
|
+
if (isNonRoutePlugin(spec)) {
|
|
698
|
+
return {
|
|
699
|
+
resolved: null,
|
|
700
|
+
isNonRoute: true,
|
|
701
|
+
reason: `non-route plugin: ${spec}`
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
const projectRoot = findProjectRoot(fromFileAbs);
|
|
706
|
+
|
|
707
|
+
// 3. TypeScript path mappings
|
|
708
|
+
if (projectRoot) {
|
|
709
|
+
const tsConfig = loadTsConfigPaths(projectRoot);
|
|
710
|
+
if (tsConfig) {
|
|
711
|
+
const resolved = resolveWithTsConfigPaths(spec, tsConfig);
|
|
712
|
+
if (resolved) {
|
|
713
|
+
return { resolved, isNonRoute: false, reason: "tsconfig paths" };
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// 4. Node modules resolution
|
|
719
|
+
if (projectRoot) {
|
|
720
|
+
const resolved = resolveFromNodeModules(spec, projectRoot);
|
|
721
|
+
if (resolved) {
|
|
722
|
+
return { resolved, isNonRoute: false, reason: "node_modules" };
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
return {
|
|
727
|
+
resolved: null,
|
|
728
|
+
isNonRoute: false,
|
|
729
|
+
reason: `unresolved package: ${spec}`
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
function extractStringLiteral(node) {
|
|
734
|
+
return t.isStringLiteral(node) ? node.value : null;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function extractPrefixFromOpts(node) {
|
|
738
|
+
if (!t.isObjectExpression(node)) return null;
|
|
739
|
+
for (const p of node.properties) {
|
|
740
|
+
if (!t.isObjectProperty(p)) continue;
|
|
741
|
+
const key =
|
|
742
|
+
t.isIdentifier(p.key) ? p.key.name :
|
|
743
|
+
t.isStringLiteral(p.key) ? p.key.value :
|
|
744
|
+
null;
|
|
745
|
+
if (key === "prefix" && t.isStringLiteral(p.value)) return p.value.value;
|
|
746
|
+
}
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function extractRouteObject(objExpr) {
|
|
751
|
+
let url = null;
|
|
752
|
+
let methods = [];
|
|
753
|
+
let hasHandler = false;
|
|
754
|
+
|
|
755
|
+
for (const p of objExpr.properties) {
|
|
756
|
+
if (!t.isObjectProperty(p)) continue;
|
|
757
|
+
|
|
758
|
+
const key =
|
|
759
|
+
t.isIdentifier(p.key) ? p.key.name :
|
|
760
|
+
t.isStringLiteral(p.key) ? p.key.value :
|
|
761
|
+
null;
|
|
762
|
+
if (!key) continue;
|
|
763
|
+
|
|
764
|
+
if (key === "url" && t.isStringLiteral(p.value)) url = p.value.value;
|
|
765
|
+
|
|
766
|
+
if (key === "method") {
|
|
767
|
+
if (t.isStringLiteral(p.value)) methods = [p.value.value];
|
|
768
|
+
if (t.isArrayExpression(p.value)) {
|
|
769
|
+
methods = p.value.elements.filter(e => t.isStringLiteral(e)).map(e => e.value);
|
|
277
770
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (key === "handler") hasHandler = true;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
return { url, methods, hasHandler };
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function detectFastifyEntry(repoRoot) {
|
|
780
|
+
const candidates = [
|
|
781
|
+
"src/server.ts","src/server.js",
|
|
782
|
+
"server.ts","server.js",
|
|
783
|
+
"src/index.ts","src/index.js",
|
|
784
|
+
"index.ts","index.js",
|
|
785
|
+
"apps/api/src/server.ts",
|
|
786
|
+
"apps/api/src/index.ts",
|
|
787
|
+
];
|
|
788
|
+
for (const rel of candidates) {
|
|
789
|
+
const abs = path.join(repoRoot, rel);
|
|
790
|
+
if (existsFile(abs)) return rel;
|
|
791
|
+
}
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function resolveFastifyRoutesFromEntry(repoRoot, entryAbs) {
|
|
796
|
+
const seen = new Set();
|
|
797
|
+
const routes = [];
|
|
798
|
+
const gaps = [];
|
|
799
|
+
|
|
800
|
+
function scanFile(fileAbs, prefix) {
|
|
801
|
+
if (!fileAbs || seen.has(fileAbs)) return;
|
|
802
|
+
seen.add(fileAbs);
|
|
803
|
+
|
|
804
|
+
const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
|
|
805
|
+
const code = safeRead(fileAbs);
|
|
806
|
+
if (!code) return;
|
|
807
|
+
|
|
808
|
+
let ast;
|
|
809
|
+
try { ast = parseFile(code); } catch { return; }
|
|
810
|
+
|
|
811
|
+
// fastify instance identifiers
|
|
812
|
+
const fastifyNames = new Set(["fastify", "app", "server"]);
|
|
813
|
+
|
|
814
|
+
traverse(ast, {
|
|
815
|
+
VariableDeclarator(p) {
|
|
816
|
+
if (!t.isIdentifier(p.node.id)) return;
|
|
817
|
+
const id = p.node.id.name;
|
|
818
|
+
const init = p.node.init;
|
|
819
|
+
if (!init) return;
|
|
820
|
+
if (t.isCallExpression(init) && t.isIdentifier(init.callee)) {
|
|
821
|
+
const cal = init.callee.name;
|
|
822
|
+
if (cal === "Fastify" || cal === "fastify") fastifyNames.add(id);
|
|
823
|
+
}
|
|
824
|
+
},
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
function resolveImportSpecForLocal(localName) {
|
|
828
|
+
let spec = null;
|
|
829
|
+
|
|
830
|
+
traverse(ast, {
|
|
831
|
+
ImportDeclaration(ip) {
|
|
832
|
+
for (const s of ip.node.specifiers) {
|
|
833
|
+
if (
|
|
834
|
+
(t.isImportDefaultSpecifier(s) || t.isImportSpecifier(s)) &&
|
|
835
|
+
s.local.name === localName
|
|
836
|
+
) {
|
|
837
|
+
spec = ip.node.source.value;
|
|
293
838
|
}
|
|
294
|
-
} else {
|
|
295
|
-
method = match[1];
|
|
296
|
-
routePath = match[2];
|
|
297
839
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
840
|
+
},
|
|
841
|
+
VariableDeclarator(vp) {
|
|
842
|
+
if (!t.isIdentifier(vp.node.id) || vp.node.id.name !== localName) return;
|
|
843
|
+
const init = vp.node.init;
|
|
844
|
+
if (!t.isCallExpression(init)) return;
|
|
845
|
+
if (!t.isIdentifier(init.callee) || init.callee.name !== "require") return;
|
|
846
|
+
const a0 = init.arguments[0];
|
|
847
|
+
if (t.isStringLiteral(a0)) spec = a0.value;
|
|
848
|
+
},
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
return spec;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
traverse(ast, {
|
|
855
|
+
CallExpression(p) {
|
|
856
|
+
const callee = p.node.callee;
|
|
857
|
+
if (!t.isMemberExpression(callee)) return;
|
|
858
|
+
if (!t.isIdentifier(callee.object) || !t.isIdentifier(callee.property)) return;
|
|
859
|
+
|
|
860
|
+
const obj = callee.object.name;
|
|
861
|
+
const prop = callee.property.name;
|
|
862
|
+
|
|
863
|
+
if (!fastifyNames.has(obj)) return;
|
|
864
|
+
|
|
865
|
+
// fastify.get('/x', ...)
|
|
866
|
+
if (FASTIFY_METHODS.has(prop)) {
|
|
867
|
+
const routeStr = extractStringLiteral(p.node.arguments[0]);
|
|
868
|
+
if (!routeStr) return;
|
|
869
|
+
|
|
304
870
|
routes.push({
|
|
305
|
-
method: canonicalizeMethod(
|
|
306
|
-
path:
|
|
307
|
-
handler:
|
|
308
|
-
framework:
|
|
309
|
-
confidence:
|
|
310
|
-
evidence:
|
|
871
|
+
method: canonicalizeMethod(prop),
|
|
872
|
+
path: joinPrefix(prefix, routeStr),
|
|
873
|
+
handler: fileRel,
|
|
874
|
+
framework: "fastify",
|
|
875
|
+
confidence: "med",
|
|
876
|
+
evidence: evidenceFromLoc({
|
|
877
|
+
fileAbs,
|
|
878
|
+
fileRel,
|
|
879
|
+
loc: p.node.loc,
|
|
880
|
+
reason: `Fastify ${prop.toUpperCase()}("${routeStr}")`,
|
|
881
|
+
}),
|
|
311
882
|
});
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
// fastify.route({ method, url, handler })
|
|
887
|
+
if (prop === "route") {
|
|
888
|
+
const arg0 = p.node.arguments[0];
|
|
889
|
+
if (!t.isObjectExpression(arg0)) return;
|
|
890
|
+
|
|
891
|
+
const r = extractRouteObject(arg0);
|
|
892
|
+
if (!r.url) return;
|
|
893
|
+
|
|
894
|
+
const fullPath = joinPrefix(prefix, r.url);
|
|
895
|
+
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
896
|
+
|
|
897
|
+
for (const m of ms) {
|
|
898
|
+
routes.push({
|
|
899
|
+
method: m,
|
|
900
|
+
path: fullPath,
|
|
901
|
+
handler: fileRel,
|
|
902
|
+
framework: "fastify",
|
|
903
|
+
confidence: r.hasHandler ? "med" : "low",
|
|
904
|
+
evidence: evidenceFromLoc({
|
|
905
|
+
fileAbs,
|
|
906
|
+
fileRel,
|
|
907
|
+
loc: p.node.loc,
|
|
908
|
+
reason: `Fastify.route({ url: "${r.url}" })`,
|
|
909
|
+
}),
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// fastify.register(plugin, { prefix })
|
|
916
|
+
if (prop === "register") {
|
|
917
|
+
const pluginArg = p.node.arguments[0];
|
|
918
|
+
const optsArg = p.node.arguments[1];
|
|
919
|
+
const childPrefixRaw = extractPrefixFromOpts(optsArg);
|
|
920
|
+
const childPrefix = childPrefixRaw ? joinPrefix(prefix, childPrefixRaw) : prefix;
|
|
921
|
+
|
|
922
|
+
// inline plugin: fastify.register((f, opts) => { f.get(...) }, { prefix })
|
|
923
|
+
if (t.isFunctionExpression(pluginArg) || t.isArrowFunctionExpression(pluginArg)) {
|
|
924
|
+
const param0 = pluginArg.params[0];
|
|
925
|
+
const innerName = t.isIdentifier(param0) ? param0.name : "fastify";
|
|
926
|
+
|
|
927
|
+
traverse(
|
|
928
|
+
pluginArg.body,
|
|
929
|
+
{
|
|
930
|
+
CallExpression(pp) {
|
|
931
|
+
const c = pp.node.callee;
|
|
932
|
+
if (!t.isMemberExpression(c)) return;
|
|
933
|
+
if (!t.isIdentifier(c.object) || !t.isIdentifier(c.property)) return;
|
|
934
|
+
if (c.object.name !== innerName) return;
|
|
935
|
+
|
|
936
|
+
const pr = c.property.name;
|
|
937
|
+
|
|
938
|
+
if (FASTIFY_METHODS.has(pr)) {
|
|
939
|
+
const rs = extractStringLiteral(pp.node.arguments[0]);
|
|
940
|
+
if (!rs) return;
|
|
941
|
+
|
|
942
|
+
routes.push({
|
|
943
|
+
method: canonicalizeMethod(pr),
|
|
944
|
+
path: joinPrefix(childPrefix, rs),
|
|
945
|
+
handler: fileRel,
|
|
946
|
+
framework: "fastify",
|
|
947
|
+
confidence: "med",
|
|
948
|
+
evidence: evidenceFromLoc({
|
|
949
|
+
fileAbs,
|
|
950
|
+
fileRel,
|
|
951
|
+
loc: pp.node.loc,
|
|
952
|
+
reason: `Fastify plugin ${pr.toUpperCase()}("${rs}") prefix="${childPrefixRaw || ""}"`,
|
|
953
|
+
}),
|
|
954
|
+
});
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
if (pr === "route") {
|
|
958
|
+
const a0 = pp.node.arguments[0];
|
|
959
|
+
if (!t.isObjectExpression(a0)) return;
|
|
960
|
+
const r = extractRouteObject(a0);
|
|
961
|
+
if (!r.url) return;
|
|
962
|
+
|
|
963
|
+
const fullPath = joinPrefix(childPrefix, r.url);
|
|
964
|
+
const ms = (r.methods.length ? r.methods : ["*"]).map(canonicalizeMethod);
|
|
965
|
+
|
|
966
|
+
for (const m of ms) {
|
|
967
|
+
routes.push({
|
|
968
|
+
method: m,
|
|
969
|
+
path: fullPath,
|
|
970
|
+
handler: fileRel,
|
|
971
|
+
framework: "fastify",
|
|
972
|
+
confidence: r.hasHandler ? "med" : "low",
|
|
973
|
+
evidence: evidenceFromLoc({
|
|
974
|
+
fileAbs,
|
|
975
|
+
fileRel,
|
|
976
|
+
loc: pp.node.loc,
|
|
977
|
+
reason: `Fastify plugin route("${r.url}") prefix="${childPrefixRaw || ""}"`,
|
|
978
|
+
}),
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
},
|
|
983
|
+
},
|
|
984
|
+
p.scope,
|
|
985
|
+
p
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
return;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// imported plugin identifier: resolve module (relative, package, or TS paths)
|
|
992
|
+
if (t.isIdentifier(pluginArg)) {
|
|
993
|
+
const localName = pluginArg.name;
|
|
994
|
+
const spec = resolveImportSpecForLocal(localName);
|
|
995
|
+
|
|
996
|
+
if (!spec) {
|
|
997
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, name: localName });
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// Handle @fastify/autoload: scan the specified directory
|
|
1002
|
+
if (isAutoloadPlugin(spec)) {
|
|
1003
|
+
const autoloadDir = extractAutoloadDir(optsArg, fileAbs);
|
|
1004
|
+
if (autoloadDir && existsDir(autoloadDir)) {
|
|
1005
|
+
scanAutoloadDir(autoloadDir, childPrefix);
|
|
1006
|
+
}
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const { resolved, isNonRoute, reason } = resolveModuleSpec(fileAbs, spec);
|
|
1011
|
+
|
|
1012
|
+
// Skip non-route plugins silently (they don't add routes)
|
|
1013
|
+
if (isNonRoute) {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (!resolved) {
|
|
1018
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec, reason });
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
scanFile(resolved, childPrefix);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Direct require/import: fastify.register(require('./routes'))
|
|
1026
|
+
if (t.isCallExpression(pluginArg)) {
|
|
1027
|
+
const callee = pluginArg.callee;
|
|
1028
|
+
if (t.isIdentifier(callee) && callee.name === "require") {
|
|
1029
|
+
const reqArg = pluginArg.arguments[0];
|
|
1030
|
+
if (t.isStringLiteral(reqArg)) {
|
|
1031
|
+
const spec = reqArg.value;
|
|
1032
|
+
|
|
1033
|
+
// Handle @fastify/autoload via require
|
|
1034
|
+
if (isAutoloadPlugin(spec)) {
|
|
1035
|
+
const autoloadDir = extractAutoloadDir(optsArg, fileAbs);
|
|
1036
|
+
if (autoloadDir && existsDir(autoloadDir)) {
|
|
1037
|
+
scanAutoloadDir(autoloadDir, childPrefix);
|
|
1038
|
+
}
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const { resolved, isNonRoute, reason } = resolveModuleSpec(fileAbs, spec);
|
|
1043
|
+
|
|
1044
|
+
if (isNonRoute) return;
|
|
1045
|
+
|
|
1046
|
+
if (!resolved) {
|
|
1047
|
+
gaps.push({ kind: "fastify_plugin_unresolved", file: fileRel, spec, reason });
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
scanFile(resolved, childPrefix);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
},
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
/**
|
|
1061
|
+
* Extract the 'dir' option from autoload config
|
|
1062
|
+
* Handles: { dir: path.join(__dirname, 'routes') } or { dir: './routes' }
|
|
1063
|
+
*/
|
|
1064
|
+
function extractAutoloadDir(optsNode, fromFileAbs) {
|
|
1065
|
+
if (!t.isObjectExpression(optsNode)) return null;
|
|
1066
|
+
|
|
1067
|
+
for (const prop of optsNode.properties) {
|
|
1068
|
+
if (!t.isObjectProperty(prop)) continue;
|
|
1069
|
+
|
|
1070
|
+
const key = t.isIdentifier(prop.key) ? prop.key.name :
|
|
1071
|
+
t.isStringLiteral(prop.key) ? prop.key.value : null;
|
|
1072
|
+
|
|
1073
|
+
if (key !== "dir") continue;
|
|
1074
|
+
|
|
1075
|
+
// Simple string: { dir: './routes' }
|
|
1076
|
+
if (t.isStringLiteral(prop.value)) {
|
|
1077
|
+
return path.resolve(path.dirname(fromFileAbs), prop.value.value);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// path.join(__dirname, 'routes')
|
|
1081
|
+
if (t.isCallExpression(prop.value)) {
|
|
1082
|
+
const callee = prop.value.callee;
|
|
1083
|
+
if (t.isMemberExpression(callee) &&
|
|
1084
|
+
t.isIdentifier(callee.object) && callee.object.name === "path" &&
|
|
1085
|
+
t.isIdentifier(callee.property) && callee.property.name === "join") {
|
|
1086
|
+
const args = prop.value.arguments;
|
|
1087
|
+
// path.join(__dirname, 'subdir')
|
|
1088
|
+
if (args.length >= 2 && t.isIdentifier(args[0]) && args[0].name === "__dirname") {
|
|
1089
|
+
const parts = args.slice(1)
|
|
1090
|
+
.filter(a => t.isStringLiteral(a))
|
|
1091
|
+
.map(a => a.value);
|
|
1092
|
+
if (parts.length > 0) {
|
|
1093
|
+
return path.resolve(path.dirname(fromFileAbs), ...parts);
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
312
1096
|
}
|
|
313
1097
|
}
|
|
314
|
-
|
|
1098
|
+
|
|
1099
|
+
// Template literal or identifier - can't resolve statically
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return null;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
/**
|
|
1107
|
+
* Scan a directory for route files (used by @fastify/autoload)
|
|
1108
|
+
*/
|
|
1109
|
+
function scanAutoloadDir(dirAbs, prefix) {
|
|
1110
|
+
if (!existsDir(dirAbs)) return;
|
|
1111
|
+
|
|
1112
|
+
let entries;
|
|
1113
|
+
try {
|
|
1114
|
+
entries = fs.readdirSync(dirAbs, { withFileTypes: true });
|
|
1115
|
+
} catch {
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
for (const ent of entries) {
|
|
1120
|
+
const fullPath = path.join(dirAbs, ent.name);
|
|
1121
|
+
|
|
1122
|
+
if (ent.isDirectory()) {
|
|
1123
|
+
// Subdirectory: recurse with directory name as prefix segment
|
|
1124
|
+
// @fastify/autoload uses directory names as route prefixes
|
|
1125
|
+
const subPrefix = joinPrefix(prefix, "/" + ent.name);
|
|
1126
|
+
scanAutoloadDir(fullPath, subPrefix);
|
|
1127
|
+
} else if (ent.isFile() && /\.(ts|js)$/.test(ent.name) && !ent.name.endsWith(".d.ts")) {
|
|
1128
|
+
// Skip index files for prefix (they define routes at current prefix level)
|
|
1129
|
+
const isIndex = /^index\.(ts|js)$/.test(ent.name);
|
|
1130
|
+
const filePrefix = isIndex ? prefix : joinPrefix(prefix, "/" + ent.name.replace(/\.(ts|js)$/, ""));
|
|
1131
|
+
|
|
1132
|
+
scanFile(fullPath, filePrefix);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
315
1135
|
}
|
|
316
|
-
|
|
1136
|
+
|
|
1137
|
+
scanFile(entryAbs, "/");
|
|
317
1138
|
return { routes, gaps };
|
|
318
1139
|
}
|
|
319
1140
|
|
|
1141
|
+
async function resolveFastifyRoutes(repoRoot, entryRel = null) {
|
|
1142
|
+
const entry = entryRel || detectFastifyEntry(repoRoot);
|
|
1143
|
+
if (!entry) return { routes: [], gaps: [{ kind: "fastify_entry_missing", file: null }] };
|
|
1144
|
+
|
|
1145
|
+
const entryAbs = path.isAbsolute(entry) ? entry : path.join(repoRoot, entry);
|
|
1146
|
+
if (!existsFile(entryAbs)) {
|
|
1147
|
+
return { routes: [], gaps: [{ kind: "fastify_entry_missing", file: entry }] };
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
return resolveFastifyRoutesFromEntry(repoRoot, entryAbs);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
320
1153
|
// ============================================================================
|
|
321
1154
|
// ROUTE INDEX
|
|
322
1155
|
// ============================================================================
|
|
@@ -329,87 +1162,88 @@ class RouteIndex {
|
|
|
329
1162
|
this.parameterized = [];
|
|
330
1163
|
this.gaps = [];
|
|
331
1164
|
}
|
|
332
|
-
|
|
333
|
-
async build(repoRoot) {
|
|
334
|
-
// Resolve Next.js routes
|
|
1165
|
+
|
|
1166
|
+
async build(repoRoot, options = {}) {
|
|
335
1167
|
const nextRoutes = await resolveNextRoutes(repoRoot);
|
|
336
1168
|
this.routes.push(...nextRoutes);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
const { routes: fastifyRoutes, gaps } = await resolveFastifyRoutes(repoRoot);
|
|
1169
|
+
|
|
1170
|
+
const { routes: fastifyRoutes, gaps } = await resolveFastifyRoutes(repoRoot, options.fastifyEntry || null);
|
|
340
1171
|
this.routes.push(...fastifyRoutes);
|
|
341
|
-
this.gaps.push(...gaps);
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (!this.
|
|
351
|
-
this.
|
|
352
|
-
|
|
353
|
-
if (
|
|
354
|
-
|
|
355
|
-
|
|
1172
|
+
this.gaps.push(...(gaps || []));
|
|
1173
|
+
|
|
1174
|
+
for (const r of this.routes) {
|
|
1175
|
+
const m = canonicalizeMethod(r.method);
|
|
1176
|
+
const p = canonicalizePath(r.path);
|
|
1177
|
+
|
|
1178
|
+
r.method = m;
|
|
1179
|
+
r.path = p;
|
|
1180
|
+
|
|
1181
|
+
if (!this.byMethod.has(m)) this.byMethod.set(m, []);
|
|
1182
|
+
this.byMethod.get(m).push(r);
|
|
1183
|
+
|
|
1184
|
+
if (!this.byPath.has(p)) this.byPath.set(p, []);
|
|
1185
|
+
this.byPath.get(p).push(r);
|
|
1186
|
+
|
|
1187
|
+
if (isParameterizedPath(p)) this.parameterized.push(r);
|
|
356
1188
|
}
|
|
357
|
-
|
|
1189
|
+
|
|
358
1190
|
return this;
|
|
359
1191
|
}
|
|
360
|
-
|
|
361
|
-
findRoutes(method,
|
|
362
|
-
const
|
|
363
|
-
const
|
|
1192
|
+
|
|
1193
|
+
findRoutes(method, p) {
|
|
1194
|
+
const m = canonicalizeMethod(method);
|
|
1195
|
+
const cp = canonicalizePath(p);
|
|
1196
|
+
|
|
364
1197
|
const matches = [];
|
|
365
|
-
|
|
1198
|
+
|
|
366
1199
|
// Exact path match
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
if (matchMethod(route.method, canonicalMethod)) {
|
|
370
|
-
matches.push(route);
|
|
371
|
-
}
|
|
1200
|
+
for (const r of this.byPath.get(cp) || []) {
|
|
1201
|
+
if (matchMethod(r.method, m)) matches.push(r);
|
|
372
1202
|
}
|
|
373
|
-
|
|
374
|
-
// Wildcard method match
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
if (route.path === canonicalPath && !matches.includes(route)) {
|
|
378
|
-
matches.push(route);
|
|
379
|
-
}
|
|
1203
|
+
|
|
1204
|
+
// Wildcard method match on exact path
|
|
1205
|
+
for (const r of this.byMethod.get("*") || []) {
|
|
1206
|
+
if (r.path === cp && !matches.includes(r)) matches.push(r);
|
|
380
1207
|
}
|
|
381
|
-
|
|
1208
|
+
|
|
382
1209
|
// Parameterized route match
|
|
383
|
-
for (const
|
|
384
|
-
if (
|
|
385
|
-
|
|
1210
|
+
for (const r of this.parameterized) {
|
|
1211
|
+
if (r.path === cp) continue;
|
|
1212
|
+
if (matchPath(r.path, cp) && matchMethod(r.method, m)) {
|
|
1213
|
+
if (!matches.includes(r)) matches.push(r);
|
|
386
1214
|
}
|
|
387
1215
|
}
|
|
388
|
-
|
|
1216
|
+
|
|
389
1217
|
return matches;
|
|
390
1218
|
}
|
|
391
|
-
|
|
392
|
-
findClosestRoutes(
|
|
393
|
-
const
|
|
394
|
-
const pathParts =
|
|
395
|
-
|
|
396
|
-
const scored = this.routes.map(
|
|
397
|
-
const routeParts =
|
|
1219
|
+
|
|
1220
|
+
findClosestRoutes(p, limit = 3) {
|
|
1221
|
+
const cp = canonicalizePath(p);
|
|
1222
|
+
const pathParts = cp.split("/").filter(Boolean);
|
|
1223
|
+
|
|
1224
|
+
const scored = this.routes.map((r) => {
|
|
1225
|
+
const routeParts = r.path.split("/").filter(Boolean);
|
|
398
1226
|
let score = 0;
|
|
399
|
-
|
|
1227
|
+
|
|
400
1228
|
for (let i = 0; i < Math.min(pathParts.length, routeParts.length); i++) {
|
|
401
|
-
if (pathParts[i] === routeParts[i]
|
|
402
|
-
|
|
403
|
-
|
|
1229
|
+
if (pathParts[i] === routeParts[i]) score += 1;
|
|
1230
|
+
else if (routeParts[i].startsWith(":")) score += 0.8;
|
|
1231
|
+
else if (routeParts[i].startsWith("*")) { score += 0.6; break; }
|
|
1232
|
+
else break;
|
|
404
1233
|
}
|
|
405
|
-
|
|
406
|
-
if (pathParts.length === routeParts.length) score += 0.
|
|
407
|
-
|
|
1234
|
+
|
|
1235
|
+
if (pathParts.length === routeParts.length) score += 0.25;
|
|
1236
|
+
if (isParameterizedPath(r.path)) score += 0.05;
|
|
1237
|
+
|
|
1238
|
+
return { route: r, score };
|
|
408
1239
|
});
|
|
409
|
-
|
|
410
|
-
return scored
|
|
1240
|
+
|
|
1241
|
+
return scored
|
|
1242
|
+
.sort((a, b) => b.score - a.score)
|
|
1243
|
+
.slice(0, Math.max(0, limit))
|
|
1244
|
+
.map((s) => s.route);
|
|
411
1245
|
}
|
|
412
|
-
|
|
1246
|
+
|
|
413
1247
|
getRouteMap() {
|
|
414
1248
|
return {
|
|
415
1249
|
server: this.routes,
|
|
@@ -427,43 +1261,53 @@ class RouteIndex {
|
|
|
427
1261
|
async function validateRouteExists(claim, repoRoot, routeIndex) {
|
|
428
1262
|
const index = routeIndex || new RouteIndex();
|
|
429
1263
|
if (!routeIndex) await index.build(repoRoot);
|
|
430
|
-
|
|
431
|
-
const method = claim
|
|
432
|
-
const routePath = claim
|
|
433
|
-
|
|
1264
|
+
|
|
1265
|
+
const method = claim?.method || "*";
|
|
1266
|
+
const routePath = claim?.path;
|
|
1267
|
+
|
|
1268
|
+
if (!routePath) {
|
|
1269
|
+
return {
|
|
1270
|
+
result: "unknown",
|
|
1271
|
+
confidence: "low",
|
|
1272
|
+
evidence: [],
|
|
1273
|
+
nextSteps: ["Claim missing path"],
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
434
1277
|
const matches = index.findRoutes(method, routePath);
|
|
435
|
-
|
|
1278
|
+
|
|
436
1279
|
if (matches.length > 0) {
|
|
1280
|
+
const best = matches[0];
|
|
437
1281
|
return {
|
|
438
|
-
result:
|
|
439
|
-
confidence:
|
|
440
|
-
evidence:
|
|
441
|
-
matchedRoute:
|
|
1282
|
+
result: "true",
|
|
1283
|
+
confidence: best.confidence || "med",
|
|
1284
|
+
evidence: best.evidence || [],
|
|
1285
|
+
matchedRoute: best,
|
|
442
1286
|
};
|
|
443
1287
|
}
|
|
444
|
-
|
|
445
|
-
const closest = index.findClosestRoutes(routePath);
|
|
446
|
-
const hasGaps = index.gaps.length > 0;
|
|
447
|
-
|
|
1288
|
+
|
|
1289
|
+
const closest = index.findClosestRoutes(routePath, 3);
|
|
1290
|
+
const hasGaps = (index.gaps || []).length > 0;
|
|
1291
|
+
|
|
448
1292
|
if (hasGaps) {
|
|
449
1293
|
return {
|
|
450
|
-
result:
|
|
451
|
-
confidence:
|
|
1294
|
+
result: "unknown",
|
|
1295
|
+
confidence: "low",
|
|
452
1296
|
evidence: [],
|
|
453
1297
|
closestRoutes: closest,
|
|
454
1298
|
gaps: index.gaps,
|
|
455
|
-
nextSteps: [
|
|
1299
|
+
nextSteps: ["Some routes may not be detected due to unresolved plugins/imports."],
|
|
456
1300
|
};
|
|
457
1301
|
}
|
|
458
|
-
|
|
1302
|
+
|
|
459
1303
|
return {
|
|
460
|
-
result:
|
|
461
|
-
confidence:
|
|
1304
|
+
result: "false",
|
|
1305
|
+
confidence: "high",
|
|
462
1306
|
evidence: [],
|
|
463
1307
|
closestRoutes: closest,
|
|
464
|
-
nextSteps: closest.length
|
|
465
|
-
? [`Did you mean: ${closest.map(r => `${r.method} ${r.path}`).join(
|
|
466
|
-
: [
|
|
1308
|
+
nextSteps: closest.length
|
|
1309
|
+
? [`Did you mean: ${closest.map(r => `${r.method} ${r.path}`).join(", ")}?`]
|
|
1310
|
+
: ["No similar routes found"],
|
|
467
1311
|
};
|
|
468
1312
|
}
|
|
469
1313
|
|
|
@@ -472,6 +1316,7 @@ module.exports = {
|
|
|
472
1316
|
canonicalizeMethod,
|
|
473
1317
|
resolveNextRoutes,
|
|
474
1318
|
resolveFastifyRoutes,
|
|
1319
|
+
detectFastifyEntry,
|
|
475
1320
|
RouteIndex,
|
|
476
1321
|
validateRouteExists,
|
|
477
1322
|
};
|