next-arch-map 0.1.29 → 0.1.30
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/analyzers/prismaSchema.js +8 -6
- package/dist/cli.js +159 -228
- package/package.json +1 -1
- package/viewer/src/App.tsx +30 -38
- package/viewer/src/Filters.tsx +4 -11
- package/viewer/src/GraphView.tsx +22 -33
- package/viewer/src/NodeDetails.tsx +6 -10
- package/viewer/src/types.ts +8 -7
|
@@ -75,9 +75,7 @@ export async function analyzePrismaSchema(projectRoot) {
|
|
|
75
75
|
}
|
|
76
76
|
return {
|
|
77
77
|
nodes: nodes.sort((a, b) => a.id.localeCompare(b.id)),
|
|
78
|
-
edges: edges.sort((a, b) => a.kind.localeCompare(b.kind) ||
|
|
79
|
-
a.from.localeCompare(b.from) ||
|
|
80
|
-
a.to.localeCompare(b.to)),
|
|
78
|
+
edges: edges.sort((a, b) => a.kind.localeCompare(b.kind) || a.from.localeCompare(b.from) || a.to.localeCompare(b.to)),
|
|
81
79
|
};
|
|
82
80
|
}
|
|
83
81
|
function parseEnumNames(content) {
|
|
@@ -205,9 +203,13 @@ function parseField(line, modelNames, enumNames) {
|
|
|
205
203
|
if (restLine.includes("@unique")) {
|
|
206
204
|
column.isUnique = true;
|
|
207
205
|
}
|
|
208
|
-
const
|
|
209
|
-
if (
|
|
210
|
-
|
|
206
|
+
const defaultAttr = extractAttribute(restTokens, "@default");
|
|
207
|
+
if (defaultAttr) {
|
|
208
|
+
const innerStart = "@default(".length;
|
|
209
|
+
const innerEnd = defaultAttr.lastIndexOf(")");
|
|
210
|
+
if (innerEnd > innerStart) {
|
|
211
|
+
column.default = defaultAttr.slice(innerStart, innerEnd);
|
|
212
|
+
}
|
|
211
213
|
}
|
|
212
214
|
const mapMatch = /@map\("([^"]*)"\)/.exec(restLine);
|
|
213
215
|
if (mapMatch) {
|
package/dist/cli.js
CHANGED
|
@@ -4,9 +4,164 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { analyzeProject, diffGraphs } from "./index.js";
|
|
6
6
|
import { generateDescribeContext } from "./describe.js";
|
|
7
|
-
import { captureScreenshots, generateParamsTemplate
|
|
7
|
+
import { captureScreenshots, generateParamsTemplate } from "./screenshot.js";
|
|
8
8
|
import { serve } from "./serve.js";
|
|
9
9
|
import { readJsonFile, writeJsonFile } from "./utils.js";
|
|
10
|
+
function parseArgs(args, defs) {
|
|
11
|
+
const result = {};
|
|
12
|
+
for (const def of Object.values(defs)) {
|
|
13
|
+
if (def.type === "string[]") {
|
|
14
|
+
result[def.key] = [];
|
|
15
|
+
}
|
|
16
|
+
else if (def.type === "boolean") {
|
|
17
|
+
result[def.key] = false;
|
|
18
|
+
}
|
|
19
|
+
else if ("default" in def && def.default !== undefined) {
|
|
20
|
+
result[def.key] = def.default;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
result[def.key] = def.type === "number" ? 0 : "";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
27
|
+
const flag = args[i];
|
|
28
|
+
if (flag === "--help" || flag === "-h") {
|
|
29
|
+
printHelp();
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
const def = defs[flag];
|
|
33
|
+
if (!def) {
|
|
34
|
+
throw new Error(`Unknown argument: ${flag}`);
|
|
35
|
+
}
|
|
36
|
+
if (def.type === "boolean") {
|
|
37
|
+
result[def.key] = true;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const value = args[i + 1];
|
|
41
|
+
if (!value) {
|
|
42
|
+
throw new Error(`Missing value for ${flag}`);
|
|
43
|
+
}
|
|
44
|
+
i += 1;
|
|
45
|
+
if (def.type === "number") {
|
|
46
|
+
const num = Number(value);
|
|
47
|
+
if (!Number.isInteger(num) || num <= 0) {
|
|
48
|
+
throw new Error(`Invalid ${flag}: ${value}`);
|
|
49
|
+
}
|
|
50
|
+
result[def.key] = num;
|
|
51
|
+
}
|
|
52
|
+
else if (def.type === "string[]") {
|
|
53
|
+
result[def.key].push(value);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
result[def.key] = value;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
// Command-specific argument wrappers
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
function parseAnalyzeArgs(args) {
|
|
65
|
+
const raw = parseArgs(args, {
|
|
66
|
+
"--project-root": { key: "projectRoot", type: "string", default: process.cwd() },
|
|
67
|
+
"--out": { key: "out", type: "string", default: "arch/graph.full.json" },
|
|
68
|
+
"--app-dir": { key: "appDirs", type: "string[]" },
|
|
69
|
+
});
|
|
70
|
+
const appDirs = raw.appDirs;
|
|
71
|
+
return {
|
|
72
|
+
projectRoot: path.resolve(raw.projectRoot),
|
|
73
|
+
out: raw.out,
|
|
74
|
+
appDirs: appDirs.length > 0 ? appDirs : undefined,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function parseDiffArgs(args) {
|
|
78
|
+
const raw = parseArgs(args, {
|
|
79
|
+
"--before": { key: "beforePath", type: "string" },
|
|
80
|
+
"--after": { key: "afterPath", type: "string" },
|
|
81
|
+
"--out": { key: "out", type: "string", default: "arch/graph.diff.json" },
|
|
82
|
+
});
|
|
83
|
+
if (!raw.beforePath || !raw.afterPath) {
|
|
84
|
+
throw new Error("The diff command requires --before <path> and --after <path>.");
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
beforePath: raw.beforePath,
|
|
88
|
+
afterPath: raw.afterPath,
|
|
89
|
+
out: raw.out,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
function parseServeArgs(args) {
|
|
93
|
+
const raw = parseArgs(args, {
|
|
94
|
+
"--project-root": { key: "projectRoot", type: "string", default: process.cwd() },
|
|
95
|
+
"--port": { key: "port", type: "number", default: 4321 },
|
|
96
|
+
"--app-dir": { key: "appDirs", type: "string[]" },
|
|
97
|
+
});
|
|
98
|
+
const appDirs = raw.appDirs;
|
|
99
|
+
return {
|
|
100
|
+
projectRoot: path.resolve(raw.projectRoot),
|
|
101
|
+
port: raw.port,
|
|
102
|
+
appDirs: appDirs.length > 0 ? appDirs : undefined,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function parseDevArgs(args) {
|
|
106
|
+
const raw = parseArgs(args, {
|
|
107
|
+
"--project-root": { key: "projectRoot", type: "string", default: process.cwd() },
|
|
108
|
+
"--port": { key: "port", type: "number", default: 4321 },
|
|
109
|
+
"--viewer-dir": { key: "viewerDir", type: "string" },
|
|
110
|
+
"--app-dir": { key: "appDirs", type: "string[]" },
|
|
111
|
+
});
|
|
112
|
+
let viewerDir = raw.viewerDir;
|
|
113
|
+
if (!viewerDir) {
|
|
114
|
+
const packageDir = path.dirname(path.dirname(new URL(import.meta.url).pathname));
|
|
115
|
+
const builtinViewer = path.join(packageDir, "viewer");
|
|
116
|
+
if (fs.existsSync(path.join(builtinViewer, "package.json"))) {
|
|
117
|
+
viewerDir = builtinViewer;
|
|
118
|
+
}
|
|
119
|
+
if (!viewerDir) {
|
|
120
|
+
throw new Error("Could not find viewer directory. Provide --viewer-dir <path> pointing to the viewer app.");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
viewerDir = path.resolve(viewerDir);
|
|
125
|
+
}
|
|
126
|
+
const appDirs = raw.appDirs;
|
|
127
|
+
return {
|
|
128
|
+
projectRoot: path.resolve(raw.projectRoot),
|
|
129
|
+
port: raw.port,
|
|
130
|
+
viewerDir,
|
|
131
|
+
appDirs: appDirs.length > 0 ? appDirs : undefined,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function parseScreenshotArgs(args) {
|
|
135
|
+
const raw = parseArgs(args, {
|
|
136
|
+
"--base-url": { key: "baseUrl", type: "string" },
|
|
137
|
+
"--graph": { key: "graphPath", type: "string", default: "arch/graph.full.json" },
|
|
138
|
+
"--out-dir": { key: "outDir", type: "string", default: "arch/screenshots" },
|
|
139
|
+
"--params": { key: "paramsPath", type: "string", default: "arch/screenshot-params.json" },
|
|
140
|
+
"--generate-params": { key: "generateParams", type: "boolean" },
|
|
141
|
+
});
|
|
142
|
+
return {
|
|
143
|
+
baseUrl: raw.baseUrl,
|
|
144
|
+
graphPath: raw.graphPath,
|
|
145
|
+
outDir: raw.outDir,
|
|
146
|
+
paramsPath: raw.paramsPath,
|
|
147
|
+
generateParams: raw.generateParams,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
function parseDescribeArgs(args) {
|
|
151
|
+
const raw = parseArgs(args, {
|
|
152
|
+
"--graph": { key: "graphPath", type: "string", default: "arch/graph.full.json" },
|
|
153
|
+
"--out": { key: "outPath", type: "string", default: "arch/describe-context.md" },
|
|
154
|
+
"--all": { key: "all", type: "boolean" },
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
graphPath: raw.graphPath,
|
|
158
|
+
outPath: raw.outPath,
|
|
159
|
+
all: raw.all,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// Commands
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
10
165
|
async function main() {
|
|
11
166
|
const [commandOrArg, ...rest] = process.argv.slice(2);
|
|
12
167
|
if (commandOrArg === "describe") {
|
|
@@ -74,233 +229,6 @@ async function main() {
|
|
|
74
229
|
writeJsonFile(outputFile, graph);
|
|
75
230
|
logAnalyzeSummary(graph, outputFile, options.projectRoot);
|
|
76
231
|
}
|
|
77
|
-
function parseAnalyzeArgs(args) {
|
|
78
|
-
let projectRoot = process.cwd();
|
|
79
|
-
let out = "arch/graph.full.json";
|
|
80
|
-
const appDirs = [];
|
|
81
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
82
|
-
const argument = args[index];
|
|
83
|
-
if (argument === "--project-root" && args[index + 1]) {
|
|
84
|
-
projectRoot = path.resolve(args[index + 1]);
|
|
85
|
-
index += 1;
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
if (argument === "--out" && args[index + 1]) {
|
|
89
|
-
out = args[index + 1];
|
|
90
|
-
index += 1;
|
|
91
|
-
continue;
|
|
92
|
-
}
|
|
93
|
-
if (argument === "--app-dir" && args[index + 1]) {
|
|
94
|
-
appDirs.push(args[index + 1]);
|
|
95
|
-
index += 1;
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (argument === "--help" || argument === "-h") {
|
|
99
|
-
printHelp();
|
|
100
|
-
process.exit(0);
|
|
101
|
-
}
|
|
102
|
-
throw new Error(`Unknown argument: ${argument}`);
|
|
103
|
-
}
|
|
104
|
-
return {
|
|
105
|
-
projectRoot,
|
|
106
|
-
out,
|
|
107
|
-
appDirs: appDirs.length > 0 ? appDirs : undefined,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
function parseDiffArgs(args) {
|
|
111
|
-
let beforePath = "";
|
|
112
|
-
let afterPath = "";
|
|
113
|
-
let out = "arch/graph.diff.json";
|
|
114
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
115
|
-
const argument = args[index];
|
|
116
|
-
if (argument === "--before" && args[index + 1]) {
|
|
117
|
-
beforePath = args[index + 1];
|
|
118
|
-
index += 1;
|
|
119
|
-
continue;
|
|
120
|
-
}
|
|
121
|
-
if (argument === "--after" && args[index + 1]) {
|
|
122
|
-
afterPath = args[index + 1];
|
|
123
|
-
index += 1;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
if (argument === "--out" && args[index + 1]) {
|
|
127
|
-
out = args[index + 1];
|
|
128
|
-
index += 1;
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (argument === "--help" || argument === "-h") {
|
|
132
|
-
printHelp();
|
|
133
|
-
process.exit(0);
|
|
134
|
-
}
|
|
135
|
-
throw new Error(`Unknown argument: ${argument}`);
|
|
136
|
-
}
|
|
137
|
-
if (!beforePath || !afterPath) {
|
|
138
|
-
throw new Error("The diff command requires --before <path> and --after <path>.");
|
|
139
|
-
}
|
|
140
|
-
return {
|
|
141
|
-
beforePath,
|
|
142
|
-
afterPath,
|
|
143
|
-
out,
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
function parseServeArgs(args) {
|
|
147
|
-
let projectRoot = process.cwd();
|
|
148
|
-
let port = 4321;
|
|
149
|
-
const appDirs = [];
|
|
150
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
151
|
-
const argument = args[index];
|
|
152
|
-
if (argument === "--project-root" && args[index + 1]) {
|
|
153
|
-
projectRoot = path.resolve(args[index + 1]);
|
|
154
|
-
index += 1;
|
|
155
|
-
continue;
|
|
156
|
-
}
|
|
157
|
-
if (argument === "--port" && args[index + 1]) {
|
|
158
|
-
port = Number(args[index + 1]);
|
|
159
|
-
if (!Number.isInteger(port) || port <= 0) {
|
|
160
|
-
throw new Error(`Invalid port: ${args[index + 1]}`);
|
|
161
|
-
}
|
|
162
|
-
index += 1;
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (argument === "--app-dir" && args[index + 1]) {
|
|
166
|
-
appDirs.push(args[index + 1]);
|
|
167
|
-
index += 1;
|
|
168
|
-
continue;
|
|
169
|
-
}
|
|
170
|
-
if (argument === "--help" || argument === "-h") {
|
|
171
|
-
printHelp();
|
|
172
|
-
process.exit(0);
|
|
173
|
-
}
|
|
174
|
-
throw new Error(`Unknown argument: ${argument}`);
|
|
175
|
-
}
|
|
176
|
-
return {
|
|
177
|
-
projectRoot,
|
|
178
|
-
port,
|
|
179
|
-
appDirs: appDirs.length > 0 ? appDirs : undefined,
|
|
180
|
-
};
|
|
181
|
-
}
|
|
182
|
-
function parseDevArgs(args) {
|
|
183
|
-
let projectRoot = process.cwd();
|
|
184
|
-
let port = 4321;
|
|
185
|
-
let viewerDir = "";
|
|
186
|
-
const appDirs = [];
|
|
187
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
188
|
-
const argument = args[index];
|
|
189
|
-
if (argument === "--project-root" && args[index + 1]) {
|
|
190
|
-
projectRoot = path.resolve(args[index + 1]);
|
|
191
|
-
index += 1;
|
|
192
|
-
continue;
|
|
193
|
-
}
|
|
194
|
-
if (argument === "--port" && args[index + 1]) {
|
|
195
|
-
port = Number(args[index + 1]);
|
|
196
|
-
if (!Number.isInteger(port) || port <= 0) {
|
|
197
|
-
throw new Error(`Invalid port: ${args[index + 1]}`);
|
|
198
|
-
}
|
|
199
|
-
index += 1;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
if (argument === "--viewer-dir" && args[index + 1]) {
|
|
203
|
-
viewerDir = path.resolve(args[index + 1]);
|
|
204
|
-
index += 1;
|
|
205
|
-
continue;
|
|
206
|
-
}
|
|
207
|
-
if (argument === "--app-dir" && args[index + 1]) {
|
|
208
|
-
appDirs.push(args[index + 1]);
|
|
209
|
-
index += 1;
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
if (argument === "--help" || argument === "-h") {
|
|
213
|
-
printHelp();
|
|
214
|
-
process.exit(0);
|
|
215
|
-
}
|
|
216
|
-
throw new Error(`Unknown argument: ${argument}`);
|
|
217
|
-
}
|
|
218
|
-
if (!viewerDir) {
|
|
219
|
-
// Try to find the viewer relative to the analyzer package
|
|
220
|
-
const packageDir = path.dirname(path.dirname(new URL(import.meta.url).pathname));
|
|
221
|
-
const builtinViewer = path.join(packageDir, "viewer");
|
|
222
|
-
if (fs.existsSync(path.join(builtinViewer, "package.json"))) {
|
|
223
|
-
viewerDir = builtinViewer;
|
|
224
|
-
}
|
|
225
|
-
if (!viewerDir) {
|
|
226
|
-
throw new Error("Could not find viewer directory. Provide --viewer-dir <path> pointing to the viewer app.");
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
return {
|
|
230
|
-
projectRoot,
|
|
231
|
-
port,
|
|
232
|
-
viewerDir,
|
|
233
|
-
appDirs: appDirs.length > 0 ? appDirs : undefined,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
function parseScreenshotArgs(args) {
|
|
237
|
-
let baseUrl = "";
|
|
238
|
-
let graphPath = "arch/graph.full.json";
|
|
239
|
-
let outDir = "arch/screenshots";
|
|
240
|
-
let paramsPath = "arch/screenshot-params.json";
|
|
241
|
-
let generateParams = false;
|
|
242
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
243
|
-
const argument = args[index];
|
|
244
|
-
if (argument === "--base-url" && args[index + 1]) {
|
|
245
|
-
baseUrl = args[index + 1];
|
|
246
|
-
index += 1;
|
|
247
|
-
continue;
|
|
248
|
-
}
|
|
249
|
-
if (argument === "--graph" && args[index + 1]) {
|
|
250
|
-
graphPath = args[index + 1];
|
|
251
|
-
index += 1;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (argument === "--out-dir" && args[index + 1]) {
|
|
255
|
-
outDir = args[index + 1];
|
|
256
|
-
index += 1;
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
if (argument === "--params" && args[index + 1]) {
|
|
260
|
-
paramsPath = args[index + 1];
|
|
261
|
-
index += 1;
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
if (argument === "--generate-params") {
|
|
265
|
-
generateParams = true;
|
|
266
|
-
continue;
|
|
267
|
-
}
|
|
268
|
-
if (argument === "--help" || argument === "-h") {
|
|
269
|
-
printHelp();
|
|
270
|
-
process.exit(0);
|
|
271
|
-
}
|
|
272
|
-
throw new Error(`Unknown argument: ${argument}`);
|
|
273
|
-
}
|
|
274
|
-
return { baseUrl, graphPath, outDir, paramsPath, generateParams };
|
|
275
|
-
}
|
|
276
|
-
function parseDescribeArgs(args) {
|
|
277
|
-
let graphPath = "arch/graph.full.json";
|
|
278
|
-
let outPath = "arch/describe-context.md";
|
|
279
|
-
let all = false;
|
|
280
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
281
|
-
const argument = args[index];
|
|
282
|
-
if (argument === "--graph" && args[index + 1]) {
|
|
283
|
-
graphPath = args[index + 1];
|
|
284
|
-
index += 1;
|
|
285
|
-
continue;
|
|
286
|
-
}
|
|
287
|
-
if (argument === "--out" && args[index + 1]) {
|
|
288
|
-
outPath = args[index + 1];
|
|
289
|
-
index += 1;
|
|
290
|
-
continue;
|
|
291
|
-
}
|
|
292
|
-
if (argument === "--all") {
|
|
293
|
-
all = true;
|
|
294
|
-
continue;
|
|
295
|
-
}
|
|
296
|
-
if (argument === "--help" || argument === "-h") {
|
|
297
|
-
printHelp();
|
|
298
|
-
process.exit(0);
|
|
299
|
-
}
|
|
300
|
-
throw new Error(`Unknown argument: ${argument}`);
|
|
301
|
-
}
|
|
302
|
-
return { graphPath, outPath, all };
|
|
303
|
-
}
|
|
304
232
|
async function runDev(options) {
|
|
305
233
|
const children = [];
|
|
306
234
|
const cleanup = () => {
|
|
@@ -365,6 +293,9 @@ async function runDev(options) {
|
|
|
365
293
|
// Keep the process alive
|
|
366
294
|
await new Promise(() => { });
|
|
367
295
|
}
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
// Output helpers
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
368
299
|
function logAnalyzeSummary(graph, outputFile, projectRoot) {
|
|
369
300
|
const pageCount = graph.nodes.filter((node) => node.type === "page").length;
|
|
370
301
|
const endpointCount = graph.nodes.filter((node) => node.type === "endpoint").length;
|
package/package.json
CHANGED
package/viewer/src/App.tsx
CHANGED
|
@@ -3,15 +3,17 @@ import * as Switch from "@radix-ui/react-switch";
|
|
|
3
3
|
import { Filters } from "./Filters";
|
|
4
4
|
import { GraphView } from "./GraphView";
|
|
5
5
|
import { NodeDetails } from "./NodeDetails";
|
|
6
|
-
import
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
6
|
+
import {
|
|
7
|
+
buildEdgeKey,
|
|
8
|
+
type DiffStatus,
|
|
9
|
+
type EdgeKind,
|
|
10
|
+
type Graph,
|
|
11
|
+
type GraphDiff,
|
|
12
|
+
type Node,
|
|
13
|
+
type NodeType,
|
|
14
|
+
} from "./types";
|
|
15
|
+
|
|
16
|
+
const ALL_NODE_TYPES: NodeType[] = ["page", "endpoint", "handler", "db", "service"];
|
|
15
17
|
const ALL_EDGE_KINDS: EdgeKind[] = [
|
|
16
18
|
"page-endpoint",
|
|
17
19
|
"page-service",
|
|
@@ -20,7 +22,6 @@ const ALL_EDGE_KINDS: EdgeKind[] = [
|
|
|
20
22
|
"db-relation",
|
|
21
23
|
];
|
|
22
24
|
|
|
23
|
-
|
|
24
25
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
25
26
|
return !!value && typeof value === "object";
|
|
26
27
|
}
|
|
@@ -48,10 +49,6 @@ function isGraphDiff(value: unknown): value is GraphDiff {
|
|
|
48
49
|
);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
function buildEdgeKey(from: string, to: string, kind: EdgeKind): string {
|
|
52
|
-
return `${from}::${to}::${kind}`;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
52
|
export function App() {
|
|
56
53
|
const [graph, setGraph] = useState<Graph | null>(null);
|
|
57
54
|
const [graphDiff, setGraphDiff] = useState<GraphDiff | null>(null);
|
|
@@ -234,11 +231,8 @@ export function App() {
|
|
|
234
231
|
const baseGraph = getRenderedGraph(graph, graphDiff);
|
|
235
232
|
const pageRoutes = getPageRoutes(baseGraph);
|
|
236
233
|
const renderedGraph =
|
|
237
|
-
focusedPageRoute && baseGraph
|
|
238
|
-
|
|
239
|
-
: baseGraph;
|
|
240
|
-
const selectedNode =
|
|
241
|
-
renderedGraph?.nodes.find((node) => node.id === selectedNodeId) ?? null;
|
|
234
|
+
focusedPageRoute && baseGraph ? buildFocusedSubgraph(baseGraph, focusedPageRoute) : baseGraph;
|
|
235
|
+
const selectedNode = renderedGraph?.nodes.find((node) => node.id === selectedNodeId) ?? null;
|
|
242
236
|
const nodeStatusById = graphDiff ? buildNodeStatusById(graphDiff) : undefined;
|
|
243
237
|
const edgeStatusByKey = graphDiff ? buildEdgeStatusByKey(graphDiff) : undefined;
|
|
244
238
|
|
|
@@ -263,12 +257,8 @@ export function App() {
|
|
|
263
257
|
<div className="p-5 space-y-5">
|
|
264
258
|
{/* Header */}
|
|
265
259
|
<div>
|
|
266
|
-
<h1 className="text-base font-bold text-slate-900 tracking-tight">
|
|
267
|
-
|
|
268
|
-
</h1>
|
|
269
|
-
<p className="text-[11px] text-slate-400 mt-0.5">
|
|
270
|
-
Architecture graph viewer
|
|
271
|
-
</p>
|
|
260
|
+
<h1 className="text-base font-bold text-slate-900 tracking-tight">next-arch-map</h1>
|
|
261
|
+
<p className="text-[11px] text-slate-400 mt-0.5">Architecture graph viewer</p>
|
|
272
262
|
</div>
|
|
273
263
|
|
|
274
264
|
{/* Status badge */}
|
|
@@ -280,11 +270,7 @@ export function App() {
|
|
|
280
270
|
</span>
|
|
281
271
|
<span className="text-xs text-slate-500">
|
|
282
272
|
{nodeCount} nodes, {edgeCount} edges
|
|
283
|
-
{graphDiff && (
|
|
284
|
-
<span className="ml-1 text-emerald-600 font-medium">
|
|
285
|
-
(diff)
|
|
286
|
-
</span>
|
|
287
|
-
)}
|
|
273
|
+
{graphDiff && <span className="ml-1 text-emerald-600 font-medium">(diff)</span>}
|
|
288
274
|
</span>
|
|
289
275
|
</div>
|
|
290
276
|
) : loadError ? (
|
|
@@ -337,9 +323,7 @@ export function App() {
|
|
|
337
323
|
)}
|
|
338
324
|
{!useServer && (
|
|
339
325
|
<div>
|
|
340
|
-
<label className="text-[11px] text-slate-500 block mb-1.5">
|
|
341
|
-
Load graph JSON
|
|
342
|
-
</label>
|
|
326
|
+
<label className="text-[11px] text-slate-500 block mb-1.5">Load graph JSON</label>
|
|
343
327
|
<input
|
|
344
328
|
type="file"
|
|
345
329
|
accept="application/json,.json"
|
|
@@ -404,9 +388,7 @@ export function App() {
|
|
|
404
388
|
{isQueryLoading ? "Loading..." : "Query"}
|
|
405
389
|
</button>
|
|
406
390
|
|
|
407
|
-
{queryError &&
|
|
408
|
-
<div className="text-xs text-red-600">{queryError}</div>
|
|
409
|
-
)}
|
|
391
|
+
{queryError && <div className="text-xs text-red-600">{queryError}</div>}
|
|
410
392
|
|
|
411
393
|
{queryResult && (
|
|
412
394
|
<div className="text-xs text-slate-600">
|
|
@@ -443,8 +425,18 @@ export function App() {
|
|
|
443
425
|
<div className="h-full flex items-center justify-center">
|
|
444
426
|
<div className="text-center space-y-3">
|
|
445
427
|
<div className="text-4xl opacity-20">
|
|
446
|
-
<svg
|
|
447
|
-
|
|
428
|
+
<svg
|
|
429
|
+
className="mx-auto h-12 w-12 text-slate-300"
|
|
430
|
+
fill="none"
|
|
431
|
+
viewBox="0 0 24 24"
|
|
432
|
+
stroke="currentColor"
|
|
433
|
+
>
|
|
434
|
+
<path
|
|
435
|
+
strokeLinecap="round"
|
|
436
|
+
strokeLinejoin="round"
|
|
437
|
+
strokeWidth={1.5}
|
|
438
|
+
d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01"
|
|
439
|
+
/>
|
|
448
440
|
</svg>
|
|
449
441
|
</div>
|
|
450
442
|
<p className="text-sm text-slate-400">
|
package/viewer/src/Filters.tsx
CHANGED
|
@@ -16,6 +16,7 @@ const NODE_TYPE_COLORS: Record<NodeType, string> = {
|
|
|
16
16
|
endpoint: "bg-emerald-600",
|
|
17
17
|
handler: "bg-teal-500",
|
|
18
18
|
db: "bg-red-600",
|
|
19
|
+
action: "bg-purple-500",
|
|
19
20
|
service: "bg-violet-600",
|
|
20
21
|
};
|
|
21
22
|
|
|
@@ -38,10 +39,7 @@ export function Filters(props: FiltersProps) {
|
|
|
38
39
|
</h3>
|
|
39
40
|
<div className="space-y-1.5">
|
|
40
41
|
{allNodeTypes.map((type) => (
|
|
41
|
-
<label
|
|
42
|
-
key={type}
|
|
43
|
-
className="flex items-center gap-2 cursor-pointer group"
|
|
44
|
-
>
|
|
42
|
+
<label key={type} className="flex items-center gap-2 cursor-pointer group">
|
|
45
43
|
<Checkbox.Root
|
|
46
44
|
checked={visibleNodeTypes.has(type)}
|
|
47
45
|
onCheckedChange={() => onToggleNodeType(type)}
|
|
@@ -59,9 +57,7 @@ export function Filters(props: FiltersProps) {
|
|
|
59
57
|
</svg>
|
|
60
58
|
</Checkbox.Indicator>
|
|
61
59
|
</Checkbox.Root>
|
|
62
|
-
<span
|
|
63
|
-
className={`h-2.5 w-2.5 rounded-full ${NODE_TYPE_COLORS[type]}`}
|
|
64
|
-
/>
|
|
60
|
+
<span className={`h-2.5 w-2.5 rounded-full ${NODE_TYPE_COLORS[type]}`} />
|
|
65
61
|
<span className="text-xs text-slate-600 group-hover:text-slate-900 transition-colors">
|
|
66
62
|
{type}
|
|
67
63
|
</span>
|
|
@@ -77,10 +73,7 @@ export function Filters(props: FiltersProps) {
|
|
|
77
73
|
</h3>
|
|
78
74
|
<div className="space-y-1.5">
|
|
79
75
|
{allEdgeKinds.map((kind) => (
|
|
80
|
-
<label
|
|
81
|
-
key={kind}
|
|
82
|
-
className="flex items-center gap-2 cursor-pointer group"
|
|
83
|
-
>
|
|
76
|
+
<label key={kind} className="flex items-center gap-2 cursor-pointer group">
|
|
84
77
|
<Checkbox.Root
|
|
85
78
|
checked={visibleEdgeKinds.has(kind)}
|
|
86
79
|
onCheckedChange={() => onToggleEdgeKind(kind)}
|
package/viewer/src/GraphView.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type NodeProps,
|
|
14
14
|
} from "@xyflow/react";
|
|
15
15
|
import "@xyflow/react/dist/style.css";
|
|
16
|
-
import type
|
|
16
|
+
import { buildEdgeKey, type DiffStatus, type EdgeKind, type Graph, type NodeType } from "./types";
|
|
17
17
|
|
|
18
18
|
type GraphViewProps = {
|
|
19
19
|
graph: Graph;
|
|
@@ -30,6 +30,7 @@ const NODE_COLOR: Record<NodeType, string> = {
|
|
|
30
30
|
endpoint: "#059669",
|
|
31
31
|
db: "#dc2626",
|
|
32
32
|
handler: "#14b8a6",
|
|
33
|
+
action: "#8b5cf6",
|
|
33
34
|
service: "#7c3aed",
|
|
34
35
|
};
|
|
35
36
|
|
|
@@ -46,19 +47,17 @@ const EDGE_COLOR: Record<EdgeKind, string> = {
|
|
|
46
47
|
const DIFF_BORDER_COLOR: Record<DiffStatus, string> = {
|
|
47
48
|
added: "#22c55e",
|
|
48
49
|
removed: "#ef4444",
|
|
50
|
+
modified: "#f59e0b",
|
|
49
51
|
unchanged: "rgba(15, 23, 42, 0.12)",
|
|
50
52
|
};
|
|
51
53
|
|
|
52
54
|
const DIFF_EDGE_COLOR: Record<DiffStatus, string> = {
|
|
53
55
|
added: "#22c55e",
|
|
54
56
|
removed: "#ef4444",
|
|
57
|
+
modified: "#f59e0b",
|
|
55
58
|
unchanged: "#000000",
|
|
56
59
|
};
|
|
57
60
|
|
|
58
|
-
function buildEdgeKey(from: string, to: string, kind: EdgeKind): string {
|
|
59
|
-
return `${from}::${to}::${kind}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
61
|
/**
|
|
63
62
|
* Reorder nodes within each column so that connected nodes are placed
|
|
64
63
|
* close together vertically, minimizing long diagonal edge crossings.
|
|
@@ -156,9 +155,7 @@ function PageNode({ data }: NodeProps) {
|
|
|
156
155
|
return (
|
|
157
156
|
<div>
|
|
158
157
|
<Handle type="target" position={Position.Left} style={{ visibility: "hidden" }} />
|
|
159
|
-
<div style={{ fontSize: 12, fontWeight: 600 }}>
|
|
160
|
-
{String(d.label ?? "")}
|
|
161
|
-
</div>
|
|
158
|
+
<div style={{ fontSize: 12, fontWeight: 600 }}>{String(d.label ?? "")}</div>
|
|
162
159
|
{description && <DescriptionLine text={description} />}
|
|
163
160
|
{screenshot && (
|
|
164
161
|
<img
|
|
@@ -240,9 +237,7 @@ function DbTableNode({ data }: NodeProps) {
|
|
|
240
237
|
U
|
|
241
238
|
</span>
|
|
242
239
|
)}
|
|
243
|
-
{!col.isId && !col.isUnique &&
|
|
244
|
-
<span style={{ width: 12, display: "inline-block" }} />
|
|
245
|
-
)}
|
|
240
|
+
{!col.isId && !col.isUnique && <span style={{ width: 12, display: "inline-block" }} />}
|
|
246
241
|
<span style={{ fontWeight: 500, flex: 1 }}>{col.name}</span>
|
|
247
242
|
<span
|
|
248
243
|
style={{
|
|
@@ -280,9 +275,7 @@ function DescribedNode({ data }: NodeProps) {
|
|
|
280
275
|
return (
|
|
281
276
|
<div>
|
|
282
277
|
<Handle type="target" position={Position.Left} style={{ visibility: "hidden" }} />
|
|
283
|
-
<div style={{ fontSize: 12, fontWeight: 600 }}>
|
|
284
|
-
{String(d.label ?? "")}
|
|
285
|
-
</div>
|
|
278
|
+
<div style={{ fontSize: 12, fontWeight: 600 }}>{String(d.label ?? "")}</div>
|
|
286
279
|
{description && <DescriptionLine text={description} dark={dark} />}
|
|
287
280
|
<Handle type="source" position={Position.Right} style={{ visibility: "hidden" }} />
|
|
288
281
|
</div>
|
|
@@ -332,9 +325,7 @@ export function GraphView(props: GraphViewProps) {
|
|
|
332
325
|
const typeOrder: NodeType[] = ["page", "endpoint", "handler", "db", "service"];
|
|
333
326
|
const visibleNodes = graph.nodes.filter((node) => visibleNodeTypes.has(node.type));
|
|
334
327
|
const visibleNodeIds = new Set(visibleNodes.map((node) => node.id));
|
|
335
|
-
const nodesByType = new Map<NodeType, typeof visibleNodes>(
|
|
336
|
-
typeOrder.map((type) => [type, []]),
|
|
337
|
-
);
|
|
328
|
+
const nodesByType = new Map<NodeType, typeof visibleNodes>(typeOrder.map((type) => [type, []]));
|
|
338
329
|
|
|
339
330
|
for (const node of visibleNodes) {
|
|
340
331
|
nodesByType.get(node.type)?.push(node);
|
|
@@ -350,9 +341,7 @@ export function GraphView(props: GraphViewProps) {
|
|
|
350
341
|
const flowNodes: FlowNode[] = [];
|
|
351
342
|
const columnWidth = 300;
|
|
352
343
|
const defaultRowHeight = 80;
|
|
353
|
-
const hasScreenshots = graph.nodes.some(
|
|
354
|
-
(n) => n.type === "page" && n.meta?.screenshot,
|
|
355
|
-
);
|
|
344
|
+
const hasScreenshots = graph.nodes.some((n) => n.type === "page" && n.meta?.screenshot);
|
|
356
345
|
const pageRowHeight = hasScreenshots ? 140 : defaultRowHeight;
|
|
357
346
|
|
|
358
347
|
// Compute max db column count for row height
|
|
@@ -361,11 +350,13 @@ export function GraphView(props: GraphViewProps) {
|
|
|
361
350
|
const cols = Array.isArray(n.meta?.columns) ? (n.meta?.columns as unknown[]).length : 0;
|
|
362
351
|
return Math.max(max, cols);
|
|
363
352
|
}, 0);
|
|
364
|
-
const dbRowHeight =
|
|
353
|
+
const dbRowHeight =
|
|
354
|
+
maxDbColumns > 0 ? Math.max(defaultRowHeight, 30 + maxDbColumns * 22) : defaultRowHeight;
|
|
365
355
|
|
|
366
356
|
activeTypeOrder.forEach((type, columnIndex) => {
|
|
367
357
|
const nodes = nodesByType.get(type) ?? [];
|
|
368
|
-
const rowHeight =
|
|
358
|
+
const rowHeight =
|
|
359
|
+
type === "page" ? pageRowHeight : type === "db" ? dbRowHeight : defaultRowHeight;
|
|
369
360
|
nodes.forEach((node, rowIndex) => {
|
|
370
361
|
const isSelected = node.id === selectedNodeId;
|
|
371
362
|
const status = nodeStatusById?.get(node.id) ?? "unchanged";
|
|
@@ -375,7 +366,10 @@ export function GraphView(props: GraphViewProps) {
|
|
|
375
366
|
const screenshot = isPage ? (node.meta?.screenshot as string | undefined) : undefined;
|
|
376
367
|
const description = node.meta?.description as string | undefined;
|
|
377
368
|
const isDarkText = false;
|
|
378
|
-
const isDbTable =
|
|
369
|
+
const isDbTable =
|
|
370
|
+
node.type === "db" &&
|
|
371
|
+
Array.isArray(node.meta?.columns) &&
|
|
372
|
+
(node.meta?.columns as unknown[]).length > 0;
|
|
379
373
|
const dbColumns = isDbTable ? (node.meta?.columns as DbColumn[]) : undefined;
|
|
380
374
|
|
|
381
375
|
const nodeType = isDbTable
|
|
@@ -451,8 +445,10 @@ export function GraphView(props: GraphViewProps) {
|
|
|
451
445
|
visibleNodeIds.has(edge.to),
|
|
452
446
|
)
|
|
453
447
|
.map((edge, index) => {
|
|
454
|
-
const status =
|
|
455
|
-
|
|
448
|
+
const status =
|
|
449
|
+
edgeStatusByKey?.get(buildEdgeKey(edge.from, edge.to, edge.kind)) ?? "unchanged";
|
|
450
|
+
const strokeColor =
|
|
451
|
+
status === "unchanged" ? EDGE_COLOR[edge.kind] : DIFF_EDGE_COLOR[status];
|
|
456
452
|
|
|
457
453
|
return {
|
|
458
454
|
id: `${edge.from}=>${edge.to}::${edge.kind}::${index}`,
|
|
@@ -474,14 +470,7 @@ export function GraphView(props: GraphViewProps) {
|
|
|
474
470
|
});
|
|
475
471
|
|
|
476
472
|
return { flowNodes, flowEdges };
|
|
477
|
-
}, [
|
|
478
|
-
edgeStatusByKey,
|
|
479
|
-
graph,
|
|
480
|
-
nodeStatusById,
|
|
481
|
-
selectedNodeId,
|
|
482
|
-
visibleEdgeKinds,
|
|
483
|
-
visibleNodeTypes,
|
|
484
|
-
]);
|
|
473
|
+
}, [edgeStatusByKey, graph, nodeStatusById, selectedNodeId, visibleEdgeKinds, visibleNodeTypes]);
|
|
485
474
|
|
|
486
475
|
// Apply hover highlighting as a cheap pass over precomputed nodes/edges
|
|
487
476
|
const activeNodeId = hoveredNodeId ?? selectedNodeId;
|
|
@@ -9,15 +9,14 @@ const TYPE_BADGE_COLORS: Record<NodeType, string> = {
|
|
|
9
9
|
endpoint: "bg-emerald-100 text-emerald-700",
|
|
10
10
|
handler: "bg-teal-100 text-teal-700",
|
|
11
11
|
db: "bg-red-100 text-red-700",
|
|
12
|
+
action: "bg-purple-100 text-purple-700",
|
|
12
13
|
service: "bg-violet-100 text-violet-700",
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
export function NodeDetails({ node }: NodeDetailsProps) {
|
|
16
17
|
if (!node) {
|
|
17
18
|
return (
|
|
18
|
-
<div className="mt-5 py-4 text-center text-xs text-slate-400">
|
|
19
|
-
Click a node to inspect
|
|
20
|
-
</div>
|
|
19
|
+
<div className="mt-5 py-4 text-center text-xs text-slate-400">Click a node to inspect</div>
|
|
21
20
|
);
|
|
22
21
|
}
|
|
23
22
|
|
|
@@ -36,14 +35,10 @@ export function NodeDetails({ node }: NodeDetailsProps) {
|
|
|
36
35
|
>
|
|
37
36
|
{node.type}
|
|
38
37
|
</span>
|
|
39
|
-
<span className="text-sm font-medium text-slate-900 truncate">
|
|
40
|
-
{node.label}
|
|
41
|
-
</span>
|
|
38
|
+
<span className="text-sm font-medium text-slate-900 truncate">{node.label}</span>
|
|
42
39
|
</div>
|
|
43
40
|
|
|
44
|
-
<div className="text-[11px] text-slate-500 font-mono break-all">
|
|
45
|
-
{node.id}
|
|
46
|
-
</div>
|
|
41
|
+
<div className="text-[11px] text-slate-500 font-mono break-all">{node.id}</div>
|
|
47
42
|
|
|
48
43
|
{(node.meta?.descriptionLong ?? node.meta?.description) ? (
|
|
49
44
|
<p className="text-xs text-slate-600 leading-relaxed">
|
|
@@ -73,7 +68,8 @@ export function NodeDetails({ node }: NodeDetailsProps) {
|
|
|
73
68
|
{JSON.stringify(
|
|
74
69
|
Object.fromEntries(
|
|
75
70
|
Object.entries(node.meta).filter(
|
|
76
|
-
([key]) =>
|
|
71
|
+
([key]) =>
|
|
72
|
+
key !== "screenshot" && key !== "description" && key !== "descriptionLong",
|
|
77
73
|
),
|
|
78
74
|
),
|
|
79
75
|
null,
|
package/viewer/src/types.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
| "db"
|
|
5
|
-
| "handler"
|
|
6
|
-
| "service";
|
|
1
|
+
// Keep in sync with src/model.ts and src/diff.ts (the backend is the source of truth).
|
|
2
|
+
|
|
3
|
+
export type NodeType = "page" | "endpoint" | "db" | "handler" | "action" | "service";
|
|
7
4
|
|
|
8
5
|
export type Node = {
|
|
9
6
|
id: string;
|
|
@@ -33,7 +30,7 @@ export type Graph = {
|
|
|
33
30
|
edges: Edge[];
|
|
34
31
|
};
|
|
35
32
|
|
|
36
|
-
export type DiffStatus = "added" | "removed" | "unchanged";
|
|
33
|
+
export type DiffStatus = "added" | "removed" | "modified" | "unchanged";
|
|
37
34
|
|
|
38
35
|
export type NodeDiff = {
|
|
39
36
|
node: Node;
|
|
@@ -49,3 +46,7 @@ export type GraphDiff = {
|
|
|
49
46
|
nodes: NodeDiff[];
|
|
50
47
|
edges: EdgeDiff[];
|
|
51
48
|
};
|
|
49
|
+
|
|
50
|
+
export function buildEdgeKey(from: string, to: string, kind: EdgeKind): string {
|
|
51
|
+
return `${from}::${to}::${kind}`;
|
|
52
|
+
}
|