next-route-graph 1.1.0 → 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 (47) hide show
  1. package/CHANGELOG.md +95 -0
  2. package/CONTRIBUTING.md +74 -0
  3. package/README.md +113 -189
  4. package/SECURITY.md +19 -0
  5. package/dist/cli.js +107 -80
  6. package/dist/core/analyzer.d.ts +5 -1
  7. package/dist/core/analyzer.d.ts.map +1 -1
  8. package/dist/core/analyzer.js +21 -14
  9. package/dist/core/inspect.d.ts +6 -0
  10. package/dist/core/inspect.d.ts.map +1 -0
  11. package/dist/core/inspect.js +21 -0
  12. package/dist/core/linter.d.ts.map +1 -1
  13. package/dist/core/linter.js +43 -23
  14. package/dist/core/parser.d.ts.map +1 -1
  15. package/dist/core/parser.js +31 -16
  16. package/dist/errors/index.d.ts +4 -0
  17. package/dist/errors/index.d.ts.map +1 -1
  18. package/dist/errors/index.js +8 -1
  19. package/dist/index.d.ts +4 -2
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +4 -2
  22. package/dist/preview.d.ts +4 -0
  23. package/dist/preview.d.ts.map +1 -0
  24. package/dist/preview.js +25 -0
  25. package/dist/render/dot.d.ts.map +1 -1
  26. package/dist/render/dot.js +25 -47
  27. package/dist/render/graph.d.ts +13 -0
  28. package/dist/render/graph.d.ts.map +1 -0
  29. package/dist/render/graph.js +53 -0
  30. package/dist/render/html.d.ts +8 -0
  31. package/dist/render/html.d.ts.map +1 -0
  32. package/dist/render/html.js +718 -0
  33. package/dist/render/index.d.ts +4 -2
  34. package/dist/render/index.d.ts.map +1 -1
  35. package/dist/render/index.js +10 -3
  36. package/dist/render/mermaid.d.ts.map +1 -1
  37. package/dist/render/mermaid.js +19 -43
  38. package/dist/render/svg.d.ts +8 -0
  39. package/dist/render/svg.d.ts.map +1 -0
  40. package/dist/render/svg.js +76 -0
  41. package/dist/types/index.d.ts +5 -1
  42. package/dist/types/index.d.ts.map +1 -1
  43. package/docs/ARCHITECTURE.md +71 -0
  44. package/docs/NEW_FEATURES.md +16 -0
  45. package/docs/adr/0001-offline-reports-and-shared-route-graph.md +29 -0
  46. package/docs/route-report.png +0 -0
  47. package/package.json +12 -4
package/dist/index.js CHANGED
@@ -5,11 +5,13 @@
5
5
  * Programmatic SDK entry point.
6
6
  */
7
7
  export { analyzeRoutes } from "./core/analyzer.js";
8
+ export { inspectProject, filterRoutes } from "./core/inspect.js";
9
+ export { buildRouteGraph } from "./render/graph.js";
8
10
  export { detectAppRouter } from "./core/detector.js";
9
11
  export { lintRoutes } from "./core/linter.js";
10
12
  export { detectHttpMethods, detectRenderingType, detectRouteConfig, detectServerActions, extractRouteMetadata, getPrologueDirectives, getSegmentType, HTTP_METHODS, parseRoutePath, parseSpecialFileType, SPECIAL_FILES, stripComments, } from "./core/parser.js";
11
13
  export { scanAppDir } from "./core/scanner.js";
12
14
  // Domain Errors
13
- export { AmbiguousRouterError, NextRouteGraphError, NoAppRouterError, ProjectNotFoundError, RouteDirectoryReadError, RouteLintError, } from "./errors/index.js";
15
+ export { AmbiguousRouterError, NextRouteGraphError, NoAppRouterError, ProjectNotFoundError, RouteDirectoryReadError, RouteFileReadError, RouteLintError, } from "./errors/index.js";
14
16
  // Renderers & Strategy Pattern
