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.
- package/CHANGELOG.md +95 -0
- package/CONTRIBUTING.md +74 -0
- package/README.md +113 -189
- package/SECURITY.md +19 -0
- package/dist/cli.js +107 -80
- package/dist/core/analyzer.d.ts +5 -1
- package/dist/core/analyzer.d.ts.map +1 -1
- package/dist/core/analyzer.js +21 -14
- package/dist/core/inspect.d.ts +6 -0
- package/dist/core/inspect.d.ts.map +1 -0
- package/dist/core/inspect.js +21 -0
- package/dist/core/linter.d.ts.map +1 -1
- package/dist/core/linter.js +43 -23
- package/dist/core/parser.d.ts.map +1 -1
- package/dist/core/parser.js +31 -16
- package/dist/errors/index.d.ts +4 -0
- package/dist/errors/index.d.ts.map +1 -1
- package/dist/errors/index.js +8 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/preview.d.ts +4 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +25 -0
- package/dist/render/dot.d.ts.map +1 -1
- package/dist/render/dot.js +25 -47
- package/dist/render/graph.d.ts +13 -0
- package/dist/render/graph.d.ts.map +1 -0
- package/dist/render/graph.js +53 -0
- package/dist/render/html.d.ts +8 -0
- package/dist/render/html.d.ts.map +1 -0
- package/dist/render/html.js +718 -0
- package/dist/render/index.d.ts +4 -2
- package/dist/render/index.d.ts.map +1 -1
- package/dist/render/index.js +10 -3
- package/dist/render/mermaid.d.ts.map +1 -1
- package/dist/render/mermaid.js +19 -43
- package/dist/render/svg.d.ts +8 -0
- package/dist/render/svg.d.ts.map +1 -0
- package/dist/render/svg.js +76 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/types/index.d.ts.map +1 -1
- package/docs/ARCHITECTURE.md +71 -0
- package/docs/NEW_FEATURES.md +16 -0
- package/docs/adr/0001-offline-reports-and-shared-route-graph.md +29 -0
- package/docs/route-report.png +0 -0
- 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 @@
|
|
|
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"}
|
package/dist/preview.js
ADDED
|
@@ -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
|
+
}
|
package/dist/render/dot.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dot.d.ts","sourceRoot":"","sources":["../../src/render/dot.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,
|
|
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"}
|
package/dist/render/dot.js
CHANGED
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
const labels = [
|
|
55
|
-
|
|
56
|
-
labels.push(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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, "&")
|
|
48
|
+
.replace(/</g, "<")
|
|
49
|
+
.replace(/>/g, ">")
|
|
50
|
+
.replace(/"/g, """)
|
|
51
|
+
.replace(/'/g, "'")
|
|
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"}
|