next-arch-map 0.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/README.md +58 -0
- package/dist/analyzers/endpointsToDb.d.ts +11 -0
- package/dist/analyzers/endpointsToDb.js +348 -0
- package/dist/analyzers/pagesToEndpoints.d.ts +10 -0
- package/dist/analyzers/pagesToEndpoints.js +321 -0
- package/dist/analyzers/pagesToUi.d.ts +11 -0
- package/dist/analyzers/pagesToUi.js +118 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +311 -0
- package/dist/diff.d.ts +15 -0
- package/dist/diff.js +68 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +30 -0
- package/dist/merge.d.ts +6 -0
- package/dist/merge.js +29 -0
- package/dist/model.d.ts +18 -0
- package/dist/model.js +1 -0
- package/dist/query.d.ts +6 -0
- package/dist/query.js +79 -0
- package/dist/serve.d.ts +6 -0
- package/dist/serve.js +166 -0
- package/dist/utils.d.ts +82 -0
- package/dist/utils.js +306 -0
- package/examples/nextjs-dev-route.md +25 -0
- package/package.json +49 -0
- package/viewer/index.html +12 -0
- package/viewer/package-lock.json +1218 -0
- package/viewer/package.json +23 -0
- package/viewer/src/App.tsx +655 -0
- package/viewer/src/Filters.tsx +49 -0
- package/viewer/src/GraphView.tsx +200 -0
- package/viewer/src/NodeDetails.tsx +55 -0
- package/viewer/src/index.css +32 -0
- package/viewer/src/main.tsx +10 -0
- package/viewer/src/types.ts +51 -0
- package/viewer/tsconfig.app.json +20 -0
- package/viewer/tsconfig.json +7 -0
- package/viewer/tsconfig.node.json +11 -0
- package/viewer/vite.config.ts +6 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { analyzeProject, diffGraphs } from "./index.js";
|
|
6
|
+
import { serve } from "./serve.js";
|
|
7
|
+
import { readJsonFile, writeJsonFile } from "./utils.js";
|
|
8
|
+
async function main() {
|
|
9
|
+
const [commandOrArg, ...rest] = process.argv.slice(2);
|
|
10
|
+
if (commandOrArg === "dev") {
|
|
11
|
+
const options = parseDevArgs(rest);
|
|
12
|
+
await runDev(options);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
if (commandOrArg === "serve") {
|
|
16
|
+
const options = parseServeArgs(rest);
|
|
17
|
+
await serve(options);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (commandOrArg === "diff") {
|
|
21
|
+
const options = parseDiffArgs(rest);
|
|
22
|
+
const beforePath = path.resolve(options.beforePath);
|
|
23
|
+
const afterPath = path.resolve(options.afterPath);
|
|
24
|
+
const outputFile = path.resolve(options.out);
|
|
25
|
+
const beforeGraph = readJsonFile(beforePath);
|
|
26
|
+
const afterGraph = readJsonFile(afterPath);
|
|
27
|
+
const diff = diffGraphs(beforeGraph, afterGraph);
|
|
28
|
+
writeJsonFile(outputFile, diff);
|
|
29
|
+
logDiffSummary(diff, beforePath, afterPath, outputFile);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const analyzeArgs = commandOrArg === "analyze" ? rest : process.argv.slice(2);
|
|
33
|
+
const options = parseAnalyzeArgs(analyzeArgs);
|
|
34
|
+
const graph = await analyzeProject({
|
|
35
|
+
projectRoot: options.projectRoot,
|
|
36
|
+
appDirs: options.appDirs,
|
|
37
|
+
});
|
|
38
|
+
const outputFile = path.resolve(options.projectRoot, options.out);
|
|
39
|
+
writeJsonFile(outputFile, graph);
|
|
40
|
+
logAnalyzeSummary(graph, outputFile, options.projectRoot);
|
|
41
|
+
}
|
|
42
|
+
function parseAnalyzeArgs(args) {
|
|
43
|
+
let projectRoot = process.cwd();
|
|
44
|
+
let out = "arch/graph.full.json";
|
|
45
|
+
const appDirs = [];
|
|
46
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
47
|
+
const argument = args[index];
|
|
48
|
+
if (argument === "--project-root" && args[index + 1]) {
|
|
49
|
+
projectRoot = path.resolve(args[index + 1]);
|
|
50
|
+
index += 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (argument === "--out" && args[index + 1]) {
|
|
54
|
+
out = args[index + 1];
|
|
55
|
+
index += 1;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (argument === "--app-dir" && args[index + 1]) {
|
|
59
|
+
appDirs.push(args[index + 1]);
|
|
60
|
+
index += 1;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (argument === "--help" || argument === "-h") {
|
|
64
|
+
printHelp();
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
throw new Error(`Unknown argument: ${argument}`);
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
projectRoot,
|
|
71
|
+
out,
|
|
72
|
+
appDirs: appDirs.length > 0 ? appDirs : undefined,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function parseDiffArgs(args) {
|
|
76
|
+
let beforePath = "";
|
|
77
|
+
let afterPath = "";
|
|
78
|
+
let out = "arch/graph.diff.json";
|
|
79
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
80
|
+
const argument = args[index];
|
|
81
|
+
if (argument === "--before" && args[index + 1]) {
|
|
82
|
+
beforePath = args[index + 1];
|
|
83
|
+
index += 1;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (argument === "--after" && args[index + 1]) {
|
|
87
|
+
afterPath = args[index + 1];
|
|
88
|
+
index += 1;
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (argument === "--out" && args[index + 1]) {
|
|
92
|
+
out = args[index + 1];
|
|
93
|
+
index += 1;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (argument === "--help" || argument === "-h") {
|
|
97
|
+
printHelp();
|
|
98
|
+
process.exit(0);
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`Unknown argument: ${argument}`);
|
|
101
|
+
}
|
|
102
|
+
if (!beforePath || !afterPath) {
|
|
103
|
+
throw new Error("The diff command requires --before <path> and --after <path>.");
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
beforePath,
|
|
107
|
+
afterPath,
|
|
108
|
+
out,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function parseServeArgs(args) {
|
|
112
|
+
let projectRoot = process.cwd();
|
|
113
|
+
let port = 4321;
|
|
114
|
+
const appDirs = [];
|
|
115
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
116
|
+
const argument = args[index];
|
|
117
|
+
if (argument === "--project-root" && args[index + 1]) {
|
|
118
|
+
projectRoot = path.resolve(args[index + 1]);
|
|
119
|
+
index += 1;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (argument === "--port" && args[index + 1]) {
|
|
123
|
+
port = Number(args[index + 1]);
|
|
124
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
125
|
+
throw new Error(`Invalid port: ${args[index + 1]}`);
|
|
126
|
+
}
|
|
127
|
+
index += 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (argument === "--app-dir" && args[index + 1]) {
|
|
131
|
+
appDirs.push(args[index + 1]);
|
|
132
|
+
index += 1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (argument === "--help" || argument === "-h") {
|
|
136
|
+
printHelp();
|
|
137
|
+
process.exit(0);
|
|
138
|
+
}
|
|
139
|
+
throw new Error(`Unknown argument: ${argument}`);
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
projectRoot,
|
|
143
|
+
port,
|
|
144
|
+
appDirs: appDirs.length > 0 ? appDirs : undefined,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function parseDevArgs(args) {
|
|
148
|
+
let projectRoot = process.cwd();
|
|
149
|
+
let port = 4321;
|
|
150
|
+
let viewerDir = "";
|
|
151
|
+
const appDirs = [];
|
|
152
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
153
|
+
const argument = args[index];
|
|
154
|
+
if (argument === "--project-root" && args[index + 1]) {
|
|
155
|
+
projectRoot = path.resolve(args[index + 1]);
|
|
156
|
+
index += 1;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (argument === "--port" && args[index + 1]) {
|
|
160
|
+
port = Number(args[index + 1]);
|
|
161
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
162
|
+
throw new Error(`Invalid port: ${args[index + 1]}`);
|
|
163
|
+
}
|
|
164
|
+
index += 1;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (argument === "--viewer-dir" && args[index + 1]) {
|
|
168
|
+
viewerDir = path.resolve(args[index + 1]);
|
|
169
|
+
index += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (argument === "--app-dir" && args[index + 1]) {
|
|
173
|
+
appDirs.push(args[index + 1]);
|
|
174
|
+
index += 1;
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (argument === "--help" || argument === "-h") {
|
|
178
|
+
printHelp();
|
|
179
|
+
process.exit(0);
|
|
180
|
+
}
|
|
181
|
+
throw new Error(`Unknown argument: ${argument}`);
|
|
182
|
+
}
|
|
183
|
+
if (!viewerDir) {
|
|
184
|
+
// Try to find the viewer relative to the analyzer package
|
|
185
|
+
const packageDir = path.dirname(path.dirname(new URL(import.meta.url).pathname));
|
|
186
|
+
const builtinViewer = path.join(packageDir, "viewer");
|
|
187
|
+
if (fs.existsSync(path.join(builtinViewer, "package.json"))) {
|
|
188
|
+
viewerDir = builtinViewer;
|
|
189
|
+
}
|
|
190
|
+
if (!viewerDir) {
|
|
191
|
+
throw new Error("Could not find viewer directory. Provide --viewer-dir <path> pointing to the viewer app.");
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
projectRoot,
|
|
196
|
+
port,
|
|
197
|
+
viewerDir,
|
|
198
|
+
appDirs: appDirs.length > 0 ? appDirs : undefined,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
async function runDev(options) {
|
|
202
|
+
const children = [];
|
|
203
|
+
const cleanup = () => {
|
|
204
|
+
for (const child of children) {
|
|
205
|
+
child.kill("SIGTERM");
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
process.once("SIGINT", () => {
|
|
209
|
+
cleanup();
|
|
210
|
+
process.exit(0);
|
|
211
|
+
});
|
|
212
|
+
process.once("SIGTERM", () => {
|
|
213
|
+
cleanup();
|
|
214
|
+
process.exit(0);
|
|
215
|
+
});
|
|
216
|
+
// Start the analyzer serve process
|
|
217
|
+
const serveProcess = spawn(process.execPath, [
|
|
218
|
+
path.resolve(path.dirname(new URL(import.meta.url).pathname), "cli.js"),
|
|
219
|
+
"serve",
|
|
220
|
+
"--project-root",
|
|
221
|
+
options.projectRoot,
|
|
222
|
+
"--port",
|
|
223
|
+
String(options.port),
|
|
224
|
+
...(options.appDirs ?? []).flatMap((dir) => ["--app-dir", dir]),
|
|
225
|
+
], { stdio: "inherit" });
|
|
226
|
+
children.push(serveProcess);
|
|
227
|
+
// Start the viewer dev server
|
|
228
|
+
const viewerProcess = spawn("npm", ["run", "dev"], {
|
|
229
|
+
cwd: options.viewerDir,
|
|
230
|
+
stdio: "inherit",
|
|
231
|
+
shell: process.platform === "win32",
|
|
232
|
+
});
|
|
233
|
+
children.push(viewerProcess);
|
|
234
|
+
// If either process exits, tear down the other
|
|
235
|
+
const onExit = (name) => (code) => {
|
|
236
|
+
console.log(`${name} exited with code ${code}`);
|
|
237
|
+
cleanup();
|
|
238
|
+
process.exit(code ?? 1);
|
|
239
|
+
};
|
|
240
|
+
serveProcess.once("exit", onExit("analyzer"));
|
|
241
|
+
viewerProcess.once("exit", onExit("viewer"));
|
|
242
|
+
// Keep the process alive
|
|
243
|
+
await new Promise(() => { });
|
|
244
|
+
}
|
|
245
|
+
function logAnalyzeSummary(graph, outputFile, projectRoot) {
|
|
246
|
+
const pageCount = graph.nodes.filter((node) => node.type === "page").length;
|
|
247
|
+
const endpointCount = graph.nodes.filter((node) => node.type === "endpoint").length;
|
|
248
|
+
const dbCount = graph.nodes.filter((node) => node.type === "db").length;
|
|
249
|
+
const uiCount = graph.nodes.filter((node) => node.type === "ui").length;
|
|
250
|
+
console.log([
|
|
251
|
+
`pages=${pageCount}`,
|
|
252
|
+
`endpoints=${endpointCount}`,
|
|
253
|
+
`db=${dbCount}`,
|
|
254
|
+
`ui=${uiCount}`,
|
|
255
|
+
`edges=${graph.edges.length}`,
|
|
256
|
+
`file=${path.relative(projectRoot, outputFile)}`,
|
|
257
|
+
].join(" "));
|
|
258
|
+
}
|
|
259
|
+
function logDiffSummary(diff, beforePath, afterPath, outputFile) {
|
|
260
|
+
const addedNodes = diff.nodes.filter((node) => node.status === "added").length;
|
|
261
|
+
const removedNodes = diff.nodes.filter((node) => node.status === "removed").length;
|
|
262
|
+
const addedEdges = diff.edges.filter((edge) => edge.status === "added").length;
|
|
263
|
+
const removedEdges = diff.edges.filter((edge) => edge.status === "removed").length;
|
|
264
|
+
console.log([
|
|
265
|
+
"mode=diff",
|
|
266
|
+
`before=${path.relative(process.cwd(), beforePath)}`,
|
|
267
|
+
`after=${path.relative(process.cwd(), afterPath)}`,
|
|
268
|
+
`out=${path.relative(process.cwd(), outputFile)}`,
|
|
269
|
+
`nodes=${diff.nodes.length}`,
|
|
270
|
+
`edges=${diff.edges.length}`,
|
|
271
|
+
`addedNodes=${addedNodes}`,
|
|
272
|
+
`removedNodes=${removedNodes}`,
|
|
273
|
+
`addedEdges=${addedEdges}`,
|
|
274
|
+
`removedEdges=${removedEdges}`,
|
|
275
|
+
].join(" "));
|
|
276
|
+
}
|
|
277
|
+
function printHelp() {
|
|
278
|
+
console.log(`next-arch-map analyze [options]
|
|
279
|
+
next-arch-map diff --before <path> --after <path> [--out <path>]
|
|
280
|
+
next-arch-map serve [options]
|
|
281
|
+
next-arch-map dev [options]
|
|
282
|
+
|
|
283
|
+
Analyze options:
|
|
284
|
+
--project-root <path> Project root to analyze. Defaults to the current working directory.
|
|
285
|
+
--out <path> Output JSON path, relative to the project root by default.
|
|
286
|
+
--app-dir <path> App Router directory to scan. Can be provided multiple times.
|
|
287
|
+
|
|
288
|
+
Diff options:
|
|
289
|
+
--before <path> Path to the baseline graph JSON.
|
|
290
|
+
--after <path> Path to the updated graph JSON.
|
|
291
|
+
--out <path> Output diff JSON path. Defaults to arch/graph.diff.json.
|
|
292
|
+
|
|
293
|
+
Serve options:
|
|
294
|
+
--project-root <path> Project root to analyze. Defaults to the current working directory.
|
|
295
|
+
--port <number> Port to listen on. Defaults to 4321.
|
|
296
|
+
--app-dir <path> App Router directory to scan. Can be provided multiple times.
|
|
297
|
+
|
|
298
|
+
Dev options:
|
|
299
|
+
--project-root <path> Project root to analyze. Defaults to the current working directory.
|
|
300
|
+
--port <number> Port for the analyzer server. Defaults to 4321.
|
|
301
|
+
--viewer-dir <path> Path to the viewer app. Auto-detected if bundled with the package.
|
|
302
|
+
--app-dir <path> App Router directory to scan. Can be provided multiple times.
|
|
303
|
+
|
|
304
|
+
General:
|
|
305
|
+
--help Show this help message.`);
|
|
306
|
+
}
|
|
307
|
+
void main().catch((error) => {
|
|
308
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
309
|
+
console.error(message);
|
|
310
|
+
process.exit(1);
|
|
311
|
+
});
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Edge, Graph, Node } from "./model.js";
|
|
2
|
+
export type DiffStatus = "added" | "removed" | "modified" | "unchanged";
|
|
3
|
+
export type NodeDiff = {
|
|
4
|
+
node: Node;
|
|
5
|
+
status: DiffStatus;
|
|
6
|
+
};
|
|
7
|
+
export type EdgeDiff = {
|
|
8
|
+
edge: Edge;
|
|
9
|
+
status: DiffStatus;
|
|
10
|
+
};
|
|
11
|
+
export type GraphDiff = {
|
|
12
|
+
nodes: NodeDiff[];
|
|
13
|
+
edges: EdgeDiff[];
|
|
14
|
+
};
|
|
15
|
+
export declare function diffGraphs(before: Graph, after: Graph): GraphDiff;
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { buildEdgeKey } from "./utils.js";
|
|
2
|
+
function nodeEqual(a, b) {
|
|
3
|
+
return a.type === b.type && a.label === b.label && JSON.stringify(a.meta) === JSON.stringify(b.meta);
|
|
4
|
+
}
|
|
5
|
+
function edgeEqual(a, b) {
|
|
6
|
+
return JSON.stringify(a.meta) === JSON.stringify(b.meta);
|
|
7
|
+
}
|
|
8
|
+
export function diffGraphs(before, after) {
|
|
9
|
+
const beforeNodes = new Map();
|
|
10
|
+
const afterNodes = new Map();
|
|
11
|
+
const beforeEdges = new Map();
|
|
12
|
+
const afterEdges = new Map();
|
|
13
|
+
for (const node of before.nodes) {
|
|
14
|
+
beforeNodes.set(node.id, node);
|
|
15
|
+
}
|
|
16
|
+
for (const node of after.nodes) {
|
|
17
|
+
afterNodes.set(node.id, node);
|
|
18
|
+
}
|
|
19
|
+
for (const edge of before.edges) {
|
|
20
|
+
beforeEdges.set(buildEdgeKey(edge.from, edge.to, edge.kind), edge);
|
|
21
|
+
}
|
|
22
|
+
for (const edge of after.edges) {
|
|
23
|
+
afterEdges.set(buildEdgeKey(edge.from, edge.to, edge.kind), edge);
|
|
24
|
+
}
|
|
25
|
+
const nodes = [];
|
|
26
|
+
const edges = [];
|
|
27
|
+
const allNodeKeys = new Set([...beforeNodes.keys(), ...afterNodes.keys()]);
|
|
28
|
+
for (const key of allNodeKeys) {
|
|
29
|
+
const beforeNode = beforeNodes.get(key);
|
|
30
|
+
const afterNode = afterNodes.get(key);
|
|
31
|
+
if (beforeNode && !afterNode) {
|
|
32
|
+
nodes.push({ node: beforeNode, status: "removed" });
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!beforeNode && afterNode) {
|
|
36
|
+
nodes.push({ node: afterNode, status: "added" });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (beforeNode && afterNode) {
|
|
40
|
+
const status = nodeEqual(beforeNode, afterNode) ? "unchanged" : "modified";
|
|
41
|
+
nodes.push({ node: afterNode, status });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const allEdgeKeys = new Set([...beforeEdges.keys(), ...afterEdges.keys()]);
|
|
45
|
+
for (const key of allEdgeKeys) {
|
|
46
|
+
const beforeEdge = beforeEdges.get(key);
|
|
47
|
+
const afterEdge = afterEdges.get(key);
|
|
48
|
+
if (beforeEdge && !afterEdge) {
|
|
49
|
+
edges.push({ edge: beforeEdge, status: "removed" });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!beforeEdge && afterEdge) {
|
|
53
|
+
edges.push({ edge: afterEdge, status: "added" });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (beforeEdge && afterEdge) {
|
|
57
|
+
const status = edgeEqual(beforeEdge, afterEdge) ? "unchanged" : "modified";
|
|
58
|
+
edges.push({ edge: afterEdge, status });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
nodes.sort((left, right) => left.node.id.localeCompare(right.node.id));
|
|
62
|
+
edges.sort((left, right) => {
|
|
63
|
+
const leftKey = `${left.edge.kind}:${left.edge.from}:${left.edge.to}`;
|
|
64
|
+
const rightKey = `${right.edge.kind}:${right.edge.from}:${right.edge.to}`;
|
|
65
|
+
return leftKey.localeCompare(rightKey);
|
|
66
|
+
});
|
|
67
|
+
return { nodes, edges };
|
|
68
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Graph } from "./model.js";
|
|
2
|
+
export type AnalyzeProjectOptions = {
|
|
3
|
+
projectRoot: string;
|
|
4
|
+
appDirs?: string[];
|
|
5
|
+
extraScanDirs?: string[];
|
|
6
|
+
apiDirs?: string[];
|
|
7
|
+
httpClientIdentifiers?: string[];
|
|
8
|
+
httpClientMethods?: string[];
|
|
9
|
+
dbClientIdentifiers?: string[];
|
|
10
|
+
uiImportPathGlobs?: string[];
|
|
11
|
+
};
|
|
12
|
+
export declare function analyzeProject(options: AnalyzeProjectOptions): Promise<Graph>;
|
|
13
|
+
export { diffGraphs } from "./diff.js";
|
|
14
|
+
export type { DiffStatus, EdgeDiff, GraphDiff, NodeDiff } from "./diff.js";
|
|
15
|
+
export type { Edge, EdgeKind, Graph, Node, NodeType } from "./model.js";
|
|
16
|
+
export { analyzePagesToEndpoints } from "./analyzers/pagesToEndpoints.js";
|
|
17
|
+
export { analyzeEndpointsToDb } from "./analyzers/endpointsToDb.js";
|
|
18
|
+
export { analyzePagesToUi } from "./analyzers/pagesToUi.js";
|
|
19
|
+
export { mergeGraphs, mergePartial } from "./merge.js";
|
|
20
|
+
export { getDbModelsForPage, getEndpointsForPage, getPagesForDbModel, } from "./query.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { analyzeEndpointsToDb } from "./analyzers/endpointsToDb.js";
|
|
2
|
+
import { analyzePagesToEndpoints } from "./analyzers/pagesToEndpoints.js";
|
|
3
|
+
import { analyzePagesToUi } from "./analyzers/pagesToUi.js";
|
|
4
|
+
import { mergePartial } from "./merge.js";
|
|
5
|
+
export async function analyzeProject(options) {
|
|
6
|
+
const pagesToEndpoints = await analyzePagesToEndpoints({
|
|
7
|
+
projectRoot: options.projectRoot,
|
|
8
|
+
appDirs: options.appDirs,
|
|
9
|
+
extraScanDirs: options.extraScanDirs,
|
|
10
|
+
httpClientIdentifiers: options.httpClientIdentifiers,
|
|
11
|
+
httpClientMethods: options.httpClientMethods,
|
|
12
|
+
});
|
|
13
|
+
const endpointsToDb = await analyzeEndpointsToDb({
|
|
14
|
+
projectRoot: options.projectRoot,
|
|
15
|
+
apiDirs: options.apiDirs,
|
|
16
|
+
dbClientIdentifiers: options.dbClientIdentifiers,
|
|
17
|
+
});
|
|
18
|
+
const pagesToUi = await analyzePagesToUi({
|
|
19
|
+
projectRoot: options.projectRoot,
|
|
20
|
+
appDirs: options.appDirs,
|
|
21
|
+
uiImportPathGlobs: options.uiImportPathGlobs,
|
|
22
|
+
});
|
|
23
|
+
return mergePartial(mergePartial(pagesToEndpoints, endpointsToDb), pagesToUi);
|
|
24
|
+
}
|
|
25
|
+
export { diffGraphs } from "./diff.js";
|
|
26
|
+
export { analyzePagesToEndpoints } from "./analyzers/pagesToEndpoints.js";
|
|
27
|
+
export { analyzeEndpointsToDb } from "./analyzers/endpointsToDb.js";
|
|
28
|
+
export { analyzePagesToUi } from "./analyzers/pagesToUi.js";
|
|
29
|
+
export { mergeGraphs, mergePartial } from "./merge.js";
|
|
30
|
+
export { getDbModelsForPage, getEndpointsForPage, getPagesForDbModel, } from "./query.js";
|
package/dist/merge.d.ts
ADDED
package/dist/merge.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { buildEdgeKey, mergeEdge, mergeNode } from "./utils.js";
|
|
2
|
+
export function mergeGraphs(graphs) {
|
|
3
|
+
return graphs.reduce((accumulator, graph) => mergePartial(accumulator, graph), { nodes: [], edges: [] });
|
|
4
|
+
}
|
|
5
|
+
export function mergePartial(base, additions) {
|
|
6
|
+
const nodesById = new Map();
|
|
7
|
+
const edgesByKey = new Map();
|
|
8
|
+
for (const node of base.nodes) {
|
|
9
|
+
nodesById.set(node.id, node);
|
|
10
|
+
}
|
|
11
|
+
for (const node of additions.nodes) {
|
|
12
|
+
const existingNode = nodesById.get(node.id);
|
|
13
|
+
nodesById.set(node.id, existingNode ? mergeNode(existingNode, node) : node);
|
|
14
|
+
}
|
|
15
|
+
for (const edge of base.edges) {
|
|
16
|
+
edgesByKey.set(buildEdgeKey(edge.from, edge.to, edge.kind), edge);
|
|
17
|
+
}
|
|
18
|
+
for (const edge of additions.edges) {
|
|
19
|
+
const edgeKey = buildEdgeKey(edge.from, edge.to, edge.kind);
|
|
20
|
+
const existingEdge = edgesByKey.get(edgeKey);
|
|
21
|
+
edgesByKey.set(edgeKey, existingEdge ? mergeEdge(existingEdge, edge) : edge);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
nodes: [...nodesById.values()].sort((left, right) => left.id.localeCompare(right.id)),
|
|
25
|
+
edges: [...edgesByKey.values()].sort((left, right) => left.kind.localeCompare(right.kind) ||
|
|
26
|
+
left.from.localeCompare(right.from) ||
|
|
27
|
+
left.to.localeCompare(right.to)),
|
|
28
|
+
};
|
|
29
|
+
}
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type NodeType = "page" | "endpoint" | "db" | "ui" | "handler" | "action";
|
|
2
|
+
export type Node = {
|
|
3
|
+
id: string;
|
|
4
|
+
type: NodeType;
|
|
5
|
+
label: string;
|
|
6
|
+
meta?: Record<string, any>;
|
|
7
|
+
};
|
|
8
|
+
export type EdgeKind = "page-endpoint" | "endpoint-db" | "page-ui" | "endpoint-handler" | "page-action" | "action-endpoint";
|
|
9
|
+
export type Edge = {
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
kind: EdgeKind;
|
|
13
|
+
meta?: Record<string, any>;
|
|
14
|
+
};
|
|
15
|
+
export type Graph = {
|
|
16
|
+
nodes: Node[];
|
|
17
|
+
edges: Edge[];
|
|
18
|
+
};
|
package/dist/model.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/query.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Graph, Node } from "./model.js";
|
|
2
|
+
export declare function getPageNode(graph: Graph, route: string): Node | null;
|
|
3
|
+
export declare function getDbNode(graph: Graph, modelName: string): Node | null;
|
|
4
|
+
export declare function getEndpointsForPage(graph: Graph, route: string): Node[];
|
|
5
|
+
export declare function getDbModelsForPage(graph: Graph, route: string): Node[];
|
|
6
|
+
export declare function getPagesForDbModel(graph: Graph, modelName: string): Node[];
|
package/dist/query.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export function getPageNode(graph, route) {
|
|
2
|
+
const id = `page:${route}`;
|
|
3
|
+
return graph.nodes.find((node) => node.id === id) ?? null;
|
|
4
|
+
}
|
|
5
|
+
export function getDbNode(graph, modelName) {
|
|
6
|
+
const id = `db:${modelName}`;
|
|
7
|
+
return graph.nodes.find((node) => node.id === id) ?? null;
|
|
8
|
+
}
|
|
9
|
+
export function getEndpointsForPage(graph, route) {
|
|
10
|
+
const pageNode = getPageNode(graph, route);
|
|
11
|
+
if (!pageNode) {
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
const endpointIds = new Set();
|
|
15
|
+
const actionIds = new Set();
|
|
16
|
+
for (const edge of graph.edges) {
|
|
17
|
+
if (edge.kind === "page-endpoint" && edge.from === pageNode.id) {
|
|
18
|
+
endpointIds.add(edge.to);
|
|
19
|
+
}
|
|
20
|
+
if (edge.kind === "page-action" && edge.from === pageNode.id) {
|
|
21
|
+
actionIds.add(edge.to);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (actionIds.size > 0) {
|
|
25
|
+
for (const edge of graph.edges) {
|
|
26
|
+
if (edge.kind === "action-endpoint" && actionIds.has(edge.from)) {
|
|
27
|
+
endpointIds.add(edge.to);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return graph.nodes.filter((node) => endpointIds.has(node.id));
|
|
32
|
+
}
|
|
33
|
+
export function getDbModelsForPage(graph, route) {
|
|
34
|
+
const endpoints = getEndpointsForPage(graph, route);
|
|
35
|
+
if (endpoints.length === 0) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const endpointIds = new Set(endpoints.map((node) => node.id));
|
|
39
|
+
const dbIds = new Set();
|
|
40
|
+
for (const edge of graph.edges) {
|
|
41
|
+
if (edge.kind === "endpoint-db" && endpointIds.has(edge.from)) {
|
|
42
|
+
dbIds.add(edge.to);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return graph.nodes.filter((node) => dbIds.has(node.id));
|
|
46
|
+
}
|
|
47
|
+
export function getPagesForDbModel(graph, modelName) {
|
|
48
|
+
const dbNode = getDbNode(graph, modelName);
|
|
49
|
+
if (!dbNode) {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
const endpointIds = new Set();
|
|
53
|
+
const pageIds = new Set();
|
|
54
|
+
const actionIds = new Set();
|
|
55
|
+
for (const edge of graph.edges) {
|
|
56
|
+
if (edge.kind === "endpoint-db" && edge.to === dbNode.id) {
|
|
57
|
+
endpointIds.add(edge.from);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (endpointIds.size === 0) {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
for (const edge of graph.edges) {
|
|
64
|
+
if (edge.kind === "page-endpoint" && endpointIds.has(edge.to)) {
|
|
65
|
+
pageIds.add(edge.from);
|
|
66
|
+
}
|
|
67
|
+
if (edge.kind === "action-endpoint" && endpointIds.has(edge.to)) {
|
|
68
|
+
actionIds.add(edge.from);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (actionIds.size > 0) {
|
|
72
|
+
for (const edge of graph.edges) {
|
|
73
|
+
if (edge.kind === "page-action" && actionIds.has(edge.to)) {
|
|
74
|
+
pageIds.add(edge.from);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return graph.nodes.filter((node) => pageIds.has(node.id));
|
|
79
|
+
}
|