next-route-graph 1.0.2 → 1.2.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.
Files changed (74) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/CONTRIBUTING.md +74 -0
  3. package/README.md +169 -134
  4. package/SECURITY.md +19 -0
  5. package/dist/cli.d.ts +3 -0
  6. package/dist/cli.d.ts.map +1 -0
  7. package/dist/cli.js +126 -44
  8. package/dist/core/analyzer.d.ts +16 -0
  9. package/dist/core/analyzer.d.ts.map +1 -0
  10. package/dist/core/analyzer.js +113 -29
  11. package/dist/core/detector.d.ts +19 -0
  12. package/dist/core/detector.d.ts.map +1 -0
  13. package/dist/core/detector.js +31 -3
  14. package/dist/core/inspect.d.ts +6 -0
  15. package/dist/core/inspect.d.ts.map +1 -0
  16. package/dist/core/inspect.js +21 -0
  17. package/dist/core/linter.d.ts +11 -0
  18. package/dist/core/linter.d.ts.map +1 -0
  19. package/dist/core/linter.js +194 -0
  20. package/dist/core/parser.d.ts +95 -0
  21. package/dist/core/parser.d.ts.map +1 -0
  22. package/dist/core/parser.js +242 -15
  23. package/dist/core/scanner.d.ts +12 -0
  24. package/dist/core/scanner.d.ts.map +1 -0
  25. package/dist/core/scanner.js +32 -6
  26. package/dist/errors/index.d.ts +35 -0
  27. package/dist/errors/index.d.ts.map +1 -0
  28. package/dist/errors/index.js +65 -0
  29. package/dist/index.d.ts +18 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +17 -0
  32. package/dist/preview.d.ts +4 -0
  33. package/dist/preview.d.ts.map +1 -0
  34. package/dist/preview.js +25 -0
  35. package/dist/render/dot.d.ts +13 -0
  36. package/dist/render/dot.d.ts.map +1 -0
  37. package/dist/render/dot.js +66 -0
  38. package/dist/render/flat.d.ts +13 -0
  39. package/dist/render/flat.d.ts.map +1 -0
  40. package/dist/render/flat.js +14 -2
  41. package/dist/render/graph.d.ts +13 -0
  42. package/dist/render/graph.d.ts.map +1 -0
  43. package/dist/render/graph.js +53 -0
  44. package/dist/render/html.d.ts +8 -0
  45. package/dist/render/html.d.ts.map +1 -0
  46. package/dist/render/html.js +718 -0
  47. package/dist/render/index.d.ts +20 -0
  48. package/dist/render/index.d.ts.map +1 -0
  49. package/dist/render/index.js +37 -0
  50. package/dist/render/json.d.ts +13 -0
  51. package/dist/render/json.d.ts.map +1 -0
  52. package/dist/render/json.js +12 -14
  53. package/dist/render/markdown.d.ts +13 -0
  54. package/dist/render/markdown.d.ts.map +1 -0
  55. package/dist/render/markdown.js +59 -0
  56. package/dist/render/mermaid.d.ts +13 -0
  57. package/dist/render/mermaid.d.ts.map +1 -0
  58. package/dist/render/mermaid.js +64 -0
  59. package/dist/render/renderer.interface.d.ts +8 -0
  60. package/dist/render/renderer.interface.d.ts.map +1 -0
  61. package/dist/render/renderer.interface.js +1 -0
  62. package/dist/render/svg.d.ts +8 -0
  63. package/dist/render/svg.d.ts.map +1 -0
  64. package/dist/render/svg.js +76 -0
  65. package/dist/render/tree.d.ts +13 -0
  66. package/dist/render/tree.d.ts.map +1 -0
  67. package/dist/render/tree.js +57 -7
  68. package/dist/types/index.d.ts +132 -0
  69. package/dist/types/index.d.ts.map +1 -0
  70. package/docs/ARCHITECTURE.md +71 -0
  71. package/docs/NEW_FEATURES.md +16 -0
  72. package/docs/adr/0001-offline-reports-and-shared-route-graph.md +29 -0
  73. package/docs/route-report.png +0 -0
  74. package/package.json +71 -52
