@route-intelligence/core 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,1495 @@
1
+ import {
2
+ computeMetrics,
3
+ detectInfiniteRedirects,
4
+ findCycles,
5
+ findDeadRoutes,
6
+ findShortestPath,
7
+ getMostConnected,
8
+ metricsToMetadata
9
+ } from "./chunk-56XSRN7B.js";
10
+
11
+ // src/analyzer.ts
12
+ import { EventEmitter } from "events";
13
+ import { defineConfig } from "@route-intelligence/shared";
14
+ import chokidar from "chokidar";
15
+
16
+ // src/export/DotExporter.ts
17
+ function exportDot(graph) {
18
+ const lines = ["digraph RouteGraph {", " rankdir=LR;", " node [shape=box];"];
19
+ for (const nodeId of graph.getAllNodeIds()) {
20
+ const node = graph.getNode(nodeId);
21
+ if (!node) continue;
22
+ lines.push(` "${escapeDot(nodeId)}" [label="${escapeDot(`${node.type}\\n${node.path}`)}"];`);
23
+ }
24
+ for (const edge of graph.getAllEdges()) {
25
+ const attrs = graph.getUnderlyingGraph().getEdgeAttributes(edge.id);
26
+ lines.push(
27
+ ` "${escapeDot(edge.source)}" -> "${escapeDot(edge.target)}" [label="${attrs.type}"];`
28
+ );
29
+ }
30
+ lines.push("}");
31
+ return lines.join("\n");
32
+ }
33
+ function escapeDot(s) {
34
+ return s.replace(/"/g, '\\"').replace(/\n/g, "\\n");
35
+ }
36
+
37
+ // src/export/MermaidExporter.ts
38
+ function exportMermaid(graph) {
39
+ const tree = buildPathTree(graph);
40
+ const lines = ['%%{init: {"flowchart": {"defaultRenderer": "elk"}}}%%', "flowchart LR"];
41
+ const declaredIds = /* @__PURE__ */ new Set();
42
+ emitPathTree(tree, null, lines, declaredIds);
43
+ for (const edge of graph.getAllEdges()) {
44
+ const attrs = graph.getUnderlyingGraph().getEdgeAttributes(edge.id);
45
+ if (attrs.type === "layout-parent" || edge.source === edge.target) continue;
46
+ const sourceId = graphNodeToSegmentId(graph, edge.source);
47
+ const targetId = graphNodeToSegmentId(graph, edge.target);
48
+ if (!sourceId || !targetId || !declaredIds.has(sourceId) || !declaredIds.has(targetId)) {
49
+ continue;
50
+ }
51
+ lines.push(` ${sourceId} -.->|${attrs.type}| ${targetId}`);
52
+ }
53
+ return lines.join("\n");
54
+ }
55
+ function graphNodeToSegmentId(graph, nodeId) {
56
+ const node = graph.getNode(nodeId);
57
+ if (!node) return void 0;
58
+ const segments = parsePathSegments(node.path);
59
+ if (segments.length === 0) return "seg_root";
60
+ return segmentNodeId(`/${segments.join("/")}`);
61
+ }
62
+ function buildPathTree(graph) {
63
+ const root = {
64
+ segment: null,
65
+ segmentPath: "/",
66
+ attachedNodes: [],
67
+ children: /* @__PURE__ */ new Map()
68
+ };
69
+ for (const nodeId of graph.getAllNodeIds()) {
70
+ const node = graph.getNode(nodeId);
71
+ if (!node) continue;
72
+ const segments = parsePathSegments(node.path);
73
+ let current = root;
74
+ for (const segment of segments) {
75
+ let child = current.children.get(segment);
76
+ if (!child) {
77
+ const segmentPath = current.segmentPath === "/" ? `/${segment}` : `${current.segmentPath}/${segment}`;
78
+ child = {
79
+ segment,
80
+ segmentPath,
81
+ attachedNodes: [],
82
+ children: /* @__PURE__ */ new Map()
83
+ };
84
+ current.children.set(segment, child);
85
+ }
86
+ current = child;
87
+ }
88
+ current.attachedNodes.push({
89
+ graphNodeId: nodeId,
90
+ type: node.type,
91
+ path: node.path
92
+ });
93
+ }
94
+ return root;
95
+ }
96
+ function emitPathTree(node, parentId, lines, declaredIds, indent = " ") {
97
+ const id = segmentNodeId(node.segmentPath);
98
+ declareSegmentNode(node, id, lines, declaredIds, indent);
99
+ if (parentId) {
100
+ lines.push(`${indent}${parentId} --> ${id}`);
101
+ }
102
+ const children = sortPathTreeChildren(node.children);
103
+ if (children.length === 0) {
104
+ return id;
105
+ }
106
+ if (children.length === 1) {
107
+ const [onlyChild] = children;
108
+ if (onlyChild) {
109
+ emitPathTree(onlyChild, id, lines, declaredIds, indent);
110
+ }
111
+ return id;
112
+ }
113
+ const subgraphId = `sg_${sanitizeId(node.segmentPath)}`;
114
+ const subgraphTitle = node.segment ? `${node.segment}/` : "/";
115
+ lines.push(`${indent}subgraph ${subgraphId}["${escapeLabel(subgraphTitle)}"]`);
116
+ for (const child of children) {
117
+ emitPathTree(child, id, lines, declaredIds, `${indent} `);
118
+ }
119
+ lines.push(`${indent}end`);
120
+ return id;
121
+ }
122
+ function declareSegmentNode(node, id, lines, declaredIds, indent) {
123
+ if (declaredIds.has(id)) return;
124
+ declaredIds.add(id);
125
+ lines.push(`${indent}${id}["${escapeLabel(formatSegmentLabel(node))}"]`);
126
+ }
127
+ function formatSegmentLabel(node) {
128
+ const parts = [];
129
+ if (node.segment === null) {
130
+ parts.push("/");
131
+ } else {
132
+ parts.push(node.segment);
133
+ }
134
+ for (const attached of node.attachedNodes) {
135
+ parts.push(formatAttachedLabel(attached, node.segmentPath));
136
+ }
137
+ return parts.join("<br/>");
138
+ }
139
+ function formatAttachedLabel(node, segmentPath) {
140
+ const hashIndex = node.path.indexOf("#");
141
+ const suffix = hashIndex >= 0 ? node.path.slice(hashIndex) : "";
142
+ const urlPath = hashIndex >= 0 ? node.path.slice(0, hashIndex) : node.path;
143
+ if (suffix) {
144
+ return `${node.type} ${suffix}`;
145
+ }
146
+ if (urlPath === segmentPath) {
147
+ return node.type;
148
+ }
149
+ if (segmentPath === "/" && urlPath === "/") {
150
+ return node.type;
151
+ }
152
+ const relative = urlPath.startsWith(`${segmentPath}/`) ? urlPath.slice(segmentPath.length + 1) : urlPath;
153
+ return `${node.type}: ${relative}`;
154
+ }
155
+ function parsePathSegments(path) {
156
+ const urlPart = path.split("#")[0] ?? path;
157
+ if (urlPart === "/" || urlPart === "") return [];
158
+ return urlPart.split("/").filter(Boolean);
159
+ }
160
+ function segmentNodeId(segmentPath) {
161
+ if (segmentPath === "/") return "seg_root";
162
+ const segments = parsePathSegments(segmentPath);
163
+ return `seg_${segments.map((segment) => sanitizeSegmentForId(segment)).join("__")}`;
164
+ }
165
+ function sanitizeSegmentForId(segment) {
166
+ const sanitized = segment.replace(/[^a-zA-Z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");
167
+ return sanitized || "wildcard";
168
+ }
169
+ function sortPathTreeChildren(children) {
170
+ return [...children.values()].sort((a, b) => (a.segment ?? "").localeCompare(b.segment ?? ""));
171
+ }
172
+ function sanitizeId(id) {
173
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
174
+ }
175
+ function escapeLabel(label) {
176
+ return label.replace(/"/g, '\\"');
177
+ }
178
+
179
+ // src/export/HtmlExporter.ts
180
+ function exportHtml(graph, root) {
181
+ const json = JSON.stringify(graph.toJSON(root));
182
+ const mermaid = exportMermaid(graph);
183
+ return `<!DOCTYPE html>
184
+ <html lang="en">
185
+ <head>
186
+ <meta charset="UTF-8">
187
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
188
+ <title>Route Intelligence Report</title>
189
+ <script type="module">
190
+ import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
191
+ mermaid.initialize({
192
+ startOnLoad: true,
193
+ theme: 'dark',
194
+ flowchart: { htmlLabels: true, padding: 12, nodeSpacing: 40, rankSpacing: 55 },
195
+ });
196
+ </script>
197
+ <style>
198
+ body { font-family: system-ui, sans-serif; margin: 0; padding: 2rem; background: #0a0a0a; color: #fafafa; }
199
+ h1 { margin-bottom: 0.5rem; }
200
+ .meta { color: #888; margin-bottom: 2rem; }
201
+ pre { background: #1a1a1a; padding: 1rem; border-radius: 8px; overflow: auto; }
202
+ .mermaid { background: #1a1a1a; padding: 1rem; border-radius: 8px; }
203
+ </style>
204
+ </head>
205
+ <body>
206
+ <h1>Route Intelligence Report</h1>
207
+ <p class="meta">Generated from ${root}</p>
208
+ <h2>Route Graph</h2>
209
+ <pre class="mermaid">${escapeHtml(mermaid)}</pre>
210
+ <h2>Graph Data</h2>
211
+ <pre id="graph-data"></pre>
212
+ <script>
213
+ document.getElementById('graph-data').textContent = JSON.stringify(${json}, null, 2);
214
+ </script>
215
+ </body>
216
+ </html>`;
217
+ }
218
+ function escapeHtml(s) {
219
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
220
+ }
221
+ function exportMarkdown(graph, root) {
222
+ const metadata = graph.toJSON(root).metadata;
223
+ const lines = [
224
+ "# Route Intelligence Report",
225
+ "",
226
+ `**Root:** \`${root}\``,
227
+ "",
228
+ "## Summary",
229
+ "",
230
+ `- Routes: ${metadata?.totalRoutes ?? 0}`,
231
+ `- Layouts: ${metadata?.totalLayouts ?? 0}`,
232
+ `- API Routes: ${metadata?.totalApiRoutes ?? 0}`,
233
+ `- Dead Routes: ${metadata?.deadRouteCount ?? 0}`,
234
+ `- Cycles: ${metadata?.cycleCount ?? 0}`,
235
+ "",
236
+ "## Routes",
237
+ ""
238
+ ];
239
+ for (const nodeId of graph.getAllNodeIds()) {
240
+ const node = graph.getNode(nodeId);
241
+ if (!node || node.type !== "route") continue;
242
+ lines.push(`- \`${node.path}\` \u2014 ${node.filePath}${node.isDead ? " *(dead)*" : ""}`);
243
+ }
244
+ lines.push("", "## Mermaid Diagram", "", "```mermaid", exportMermaid(graph), "```");
245
+ return lines.join("\n");
246
+ }
247
+
248
+ // src/export/JsonExporter.ts
249
+ function exportJson(graph, root) {
250
+ return JSON.stringify(graph.toJSON(root), null, 2);
251
+ }
252
+
253
+ // src/export/PlantUMLExporter.ts
254
+ function exportPlantUML(graph) {
255
+ const lines = ["@startuml", "skinparam componentStyle rectangle"];
256
+ for (const nodeId of graph.getAllNodeIds()) {
257
+ const node = graph.getNode(nodeId);
258
+ if (!node) continue;
259
+ lines.push(`component "${node.type}\\n${node.path}" as ${sanitizeId2(nodeId)}`);
260
+ }
261
+ for (const edge of graph.getAllEdges()) {
262
+ const attrs = graph.getUnderlyingGraph().getEdgeAttributes(edge.id);
263
+ lines.push(`${sanitizeId2(edge.source)} --> ${sanitizeId2(edge.target)} : ${attrs.type}`);
264
+ }
265
+ lines.push("@enduml");
266
+ return lines.join("\n");
267
+ }
268
+ function sanitizeId2(id) {
269
+ return id.replace(/[^a-zA-Z0-9_]/g, "_");
270
+ }
271
+
272
+ // src/graph/RouteGraph.ts
273
+ import { MultiDirectedGraph } from "graphology";
274
+ var RouteGraph = class _RouteGraph {
275
+ graph;
276
+ constructor() {
277
+ this.graph = new MultiDirectedGraph({ allowSelfLoops: true });
278
+ }
279
+ hasNode(id) {
280
+ return this.graph.hasNode(id);
281
+ }
282
+ getNode(id) {
283
+ if (!this.graph.hasNode(id)) return void 0;
284
+ return this.graph.getNodeAttributes(id);
285
+ }
286
+ getNodePath(id) {
287
+ return this.getNode(id)?.path;
288
+ }
289
+ findNodeByPath(path) {
290
+ for (const nodeId of this.graph.nodes()) {
291
+ const attrs = this.graph.getNodeAttributes(nodeId);
292
+ if (attrs.path === path) return nodeId;
293
+ }
294
+ return void 0;
295
+ }
296
+ findNodesByType(type) {
297
+ return this.graph.filterNodes((_, attrs) => attrs.type === type);
298
+ }
299
+ addNode(id, attributes) {
300
+ if (this.graph.hasNode(id)) {
301
+ this.graph.mergeNodeAttributes(id, attributes);
302
+ return;
303
+ }
304
+ this.graph.addNode(id, attributes);
305
+ }
306
+ removeNode(id) {
307
+ if (this.graph.hasNode(id)) {
308
+ this.graph.dropNode(id);
309
+ }
310
+ }
311
+ addEdge(id, source, target, attributes) {
312
+ if (!this.graph.hasNode(source) || !this.graph.hasNode(target)) return;
313
+ if (this.graph.hasEdge(id)) {
314
+ this.graph.mergeEdgeAttributes(id, attributes);
315
+ return;
316
+ }
317
+ this.graph.addEdgeWithKey(id, source, target, attributes);
318
+ }
319
+ removeEdge(id) {
320
+ if (this.graph.hasEdge(id)) {
321
+ this.graph.dropEdge(id);
322
+ }
323
+ }
324
+ getAllNodeIds() {
325
+ return this.graph.nodes();
326
+ }
327
+ getAllEdges() {
328
+ return this.graph.mapEdges((edge, attrs, source, target) => ({
329
+ id: edge,
330
+ source,
331
+ target
332
+ }));
333
+ }
334
+ getIncomingEdges(nodeId) {
335
+ if (!this.graph.hasNode(nodeId)) return [];
336
+ return this.graph.inEdges(nodeId).map((edgeId) => ({
337
+ id: edgeId,
338
+ source: this.graph.source(edgeId),
339
+ attributes: this.graph.getEdgeAttributes(edgeId)
340
+ }));
341
+ }
342
+ getOutgoingEdges(nodeId) {
343
+ if (!this.graph.hasNode(nodeId)) return [];
344
+ return this.graph.outEdges(nodeId).map((edgeId) => ({
345
+ id: edgeId,
346
+ target: this.graph.target(edgeId),
347
+ attributes: this.graph.getEdgeAttributes(edgeId)
348
+ }));
349
+ }
350
+ getUnderlyingGraph() {
351
+ return this.graph;
352
+ }
353
+ toJSON(root, metadata) {
354
+ const nodes = this.graph.nodes().map((id) => ({
355
+ id,
356
+ attributes: this.graph.getNodeAttributes(id)
357
+ }));
358
+ const edges = this.graph.edges().map((id) => ({
359
+ id,
360
+ source: this.graph.source(id),
361
+ target: this.graph.target(id),
362
+ attributes: this.graph.getEdgeAttributes(id)
363
+ }));
364
+ return {
365
+ version: "1.0",
366
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
367
+ root,
368
+ nodes,
369
+ edges,
370
+ metadata
371
+ };
372
+ }
373
+ static fromJSON(data) {
374
+ const graph = new _RouteGraph();
375
+ for (const node of data.nodes) {
376
+ graph.addNode(node.id, node.attributes);
377
+ }
378
+ for (const edge of data.edges) {
379
+ graph.addEdge(edge.id, edge.source, edge.target, edge.attributes);
380
+ }
381
+ return graph;
382
+ }
383
+ };
384
+
385
+ // src/incremental/IncrementalCache.ts
386
+ import { createHash } from "crypto";
387
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
388
+ import { join } from "path";
389
+ var IncrementalCache = class {
390
+ fileHashes = /* @__PURE__ */ new Map();
391
+ fileDependencies = /* @__PURE__ */ new Map();
392
+ parsedFiles = /* @__PURE__ */ new Map();
393
+ graphSnapshot = null;
394
+ cacheDir;
395
+ constructor(cacheDir) {
396
+ this.cacheDir = cacheDir;
397
+ this.load();
398
+ }
399
+ hashFile(content) {
400
+ return createHash("sha256").update(content).digest("hex");
401
+ }
402
+ getFileHash(filePath) {
403
+ return this.fileHashes.get(filePath);
404
+ }
405
+ setFileHash(filePath, hash) {
406
+ this.fileHashes.set(filePath, hash);
407
+ }
408
+ getDependencies(filePath) {
409
+ return this.fileDependencies.get(filePath) ?? /* @__PURE__ */ new Set();
410
+ }
411
+ setDependencies(filePath, nodeIds) {
412
+ this.fileDependencies.set(filePath, nodeIds);
413
+ }
414
+ getParsedFile(filePath) {
415
+ return this.parsedFiles.get(filePath);
416
+ }
417
+ setParsedFile(filePath, file) {
418
+ this.parsedFiles.set(filePath, file);
419
+ }
420
+ getGraphSnapshot() {
421
+ return this.graphSnapshot;
422
+ }
423
+ setGraphSnapshot(snapshot) {
424
+ this.graphSnapshot = snapshot;
425
+ }
426
+ invalidate(filePath) {
427
+ const nodeIds = [...this.fileDependencies.get(filePath) ?? []];
428
+ this.parsedFiles.delete(filePath);
429
+ this.fileHashes.delete(filePath);
430
+ this.fileDependencies.delete(filePath);
431
+ return { files: [filePath], nodeIds };
432
+ }
433
+ save() {
434
+ if (!existsSync(this.cacheDir)) {
435
+ mkdirSync(this.cacheDir, { recursive: true });
436
+ }
437
+ const data = {
438
+ fileHashes: Object.fromEntries(this.fileHashes),
439
+ fileDependencies: Object.fromEntries(
440
+ [...this.fileDependencies.entries()].map(([k, v]) => [k, [...v]])
441
+ ),
442
+ graphSnapshot: this.graphSnapshot ?? void 0
443
+ };
444
+ writeFileSync(join(this.cacheDir, "cache.json"), JSON.stringify(data, null, 2));
445
+ }
446
+ load() {
447
+ const cachePath = join(this.cacheDir, "cache.json");
448
+ if (!existsSync(cachePath)) return;
449
+ try {
450
+ const data = JSON.parse(readFileSync(cachePath, "utf-8"));
451
+ this.fileHashes = new Map(Object.entries(data.fileHashes));
452
+ this.fileDependencies = new Map(
453
+ Object.entries(data.fileDependencies).map(([k, v]) => [k, new Set(v)])
454
+ );
455
+ this.graphSnapshot = data.graphSnapshot ?? null;
456
+ } catch {
457
+ }
458
+ }
459
+ };
460
+ function computeGraphPatch(before, after) {
461
+ const beforeNodeIds = new Set(before.nodes.map((n) => n.id));
462
+ const afterNodeIds = new Set(after.nodes.map((n) => n.id));
463
+ const beforeEdgeIds = new Set(before.edges.map((e) => e.id));
464
+ const afterEdgeIds = new Set(after.edges.map((e) => e.id));
465
+ return {
466
+ addedNodes: after.nodes.filter((n) => !beforeNodeIds.has(n.id)),
467
+ removedNodeIds: before.nodes.filter((n) => !afterNodeIds.has(n.id)).map((n) => n.id),
468
+ addedEdges: after.edges.filter((e) => !beforeEdgeIds.has(e.id)),
469
+ removedEdgeIds: before.edges.filter((e) => !afterEdgeIds.has(e.id)).map((e) => e.id),
470
+ modifiedNodeIds: after.nodes.filter((n) => {
471
+ if (!beforeNodeIds.has(n.id)) return false;
472
+ const prev = before.nodes.find((bn) => bn.id === n.id);
473
+ return JSON.stringify(prev?.attributes) !== JSON.stringify(n.attributes);
474
+ }).map((n) => n.id)
475
+ };
476
+ }
477
+ function readFileContent(filePath) {
478
+ return readFileSync(filePath, "utf-8");
479
+ }
480
+
481
+ // src/incremental/Invalidator.ts
482
+ var Invalidator = class {
483
+ constructor(cache) {
484
+ this.cache = cache;
485
+ }
486
+ cache;
487
+ computeInvalidation(event) {
488
+ if (event.type === "unlink") {
489
+ return this.cache.invalidate(event.path);
490
+ }
491
+ if (event.type === "change" || event.type === "add") {
492
+ try {
493
+ const content = readFileContent(event.path);
494
+ const newHash = this.cache.hashFile(content);
495
+ const oldHash = this.cache.getFileHash(event.path);
496
+ if (oldHash === newHash) {
497
+ return { files: [], nodeIds: [] };
498
+ }
499
+ this.cache.setFileHash(event.path, newHash);
500
+ return this.cache.invalidate(event.path);
501
+ } catch {
502
+ return this.cache.invalidate(event.path);
503
+ }
504
+ }
505
+ return { files: [], nodeIds: [] };
506
+ }
507
+ getAffectedFiles(invalidation) {
508
+ return invalidation.files;
509
+ }
510
+ };
511
+
512
+ // src/pipeline/stages/FileSystemStage.ts
513
+ import fg from "fast-glob";
514
+ import picomatch from "picomatch";
515
+ var FileSystemStage = class {
516
+ name = "FileSystemStage";
517
+ async run(ctx) {
518
+ const include = ctx.config.include ?? ["**/*.{ts,tsx,js,jsx}"];
519
+ const exclude = ctx.config.exclude ?? [
520
+ "**/node_modules/**",
521
+ "**/.next/**",
522
+ "**/dist/**",
523
+ "**/*.test.*",
524
+ "**/*.spec.*"
525
+ ];
526
+ const files = await fg(include, {
527
+ cwd: ctx.root,
528
+ absolute: true,
529
+ ignore: exclude,
530
+ onlyFiles: true
531
+ });
532
+ const isExcluded = picomatch(exclude);
533
+ ctx.files = files.filter((f) => !isExcluded(f));
534
+ }
535
+ };
536
+
537
+ // src/pipeline/stages/NavigationAnalysisStage.ts
538
+ import { createDefaultEdgeAttributes } from "@route-intelligence/shared";
539
+
540
+ // src/pipeline/types.ts
541
+ function createProjectContext(root, config, pluginConfig = {}) {
542
+ return { root, config, pluginConfig };
543
+ }
544
+ function createAnalysisContext(ctx, plugin) {
545
+ const pluginConfig = ctx.pluginConfigs.get(plugin.id) ?? {};
546
+ return {
547
+ root: ctx.root,
548
+ config: ctx.config,
549
+ pluginConfig,
550
+ graph: ctx.graph,
551
+ routes: ctx.routes
552
+ };
553
+ }
554
+
555
+ // src/pipeline/stages/NavigationAnalysisStage.ts
556
+ function resolveTargetPath(destination, ctx) {
557
+ switch (destination.kind) {
558
+ case "static":
559
+ return { path: destination.path, isExternal: false };
560
+ case "template-literal":
561
+ return { path: destination.template.replace(/\$\{[^}]+\}/g, "[param]"), isExternal: false };
562
+ case "external":
563
+ return { isExternal: true };
564
+ case "dynamic":
565
+ return { isExternal: false };
566
+ default: {
567
+ const _exhaustive = destination;
568
+ return _exhaustive;
569
+ }
570
+ }
571
+ }
572
+ function findSourceNodeId(filePath, ctx) {
573
+ for (const [id, route] of ctx.routes) {
574
+ if (route.filePath === filePath) return id;
575
+ }
576
+ return void 0;
577
+ }
578
+ function findTargetNodeId(path, ctx) {
579
+ if (!path) return void 0;
580
+ return ctx.graph.findNodeByPath(path);
581
+ }
582
+ var NavigationAnalysisStage = class {
583
+ name = "NavigationAnalysisStage";
584
+ async run(ctx) {
585
+ for (const plugin of ctx.plugins) {
586
+ const analysisCtx = createAnalysisContext(ctx, plugin);
587
+ for (const [filePath, semanticFile] of ctx.semanticFiles) {
588
+ const result = await plugin.analyzeFile(semanticFile, analysisCtx);
589
+ for (const edge of result.edges) {
590
+ const sourceId = edge.sourceId || findSourceNodeId(filePath, ctx);
591
+ if (!sourceId) continue;
592
+ let targetId = edge.targetId;
593
+ if (!targetId && edge.targetPath) {
594
+ targetId = findTargetNodeId(edge.targetPath, ctx);
595
+ }
596
+ if (!targetId) {
597
+ const resolved = resolveTargetPath(
598
+ edge.targetPath ? { kind: "static", path: edge.targetPath } : { kind: "dynamic", expression: "unknown" },
599
+ ctx
600
+ );
601
+ if (resolved.path) {
602
+ targetId = findTargetNodeId(resolved.path, ctx);
603
+ }
604
+ }
605
+ if (!targetId) continue;
606
+ const edgeType = edge.conditions.length > 0 ? "conditional-navigation" : edge.type;
607
+ ctx.graph.addEdge(
608
+ `nav:${sourceId}->${targetId}:${edge.loc.line}`,
609
+ sourceId,
610
+ targetId,
611
+ createDefaultEdgeAttributes({
612
+ type: edgeType,
613
+ source: edge.source,
614
+ method: edge.method,
615
+ isExternal: edge.isExternal,
616
+ conditions: edge.conditions,
617
+ loc: edge.loc
618
+ })
619
+ );
620
+ }
621
+ }
622
+ }
623
+ }
624
+ };
625
+ var MiddlewareAnalysisStage = class {
626
+ name = "MiddlewareAnalysisStage";
627
+ async run(ctx) {
628
+ for (const plugin of ctx.plugins) {
629
+ const projectCtx = {
630
+ root: ctx.root,
631
+ config: ctx.config,
632
+ pluginConfig: ctx.pluginConfigs.get(plugin.id) ?? {}
633
+ };
634
+ await plugin.enrichGraph(ctx.graph, projectCtx);
635
+ }
636
+ }
637
+ };
638
+ var ConditionalAnalysisStage = class {
639
+ name = "ConditionalAnalysisStage";
640
+ async run(ctx) {
641
+ for (const nodeId of ctx.graph.getAllNodeIds()) {
642
+ const node = ctx.graph.getNode(nodeId);
643
+ if (!node) continue;
644
+ const file = ctx.semanticFiles.get(node.filePath);
645
+ if (!file) continue;
646
+ const allConditions = file.conditionalBlocks.flatMap((b) => b.conditions);
647
+ if (allConditions.length > 0) {
648
+ ctx.graph.addNode(nodeId, {
649
+ ...node,
650
+ conditions: [...node.conditions, ...allConditions]
651
+ });
652
+ }
653
+ }
654
+ }
655
+ };
656
+ var GraphEnrichmentStage = class {
657
+ name = "GraphEnrichmentStage";
658
+ async run(ctx) {
659
+ for (const nodeId of ctx.graph.getAllNodeIds()) {
660
+ const node = ctx.graph.getNode(nodeId);
661
+ if (!node) continue;
662
+ const depth = node.path.split("/").filter(Boolean).length;
663
+ ctx.graph.addNode(nodeId, { ...node, depth });
664
+ }
665
+ }
666
+ };
667
+
668
+ // src/pipeline/stages/ParseStage.ts
669
+ import { Project } from "ts-morph";
670
+
671
+ // src/ast/SemanticFile.ts
672
+ import { Node as Node3 } from "ts-morph";
673
+
674
+ // src/ast/ComponentClassifier.ts
675
+ function isClientComponent(sourceFile) {
676
+ const statements = sourceFile.getStatements();
677
+ for (const stmt of statements.slice(0, 5)) {
678
+ const text = stmt.getText().trim();
679
+ if (text === "'use client'" || text === '"use client"') return true;
680
+ if (!text.startsWith("'use ") && !text.startsWith('"use ')) break;
681
+ }
682
+ return false;
683
+ }
684
+ function isServerComponent(sourceFile) {
685
+ return !isClientComponent(sourceFile);
686
+ }
687
+
688
+ // src/ast/ConditionalVisitor.ts
689
+ import { Node } from "ts-morph";
690
+ function toLoc(node, filePath) {
691
+ return {
692
+ filePath,
693
+ line: node.getStartLineNumber(),
694
+ column: 1
695
+ };
696
+ }
697
+ function inferConditionKind(expression) {
698
+ const lower = expression.toLowerCase();
699
+ if (lower.includes("auth") || lower.includes("session")) return "auth";
700
+ if (lower.includes("role")) return "role";
701
+ if (lower.includes("permission")) return "permission";
702
+ if (lower.includes("feature") || lower.includes("flag")) return "feature-flag";
703
+ if (lower.includes("subscription") || lower.includes("plan")) return "subscription";
704
+ if (lower.includes("cookie")) return "cookie";
705
+ if (lower.includes("header")) return "header";
706
+ if (lower.includes("locale")) return "locale";
707
+ if (lower.includes("process.env")) return "env";
708
+ if (lower.includes("searchparams")) return "search-param";
709
+ return "unknown";
710
+ }
711
+ function expressionToConditions(expression, negated = false) {
712
+ return [
713
+ {
714
+ kind: inferConditionKind(expression),
715
+ expression,
716
+ negated,
717
+ confidence: "inferred"
718
+ }
719
+ ];
720
+ }
721
+ function extractConditionalBlocks(sourceFile, filePath) {
722
+ const blocks = [];
723
+ sourceFile.forEachDescendant((node) => {
724
+ if (Node.isIfStatement(node)) {
725
+ const expr = node.getExpression().getText();
726
+ blocks.push({
727
+ expression: expr,
728
+ conditions: expressionToConditions(expr),
729
+ loc: toLoc(node, filePath)
730
+ });
731
+ }
732
+ if (Node.isSwitchStatement(node)) {
733
+ const expr = node.getExpression().getText();
734
+ blocks.push({
735
+ expression: expr,
736
+ conditions: expressionToConditions(expr),
737
+ loc: toLoc(node, filePath)
738
+ });
739
+ }
740
+ if (Node.isConditionalExpression(node)) {
741
+ const expr = node.getCondition().getText();
742
+ blocks.push({
743
+ expression: expr,
744
+ conditions: expressionToConditions(expr),
745
+ loc: toLoc(node, filePath)
746
+ });
747
+ }
748
+ });
749
+ return blocks;
750
+ }
751
+
752
+ // src/ast/NavigationVisitor.ts
753
+ import {
754
+ Node as Node2,
755
+ SyntaxKind
756
+ } from "ts-morph";
757
+ function toLoc2(node, filePath) {
758
+ const start = node.getStartLineNumber();
759
+ const col = node.getStart() - node.getSourceFile().getLineAndColumnAtPos(node.getStart()).column;
760
+ return {
761
+ filePath,
762
+ line: start,
763
+ column: node.getStartLineNumber() > 0 ? 1 : 0
764
+ };
765
+ }
766
+ function resolveDestination(expr) {
767
+ if (!expr) return { kind: "dynamic", expression: "unknown" };
768
+ if (Node2.isStringLiteral(expr) || Node2.isNoSubstitutionTemplateLiteral(expr)) {
769
+ const path = expr.getLiteralText();
770
+ if (path.startsWith("http://") || path.startsWith("https://")) {
771
+ return { kind: "external", url: path };
772
+ }
773
+ return { kind: "static", path };
774
+ }
775
+ if (Node2.isTemplateExpression(expr)) {
776
+ const head = expr.getHead().getLiteralText();
777
+ const spans = expr.getTemplateSpans();
778
+ const params = [];
779
+ let template = head;
780
+ for (const span of spans) {
781
+ const paramExpr = span.getExpression();
782
+ params.push(paramExpr.getText());
783
+ template += `\${${paramExpr.getText()}}`;
784
+ template += span.getLiteral().getLiteralText();
785
+ }
786
+ return { kind: "template-literal", template, params };
787
+ }
788
+ return { kind: "dynamic", expression: expr.getText() };
789
+ }
790
+ var NAVIGATION_CALLEES = /* @__PURE__ */ new Set([
791
+ "redirect",
792
+ "permanentRedirect",
793
+ "push",
794
+ "replace",
795
+ "prefetch",
796
+ "back",
797
+ "forward"
798
+ ]);
799
+ function getCalleeName(call) {
800
+ const expr = call.getExpression();
801
+ if (Node2.isIdentifier(expr)) {
802
+ return expr.getText();
803
+ }
804
+ if (Node2.isPropertyAccessExpression(expr)) {
805
+ const obj = expr.getExpression().getText();
806
+ const name = expr.getName();
807
+ if (obj === "router" || obj.endsWith("Router")) {
808
+ return `router.${name}`;
809
+ }
810
+ if (obj === "NextResponse") {
811
+ return `NextResponse.${name}`;
812
+ }
813
+ if (obj === "history") {
814
+ return `history.${name}`;
815
+ }
816
+ if (obj === "window.location") {
817
+ return "window.location";
818
+ }
819
+ return `${obj}.${name}`;
820
+ }
821
+ return expr.getText();
822
+ }
823
+ function extractConditionsFromNode(node) {
824
+ let current = node;
825
+ const conditions = [];
826
+ while (current) {
827
+ const parent = current.getParent();
828
+ if (!parent) break;
829
+ if (Node2.isIfStatement(parent)) {
830
+ conditions.push({
831
+ kind: inferConditionKind2(parent.getExpression().getText()),
832
+ expression: parent.getExpression().getText(),
833
+ negated: parent.getElseStatement() === current,
834
+ confidence: "inferred"
835
+ });
836
+ }
837
+ if (Node2.isConditionalExpression(parent)) {
838
+ conditions.push({
839
+ kind: "unknown",
840
+ expression: parent.getCondition().getText(),
841
+ negated: parent.getWhenFalse() === current,
842
+ confidence: "inferred"
843
+ });
844
+ }
845
+ current = parent;
846
+ }
847
+ return conditions;
848
+ }
849
+ function inferConditionKind2(expression) {
850
+ const lower = expression.toLowerCase();
851
+ if (lower.includes("auth") || lower.includes("session") || lower.includes("loggedin")) {
852
+ return "auth";
853
+ }
854
+ if (lower.includes("role")) return "role";
855
+ if (lower.includes("permission")) return "permission";
856
+ if (lower.includes("feature") || lower.includes("flag")) return "feature-flag";
857
+ if (lower.includes("cookie")) return "cookie";
858
+ if (lower.includes("header")) return "header";
859
+ if (lower.includes("locale") || lower.includes("lang")) return "locale";
860
+ if (lower.includes("process.env")) return "env";
861
+ if (lower.includes("searchparams") || lower.includes("query")) return "search-param";
862
+ return "unknown";
863
+ }
864
+ function extractNavigationCalls(sourceFile, filePath, customWrappers = []) {
865
+ const calls = [];
866
+ sourceFile.forEachDescendant((node) => {
867
+ if (!Node2.isCallExpression(node)) return;
868
+ const callee = getCalleeName(node);
869
+ const calleeBase = callee.split(".").pop() ?? callee;
870
+ const isNavCall = NAVIGATION_CALLEES.has(calleeBase) || callee.startsWith("router.") || callee.startsWith("NextResponse.") || callee.startsWith("history.") || customWrappers.some((w) => callee.includes(w));
871
+ if (!isNavCall) return;
872
+ const args = node.getArguments();
873
+ let destination = { kind: "dynamic", expression: "unknown" };
874
+ let method;
875
+ if (calleeBase === "back" || calleeBase === "forward") {
876
+ destination = { kind: "dynamic", expression: calleeBase };
877
+ } else if (args[0] && Node2.isExpression(args[0])) {
878
+ destination = resolveDestination(args[0]);
879
+ }
880
+ if (calleeBase === "replace" || callee.includes("permanentRedirect")) {
881
+ method = "replace";
882
+ } else if (calleeBase === "push") {
883
+ method = "push";
884
+ }
885
+ calls.push({
886
+ callee,
887
+ destination,
888
+ method,
889
+ conditions: extractConditionsFromNode(node),
890
+ loc: toLoc2(node, filePath)
891
+ });
892
+ });
893
+ return calls;
894
+ }
895
+ function extractJsxLinks(sourceFile, filePath) {
896
+ const links = [];
897
+ sourceFile.forEachDescendant((node) => {
898
+ if (!Node2.isJsxOpeningElement(node) && !Node2.isJsxSelfClosingElement(node)) return;
899
+ const tagName = Node2.isJsxOpeningElement(node) ? node.getTagNameNode().getText() : node.getTagNameNode().getText();
900
+ if (tagName !== "Link" && !tagName.endsWith(".Link")) return;
901
+ const hrefAttr = node.getAttribute("href");
902
+ if (!hrefAttr) return;
903
+ const initializer = hrefAttr.getInitializer();
904
+ let destination = { kind: "dynamic", expression: "unknown" };
905
+ if (initializer && Node2.isJsxExpression(initializer)) {
906
+ const expr = initializer.getExpression();
907
+ if (expr) destination = resolveDestination(expr);
908
+ } else if (initializer && Node2.isStringLiteral(initializer)) {
909
+ destination = resolveDestination(initializer);
910
+ }
911
+ const prefetchAttr = node.getAttribute("prefetch");
912
+ let prefetch;
913
+ if (prefetchAttr && Node2.isJsxAttribute(prefetchAttr)) {
914
+ const init = prefetchAttr.getInitializer();
915
+ if (init && Node2.isStringLiteral(init)) {
916
+ prefetch = init.getLiteralText() !== "false";
917
+ } else {
918
+ prefetch = true;
919
+ }
920
+ }
921
+ links.push({
922
+ componentName: tagName,
923
+ destination,
924
+ prefetch,
925
+ conditions: extractConditionsFromNode(node),
926
+ loc: toLoc2(node, filePath)
927
+ });
928
+ });
929
+ return links;
930
+ }
931
+ function extractWindowLocationAssignments(sourceFile, filePath) {
932
+ const calls = [];
933
+ sourceFile.forEachDescendant((node) => {
934
+ if (!Node2.isBinaryExpression(node)) return;
935
+ if (node.getOperatorToken().getKind() !== SyntaxKind.EqualsToken) return;
936
+ const left = node.getLeft().getText();
937
+ if (!left.includes("window.location") && !left.includes("location.href")) return;
938
+ calls.push({
939
+ callee: "window.location",
940
+ destination: resolveDestination(node.getRight()),
941
+ method: "replace",
942
+ conditions: extractConditionsFromNode(node),
943
+ loc: toLoc2(node, filePath)
944
+ });
945
+ });
946
+ return calls;
947
+ }
948
+ function extractWindowOpenCalls(sourceFile, filePath) {
949
+ const calls = [];
950
+ sourceFile.forEachDescendant((node) => {
951
+ if (!Node2.isCallExpression(node)) return;
952
+ const callee = getCalleeName(node);
953
+ if (callee !== "window.open" && callee !== "open") return;
954
+ const args = node.getArguments();
955
+ calls.push({
956
+ callee: "window.open",
957
+ destination: args[0] && Node2.isExpression(args[0]) ? resolveDestination(args[0]) : { kind: "dynamic", expression: "unknown" },
958
+ conditions: extractConditionsFromNode(node),
959
+ loc: toLoc2(node, filePath)
960
+ });
961
+ });
962
+ return calls;
963
+ }
964
+
965
+ // src/ast/SemanticFile.ts
966
+ function extractImports(sourceFile, filePath) {
967
+ const imports = [];
968
+ for (const imp of sourceFile.getImportDeclarations()) {
969
+ const moduleSpecifier = imp.getModuleSpecifierValue();
970
+ const namedImports = imp.getNamedImports().map((n) => n.getName());
971
+ const defaultImport = imp.getDefaultImport()?.getText();
972
+ imports.push({
973
+ moduleSpecifier,
974
+ namedImports,
975
+ defaultImport,
976
+ loc: { filePath, line: imp.getStartLineNumber(), column: 1 }
977
+ });
978
+ }
979
+ return imports;
980
+ }
981
+ function extractExports(sourceFile, filePath) {
982
+ const exports = [];
983
+ for (const [name, decls] of sourceFile.getExportedDeclarations()) {
984
+ const firstDecl = decls[0];
985
+ if (firstDecl && typeof firstDecl !== "string" && "getStartLineNumber" in firstDecl) {
986
+ exports.push({
987
+ name,
988
+ isDefault: false,
989
+ loc: { filePath, line: firstDecl.getStartLineNumber(), column: 1 }
990
+ });
991
+ }
992
+ }
993
+ const defaultExport = sourceFile.getDefaultExportSymbol();
994
+ if (defaultExport) {
995
+ exports.push({
996
+ name: "default",
997
+ isDefault: true,
998
+ loc: { filePath, line: 1, column: 1 }
999
+ });
1000
+ }
1001
+ return exports;
1002
+ }
1003
+ function extractComponents(sourceFile, filePath) {
1004
+ const components = [];
1005
+ for (const fn of sourceFile.getFunctions()) {
1006
+ const name = fn.getName();
1007
+ if (name && /^[A-Z]/.test(name)) {
1008
+ components.push({
1009
+ name,
1010
+ isDefault: fn.isDefaultExport(),
1011
+ loc: { filePath, line: fn.getStartLineNumber(), column: 1 }
1012
+ });
1013
+ }
1014
+ }
1015
+ for (const stmt of sourceFile.getVariableStatements()) {
1016
+ for (const decl of stmt.getDeclarations()) {
1017
+ const name = decl.getName();
1018
+ if (/^[A-Z]/.test(name)) {
1019
+ const init = decl.getInitializer();
1020
+ if (init && (Node3.isArrowFunction(init) || Node3.isFunctionExpression(init) || Node3.isCallExpression(init))) {
1021
+ components.push({
1022
+ name,
1023
+ isDefault: stmt.isDefaultExport(),
1024
+ loc: { filePath, line: decl.getStartLineNumber(), column: 1 }
1025
+ });
1026
+ }
1027
+ }
1028
+ }
1029
+ }
1030
+ return components;
1031
+ }
1032
+ function createSemanticFile(sourceFile, filePath, customNavigationWrappers = []) {
1033
+ const navCalls = [
1034
+ ...extractNavigationCalls(sourceFile, filePath, customNavigationWrappers),
1035
+ ...extractWindowLocationAssignments(sourceFile, filePath),
1036
+ ...extractWindowOpenCalls(sourceFile, filePath)
1037
+ ];
1038
+ const jsxLinks = extractJsxLinks(sourceFile, filePath);
1039
+ const conditionalBlocks = extractConditionalBlocks(sourceFile, filePath);
1040
+ const imports = extractImports(sourceFile, filePath);
1041
+ const exports = extractExports(sourceFile, filePath);
1042
+ const components = extractComponents(sourceFile, filePath);
1043
+ const client = isClientComponent(sourceFile);
1044
+ const server = isServerComponent(sourceFile);
1045
+ return {
1046
+ path: filePath,
1047
+ get isClientComponent() {
1048
+ return client;
1049
+ },
1050
+ get isServerComponent() {
1051
+ return server;
1052
+ },
1053
+ get exports() {
1054
+ return exports;
1055
+ },
1056
+ get imports() {
1057
+ return imports;
1058
+ },
1059
+ get components() {
1060
+ return components;
1061
+ },
1062
+ get navigationCalls() {
1063
+ return navCalls;
1064
+ },
1065
+ get jsxLinks() {
1066
+ return jsxLinks;
1067
+ },
1068
+ get conditionalBlocks() {
1069
+ return conditionalBlocks;
1070
+ },
1071
+ sourceFile
1072
+ };
1073
+ }
1074
+
1075
+ // src/pipeline/stages/ParseStage.ts
1076
+ var ParseStage = class {
1077
+ name = "ParseStage";
1078
+ project = null;
1079
+ async run(ctx) {
1080
+ if (!this.project) {
1081
+ this.project = new Project({
1082
+ skipAddingFilesFromTsConfig: true,
1083
+ compilerOptions: {
1084
+ allowJs: true,
1085
+ jsx: 4
1086
+ }
1087
+ });
1088
+ }
1089
+ const customWrappers = this.getCustomWrappers(ctx);
1090
+ for (const filePath of ctx.files) {
1091
+ if (!/\.(tsx?|jsx?)$/.test(filePath)) continue;
1092
+ try {
1093
+ let sourceFile = this.project.getSourceFile(filePath);
1094
+ if (!sourceFile) {
1095
+ sourceFile = this.project.addSourceFileAtPath(filePath);
1096
+ }
1097
+ ctx.semanticFiles.set(filePath, createSemanticFile(sourceFile, filePath, customWrappers));
1098
+ } catch {
1099
+ }
1100
+ }
1101
+ }
1102
+ getCustomWrappers(ctx) {
1103
+ const wrappers = [];
1104
+ for (const config of ctx.pluginConfigs.values()) {
1105
+ const custom = config.customNavigationWrappers;
1106
+ if (Array.isArray(custom)) {
1107
+ wrappers.push(...custom);
1108
+ }
1109
+ }
1110
+ return wrappers;
1111
+ }
1112
+ };
1113
+
1114
+ // src/pipeline/stages/RouteDiscoveryStage.ts
1115
+ import {
1116
+ createDefaultEdgeAttributes as createDefaultEdgeAttributes2,
1117
+ createDefaultNodeAttributes
1118
+ } from "@route-intelligence/shared";
1119
+ var RouteDiscoveryStage = class {
1120
+ name = "RouteDiscoveryStage";
1121
+ async run(ctx) {
1122
+ ctx.routes.clear();
1123
+ for (const plugin of ctx.plugins) {
1124
+ const pluginConfig = ctx.pluginConfigs.get(plugin.id) ?? {};
1125
+ const projectCtx = createProjectContext(ctx.root, ctx.config, pluginConfig);
1126
+ for await (const route of plugin.discoverRoutes(projectCtx)) {
1127
+ ctx.routes.set(route.id, route);
1128
+ ctx.graph.addNode(
1129
+ route.id,
1130
+ createDefaultNodeAttributes({
1131
+ type: route.type,
1132
+ path: route.urlPath,
1133
+ filePath: route.filePath,
1134
+ segment: route.segment,
1135
+ isDynamic: route.isDynamic,
1136
+ isCatchAll: route.isCatchAll,
1137
+ isOptionalCatchAll: route.isOptionalCatchAll,
1138
+ isParallelSlot: route.isParallelSlot,
1139
+ slotName: route.slotName,
1140
+ isIntercepted: route.isIntercepted,
1141
+ interceptLevel: route.interceptLevel,
1142
+ isRouteGroup: route.isRouteGroup,
1143
+ groupName: route.groupName,
1144
+ isClientComponent: false,
1145
+ isServerComponent: true,
1146
+ tags: route.tags,
1147
+ loc: { filePath: route.filePath, line: 1, column: 1 }
1148
+ })
1149
+ );
1150
+ }
1151
+ }
1152
+ }
1153
+ };
1154
+ var GraphBuildStage = class {
1155
+ name = "GraphBuildStage";
1156
+ async run(ctx) {
1157
+ for (const route of ctx.routes.values()) {
1158
+ if (route.parentLayoutId && ctx.graph.hasNode(route.parentLayoutId)) {
1159
+ ctx.graph.addEdge(
1160
+ `layout-parent:${route.parentLayoutId}->${route.id}`,
1161
+ route.parentLayoutId,
1162
+ route.id,
1163
+ createDefaultEdgeAttributes2({
1164
+ type: "layout-parent",
1165
+ source: "unknown"
1166
+ })
1167
+ );
1168
+ }
1169
+ if (route.parentTemplateId && ctx.graph.hasNode(route.parentTemplateId)) {
1170
+ ctx.graph.addEdge(
1171
+ `template-parent:${route.parentTemplateId}->${route.id}`,
1172
+ route.parentTemplateId,
1173
+ route.id,
1174
+ createDefaultEdgeAttributes2({
1175
+ type: "template-parent",
1176
+ source: "unknown"
1177
+ })
1178
+ );
1179
+ }
1180
+ }
1181
+ }
1182
+ };
1183
+
1184
+ // src/pipeline/stages/SemanticStage.ts
1185
+ var SemanticStage = class {
1186
+ name = "SemanticStage";
1187
+ async run(_ctx) {
1188
+ }
1189
+ };
1190
+
1191
+ // src/pipeline/stages/StaticAnalysisStage.ts
1192
+ var StaticAnalysisStage = class {
1193
+ name = "StaticAnalysisStage";
1194
+ async run(ctx) {
1195
+ const diagnostics = [];
1196
+ diagnostics.push(...this.checkDeadRoutes(ctx));
1197
+ diagnostics.push(...this.checkBrokenLinks(ctx));
1198
+ diagnostics.push(...this.checkRedirectCycles(ctx));
1199
+ diagnostics.push(...this.checkCircularNavigation(ctx));
1200
+ diagnostics.push(...this.checkDuplicateRoutes(ctx));
1201
+ diagnostics.push(...this.checkOpenRedirects(ctx));
1202
+ for (const plugin of ctx.plugins) {
1203
+ const projectCtx = createProjectContext(
1204
+ ctx.root,
1205
+ ctx.config,
1206
+ ctx.pluginConfigs.get(plugin.id) ?? {}
1207
+ );
1208
+ const pluginDiags = await plugin.runDiagnostics(ctx.graph);
1209
+ diagnostics.push(...pluginDiags);
1210
+ }
1211
+ for (const diag of diagnostics) {
1212
+ if (diag.nodeId && ctx.graph.hasNode(diag.nodeId)) {
1213
+ const node = ctx.graph.getNode(diag.nodeId);
1214
+ if (node) {
1215
+ ctx.graph.addNode(diag.nodeId, {
1216
+ ...node,
1217
+ diagnostics: [...node.diagnostics, diag],
1218
+ isDead: diag.ruleId === "dead-route" ? true : node.isDead
1219
+ });
1220
+ }
1221
+ }
1222
+ }
1223
+ ctx.diagnostics = diagnostics;
1224
+ }
1225
+ checkDeadRoutes(ctx) {
1226
+ const dead = findDeadRoutes(ctx.graph);
1227
+ return dead.map((nodeId) => {
1228
+ const node = ctx.graph.getNode(nodeId);
1229
+ return {
1230
+ ruleId: "dead-route",
1231
+ severity: "warning",
1232
+ message: `Route "${node?.path ?? nodeId}" appears to be unreachable`,
1233
+ nodeId,
1234
+ loc: node?.loc
1235
+ };
1236
+ });
1237
+ }
1238
+ checkBrokenLinks(ctx) {
1239
+ const diags = [];
1240
+ for (const edge of ctx.graph.getAllEdges()) {
1241
+ const attrs = ctx.graph.getUnderlyingGraph().getEdgeAttributes(edge.id);
1242
+ if (attrs.type !== "navigation" && attrs.type !== "conditional-navigation" && attrs.type !== "prefetch") {
1243
+ continue;
1244
+ }
1245
+ if (attrs.isExternal) continue;
1246
+ const targetNode = ctx.graph.getNode(edge.target);
1247
+ if (!targetNode && !attrs.conditions.some((c) => c.kind === "unknown")) {
1248
+ diags.push({
1249
+ ruleId: "broken-link",
1250
+ severity: "error",
1251
+ message: "Navigation target does not resolve to a known route",
1252
+ edgeId: edge.id,
1253
+ loc: attrs.loc
1254
+ });
1255
+ }
1256
+ }
1257
+ return diags;
1258
+ }
1259
+ checkRedirectCycles(ctx) {
1260
+ const cycles = detectInfiniteRedirects(ctx.graph);
1261
+ return cycles.map((cycle) => ({
1262
+ ruleId: "redirect-cycle",
1263
+ severity: "error",
1264
+ message: `Redirect cycle detected: ${cycle.nodes.join(" -> ")}`,
1265
+ nodeId: cycle.nodes[0]
1266
+ }));
1267
+ }
1268
+ checkCircularNavigation(ctx) {
1269
+ const cycles = findCycles(ctx.graph);
1270
+ return cycles.map((cycle) => ({
1271
+ ruleId: "circular-navigation",
1272
+ severity: "warning",
1273
+ message: `Navigation cycle detected: ${cycle.nodes.join(" -> ")}`,
1274
+ nodeId: cycle.nodes[0]
1275
+ }));
1276
+ }
1277
+ checkDuplicateRoutes(ctx) {
1278
+ const pathMap = /* @__PURE__ */ new Map();
1279
+ for (const nodeId of ctx.graph.getAllNodeIds()) {
1280
+ const node = ctx.graph.getNode(nodeId);
1281
+ if (!node || node.type !== "route") continue;
1282
+ const existing = pathMap.get(node.path) ?? [];
1283
+ existing.push(nodeId);
1284
+ pathMap.set(node.path, existing);
1285
+ }
1286
+ const diags = [];
1287
+ for (const [path, ids] of pathMap) {
1288
+ if (ids.length > 1) {
1289
+ diags.push({
1290
+ ruleId: "duplicate-route",
1291
+ severity: "error",
1292
+ message: `Duplicate route path "${path}" found in ${ids.length} locations`,
1293
+ nodeId: ids[0]
1294
+ });
1295
+ }
1296
+ }
1297
+ return diags;
1298
+ }
1299
+ checkOpenRedirects(ctx) {
1300
+ const diags = [];
1301
+ const redirectTypes = ["redirect", "permanent-redirect"];
1302
+ for (const edge of ctx.graph.getAllEdges()) {
1303
+ const attrs = ctx.graph.getUnderlyingGraph().getEdgeAttributes(edge.id);
1304
+ if (!redirectTypes.includes(attrs.type)) continue;
1305
+ const hasDynamicCondition = attrs.conditions.some(
1306
+ (c) => c.expression.includes("searchParams") || c.expression.includes("query")
1307
+ );
1308
+ if (hasDynamicCondition) {
1309
+ diags.push({
1310
+ ruleId: "open-redirect",
1311
+ severity: "warning",
1312
+ message: "Potential open redirect: destination may be user-controlled",
1313
+ edgeId: edge.id,
1314
+ loc: attrs.loc
1315
+ });
1316
+ }
1317
+ }
1318
+ return diags;
1319
+ }
1320
+ };
1321
+ var MetricsStage = class {
1322
+ name = "MetricsStage";
1323
+ async run(ctx) {
1324
+ const { metricsToMetadata: metricsToMetadata3 } = await import("./metrics-SMPFGQCR.js");
1325
+ ctx.metadata = metricsToMetadata3(
1326
+ ctx.graph,
1327
+ ctx.plugins.map((p) => p.id)
1328
+ );
1329
+ }
1330
+ };
1331
+ var OutputStage = class {
1332
+ name = "OutputStage";
1333
+ async run(_ctx) {
1334
+ }
1335
+ };
1336
+
1337
+ // src/pipeline/Pipeline.ts
1338
+ var Pipeline = class {
1339
+ stages;
1340
+ constructor(stages) {
1341
+ this.stages = stages ?? [
1342
+ new FileSystemStage(),
1343
+ new ParseStage(),
1344
+ new SemanticStage(),
1345
+ new RouteDiscoveryStage(),
1346
+ new GraphBuildStage(),
1347
+ new NavigationAnalysisStage(),
1348
+ new MiddlewareAnalysisStage(),
1349
+ new ConditionalAnalysisStage(),
1350
+ new GraphEnrichmentStage(),
1351
+ new StaticAnalysisStage(),
1352
+ new MetricsStage(),
1353
+ new OutputStage()
1354
+ ];
1355
+ }
1356
+ async run(ctx) {
1357
+ for (const stage of this.stages) {
1358
+ await stage.run(ctx);
1359
+ }
1360
+ return {
1361
+ diagnostics: ctx.diagnostics ?? [],
1362
+ metadata: ctx.metadata ?? {
1363
+ pluginIds: ctx.plugins.map((p) => p.id),
1364
+ totalRoutes: 0,
1365
+ totalLayouts: 0,
1366
+ totalApiRoutes: 0,
1367
+ deadRouteCount: 0,
1368
+ cycleCount: 0
1369
+ }
1370
+ };
1371
+ }
1372
+ };
1373
+
1374
+ // src/plugin/PluginRegistry.ts
1375
+ var PluginRegistry = class {
1376
+ plugins = [];
1377
+ register(plugin) {
1378
+ this.plugins.push(plugin);
1379
+ }
1380
+ registerAll(plugins) {
1381
+ for (const plugin of plugins) {
1382
+ this.register(plugin);
1383
+ }
1384
+ }
1385
+ getAll() {
1386
+ return [...this.plugins];
1387
+ }
1388
+ async detectActive(root, config) {
1389
+ const active = [];
1390
+ for (const plugin of this.plugins) {
1391
+ const ctx = { root, config, pluginConfig: {} };
1392
+ if (await plugin.detect(ctx)) {
1393
+ active.push(plugin);
1394
+ }
1395
+ }
1396
+ return active;
1397
+ }
1398
+ };
1399
+
1400
+ // src/analyzer.ts
1401
+ var RouteAnalyzer = class {
1402
+ constructor(config) {
1403
+ this.config = config;
1404
+ this.registry.registerAll(config.plugins);
1405
+ }
1406
+ config;
1407
+ registry = new PluginRegistry();
1408
+ async analyze() {
1409
+ const plugins = await this.registry.detectActive(this.config.root, this.config);
1410
+ const graph = new RouteGraph();
1411
+ const ctx = {
1412
+ root: this.config.root,
1413
+ config: this.config,
1414
+ graph,
1415
+ plugins,
1416
+ files: [],
1417
+ semanticFiles: /* @__PURE__ */ new Map(),
1418
+ routes: /* @__PURE__ */ new Map(),
1419
+ pluginConfigs: /* @__PURE__ */ new Map()
1420
+ };
1421
+ for (const plugin of plugins) {
1422
+ const projectCtx = { root: this.config.root, config: this.config, pluginConfig: {} };
1423
+ const pluginConfig = await plugin.configure(projectCtx);
1424
+ ctx.pluginConfigs.set(plugin.id, pluginConfig);
1425
+ }
1426
+ const pipeline = new Pipeline();
1427
+ const { diagnostics, metadata } = await pipeline.run(ctx);
1428
+ return { graph, diagnostics, metadata };
1429
+ }
1430
+ watch() {
1431
+ const emitter = new EventEmitter();
1432
+ const cacheDir = this.config.cache?.directory ?? ".route-intelligence";
1433
+ const cache = new IncrementalCache(cacheDir);
1434
+ const invalidator = new Invalidator(cache);
1435
+ let previousSnapshot = cache.getGraphSnapshot();
1436
+ let watcher = null;
1437
+ const runAnalysis = async () => {
1438
+ try {
1439
+ const result = await this.analyze();
1440
+ const routeGraph = result.graph;
1441
+ const snapshot = routeGraph.toJSON(this.config.root, result.metadata);
1442
+ cache.setGraphSnapshot(snapshot);
1443
+ cache.save();
1444
+ if (previousSnapshot) {
1445
+ const patch = computeGraphPatch(previousSnapshot, snapshot);
1446
+ emitter.emit("update", patch);
1447
+ }
1448
+ previousSnapshot = snapshot;
1449
+ } catch (error) {
1450
+ emitter.emit("error", error instanceof Error ? error : new Error(String(error)));
1451
+ }
1452
+ };
1453
+ watcher = chokidar.watch(this.config.root, {
1454
+ ignored: this.config.exclude ?? ["**/node_modules/**", "**/.next/**"],
1455
+ ignoreInitial: true
1456
+ });
1457
+ watcher.on("all", async (event, path) => {
1458
+ const type = event === "add" ? "add" : event === "unlink" ? "unlink" : "change";
1459
+ invalidator.computeInvalidation({ type, path });
1460
+ await runAnalysis();
1461
+ });
1462
+ void runAnalysis();
1463
+ emitter.stop = async () => {
1464
+ await watcher?.close();
1465
+ };
1466
+ return emitter;
1467
+ }
1468
+ };
1469
+ function createAnalyzer(config) {
1470
+ return new RouteAnalyzer(config);
1471
+ }
1472
+ export {
1473
+ IncrementalCache,
1474
+ Invalidator,
1475
+ Pipeline,
1476
+ PluginRegistry,
1477
+ RouteGraph,
1478
+ computeGraphPatch,
1479
+ computeMetrics,
1480
+ createAnalyzer,
1481
+ createSemanticFile,
1482
+ defineConfig,
1483
+ detectInfiniteRedirects,
1484
+ exportDot,
1485
+ exportHtml,
1486
+ exportJson,
1487
+ exportMarkdown,
1488
+ exportMermaid,
1489
+ exportPlantUML,
1490
+ findCycles,
1491
+ findDeadRoutes,
1492
+ findShortestPath,
1493
+ getMostConnected,
1494
+ metricsToMetadata
1495
+ };