next-arch-map 0.1.16 → 0.1.18
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/cli.js +0 -2
- package/dist/index.d.ts +0 -2
- package/dist/index.js +1 -8
- package/dist/model.d.ts +2 -2
- package/dist/serve.js +0 -2
- package/dist/utils.d.ts +0 -9
- package/dist/utils.js +0 -11
- package/package.json +2 -2
- package/viewer/src/App.tsx +9 -5
- package/viewer/src/Filters.tsx +0 -1
- package/viewer/src/GraphView.tsx +125 -7
- package/viewer/src/types.ts +0 -2
- package/dist/analyzers/pagesToUi.d.ts +0 -11
- package/dist/analyzers/pagesToUi.js +0 -124
package/dist/cli.js
CHANGED
|
@@ -266,12 +266,10 @@ function logAnalyzeSummary(graph, outputFile, projectRoot) {
|
|
|
266
266
|
const pageCount = graph.nodes.filter((node) => node.type === "page").length;
|
|
267
267
|
const endpointCount = graph.nodes.filter((node) => node.type === "endpoint").length;
|
|
268
268
|
const dbCount = graph.nodes.filter((node) => node.type === "db").length;
|
|
269
|
-
const uiCount = graph.nodes.filter((node) => node.type === "ui").length;
|
|
270
269
|
console.log([
|
|
271
270
|
`pages=${pageCount}`,
|
|
272
271
|
`endpoints=${endpointCount}`,
|
|
273
272
|
`db=${dbCount}`,
|
|
274
|
-
`ui=${uiCount}`,
|
|
275
273
|
`edges=${graph.edges.length}`,
|
|
276
274
|
`file=${path.relative(projectRoot, outputFile)}`,
|
|
277
275
|
].join(" "));
|
package/dist/index.d.ts
CHANGED
|
@@ -7,7 +7,6 @@ export type AnalyzeProjectOptions = {
|
|
|
7
7
|
httpClientIdentifiers?: string[];
|
|
8
8
|
httpClientMethods?: string[];
|
|
9
9
|
dbClientIdentifiers?: string[];
|
|
10
|
-
uiImportPathGlobs?: string[];
|
|
11
10
|
};
|
|
12
11
|
export declare function analyzeProject(options: AnalyzeProjectOptions): Promise<Graph>;
|
|
13
12
|
export { diffGraphs } from "./diff.js";
|
|
@@ -15,6 +14,5 @@ export type { DiffStatus, EdgeDiff, GraphDiff, NodeDiff } from "./diff.js";
|
|
|
15
14
|
export type { Edge, EdgeKind, Graph, Node, NodeType } from "./model.js";
|
|
16
15
|
export { analyzePagesToEndpoints } from "./analyzers/pagesToEndpoints.js";
|
|
17
16
|
export { analyzeEndpointsToDb } from "./analyzers/endpointsToDb.js";
|
|
18
|
-
export { analyzePagesToUi } from "./analyzers/pagesToUi.js";
|
|
19
17
|
export { mergeGraphs, mergePartial } from "./merge.js";
|
|
20
18
|
export { getDbModelsForPage, getEndpointsForPage, getPagesForDbModel } from "./query.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { analyzeEndpointsToDb } from "./analyzers/endpointsToDb.js";
|
|
2
2
|
import { analyzePagesToEndpoints } from "./analyzers/pagesToEndpoints.js";
|
|
3
|
-
import { analyzePagesToUi } from "./analyzers/pagesToUi.js";
|
|
4
3
|
import { mergePartial } from "./merge.js";
|
|
5
4
|
export async function analyzeProject(options) {
|
|
6
5
|
const pagesToEndpoints = await analyzePagesToEndpoints({
|
|
@@ -15,16 +14,10 @@ export async function analyzeProject(options) {
|
|
|
15
14
|
apiDirs: options.apiDirs,
|
|
16
15
|
dbClientIdentifiers: options.dbClientIdentifiers,
|
|
17
16
|
});
|
|
18
|
-
|
|
19
|
-
projectRoot: options.projectRoot,
|
|
20
|
-
appDirs: options.appDirs,
|
|
21
|
-
uiImportPathGlobs: options.uiImportPathGlobs,
|
|
22
|
-
});
|
|
23
|
-
return mergePartial(mergePartial(pagesToEndpoints, endpointsToDb), pagesToUi);
|
|
17
|
+
return mergePartial(pagesToEndpoints, endpointsToDb);
|
|
24
18
|
}
|
|
25
19
|
export { diffGraphs } from "./diff.js";
|
|
26
20
|
export { analyzePagesToEndpoints } from "./analyzers/pagesToEndpoints.js";
|
|
27
21
|
export { analyzeEndpointsToDb } from "./analyzers/endpointsToDb.js";
|
|
28
|
-
export { analyzePagesToUi } from "./analyzers/pagesToUi.js";
|
|
29
22
|
export { mergeGraphs, mergePartial } from "./merge.js";
|
|
30
23
|
export { getDbModelsForPage, getEndpointsForPage, getPagesForDbModel } from "./query.js";
|
package/dist/model.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export type NodeType = "page" | "endpoint" | "db" | "
|
|
1
|
+
export type NodeType = "page" | "endpoint" | "db" | "handler" | "action";
|
|
2
2
|
export type Node = {
|
|
3
3
|
id: string;
|
|
4
4
|
type: NodeType;
|
|
5
5
|
label: string;
|
|
6
6
|
meta?: Record<string, any>;
|
|
7
7
|
};
|
|
8
|
-
export type EdgeKind = "page-endpoint" | "endpoint-db" | "
|
|
8
|
+
export type EdgeKind = "page-endpoint" | "endpoint-db" | "endpoint-handler" | "page-action" | "action-endpoint";
|
|
9
9
|
export type Edge = {
|
|
10
10
|
from: string;
|
|
11
11
|
to: string;
|
package/dist/serve.js
CHANGED
|
@@ -29,7 +29,6 @@ export async function serve(options) {
|
|
|
29
29
|
const handlerCount = currentGraph.nodes.filter((node) => node.type === "handler").length;
|
|
30
30
|
const actionCount = currentGraph.nodes.filter((node) => node.type === "action").length;
|
|
31
31
|
const dbCount = currentGraph.nodes.filter((node) => node.type === "db").length;
|
|
32
|
-
const uiCount = currentGraph.nodes.filter((node) => node.type === "ui").length;
|
|
33
32
|
console.log([
|
|
34
33
|
"mode=serve",
|
|
35
34
|
`pages=${pageCount}`,
|
|
@@ -37,7 +36,6 @@ export async function serve(options) {
|
|
|
37
36
|
`endpoints=${endpointCount}`,
|
|
38
37
|
`handlers=${handlerCount}`,
|
|
39
38
|
`db=${dbCount}`,
|
|
40
|
-
`ui=${uiCount}`,
|
|
41
39
|
`nodes=${currentGraph.nodes.length}`,
|
|
42
40
|
`edges=${currentGraph.edges.length}`,
|
|
43
41
|
].join(" "));
|
package/dist/utils.d.ts
CHANGED
|
@@ -71,12 +71,3 @@ export declare function buildDbNode(modelName: string, filePath: string): {
|
|
|
71
71
|
model: string;
|
|
72
72
|
};
|
|
73
73
|
};
|
|
74
|
-
export declare function buildUiNode(componentName: string, filePath: string): {
|
|
75
|
-
id: string;
|
|
76
|
-
type: "ui";
|
|
77
|
-
label: string;
|
|
78
|
-
meta: {
|
|
79
|
-
filePath: string;
|
|
80
|
-
component: string;
|
|
81
|
-
};
|
|
82
|
-
};
|
package/dist/utils.js
CHANGED
|
@@ -292,14 +292,3 @@ export function buildDbNode(modelName, filePath) {
|
|
|
292
292
|
},
|
|
293
293
|
};
|
|
294
294
|
}
|
|
295
|
-
export function buildUiNode(componentName, filePath) {
|
|
296
|
-
return {
|
|
297
|
-
id: `ui:${componentName}`,
|
|
298
|
-
type: "ui",
|
|
299
|
-
label: componentName,
|
|
300
|
-
meta: {
|
|
301
|
-
filePath,
|
|
302
|
-
component: componentName,
|
|
303
|
-
},
|
|
304
|
-
};
|
|
305
|
-
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-arch-map",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.18",
|
|
4
4
|
"description": "Static analyzer that builds a multi-layer architecture graph for Next.js-style apps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"examples"
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
|
-
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"build": "tsc -p tsconfig.json && chmod +x dist/cli.js",
|
|
27
27
|
"check": "tsc --noEmit -p tsconfig.json",
|
|
28
28
|
"test": "vitest run",
|
|
29
29
|
"lint": "eslint src/ tests/",
|
package/viewer/src/App.tsx
CHANGED
|
@@ -11,12 +11,10 @@ const ALL_NODE_TYPES: NodeType[] = [
|
|
|
11
11
|
"handler",
|
|
12
12
|
"action",
|
|
13
13
|
"db",
|
|
14
|
-
"ui",
|
|
15
14
|
];
|
|
16
15
|
const ALL_EDGE_KINDS: EdgeKind[] = [
|
|
17
16
|
"page-endpoint",
|
|
18
17
|
"endpoint-db",
|
|
19
|
-
"page-ui",
|
|
20
18
|
"endpoint-handler",
|
|
21
19
|
"page-action",
|
|
22
20
|
"action-endpoint",
|
|
@@ -38,7 +36,6 @@ const DATA_FLOW_EDGE_KINDS: EdgeKind[] = [
|
|
|
38
36
|
const FULL_FLOW_EDGE_KINDS: EdgeKind[] = [
|
|
39
37
|
"page-endpoint",
|
|
40
38
|
"endpoint-db",
|
|
41
|
-
"page-ui",
|
|
42
39
|
"endpoint-handler",
|
|
43
40
|
"page-action",
|
|
44
41
|
"action-endpoint",
|
|
@@ -62,7 +59,6 @@ const FULL_FLOW_NODE_TYPES: NodeType[] = [
|
|
|
62
59
|
"endpoint",
|
|
63
60
|
"handler",
|
|
64
61
|
"db",
|
|
65
|
-
"ui",
|
|
66
62
|
];
|
|
67
63
|
|
|
68
64
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
@@ -388,6 +384,15 @@ export function App() {
|
|
|
388
384
|
</Switch.Root>
|
|
389
385
|
</div>
|
|
390
386
|
{useServer && (
|
|
387
|
+
<button
|
|
388
|
+
type="button"
|
|
389
|
+
onClick={() => setShowAdvanced((prev) => !prev)}
|
|
390
|
+
className="text-[11px] text-slate-400 hover:text-slate-600 transition-colors cursor-pointer"
|
|
391
|
+
>
|
|
392
|
+
{showAdvanced ? "Hide" : "Advanced"}
|
|
393
|
+
</button>
|
|
394
|
+
)}
|
|
395
|
+
{useServer && showAdvanced && (
|
|
391
396
|
<input
|
|
392
397
|
type="text"
|
|
393
398
|
value={serverUrl}
|
|
@@ -584,7 +589,6 @@ function buildFocusedSubgraph(graph: Graph, route: string): Graph {
|
|
|
584
589
|
"page-endpoint",
|
|
585
590
|
"endpoint-handler",
|
|
586
591
|
"endpoint-db",
|
|
587
|
-
"page-ui",
|
|
588
592
|
]);
|
|
589
593
|
const reachableNodeIds = new Set<string>([pageId]);
|
|
590
594
|
const worklist = [pageId];
|
package/viewer/src/Filters.tsx
CHANGED
package/viewer/src/GraphView.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useCallback, useMemo, useState } from "react";
|
|
2
2
|
import {
|
|
3
3
|
Background,
|
|
4
4
|
Controls,
|
|
@@ -27,7 +27,6 @@ const NODE_COLOR: Record<NodeType, string> = {
|
|
|
27
27
|
page: "#3b82f6",
|
|
28
28
|
endpoint: "#059669",
|
|
29
29
|
db: "#dc2626",
|
|
30
|
-
ui: "#f97316",
|
|
31
30
|
handler: "#14b8a6",
|
|
32
31
|
action: "#fbbf24",
|
|
33
32
|
};
|
|
@@ -36,7 +35,6 @@ const NODE_BORDER: Record<NodeType, string> = {
|
|
|
36
35
|
page: "#2563eb",
|
|
37
36
|
endpoint: "#047857",
|
|
38
37
|
db: "#b91c1c",
|
|
39
|
-
ui: "#ea580c",
|
|
40
38
|
handler: "#0d9488",
|
|
41
39
|
action: "#f59e0b",
|
|
42
40
|
};
|
|
@@ -44,7 +42,6 @@ const NODE_BORDER: Record<NodeType, string> = {
|
|
|
44
42
|
const EDGE_COLOR: Record<EdgeKind, string> = {
|
|
45
43
|
"page-endpoint": "#06b6d4",
|
|
46
44
|
"endpoint-db": "#f97316",
|
|
47
|
-
"page-ui": "#8b5cf6",
|
|
48
45
|
"endpoint-handler": "#22c55e",
|
|
49
46
|
"page-action": "#eab308",
|
|
50
47
|
"action-endpoint": "#a855f7",
|
|
@@ -68,6 +65,88 @@ function buildEdgeKey(from: string, to: string, kind: EdgeKind): string {
|
|
|
68
65
|
return `${from}::${to}::${kind}`;
|
|
69
66
|
}
|
|
70
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Reorder nodes within each column so that connected nodes are placed
|
|
70
|
+
* close together vertically, minimizing long diagonal edge crossings.
|
|
71
|
+
* Uses an iterative barycenter heuristic.
|
|
72
|
+
*/
|
|
73
|
+
function optimizeNodeOrder(
|
|
74
|
+
nodesByType: Map<NodeType, Graph["nodes"]>,
|
|
75
|
+
activeTypeOrder: NodeType[],
|
|
76
|
+
edges: Graph["edges"],
|
|
77
|
+
visibleNodeIds: Set<string>,
|
|
78
|
+
visibleEdgeKinds: Set<EdgeKind>,
|
|
79
|
+
): Map<NodeType, Graph["nodes"]> {
|
|
80
|
+
// Build adjacency map: nodeId -> list of connected nodeIds
|
|
81
|
+
const adjacency = new Map<string, string[]>();
|
|
82
|
+
for (const nodeId of visibleNodeIds) {
|
|
83
|
+
adjacency.set(nodeId, []);
|
|
84
|
+
}
|
|
85
|
+
for (const edge of edges) {
|
|
86
|
+
if (
|
|
87
|
+
!visibleEdgeKinds.has(edge.kind) ||
|
|
88
|
+
!visibleNodeIds.has(edge.from) ||
|
|
89
|
+
!visibleNodeIds.has(edge.to)
|
|
90
|
+
)
|
|
91
|
+
continue;
|
|
92
|
+
adjacency.get(edge.from)?.push(edge.to);
|
|
93
|
+
adjacency.get(edge.to)?.push(edge.from);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Track each node's row index within its column
|
|
97
|
+
const nodeRowIndex = new Map<string, number>();
|
|
98
|
+
for (const type of activeTypeOrder) {
|
|
99
|
+
const nodes = nodesByType.get(type) ?? [];
|
|
100
|
+
nodes.forEach((node, i) => nodeRowIndex.set(node.id, i));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Iterative barycenter: forward pass then reverse pass, repeated
|
|
104
|
+
const ITERATIONS = 3;
|
|
105
|
+
for (let iter = 0; iter < ITERATIONS; iter++) {
|
|
106
|
+
// Forward pass (left to right)
|
|
107
|
+
for (let col = 1; col < activeTypeOrder.length; col++) {
|
|
108
|
+
reorderColumn(activeTypeOrder[col], nodesByType, adjacency, nodeRowIndex);
|
|
109
|
+
}
|
|
110
|
+
// Reverse pass (right to left)
|
|
111
|
+
for (let col = activeTypeOrder.length - 2; col >= 0; col--) {
|
|
112
|
+
reorderColumn(activeTypeOrder[col], nodesByType, adjacency, nodeRowIndex);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return nodesByType;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function reorderColumn(
|
|
120
|
+
type: NodeType,
|
|
121
|
+
nodesByType: Map<NodeType, Graph["nodes"]>,
|
|
122
|
+
adjacency: Map<string, string[]>,
|
|
123
|
+
nodeRowIndex: Map<string, number>,
|
|
124
|
+
): void {
|
|
125
|
+
const nodes = nodesByType.get(type);
|
|
126
|
+
if (!nodes || nodes.length <= 1) return;
|
|
127
|
+
|
|
128
|
+
// Compute barycenter for each node (average row of connected nodes)
|
|
129
|
+
const barycenters = new Map<string, number>();
|
|
130
|
+
for (const node of nodes) {
|
|
131
|
+
const neighbors = adjacency.get(node.id) ?? [];
|
|
132
|
+
const neighborRows = neighbors
|
|
133
|
+
.map((nid) => nodeRowIndex.get(nid))
|
|
134
|
+
.filter((r): r is number => r !== undefined);
|
|
135
|
+
if (neighborRows.length > 0) {
|
|
136
|
+
const avg = neighborRows.reduce((s, r) => s + r, 0) / neighborRows.length;
|
|
137
|
+
barycenters.set(node.id, avg);
|
|
138
|
+
} else {
|
|
139
|
+
// No connections: keep current position as a tiebreaker
|
|
140
|
+
barycenters.set(node.id, nodeRowIndex.get(node.id) ?? 0);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
nodes.sort((a, b) => (barycenters.get(a.id) ?? 0) - (barycenters.get(b.id) ?? 0));
|
|
145
|
+
|
|
146
|
+
// Update row indices after reorder
|
|
147
|
+
nodes.forEach((node, i) => nodeRowIndex.set(node.id, i));
|
|
148
|
+
}
|
|
149
|
+
|
|
71
150
|
export function GraphView(props: GraphViewProps) {
|
|
72
151
|
const {
|
|
73
152
|
graph,
|
|
@@ -78,10 +157,34 @@ export function GraphView(props: GraphViewProps) {
|
|
|
78
157
|
nodeStatusById,
|
|
79
158
|
edgeStatusByKey,
|
|
80
159
|
} = props;
|
|
160
|
+
|
|
161
|
+
const [hoveredNodeId, setHoveredNodeId] = useState<string | null>(null);
|
|
162
|
+
|
|
81
163
|
const handleNodeClick: NodeMouseHandler = (_event, node) => onSelectNode(node.id);
|
|
164
|
+
const handleNodeMouseEnter: NodeMouseHandler = useCallback((_event, node) => {
|
|
165
|
+
setHoveredNodeId(node.id);
|
|
166
|
+
}, []);
|
|
167
|
+
const handleNodeMouseLeave: NodeMouseHandler = useCallback(() => {
|
|
168
|
+
setHoveredNodeId(null);
|
|
169
|
+
}, []);
|
|
170
|
+
|
|
171
|
+
// Build set of edge-connected node IDs for the active (hovered or selected) node
|
|
172
|
+
const activeNodeId = hoveredNodeId ?? selectedNodeId;
|
|
173
|
+
const connectedEdgeIds = useMemo(() => {
|
|
174
|
+
if (!activeNodeId) return null;
|
|
175
|
+
const ids = new Set<string>();
|
|
176
|
+
ids.add(activeNodeId);
|
|
177
|
+
for (const edge of graph.edges) {
|
|
178
|
+
if (edge.from === activeNodeId || edge.to === activeNodeId) {
|
|
179
|
+
ids.add(edge.from);
|
|
180
|
+
ids.add(edge.to);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return ids;
|
|
184
|
+
}, [activeNodeId, graph.edges]);
|
|
82
185
|
|
|
83
186
|
const { flowNodes, flowEdges } = useMemo(() => {
|
|
84
|
-
const typeOrder: NodeType[] = ["page", "action", "endpoint", "handler", "db"
|
|
187
|
+
const typeOrder: NodeType[] = ["page", "action", "endpoint", "handler", "db"];
|
|
85
188
|
const visibleNodes = graph.nodes.filter((node) => visibleNodeTypes.has(node.type));
|
|
86
189
|
const visibleNodeIds = new Set(visibleNodes.map((node) => node.id));
|
|
87
190
|
const nodesByType = new Map<NodeType, typeof visibleNodes>(
|
|
@@ -92,12 +195,16 @@ export function GraphView(props: GraphViewProps) {
|
|
|
92
195
|
nodesByType.get(node.type)?.push(node);
|
|
93
196
|
}
|
|
94
197
|
|
|
198
|
+
// Initial alphabetical sort as seed for the barycenter algorithm
|
|
95
199
|
for (const nodes of nodesByType.values()) {
|
|
96
200
|
nodes.sort((left, right) => left.label.localeCompare(right.label));
|
|
97
201
|
}
|
|
98
202
|
|
|
99
203
|
const activeTypeOrder = typeOrder.filter((type) => (nodesByType.get(type)?.length ?? 0) > 0);
|
|
100
204
|
|
|
205
|
+
// Optimize node ordering to minimize edge crossings
|
|
206
|
+
optimizeNodeOrder(nodesByType, activeTypeOrder, graph.edges, visibleNodeIds, visibleEdgeKinds);
|
|
207
|
+
|
|
101
208
|
const flowNodes: FlowNode[] = [];
|
|
102
209
|
const columnWidth = 300;
|
|
103
210
|
const rowHeight = 80;
|
|
@@ -156,6 +263,12 @@ export function GraphView(props: GraphViewProps) {
|
|
|
156
263
|
const status = edgeStatusByKey?.get(buildEdgeKey(edge.from, edge.to, edge.kind)) ?? "unchanged";
|
|
157
264
|
const strokeColor = status === "unchanged" ? EDGE_COLOR[edge.kind] : DIFF_EDGE_COLOR[status];
|
|
158
265
|
|
|
266
|
+
// Determine if this edge is highlighted (connected to active node)
|
|
267
|
+
const isHighlighted =
|
|
268
|
+
activeNodeId !== null &&
|
|
269
|
+
(edge.from === activeNodeId || edge.to === activeNodeId);
|
|
270
|
+
const isDimmed = activeNodeId !== null && !isHighlighted;
|
|
271
|
+
|
|
159
272
|
return {
|
|
160
273
|
id: `${edge.from}=>${edge.to}::${edge.kind}::${index}`,
|
|
161
274
|
source: edge.from,
|
|
@@ -163,10 +276,12 @@ export function GraphView(props: GraphViewProps) {
|
|
|
163
276
|
animated: false,
|
|
164
277
|
style: {
|
|
165
278
|
stroke: strokeColor,
|
|
166
|
-
strokeWidth: 1.5,
|
|
279
|
+
strokeWidth: isHighlighted ? 2.5 : 1.5,
|
|
167
280
|
strokeDasharray: status === "removed" ? "6 4" : undefined,
|
|
168
|
-
opacity: status === "removed" ? 0.6 : 0.85,
|
|
281
|
+
opacity: isDimmed ? 0.08 : status === "removed" ? 0.6 : 0.85,
|
|
282
|
+
transition: "opacity 0.15s ease, stroke-width 0.15s ease",
|
|
169
283
|
},
|
|
284
|
+
zIndex: isHighlighted ? 10 : 0,
|
|
170
285
|
markerEnd: {
|
|
171
286
|
type: MarkerType.ArrowClosed,
|
|
172
287
|
color: strokeColor,
|
|
@@ -176,6 +291,7 @@ export function GraphView(props: GraphViewProps) {
|
|
|
176
291
|
|
|
177
292
|
return { flowNodes, flowEdges };
|
|
178
293
|
}, [
|
|
294
|
+
activeNodeId,
|
|
179
295
|
edgeStatusByKey,
|
|
180
296
|
graph,
|
|
181
297
|
nodeStatusById,
|
|
@@ -192,6 +308,8 @@ export function GraphView(props: GraphViewProps) {
|
|
|
192
308
|
fitView
|
|
193
309
|
fitViewOptions={{ padding: 0.18 }}
|
|
194
310
|
onNodeClick={handleNodeClick}
|
|
311
|
+
onNodeMouseEnter={handleNodeMouseEnter}
|
|
312
|
+
onNodeMouseLeave={handleNodeMouseLeave}
|
|
195
313
|
onPaneClick={() => onSelectNode(null)}
|
|
196
314
|
nodesDraggable={false}
|
|
197
315
|
nodesConnectable={false}
|
package/viewer/src/types.ts
CHANGED
|
@@ -2,7 +2,6 @@ export type NodeType =
|
|
|
2
2
|
| "page"
|
|
3
3
|
| "endpoint"
|
|
4
4
|
| "db"
|
|
5
|
-
| "ui"
|
|
6
5
|
| "handler"
|
|
7
6
|
| "action";
|
|
8
7
|
|
|
@@ -16,7 +15,6 @@ export type Node = {
|
|
|
16
15
|
export type EdgeKind =
|
|
17
16
|
| "page-endpoint"
|
|
18
17
|
| "endpoint-db"
|
|
19
|
-
| "page-ui"
|
|
20
18
|
| "endpoint-handler"
|
|
21
19
|
| "page-action"
|
|
22
20
|
| "action-endpoint";
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { Edge, Node } from "../model.js";
|
|
2
|
-
type AnalyzePagesToUiOptions = {
|
|
3
|
-
projectRoot: string;
|
|
4
|
-
appDirs?: string[];
|
|
5
|
-
uiImportPathGlobs?: string[];
|
|
6
|
-
};
|
|
7
|
-
export declare function analyzePagesToUi(options: AnalyzePagesToUiOptions): Promise<{
|
|
8
|
-
nodes: Node[];
|
|
9
|
-
edges: Edge[];
|
|
10
|
-
}>;
|
|
11
|
-
export {};
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import path from "node:path";
|
|
2
|
-
import ts from "typescript";
|
|
3
|
-
import { buildEdgeKey, buildPageNode, buildUiNode, ensureNode, getExistingDirectories, getPageRouteFromFile, getSourceFile, isPageFile, resolveLocalModulePath, resolveProjectRoot, walkDirectory, } from "../utils.js";
|
|
4
|
-
const DEFAULT_APP_DIRS = ["app", "src/app"];
|
|
5
|
-
const DEFAULT_UI_IMPORT_PATH_GLOBS = [
|
|
6
|
-
"src/components/**",
|
|
7
|
-
"src/features/**/components/**",
|
|
8
|
-
"src/app/**/components/**",
|
|
9
|
-
"app/**/components/**",
|
|
10
|
-
];
|
|
11
|
-
export async function analyzePagesToUi(options) {
|
|
12
|
-
const projectRoot = resolveProjectRoot(options.projectRoot);
|
|
13
|
-
const appDirs = getExistingDirectories(projectRoot, options.appDirs ?? DEFAULT_APP_DIRS);
|
|
14
|
-
if (appDirs.length === 0) {
|
|
15
|
-
throw new Error("Could not find an app/ or src/app/ directory.");
|
|
16
|
-
}
|
|
17
|
-
const uiPathMatchers = (options.uiImportPathGlobs ?? DEFAULT_UI_IMPORT_PATH_GLOBS).map(globToRegExp);
|
|
18
|
-
const nodes = [];
|
|
19
|
-
const edges = [];
|
|
20
|
-
const nodeIds = new Set();
|
|
21
|
-
const edgeKeys = new Set();
|
|
22
|
-
for (const appDir of appDirs) {
|
|
23
|
-
for (const filePath of walkDirectory(appDir)) {
|
|
24
|
-
if (!isPageFile(filePath)) {
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
const route = getPageRouteFromFile(appDir, filePath);
|
|
29
|
-
const pageNode = ensureNode(nodes, nodeIds, buildPageNode(route, filePath));
|
|
30
|
-
const components = collectUiComponentUsages(filePath, projectRoot, uiPathMatchers);
|
|
31
|
-
for (const component of components) {
|
|
32
|
-
const uiNode = ensureNode(nodes, nodeIds, buildUiNode(component.componentName, component.filePath));
|
|
33
|
-
const edgeKey = buildEdgeKey(pageNode.id, uiNode.id, "page-ui");
|
|
34
|
-
if (edgeKeys.has(edgeKey)) {
|
|
35
|
-
continue;
|
|
36
|
-
}
|
|
37
|
-
edgeKeys.add(edgeKey);
|
|
38
|
-
edges.push({
|
|
39
|
-
from: pageNode.id,
|
|
40
|
-
to: uiNode.id,
|
|
41
|
-
kind: "page-ui",
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
catch (error) {
|
|
46
|
-
const relative = path.relative(projectRoot, filePath);
|
|
47
|
-
console.warn(`Warning: skipping ${relative}: ${error instanceof Error ? error.message : error}`);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return {
|
|
52
|
-
nodes: nodes.sort((left, right) => left.id.localeCompare(right.id)),
|
|
53
|
-
edges: edges.sort((left, right) => left.kind.localeCompare(right.kind) ||
|
|
54
|
-
left.from.localeCompare(right.from) ||
|
|
55
|
-
left.to.localeCompare(right.to)),
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
function collectUiComponentUsages(pageFilePath, projectRoot, uiPathMatchers) {
|
|
59
|
-
const sourceFile = getSourceFile(pageFilePath);
|
|
60
|
-
if (!sourceFile) {
|
|
61
|
-
return [];
|
|
62
|
-
}
|
|
63
|
-
const componentFilePaths = new Map();
|
|
64
|
-
for (const statement of sourceFile.statements) {
|
|
65
|
-
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
const importSource = statement.moduleSpecifier.text;
|
|
69
|
-
const resolvedImportPath = resolveLocalModulePath(pageFilePath, importSource, projectRoot);
|
|
70
|
-
if (!isUiLikeImport(importSource, resolvedImportPath, projectRoot, uiPathMatchers)) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
const importClause = statement.importClause;
|
|
74
|
-
if (!importClause || importClause.isTypeOnly) {
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
const componentFilePath = resolvedImportPath ?? pageFilePath;
|
|
78
|
-
if (importClause.name && isUiComponentName(importClause.name.text)) {
|
|
79
|
-
componentFilePaths.set(importClause.name.text, componentFilePath);
|
|
80
|
-
}
|
|
81
|
-
if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
|
|
82
|
-
for (const element of importClause.namedBindings.elements) {
|
|
83
|
-
if (!element.isTypeOnly && isUiComponentName(element.name.text)) {
|
|
84
|
-
componentFilePaths.set(element.name.text, componentFilePath);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
return [...componentFilePaths.entries()]
|
|
90
|
-
.sort(([left], [right]) => left.localeCompare(right))
|
|
91
|
-
.map(([componentName, filePath]) => ({ componentName, filePath }));
|
|
92
|
-
}
|
|
93
|
-
function isUiLikeImport(importSource, resolvedImportPath, projectRoot, uiPathMatchers) {
|
|
94
|
-
if (/^\.\.?\/components(\/|$)/.test(importSource) || importSource.startsWith("@/components/")) {
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
if (!resolvedImportPath) {
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
const relativePath = path.relative(projectRoot, resolvedImportPath).replace(/\\/g, "/");
|
|
101
|
-
return uiPathMatchers.some((matcher) => matcher.test(relativePath));
|
|
102
|
-
}
|
|
103
|
-
function isUiComponentName(identifierName) {
|
|
104
|
-
return /^[A-Z]/.test(identifierName);
|
|
105
|
-
}
|
|
106
|
-
function globToRegExp(glob) {
|
|
107
|
-
let pattern = "^";
|
|
108
|
-
for (let index = 0; index < glob.length; index += 1) {
|
|
109
|
-
const character = glob[index];
|
|
110
|
-
const nextCharacter = glob[index + 1];
|
|
111
|
-
if (character === "*" && nextCharacter === "*") {
|
|
112
|
-
pattern += ".*";
|
|
113
|
-
index += 1;
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (character === "*") {
|
|
117
|
-
pattern += "[^/]*";
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
pattern += /[.+^${}()|[\]\\]/.test(character) ? `\\${character}` : character;
|
|
121
|
-
}
|
|
122
|
-
pattern += "$";
|
|
123
|
-
return new RegExp(pattern);
|
|
124
|
-
}
|