15
- export { DotRenderer, FlatRenderer, getRenderer, JsonRenderer, MarkdownRenderer, MermaidRenderer, renderDot, renderFlat, renderJson, renderMarkdown, renderMermaid, renderTree, TreeRenderer, } from "./render/index.js";
17
+ export { DotRenderer, FlatRenderer, getRenderer, HtmlRenderer, JsonRenderer, MarkdownRenderer, MermaidRenderer, renderDot, renderFlat, renderHtml, renderJson, renderMarkdown, renderMermaid, renderTree, TreeRenderer, SvgRenderer, renderSvg, } from "./render/index.js";
@@ -0,0 +1,4 @@
1
+ import http from "node:http";
2
+ /** Serve a single in-memory report on loopback. No project files are exposed. */
3
+ export declare function startPreview(html: string, port: number): Promise<http.Server>;
4
+ //# sourceMappingURL=preview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,iFAAiF;AACjF,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAoBtB"}
@@ -0,0 +1,25 @@
1
+ import http from "node:http";
2
+ /** Serve a single in-memory report on loopback. No project files are exposed. */
3
+ export async function startPreview(html, port) {
4
+ const server = http.createServer((req, res) => {
5
+ res.setHeader("X-Content-Type-Options", "nosniff");
6
+ res.setHeader("Cache-Control", "no-store");
7
+ if (req.method !== "GET" && req.method !== "HEAD") {
8
+ res.writeHead(405, { Allow: "GET, HEAD" });
9
+ res.end();
10
+ }
11
+ else if (req.url !== "/") {
12
+ res.writeHead(404);
13
+ res.end();
14
+ }
15
+ else {
16
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
17
+ res.end(req.method === "HEAD" ? undefined : html);
18
+ }
19
+ });
20
+ await new Promise((resolve, reject) => {
21
+ server.once("error", reject);
22
+ server.listen(port, "127.0.0.1", resolve);
23
+ });
24
+ return server;
25
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"dot.d.ts","sourceRoot":"","sources":["../../src/render/dot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAc,MAAM,mBAAmB,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAqC9D;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA2DxD;AAED,qBAAa,WAAY,YAAW,cAAc;IAChD,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM;CAGvC"}
1
+ {"version":3,"file":"dot.d.ts","sourceRoot":"","sources":["../../src/render/dot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAEnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAa9D;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CAsDxD;AAED,qBAAa,WAAY,YAAW,cAAc;IAChD,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM;CAGvC"}
@@ -1,31 +1,13 @@
1
+ import { buildRouteGraph } from "./graph.js";
1
2
  /**
2
3
  * Escapes backslashes and double quotes for Graphviz DOT format.
3
4
  */
4
5
  function escapeDot(str) {
5
- return str.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
6
- }
7
- /**
8
- * Sanitizes project name to form a valid Graphviz identifier.
9
- */
10
- function sanitizeIdentifier(name) {
11
- return name.replace(/[^a-zA-Z0-9_]/g, "_");
12
- }
13
- /**
14
- * Finds the nearest parent route in the route list.
15
- */
16
- function findParentRoute(route, allRoutes) {
17
- if (route.path === "/")
18
- return null;
19
- const parts = route.path.split("/").filter(Boolean);
20
- while (parts.length > 1) {
21
- parts.pop();
22
- const parentPath = "/" + parts.join("/");
23
- const match = allRoutes.find((r) => r.path === parentPath);
24
- if (match)
25
- return match;
26
- }
27
- const rootRoute = allRoutes.find((r) => r.path === "/");
28
- return rootRoute && rootRoute !== route ? rootRoute : null;
6
+ return str
7
+ .replace(/\\/g, "\\\\")
8
+ .replace(/"/g, '\\"')
9
+ .replace(/\r/g, "\\r")
10
+ .replace(/\n/g, "\\n");
29
11
  }
30
12
  /**
31
13
  * Generates a Graphviz DOT representation of the routes.
@@ -47,36 +29,32 @@ export function renderDot(result) {
47
29
  lines.push("}");
48
30
  return lines.join("\n");
49
31
  }
50
- const idMap = new Map();
51
- routes.forEach((route, index) => {
52
- const id = `node_${index}`;
53
- idMap.set(route, id);
54
- const labels = [route.path];
55
- if (route.methods && route.methods.length > 0) {
56
- labels.push(`[${route.methods.join(", ")}]`);
57
- }
58
- else if (route.rendering === "client") {
59
- labels.push("[client]");
32
+ const nodes = buildRouteGraph(routes);
33
+ nodes.forEach((node) => {
34
+ const route = node.routes[0];
35
+ const id = node.id;
36
+ const labels = [node.path];
37
+ for (const endpoint of node.routes) {
38
+ labels.push(endpoint.file);
39
+ if (endpoint.methods?.length)
40
+ labels.push(`[${endpoint.methods.join(", ")}]`);
41
+ else if (endpoint.rendering === "client")
42
+ labels.push("[client]");
43
+ else if (endpoint.segmentType !== "static")
44
+ labels.push(`[${endpoint.segmentType}]`);
60
45
  }
61
- else if (route.segmentType !== "static") {
62
- labels.push(`[${route.segmentType}]`);
63
- }
64
- const labelText = escapeDot(labels.join("\\n"));
65
- const color = route.rendering === "client"
46
+ const labelText = labels.map(escapeDot).join("\\n");
47
+ const color = route?.rendering === "client"
66
48
  ? 'fillcolor="#ecfeff", color="#06b6d4"'
67
- : route.type === "route"
49
+ : route?.type === "route"
68
50
  ? 'fillcolor="#fdf4ff", color="#c026d3"'
69
51
  : 'fillcolor="#f8fafc", color="#64748b"';
70
52
  lines.push(` ${id} [label="${labelText}", ${color}];`);
71
53
  });
72
54
  lines.push("");
73
- routes.forEach((route) => {
74
- const currentId = idMap.get(route);
75
- const parent = findParentRoute(route, routes);
76
- if (parent) {
77
- const parentId = idMap.get(parent);
78
- lines.push(` ${parentId} -> ${currentId};`);
79
- }
55
+ nodes.forEach((node) => {
56
+ if (node.parentId)
57
+ lines.push(` ${node.parentId} -> ${node.id};`);
80
58
  });
81
59
  lines.push("}");
82
60
  return lines.join("\n");
@@ -0,0 +1,13 @@
1
+ import type { RouteEntry } from "../types/index.js";
2
+ export interface RouteGraphNode {
3
+ id: string;
4
+ path: string;
5
+ depth: number;
6
+ parentId?: string;
7
+ routes: RouteEntry[];
8
+ }
9
+ /** URL hierarchy, including non-endpoint segments. Duplicate URLs retain every source. */
10
+ export declare function buildRouteGraph(routes: RouteEntry[]): RouteGraphNode[];
11
+ /** Escape text and attributes in HTML and XML, including untrusted source filenames. */
12
+ export declare function escapeMarkup(value: string): string;
13
+ //# sourceMappingURL=graph.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.d.ts","sourceRoot":"","sources":["../../src/render/graph.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,UAAU,EAAE,CAAC;CACtB;AAED,0FAA0F;AAC1F,wBAAgB,eAAe,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,cAAc,EAAE,CA0CtE;AAED,wFAAwF;AACxF,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQlD"}
@@ -0,0 +1,53 @@
1
+ /** URL hierarchy, including non-endpoint segments. Duplicate URLs retain every source. */
2
+ export function buildRouteGraph(routes) {
3
+ if (routes.length === 0)
4
+ return [];
5
+ const nodes = new Map();
6
+ nodes.set("/", { id: "root", path: "/", depth: 0, routes: [] });
7
+ for (const route of routes) {
8
+ let parent = nodes.get("/");
9
+ const segments = route.path.split("/").filter(Boolean);
10
+ for (let depth = 1; depth <= segments.length; depth++) {
11
+ const path = "/" + segments.slice(0, depth).join("/");
12
+ let node = nodes.get(path);
13
+ if (!node) {
14
+ node = {
15
+ id: `segment_${nodes.size}`,
16
+ path,
17
+ depth,
18
+ parentId: parent.id,
19
+ routes: [],
20
+ };
21
+ nodes.set(path, node);
22
+ }
23
+ parent = node;
24
+ }
25
+ parent.routes.push(route);
26
+ }
27
+ // A preorder keeps connecting lines clear even when SDK input is unsorted.
28
+ const children = new Map();
29
+ for (const node of nodes.values()) {
30
+ const siblings = children.get(node.parentId ?? "") ?? [];
31
+ siblings.push(node);
32
+ children.set(node.parentId ?? "", siblings);
33
+ }
34
+ const ordered = [];
35
+ const visit = (parentId) => {
36
+ for (const node of (children.get(parentId) ?? []).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0)) {
37
+ ordered.push(node);
38
+ visit(node.id);
39
+ }
40
+ };
41
+ visit("");
42
+ return ordered;
43
+ }
44
+ /** Escape text and attributes in HTML and XML, including untrusted source filenames. */
45
+ export function escapeMarkup(value) {
46
+ return value
47
+ .replace(/&/g, "&amp;")
48
+ .replace(/</g, "&lt;")
49
+ .replace(/>/g, "&gt;")
50
+ .replace(/"/g, "&quot;")
51
+ .replace(/'/g, "&#39;")
52
+ .replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f]/g, "");
53
+ }
@@ -0,0 +1,8 @@
1
+ import type { AnalysisResult } from "../types/index.js";
2
+ import type { OutputRenderer } from "./renderer.interface.js";
3
+ /** Self-contained report. All source data is escaped; no project code is executed. */
4
+ export declare function renderHtml(result: AnalysisResult): string;
5
+ export declare class HtmlRenderer implements OutputRenderer {
6
+ render(result: AnalysisResult): string;
7
+ }
8
+ //# sourceMappingURL=html.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/render/html.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAOxD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAK9D,sFAAsF;AACtF,wBAAgB,UAAU,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM,CA+dzD;AAkOD,qBAAa,YAAa,YAAW,cAAc;IACjD,MAAM,CAAC,MAAM,EAAE,cAAc,GAAG,MAAM;CAGvC"}