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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-arch-map",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Static analyzer that builds a multi-layer architecture graph for Next.js-style apps.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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}