next-arch-map 0.1.20 → 0.1.22
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/pagesToEndpoints.js +44 -2
- package/package.json +1 -1
- package/viewer/src/App.tsx +0 -90
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import ts from "typescript";
|
|
3
|
-
import { buildActionNode, buildEdgeKey, buildEndpointNode, buildPageNode, collectStringConstants, ensureNode, getExistingDirectories, getPageRouteFromFile, getSourceFile, getStringLiteralValue, isIgnoredSourceFile, isPageFile, resolveProjectRoot, walkDirectory, } from "../utils.js";
|
|
3
|
+
import { buildActionNode, buildEdgeKey, buildEndpointNode, buildPageNode, collectStringConstants, ensureNode, getExistingDirectories, getPageRouteFromFile, getSourceFile, getStringLiteralValue, isIgnoredSourceFile, isPageFile, resolveLocalModulePath, resolveProjectRoot, walkDirectory, } from "../utils.js";
|
|
4
4
|
const DEFAULT_APP_DIRS = ["app", "src/app"];
|
|
5
5
|
const DEFAULT_EXTRA_SCAN_DIRS = ["src/features", "src/services", "src/lib", "src/hooks"];
|
|
6
6
|
const DEFAULT_HTTP_CLIENT_IDENTIFIERS = ["fetch", "axios", "apiClient"];
|
|
@@ -31,15 +31,18 @@ export async function analyzePagesToEndpoints(options) {
|
|
|
31
31
|
if (!sourceFile) {
|
|
32
32
|
continue;
|
|
33
33
|
}
|
|
34
|
-
const httpCalls = collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods);
|
|
35
34
|
// Non-page files (route handlers, layouts, helpers) under app/ only
|
|
36
35
|
// contribute endpoint nodes without creating fake page flows.
|
|
37
36
|
if (!isPageFile(filePath)) {
|
|
37
|
+
const httpCalls = collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods);
|
|
38
38
|
for (const call of httpCalls) {
|
|
39
39
|
ensureNode(nodes, nodeIds, buildEndpointNode(call.endpoint, filePath));
|
|
40
40
|
}
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
|
+
// For page files, follow imports transitively to find HTTP calls
|
|
44
|
+
// in hooks, utilities, and other local modules.
|
|
45
|
+
const httpCalls = collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifiers, httpClientMethods, sourceFileCache);
|
|
43
46
|
const route = getPageRouteFromFile(appDir, filePath);
|
|
44
47
|
ensureNode(nodes, nodeIds, buildPageNode(route, filePath));
|
|
45
48
|
for (const call of httpCalls) {
|
|
@@ -115,6 +118,45 @@ export async function analyzePagesToEndpoints(options) {
|
|
|
115
118
|
left.to.localeCompare(right.to)),
|
|
116
119
|
};
|
|
117
120
|
}
|
|
121
|
+
function collectImportSpecifiers(sourceFile) {
|
|
122
|
+
const specifiers = [];
|
|
123
|
+
for (const statement of sourceFile.statements) {
|
|
124
|
+
if (ts.isImportDeclaration(statement) &&
|
|
125
|
+
statement.moduleSpecifier &&
|
|
126
|
+
ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
127
|
+
specifiers.push(statement.moduleSpecifier.text);
|
|
128
|
+
}
|
|
129
|
+
// Re-exports: export { foo } from "./bar"
|
|
130
|
+
if (ts.isExportDeclaration(statement) &&
|
|
131
|
+
statement.moduleSpecifier &&
|
|
132
|
+
ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
133
|
+
specifiers.push(statement.moduleSpecifier.text);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return specifiers;
|
|
137
|
+
}
|
|
138
|
+
function collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifiers, httpClientMethods, sourceFileCache, visited) {
|
|
139
|
+
const resolvedPath = path.resolve(filePath);
|
|
140
|
+
const seen = visited ?? new Set();
|
|
141
|
+
if (seen.has(resolvedPath)) {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
seen.add(resolvedPath);
|
|
145
|
+
const sourceFile = getSourceFile(filePath, sourceFileCache);
|
|
146
|
+
if (!sourceFile) {
|
|
147
|
+
return [];
|
|
148
|
+
}
|
|
149
|
+
const calls = collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods);
|
|
150
|
+
for (const specifier of collectImportSpecifiers(sourceFile)) {
|
|
151
|
+
const resolved = resolveLocalModulePath(filePath, specifier, projectRoot);
|
|
152
|
+
if (!resolved) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const transitiveCalls = collectHttpCallsTransitively(resolved, projectRoot, httpClientIdentifiers, httpClientMethods, sourceFileCache, seen);
|
|
156
|
+
calls.push(...transitiveCalls);
|
|
157
|
+
}
|
|
158
|
+
return calls;
|
|
159
|
+
}
|
|
118
160
|
function collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods) {
|
|
119
161
|
const calls = [];
|
|
120
162
|
const constMap = collectStringConstants(sourceFile);
|
package/package.json
CHANGED
package/viewer/src/App.tsx
CHANGED
|
@@ -20,46 +20,6 @@ const ALL_EDGE_KINDS: EdgeKind[] = [
|
|
|
20
20
|
"action-endpoint",
|
|
21
21
|
];
|
|
22
22
|
|
|
23
|
-
type LayerPreset = "user-flow" | "data-flow" | "full-flow";
|
|
24
|
-
|
|
25
|
-
const USER_FLOW_EDGE_KINDS: EdgeKind[] = [
|
|
26
|
-
"page-action",
|
|
27
|
-
"action-endpoint",
|
|
28
|
-
"page-endpoint",
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
const DATA_FLOW_EDGE_KINDS: EdgeKind[] = [
|
|
32
|
-
"endpoint-handler",
|
|
33
|
-
"endpoint-db",
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
const FULL_FLOW_EDGE_KINDS: EdgeKind[] = [
|
|
37
|
-
"page-endpoint",
|
|
38
|
-
"endpoint-db",
|
|
39
|
-
"endpoint-handler",
|
|
40
|
-
"page-action",
|
|
41
|
-
"action-endpoint",
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
const USER_FLOW_NODE_TYPES: NodeType[] = [
|
|
45
|
-
"page",
|
|
46
|
-
"action",
|
|
47
|
-
"endpoint",
|
|
48
|
-
];
|
|
49
|
-
|
|
50
|
-
const DATA_FLOW_NODE_TYPES: NodeType[] = [
|
|
51
|
-
"endpoint",
|
|
52
|
-
"handler",
|
|
53
|
-
"db",
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
const FULL_FLOW_NODE_TYPES: NodeType[] = [
|
|
57
|
-
"page",
|
|
58
|
-
"action",
|
|
59
|
-
"endpoint",
|
|
60
|
-
"handler",
|
|
61
|
-
"db",
|
|
62
|
-
];
|
|
63
23
|
|
|
64
24
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
65
25
|
return !!value && typeof value === "object";
|
|
@@ -110,7 +70,6 @@ export function App() {
|
|
|
110
70
|
);
|
|
111
71
|
const [selectedNodeId, setSelectedNodeId] = useState<string | null>(null);
|
|
112
72
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
113
|
-
const [activePreset, setActivePreset] = useState<LayerPreset | null>(null);
|
|
114
73
|
const [showAdvanced, setShowAdvanced] = useState(false);
|
|
115
74
|
|
|
116
75
|
useEffect(() => {
|
|
@@ -229,7 +188,6 @@ export function App() {
|
|
|
229
188
|
};
|
|
230
189
|
|
|
231
190
|
const toggleNodeType = (type: NodeType) => {
|
|
232
|
-
setActivePreset(null);
|
|
233
191
|
setVisibleNodeTypes((prev) => {
|
|
234
192
|
const next = new Set(prev);
|
|
235
193
|
if (next.has(type)) next.delete(type);
|
|
@@ -239,7 +197,6 @@ export function App() {
|
|
|
239
197
|
};
|
|
240
198
|
|
|
241
199
|
const toggleEdgeKind = (kind: EdgeKind) => {
|
|
242
|
-
setActivePreset(null);
|
|
243
200
|
setVisibleEdgeKinds((prev) => {
|
|
244
201
|
const next = new Set(prev);
|
|
245
202
|
if (next.has(kind)) next.delete(kind);
|
|
@@ -248,24 +205,6 @@ export function App() {
|
|
|
248
205
|
});
|
|
249
206
|
};
|
|
250
207
|
|
|
251
|
-
const applyPreset = (preset: LayerPreset) => {
|
|
252
|
-
setActivePreset(preset);
|
|
253
|
-
if (preset === "user-flow") {
|
|
254
|
-
setVisibleNodeTypes(new Set(USER_FLOW_NODE_TYPES));
|
|
255
|
-
setVisibleEdgeKinds(new Set(USER_FLOW_EDGE_KINDS));
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (preset === "data-flow") {
|
|
260
|
-
setVisibleNodeTypes(new Set(DATA_FLOW_NODE_TYPES));
|
|
261
|
-
setVisibleEdgeKinds(new Set(DATA_FLOW_EDGE_KINDS));
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
setVisibleNodeTypes(new Set(FULL_FLOW_NODE_TYPES));
|
|
266
|
-
setVisibleEdgeKinds(new Set(FULL_FLOW_EDGE_KINDS));
|
|
267
|
-
};
|
|
268
|
-
|
|
269
208
|
const handlePageToDbQuery = async () => {
|
|
270
209
|
setIsQueryLoading(true);
|
|
271
210
|
setQueryError(null);
|
|
@@ -317,12 +256,6 @@ export function App() {
|
|
|
317
256
|
}
|
|
318
257
|
}, [baseGraph, focusedPageRoute, pageRoutes]);
|
|
319
258
|
|
|
320
|
-
const presets: { key: LayerPreset; label: string }[] = [
|
|
321
|
-
{ key: "user-flow", label: "User Flow" },
|
|
322
|
-
{ key: "data-flow", label: "Data Flow" },
|
|
323
|
-
{ key: "full-flow", label: "Full" },
|
|
324
|
-
];
|
|
325
|
-
|
|
326
259
|
return (
|
|
327
260
|
<div className="flex h-screen bg-gradient-to-b from-slate-50 to-slate-100">
|
|
328
261
|
{/* Sidebar */}
|
|
@@ -438,29 +371,6 @@ export function App() {
|
|
|
438
371
|
</div>
|
|
439
372
|
)}
|
|
440
373
|
|
|
441
|
-
{/* Presets */}
|
|
442
|
-
<div>
|
|
443
|
-
<h3 className="text-[11px] font-semibold uppercase tracking-wider text-slate-400 mb-2">
|
|
444
|
-
View preset
|
|
445
|
-
</h3>
|
|
446
|
-
<div className="flex gap-1.5">
|
|
447
|
-
{presets.map((preset) => (
|
|
448
|
-
<button
|
|
449
|
-
key={preset.key}
|
|
450
|
-
type="button"
|
|
451
|
-
onClick={() => applyPreset(preset.key)}
|
|
452
|
-
className={`flex-1 px-2 py-1.5 rounded-md text-[11px] font-medium transition-colors cursor-pointer ${
|
|
453
|
-
activePreset === preset.key
|
|
454
|
-
? "bg-indigo-600 text-white shadow-sm"
|
|
455
|
-
: "bg-white border border-slate-200 text-slate-600 hover:bg-slate-50 hover:border-slate-300"
|
|
456
|
-
}`}
|
|
457
|
-
>
|
|
458
|
-
{preset.label}
|
|
459
|
-
</button>
|
|
460
|
-
))}
|
|
461
|
-
</div>
|
|
462
|
-
</div>
|
|
463
|
-
|
|
464
374
|
{/* Filters */}
|
|
465
375
|
<Filters
|
|
466
376
|
allNodeTypes={ALL_NODE_TYPES}
|