@@ -0,0 +1,20 @@
1
+ import { OutputFormat } from "../types/index.js";
2
+ import { DotRenderer, renderDot } from "./dot.js";
3
+ import { FlatRenderer, renderFlat } from "./flat.js";
4
+ import { HtmlRenderer, renderHtml } from "./html.js";
5
+ import { JsonRenderer, renderJson } from "./json.js";
6
+ import { MarkdownRenderer, renderMarkdown } from "./markdown.js";
7
+ import { MermaidRenderer, renderMermaid } from "./mermaid.js";
8
+ import type { OutputRenderer } from "./renderer.interface.js";
9
+ import { renderTree, TreeRenderer } from "./tree.js";
10
+ import { renderSvg, SvgRenderer } from "./svg.js";
11
+ export { DotRenderer, FlatRenderer, HtmlRenderer, JsonRenderer, MarkdownRenderer, MermaidRenderer, renderDot, renderFlat, renderHtml, renderJson, renderMarkdown, renderMermaid, renderTree, TreeRenderer, renderSvg, SvgRenderer, };
12
+ export type { OutputRenderer };
13
+ /**
14
+ * Factory function to retrieve an OutputRenderer instance by format name (Strategy Pattern).
15
+ *
16
+ * @param format - Output format name (tree, flat, json, mermaid, dot, markdown, html, svg)
17
+ * @returns Configured OutputRenderer implementation
18
+ */
19
+ export declare function getRenderer(format: OutputFormat): OutputRenderer;
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/render/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAElD,OAAO,EACL,WAAW,EACX,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,gBAAgB,EAChB,eAAe,EACf,SAAS,EACT,UAAU,EACV,UAAU,EACV,UAAU,EACV,cAAc,EACd,aAAa,EACb,UAAU,EACV,YAAY,EACZ,SAAS,EACT,WAAW,GACZ,CAAC;AAEF,YAAY,EAAE,cAAc,EAAE,CAAC;AAE/B;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,YAAY,GAAG,cAAc,CAqBhE"}
@@ -0,0 +1,37 @@
1
+ import { DotRenderer, renderDot } from "./dot.js";
2
+ import { FlatRenderer, renderFlat } from "./flat.js";
3
+ import { HtmlRenderer, renderHtml } from "./html.js";
4
+ import { JsonRenderer, renderJson } from "./json.js";
5
+ import { MarkdownRenderer, renderMarkdown } from "./markdown.js";
6
+ import { MermaidRenderer, renderMermaid } from "./mermaid.js";
7
+ import { renderTree, TreeRenderer } from "./tree.js";
8
+ import { renderSvg, SvgRenderer } from "./svg.js";
9
+ export { DotRenderer, FlatRenderer, HtmlRenderer, JsonRenderer, MarkdownRenderer, MermaidRenderer, renderDot, renderFlat, renderHtml, renderJson, renderMarkdown, renderMermaid, renderTree, TreeRenderer, renderSvg, SvgRenderer, };
10
+ /**
11
+ * Factory function to retrieve an OutputRenderer instance by format name (Strategy Pattern).
12
+ *
13
+ * @param format - Output format name (tree, flat, json, mermaid, dot, markdown, html, svg)
14
+ * @returns Configured OutputRenderer implementation
15
+ */
16
+ export function getRenderer(format) {
17
+ switch (format) {
18
+ case "flat":
19
+ return new FlatRenderer();
20
+ case "json":
21
+ return new JsonRenderer();
22
+ case "mermaid":
23
+ return new MermaidRenderer();
24
+ case "dot":
25
+ return new DotRenderer();
26
+ case "markdown":
27
+ return new MarkdownRenderer();
28
+ case "html":
29
+ return new HtmlRenderer();
30
+ case "svg":
31
+ return new SvgRenderer();
32
+ case "tree":
33
+ return new TreeRenderer();
34
+ default:
35
+ throw new Error(`Unsupported output format: ${String(format)}`);
36
+ }
37
+ }
@@ -0,0 +1,13 @@
1
+ import { AnalysisResult } from "../types/index.js";
2
+ import type { OutputRenderer } from "./renderer.interface.js";
3
+ /**
4
+ * Formats analysis result as structured, indented JSON.
5
+ *
6
+ * @param result - AnalysisResult object
7
+ * @returns Serialized JSON string
8
+ */
9
+ export declare function renderJson(result: AnalysisResult): string;
10
+ export declare class JsonRenderer implements OutputRenderer {
11
+ render(result: AnalysisResult): string;
12
+ }
13
+ //# sourceMappingURL=json.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"json.d.ts","sourceRoot":"","sources":["../../src/render/json.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAEzD;AAED,qBAAa,YAAa,YAAW,cAAc;IACjD,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM;CAGvC"}
@@ -1,16 +1,14 @@
1
+ /**
2
+ * Formats analysis result as structured, indented JSON.
3
+ *
4
+ * @param result - AnalysisResult object
5
+ * @returns Serialized JSON string
6
+ */
1
7
  export function renderJson(result) {
2
- const output = {
3
- project: result.project,
4
- router: "app",
5
- root: result.root,
6
- routes: result.routes.map((r) => ({
7
- path: r.path,
8
- type: r.type,
9
- file: r.file,
10
- segmentType: r.segmentType,
11
- rendering: r.rendering,
12
- })),
13
- summary: result.summary,
14
- };
15
- return JSON.stringify(output, null, 2);
8
+ return JSON.stringify(result, null, 2);
9
+ }
10
+ export class JsonRenderer {
11
+ render(result) {
12
+ return renderJson(result);
13
+ }
16
14
  }
