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 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
- const pagesToUi = await analyzePagesToUi({
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" | "ui" | "handler" | "action";
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" | "page-ui" | "endpoint-handler" | "page-action" | "action-endpoint";
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.16",
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/",
@@ -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];
@@ -16,7 +16,6 @@ const NODE_TYPE_COLORS: Record<NodeType, string> = {
16
16
  handler: "bg-teal-500",
17
17
  action: "bg-amber-400",
18
18
  db: "bg-red-600",
19
- ui: "bg-orange-500",
20
19
  };
21
20
 
22
21
  export function Filters(props: FiltersProps) {
@@ -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", "ui"];
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}
@@ -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
- }