@@ -0,0 +1,13 @@
1
+ import { AnalysisResult } from "../types/index.js";
2
+ import type { OutputRenderer } from "./renderer.interface.js";
3
+ /**
4
+ * Formats the route analysis result as a GitHub-flavored Markdown document with a table.
5
+ *
6
+ * @param result - AnalysisResult
7
+ * @returns Formatted Markdown string
8
+ */
9
+ export declare function renderMarkdown(result: AnalysisResult): string;
10
+ export declare class MarkdownRenderer implements OutputRenderer {
11
+ render(result: AnalysisResult): string;
12
+ }
13
+ //# sourceMappingURL=markdown.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../src/render/markdown.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAS9D;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAyD7D;AAED,qBAAa,gBAAiB,YAAW,cAAc;IACrD,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM;CAGvC"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Escapes pipe characters so Markdown table cells do not break table layout.
3
+ */
4
+ function escapeCell(val) {
5
+ return val.replace(/\|/g, "\\|");
6
+ }
7
+ /**
8
+ * Formats the route analysis result as a GitHub-flavored Markdown document with a table.
9
+ *
10
+ * @param result - AnalysisResult
11
+ * @returns Formatted Markdown string
12
+ */
13
+ export function renderMarkdown(result) {
14
+ const lines = [];
15
+ lines.push(`### Next.js Route Graph: \`${result.project}\``);
16
+ lines.push("");
17
+ lines.push(`- **Router:** ${result.router}`);
18
+ lines.push(`- **Root:** \`${result.root}\``);
19
+ lines.push(`- **Total Routes:** ${result.summary.routes}`);
20
+ lines.push(`- **Breakdown:** ${result.summary.server} Server, ${result.summary.client} Client, ${result.summary.dynamic} Dynamic`);
21
+ lines.push("");
22
+ lines.push("| Route Path | Type | Segment | Rendering | Methods / Details | File |");
23
+ lines.push("| :--- | :--- | :--- | :--- | :--- | :--- |");
24
+ for (const route of result.routes) {
25
+ const methods = route.methods && route.methods.length > 0
26
+ ? `\`${escapeCell(route.methods.join(", "))}\``
27
+ : "-";
28
+ const segment = `\`${escapeCell(route.segmentType)}\``;
29
+ const rendering = route.rendering === "client" ? "**Client**" : "Server";
30
+ const file = `\`${escapeCell(route.file)}\``;
31
+ lines.push(`| \`${escapeCell(route.path)}\` | ${route.type} | ${segment} | ${rendering} | ${methods} | ${file} |`);
32
+ }
33
+ if (result.routes.length === 0) {
34
+ lines.push("| *(No routes discovered)* | - | - | - | - | - |");
35
+ }
36
+ if (result.lint && result.lint.issues.length > 0) {
37
+ lines.push("");
38
+ lines.push("#### Route Health Warnings & Issues");
39
+ lines.push("");
40
+ lines.push("| Severity | Rule | Message | File / Location |");
41
+ lines.push("| :--- | :--- | :--- | :--- |");
42
+ for (const issue of result.lint.issues) {
43
+ const badge = issue.severity === "error"
44
+ ? "🛑 Error"
45
+ : issue.severity === "warning"
46
+ ? "⚠️ Warning"
47
+ : "ℹ️ Info";
48
+ const loc = issue.filePath ? `\`${escapeCell(issue.filePath)}\`` : "-";
49
+ lines.push(`| ${badge} | \`${escapeCell(issue.rule)}\` | ${escapeCell(issue.message.replace(/\n/g, " "))} | ${loc} |`);
50
+ }
51
+ }
52
+ lines.push("");
53
+ return lines.join("\n");
54
+ }
55
+ export class MarkdownRenderer {
56
+ render(result) {
57
+ return renderMarkdown(result);
58
+ }
59
+ }
@@ -0,0 +1,13 @@
1
+ import { AnalysisResult } from "../types/index.js";
2
+ import type { OutputRenderer } from "./renderer.interface.js";
3
+ /**
4
+ * Generates a Mermaid flowchart diagram representing the App Router routes.
5
+ *
6
+ * @param result - AnalysisResult
7
+ * @returns Mermaid flowchart markdown block
8
+ */
9
+ export declare function renderMermaid(result: AnalysisResult): string;
10
+ export declare class MermaidRenderer implements OutputRenderer {
11
+ render(result: AnalysisResult): string;
12
+ }
13
+ //# sourceMappingURL=mermaid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mermaid.d.ts","sourceRoot":"","sources":["../../src/render/mermaid.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAc,MAAM,mBAAmB,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAgC9D;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA+B5D;AAED,qBAAa,eAAgB,YAAW,cAAc;IACpD,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM;CAGvC"}
@@ -0,0 +1,64 @@
1
+ import { buildRouteGraph } from "./graph.js";
2
+ /**
3
+ * Creates a clean Mermaid node label for a route.
4
+ * Replaces square brackets with HTML character entities to avoid breaking Mermaid node shape syntax.
5
+ */
6
+ function createNodeLabel(route) {
7
+ const parts = [route.path];
8
+ if (route.methods && route.methods.length > 0) {
9
+ parts.push(`(${route.methods.join("/")})`);
10
+ }
11
+ else if (route.rendering === "client") {
12
+ parts.push("(client)");
13
+ }
14
+ else if (route.segmentType !== "static") {
15
+ parts.push(`(${route.segmentType})`);
16
+ }
17
+ // Escape square brackets and quotation marks for safe Mermaid rendering
18
+ return parts
19
+ .join(" ")
20
+ .replace(/&/g, "#38;")
21
+ .replace(/</g, "#60;")
22
+ .replace(/>/g, "#62;")
23
+ .replace(/\r?\n/g, " ")
24
+ .replace(/\[/g, "#91;")
25
+ .replace(/\]/g, "#93;")
26
+ .replace(/"/g, "#quot;");
27
+ }
28
+ /**
29
+ * Generates a Mermaid flowchart diagram representing the App Router routes.
30
+ *
31
+ * @param result - AnalysisResult
32
+ * @returns Mermaid flowchart markdown block
33
+ */
34
+ export function renderMermaid(result) {
35
+ const lines = [];
36
+ lines.push("```mermaid");
37
+ lines.push("flowchart TD");
38
+ lines.push(` %% Next.js Route Graph for ${result.project.replace(/[\r\n]/g, " ")}`);
39
+ const routes = result.routes;
40
+ if (routes.length === 0) {
41
+ lines.push(' empty["(No routes discovered)"]');
42
+ lines.push("```");
43
+ return lines.join("\n");
44
+ }
45
+ for (const node of buildRouteGraph(routes)) {
46
+ const label = node.routes.length
47
+ ? node.routes.map(createNodeLabel).join(" | ")
48
+ : createNodeLabel({
49
+ path: node.path,
50
+ rendering: "server",
51
+ segmentType: "static",
52
+ });
53
+ lines.push(` ${node.id}["${label}"]`);
54
+ if (node.parentId)
55
+ lines.push(` ${node.parentId} --> ${node.id}`);
56
+ }
57
+ lines.push("```");
58
+ return lines.join("\n");
59
+ }
60
+ export class MermaidRenderer {
61
+ render(result) {
62
+ return renderMermaid(result);
63
+ }
64
+ }
@@ -0,0 +1,8 @@
1
+ import { AnalysisResult } from "../types/index.js";
2
+ /**
3
+ * Common interface for all output formatters (Strategy Pattern)
4
+ */
5
+ export interface OutputRenderer {
6
+ render(result: AnalysisResult): string;
7
+ }
8
+ //# sourceMappingURL=renderer.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.interface.d.ts","sourceRoot":"","sources":["../../src/render/renderer.interface.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAAC;CACxC"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import type { AnalysisResult } from "../types/index.js";
2
+ import type { OutputRenderer } from "./renderer.interface.js";
3
+ /** A real, offline vector diagram suitable for reports, READMEs, and design tools. */
4
+ export declare function renderSvg(result: AnalysisResult): string;
5
+ export declare class SvgRenderer implements OutputRenderer {
6
+ render(result: AnalysisResult): string;
7
+ }
8
+ //# sourceMappingURL=svg.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"svg.d.ts","sourceRoot":"","sources":["../../src/render/svg.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,sFAAsF;AACtF,wBAAgB,SAAS,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAgFxD;AAED,qBAAa,WAAY,YAAW,cAAc;IAChD,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM;CAGvC"}
@@ -0,0 +1,76 @@
1
+ import { buildRouteGraph, escapeMarkup } from "./graph.js";
2
+ /** A real, offline vector diagram suitable for reports, READMEs, and design tools. */
3
+ export function renderSvg(result) {
4
+ const nodes = buildRouteGraph(result.routes);
5
+ const rowHeight = 70;
6
+ const width = Math.max(760, ...nodes.map((node) => 340 + node.depth * 28 + Array.from(node.path).length * 9));
7
+ const height = Math.max(180, 110 + nodes.length * rowHeight);
8
+ const rows = new Map(nodes.map((node, index) => [node.id, index]));
9
+ const edges = nodes
10
+ .filter((node) => node.parentId)
11
+ .map((node) => {
12
+ const parentRow = rows.get(node.parentId);
13
+ const row = rows.get(node.id);
14
+ const x = 30 + (node.depth - 1) * 28;
15
+ return `<path d="M${x},${100 + parentRow * rowHeight} V${100 + row * rowHeight} H${x + 28}" fill="none" stroke="#475569"/>`;
16
+ })
17
+ .join("\n");
18
+ const cards = nodes
19
+ .map((node, index) => {
20
+ const x = 18 + node.depth * 28;
21
+ const y = 76 + index * rowHeight;
22
+ const route = node.routes[0];
23
+ const color = !route
24
+ ? "#64748b"
25
+ : route.type === "route"
26
+ ? "#d8b4fe"
27
+ : route.rendering === "client"
28
+ ? "#67e8f9"
29
+ : "#a5b4fc";
30
+ const description = node.routes.length === 0
31
+ ? "segment · no endpoint"
32
+ : node.routes
33
+ .map((route) => [
34
+ route.type === "route"
35
+ ? `API ${(route.methods ?? []).join(" / ")}`
36
+ : `${route.rendering} page`,
37
+ route.segmentType !== "static" ? route.segmentType : "",
38
+ ...(route.parallelSlots ?? []),
39
+ route.intercepting ? "intercepted" : "",
40
+ ]
41
+ .filter(Boolean)
42
+ .join(" · "))
43
+ .join(" | ");
44
+ const title = [
45
+ node.path,
46
+ ...node.routes.map((route) => `${route.file}${route.layoutChain?.length ? ` — layouts: ${route.layoutChain.join(" → ")}` : ""}`),
47
+ ].join("\n");
48
+ const maxDescription = Math.floor((width - x - 50) / 7);
49
+ const shortDescription = description.length > maxDescription
50
+ ? description.slice(0, maxDescription - 1) + "…"
51
+ : description;
52
+ return `<g data-node-index="${index}" tabindex="0" role="button" aria-label="${escapeMarkup(title)}">
53
+ <title>${escapeMarkup(title)}</title>
54
+ <rect x="${x}" y="${y}" width="${width - x - 18}" height="54" rx="8" fill="#172033" stroke="#334155"/>
55
+ <rect x="${x}" y="${y + 8}" width="3" height="38" rx="1" fill="${color}"/>
56
+ <text x="${x + 14}" y="${y + 22}" fill="${color}" font-size="14">${escapeMarkup(node.path)}</text>
57
+ <text x="${x + 14}" y="${y + 41}" fill="#cbd5e1" font-size="11">${escapeMarkup(shortDescription)}</text>
58
+ </g>`;
59
+ })
60
+ .join("\n");
61
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-labelledby="graph-title graph-description">
62
+ <title id="graph-title">${escapeMarkup(result.project)} — route map</title>
63
+ <desc id="graph-description">URL segment hierarchy. Source files, slots, and intercepted variants are preserved. This shows route structure, not imports or navigation.</desc>
64
+ <style>text{font-family:ui-monospace,Consolas,monospace}g:focus rect:first-of-type,g:hover rect:first-of-type{stroke:#67e8f9;stroke-width:2}g{cursor:pointer;outline:none}</style>
65
+ <rect width="100%" height="100%" rx="12" fill="#0b1020"/>
66
+ <text x="22" y="32" fill="#f1f5f9" font-size="17">${escapeMarkup(result.project)} / route map</text>
67
+ <text x="22" y="54" fill="#94a3b8" font-size="12">${result.routes.length} endpoints · ${escapeMarkup(result.root)} · URL hierarchy</text>
68
+ ${edges}
69
+ ${cards || '<text x="22" y="106" fill="#94a3b8" font-size="14">No routes discovered</text>'}
70
+ </svg>`;
71
+ }
72
+ export class SvgRenderer {
73
+ render(result) {
74
+ return renderSvg(result);
75
+ }
76
+ }
@@ -0,0 +1,13 @@
1
+ import { AnalysisResult } from "../types/index.js";
2
+ import type { OutputRenderer } from "./renderer.interface.js";
3
+ /**
4
+ * Formats the route graph and project summary as a styled terminal tree.
5
+ *
6
+ * @param result - Analysis result data
7
+ * @returns Styled terminal string representation
8
+ */
9
+ export declare function renderTree(result: AnalysisResult): string;
10
+ export declare class TreeRenderer implements OutputRenderer {
11
+ render(result: AnalysisResult): string;
12
+ }
13
+ //# sourceMappingURL=tree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree.d.ts","sourceRoot":"","sources":["../../src/render/tree.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA2GzD;AAED,qBAAa,YAAa,YAAW,cAAc;IACjD,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM;CAGvC"}
@@ -1,4 +1,10 @@
1
1
  import pc from "picocolors";
2
+ /**
3
+ * Formats the route graph and project summary as a styled terminal tree.
4
+ *
5
+ * @param result - Analysis result data
6
+ * @returns Styled terminal string representation
7
+ */
2
8
  export function renderTree(result) {
3
9
  const lines = [];
4
10
  lines.push("");
@@ -11,21 +17,34 @@ export function renderTree(result) {
11
17
  lines.push(pc.bold("Routes"));
12
18
  lines.push(pc.dim("────────────────────────────────────────"));
13
19
  const routes = result.routes;
20
+ const maxPathLen = Math.max(28, ...routes.map((r) => r.path.length));
14
21
  routes.forEach((route, index) => {
15
22
  const isLast = index === routes.length - 1;
16
23
  const prefix = isLast ? "└── " : "├── ";
17
- const pathStr = route.path.padEnd(28, " ");
24
+ const pathStr = route.path.padEnd(maxPathLen, " ");
18
25
  const fileStr = pc.dim(route.file.split("/").pop() || "");
19
- let badge = "";
26
+ const badges = [];
27
+ // HTTP Methods for API handlers
28
+ if (route.methods && route.methods.length > 0) {
29
+ badges.push(pc.magenta(`[${route.methods.join(", ")}]`));
30
+ }
31
+ // Rendering or Segment Type
20
32
  if (route.rendering === "client") {
21
- const clientBadge = pc.cyan("[client]");
22
- badge = ` ${clientBadge}`;
33
+ badges.push(pc.cyan("[client]"));
23
34
  }
24
35
  else if (route.segmentType !== "static") {
25
- const segmentBadge = `[${route.segmentType}]`;
26
- badge = ` ${pc.yellow(segmentBadge)}`;
36
+ badges.push(pc.yellow(`[${route.segmentType}]`));
37
+ }
38
+ // Parallel route slots
39
+ if (route.parallelSlots && route.parallelSlots.length > 0) {
40
+ badges.push(pc.blue(`[${route.parallelSlots.join(", ")}]`));
41
+ }
42
+ // Intercepting route indicator
43
+ if (route.intercepting) {
44
+ badges.push(pc.magenta(`[${route.intercepting}]`));
27
45
  }
28
- const routeLine = `${pc.dim(prefix)}${pc.bold(pathStr)} ${fileStr}${badge}`;
46
+ const badgeStr = badges.length > 0 ? ` ${badges.join(" ")}` : "";
47
+ const routeLine = `${pc.dim(prefix)}${pc.bold(pathStr)} ${fileStr}${badgeStr}`;
29
48
  lines.push(routeLine);
30
49
  });
31
50
  if (routes.length === 0) {
@@ -41,8 +60,39 @@ export function renderTree(result) {
41
60
  lines.push(`${pc.dim("Layouts ")}${result.summary.layouts}`);
42
61
  lines.push(`${pc.dim("Loading ")}${result.summary.loading}`);
43
62
  lines.push(`${pc.dim("Errors ")}${result.summary.errors}`);
63
+ if (result.summary.apiRoutes && result.summary.apiRoutes > 0) {
64
+ lines.push(`${pc.dim("API Routes ")}${result.summary.apiRoutes}`);
65
+ }
66
+ if (result.summary.parallelRoutes && result.summary.parallelRoutes > 0) {
67
+ lines.push(`${pc.dim("Parallel ")}${result.summary.parallelRoutes}`);
68
+ }
69
+ if (result.summary.interceptedRoutes && result.summary.interceptedRoutes > 0) {
70
+ lines.push(`${pc.dim("Intercepted ")}${result.summary.interceptedRoutes}`);
71
+ }
72
+ // Display lint issues if present
73
+ if (result.lint && result.lint.issues.length > 0) {
74
+ lines.push("");
75
+ lines.push(pc.bold("Route Health"));
76
+ lines.push(pc.dim("────────────────────────────────────────"));
77
+ for (const issue of result.lint.issues) {
78
+ const tag = issue.severity === "error"
79
+ ? pc.red("✖ [error]")
80
+ : issue.severity === "warning"
81
+ ? pc.yellow("▲ [warning]")
82
+ : pc.blue("ℹ [info]");
83
+ lines.push(`${tag} ${pc.bold(issue.rule)}: ${issue.message}`);
84
+ if (issue.hint) {
85
+ lines.push(` ${pc.dim("hint: " + issue.hint)}`);
86
+ }
87
+ }
88
+ }
44
89
  lines.push("");
45
90
  lines.push(pc.green(`✓ Analysis completed in ${result.executionTimeMs}ms`));
46
91
  lines.push("");
47
92
  return lines.join("\n");
48
93
  }
94
+ export class TreeRenderer {
95
+ render(result) {
96
+ return renderTree(result);
97
+ }
98
+ }
@@ -0,0 +1,132 @@
1
+ /**
2
+ * Supported Next.js segment dynamics
3
+ */
4
+ export type SegmentType = "static" | "dynamic" | "catch-all" | "optional-catch-all";
5
+ /**
6
+ * Next.js App Router special convention files
7
+ */
8
+ export type SpecialFileType = "page" | "layout" | "loading" | "error" | "global-error" | "not-found" | "template" | "default" | "route";
9
+ /**
10
+ * React rendering environment
11
+ */
12
+ export type RenderingType = "client" | "server";
13
+ /**
14
+ * Standard HTTP methods supported in route handlers
15
+ */
16
+ export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS";
17
+ /**
18
+ * Discovered file in the App Router tree
19
+ */
20
+ export interface DiscoveredFile {
21
+ relativePath: string;
22
+ fullPath: string;
23
+ fileName: string;
24
+ extension: string;
25
+ }
26
+ /**
27
+ * App Router convention file entry
28
+ */
29
+ export interface RouteFile {
30
+ type: SpecialFileType;
31
+ fileName: string;
32
+ relativePath: string;
33
+ rendering: RenderingType;
34
+ methods?: HttpMethod[];
35
+ hasServerActions?: boolean;
36
+ runtime?: "nodejs" | "edge" | string;
37
+ dynamicConfig?: "auto" | "force-dynamic" | "error" | "force-static" | string;
38
+ }
39
+ /**
40
+ * Represents a resolved Next.js route endpoint (page or route handler)
41
+ */
42
+ export interface RouteEntry {
43
+ path: string;
44
+ type: "page" | "route";
45
+ file: string;
46
+ segmentType: SegmentType;
47
+ rendering: RenderingType;
48
+ methods?: HttpMethod[];
49
+ hasServerActions?: boolean;
50
+ runtime?: "nodejs" | "edge" | string;
51
+ dynamicConfig?: "auto" | "force-dynamic" | "error" | "force-static" | string;
52
+ files: RouteFile[];
53
+ parallelSlots?: string[];
54
+ intercepting?: string;
55
+ layoutChain?: string[];
56
+ }
57
+ /**
58
+ * Lint severity level
59
+ */
60
+ export type LintSeverity = "error" | "warning" | "info";
61
+ /**
62
+ * Lint issue discovered during route inspection
63
+ */
64
+ export interface LintIssue {
65
+ rule: string;
66
+ severity: LintSeverity;
67
+ message: string;
68
+ routePath?: string;
69
+ filePath?: string;
70
+ hint?: string;
71
+ }
72
+ /**
73
+ * Lint summary report
74
+ */
75
+ export interface LintResult {
76
+ issues: LintIssue[];
77
+ hasErrors: boolean;
78
+ warningCount: number;
79
+ errorCount: number;
80
+ infoCount?: number;
81
+ }
82
+ /**
83
+ * Summary statistics for the scanned project
84
+ */
85
+ export interface SummaryStats {
86
+ routes: number;
87
+ dynamic: number;
88
+ client: number;
89
+ server: number;
90
+ layouts: number;
91
+ loading: number;
92
+ errors: number;
93
+ apiRoutes?: number;
94
+ parallelRoutes?: number;
95
+ interceptedRoutes?: number;
96
+ }
97
+ /**
98
+ * Overall analysis output
99
+ */
100
+ export interface AnalysisResult {
101
+ project: string;
102
+ router: "App Router";
103
+ root: string;
104
+ routes: RouteEntry[];
105
+ summary: SummaryStats;
106
+ executionTimeMs: number;
107
+ lint?: LintResult;
108
+ }
109
+ /**
110
+ * Output format types
111
+ */
112
+ export type OutputFormat = "tree" | "flat" | "json" | "mermaid" | "dot" | "markdown" | "html" | "svg";
113
+ /**
114
+ * CLI command line options
115
+ */
116
+ export interface CLIOptions {
117
+ flat?: boolean;
118
+ json?: boolean;
119
+ mermaid?: boolean;
120
+ dot?: boolean;
121
+ markdown?: boolean;
122
+ svg?: boolean;
123
+ output?: string;
124
+ html?: boolean | string;
125
+ serve?: boolean | number | string;
126
+ format?: OutputFormat;
127
+ search?: string;
128
+ check?: boolean;
129
+ strict?: boolean;
130
+ methods?: boolean;
131
+ }
132
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}