next-arch-map 0.1.23 → 0.1.25
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.
|
@@ -5,6 +5,7 @@ type AnalyzePagesToEndpointsOptions = {
|
|
|
5
5
|
extraScanDirs?: string[];
|
|
6
6
|
httpClientIdentifiers?: string[];
|
|
7
7
|
httpClientMethods?: string[];
|
|
8
|
+
sdkClientIdentifiers?: string[];
|
|
8
9
|
};
|
|
9
10
|
export declare function analyzePagesToEndpoints(options: AnalyzePagesToEndpointsOptions): Promise<Graph>;
|
|
10
11
|
export {};
|
|
@@ -5,6 +5,7 @@ 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"];
|
|
7
7
|
const DEFAULT_HTTP_CLIENT_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
8
|
+
const DEFAULT_SDK_CLIENT_IDENTIFIERS = ["supabase"];
|
|
8
9
|
export async function analyzePagesToEndpoints(options) {
|
|
9
10
|
const projectRoot = resolveProjectRoot(options.projectRoot);
|
|
10
11
|
const appDirs = getExistingDirectories(projectRoot, options.appDirs ?? DEFAULT_APP_DIRS);
|
|
@@ -14,6 +15,7 @@ export async function analyzePagesToEndpoints(options) {
|
|
|
14
15
|
const extraScanDirs = getExistingDirectories(projectRoot, options.extraScanDirs ?? DEFAULT_EXTRA_SCAN_DIRS);
|
|
15
16
|
const httpClientIdentifiers = new Set((options.httpClientIdentifiers ?? DEFAULT_HTTP_CLIENT_IDENTIFIERS).map((value) => value.trim()));
|
|
16
17
|
const httpClientMethods = new Set((options.httpClientMethods ?? DEFAULT_HTTP_CLIENT_METHODS).map((value) => value.toLowerCase()));
|
|
18
|
+
const sdkClientIdentifiers = new Set((options.sdkClientIdentifiers ?? DEFAULT_SDK_CLIENT_IDENTIFIERS).map((value) => value.trim()));
|
|
17
19
|
const nodes = [];
|
|
18
20
|
const edges = [];
|
|
19
21
|
const nodeIds = new Set();
|
|
@@ -34,7 +36,7 @@ export async function analyzePagesToEndpoints(options) {
|
|
|
34
36
|
// Non-page files (route handlers, layouts, helpers) under app/ only
|
|
35
37
|
// contribute endpoint nodes without creating fake page flows.
|
|
36
38
|
if (!isPageFile(filePath)) {
|
|
37
|
-
const httpCalls = collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods);
|
|
39
|
+
const httpCalls = collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers);
|
|
38
40
|
for (const call of httpCalls) {
|
|
39
41
|
ensureNode(nodes, nodeIds, buildEndpointNode(call.endpoint, filePath));
|
|
40
42
|
}
|
|
@@ -42,7 +44,7 @@ export async function analyzePagesToEndpoints(options) {
|
|
|
42
44
|
}
|
|
43
45
|
// For page files, follow imports transitively to find HTTP calls
|
|
44
46
|
// in hooks, utilities, and other local modules.
|
|
45
|
-
const httpCalls = collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifiers, httpClientMethods, sourceFileCache);
|
|
47
|
+
const httpCalls = collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers, sourceFileCache);
|
|
46
48
|
const route = getPageRouteFromFile(appDir, filePath);
|
|
47
49
|
ensureNode(nodes, nodeIds, buildPageNode(route, filePath));
|
|
48
50
|
for (const call of httpCalls) {
|
|
@@ -101,7 +103,7 @@ export async function analyzePagesToEndpoints(options) {
|
|
|
101
103
|
if (!sourceFile) {
|
|
102
104
|
continue;
|
|
103
105
|
}
|
|
104
|
-
for (const call of collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods)) {
|
|
106
|
+
for (const call of collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers)) {
|
|
105
107
|
ensureNode(nodes, nodeIds, buildEndpointNode(call.endpoint, filePath));
|
|
106
108
|
}
|
|
107
109
|
}
|
|
@@ -135,7 +137,7 @@ function collectImportSpecifiers(sourceFile) {
|
|
|
135
137
|
}
|
|
136
138
|
return specifiers;
|
|
137
139
|
}
|
|
138
|
-
function collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifiers, httpClientMethods, sourceFileCache, visited) {
|
|
140
|
+
function collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers, sourceFileCache, visited) {
|
|
139
141
|
const resolvedPath = path.resolve(filePath);
|
|
140
142
|
const seen = visited ?? new Set();
|
|
141
143
|
if (seen.has(resolvedPath)) {
|
|
@@ -146,18 +148,18 @@ function collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifie
|
|
|
146
148
|
if (!sourceFile) {
|
|
147
149
|
return [];
|
|
148
150
|
}
|
|
149
|
-
const calls = collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods);
|
|
151
|
+
const calls = collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers);
|
|
150
152
|
for (const specifier of collectImportSpecifiers(sourceFile)) {
|
|
151
153
|
const resolved = resolveLocalModulePath(filePath, specifier, projectRoot);
|
|
152
154
|
if (!resolved) {
|
|
153
155
|
continue;
|
|
154
156
|
}
|
|
155
|
-
const transitiveCalls = collectHttpCallsTransitively(resolved, projectRoot, httpClientIdentifiers, httpClientMethods, sourceFileCache, seen);
|
|
157
|
+
const transitiveCalls = collectHttpCallsTransitively(resolved, projectRoot, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers, sourceFileCache, seen);
|
|
156
158
|
calls.push(...transitiveCalls);
|
|
157
159
|
}
|
|
158
160
|
return calls;
|
|
159
161
|
}
|
|
160
|
-
function collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods) {
|
|
162
|
+
function collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers) {
|
|
161
163
|
const calls = [];
|
|
162
164
|
const constMap = collectStringConstants(sourceFile);
|
|
163
165
|
const visitNode = (node) => {
|
|
@@ -170,6 +172,16 @@ function collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods)
|
|
|
170
172
|
node,
|
|
171
173
|
});
|
|
172
174
|
}
|
|
175
|
+
else {
|
|
176
|
+
const sdkCall = parseSdkCall(node, sdkClientIdentifiers);
|
|
177
|
+
if (sdkCall) {
|
|
178
|
+
calls.push({
|
|
179
|
+
endpoint: sdkCall.endpoint,
|
|
180
|
+
method: sdkCall.method,
|
|
181
|
+
node,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
173
185
|
}
|
|
174
186
|
ts.forEachChild(node, visitNode);
|
|
175
187
|
};
|
|
@@ -198,6 +210,32 @@ function parseHttpCall(node, constMap, httpClientIdentifiers, httpClientMethods)
|
|
|
198
210
|
}
|
|
199
211
|
return null;
|
|
200
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Detect SDK-style calls like `supabase.auth.signInWithPassword(...)`.
|
|
215
|
+
* Walks the property access chain to find a root identifier that matches
|
|
216
|
+
* one of the configured SDK client identifiers, then uses the full
|
|
217
|
+
* method chain as the endpoint label.
|
|
218
|
+
*/
|
|
219
|
+
function parseSdkCall(node, sdkClientIdentifiers) {
|
|
220
|
+
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const segments = [];
|
|
224
|
+
let current = node.expression;
|
|
225
|
+
while (ts.isPropertyAccessExpression(current)) {
|
|
226
|
+
segments.unshift(current.name.text);
|
|
227
|
+
current = current.expression;
|
|
228
|
+
}
|
|
229
|
+
if (!ts.isIdentifier(current) || !sdkClientIdentifiers.has(current.text)) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
// Need at least one segment beyond the client name (e.g. supabase.auth.method)
|
|
233
|
+
if (segments.length < 2) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
const endpoint = `${current.text}.${segments.join(".")}`;
|
|
237
|
+
return { endpoint };
|
|
238
|
+
}
|
|
201
239
|
function getEndpointArgument(expression, constMap) {
|
|
202
240
|
if (!expression) {
|
|
203
241
|
return null;
|
package/package.json
CHANGED
package/viewer/src/App.tsx
CHANGED
|
@@ -9,15 +9,12 @@ const ALL_NODE_TYPES: NodeType[] = [
|
|
|
9
9
|
"page",
|
|
10
10
|
"endpoint",
|
|
11
11
|
"handler",
|
|
12
|
-
"action",
|
|
13
12
|
"db",
|
|
14
13
|
];
|
|
15
14
|
const ALL_EDGE_KINDS: EdgeKind[] = [
|
|
16
15
|
"page-endpoint",
|
|
17
16
|
"endpoint-db",
|
|
18
17
|
"endpoint-handler",
|
|
19
|
-
"page-action",
|
|
20
|
-
"action-endpoint",
|
|
21
18
|
];
|
|
22
19
|
|
|
23
20
|
|
package/viewer/src/Filters.tsx
CHANGED
package/viewer/src/GraphView.tsx
CHANGED
|
@@ -31,7 +31,6 @@ const NODE_COLOR: Record<NodeType, string> = {
|
|
|
31
31
|
endpoint: "#059669",
|
|
32
32
|
db: "#dc2626",
|
|
33
33
|
handler: "#14b8a6",
|
|
34
|
-
action: "#fbbf24",
|
|
35
34
|
};
|
|
36
35
|
|
|
37
36
|
const NODE_BORDER: Record<NodeType, string> = {
|
|
@@ -39,15 +38,12 @@ const NODE_BORDER: Record<NodeType, string> = {
|
|
|
39
38
|
endpoint: "#047857",
|
|
40
39
|
db: "#b91c1c",
|
|
41
40
|
handler: "#0d9488",
|
|
42
|
-
action: "#f59e0b",
|
|
43
41
|
};
|
|
44
42
|
|
|
45
43
|
const EDGE_COLOR: Record<EdgeKind, string> = {
|
|
46
44
|
"page-endpoint": "#06b6d4",
|
|
47
45
|
"endpoint-db": "#f97316",
|
|
48
46
|
"endpoint-handler": "#22c55e",
|
|
49
|
-
"page-action": "#eab308",
|
|
50
|
-
"action-endpoint": "#a855f7",
|
|
51
47
|
};
|
|
52
48
|
|
|
53
49
|
const DIFF_BORDER_COLOR: Record<DiffStatus, string> = {
|
|
@@ -241,7 +237,7 @@ export function GraphView(props: GraphViewProps) {
|
|
|
241
237
|
|
|
242
238
|
// Compute layout without hover state — this is the expensive part
|
|
243
239
|
const { flowNodes: baseFlowNodes, flowEdges: baseFlowEdges } = useMemo(() => {
|
|
244
|
-
const typeOrder: NodeType[] = ["page", "
|
|
240
|
+
const typeOrder: NodeType[] = ["page", "endpoint", "handler", "db"];
|
|
245
241
|
const visibleNodes = graph.nodes.filter((node) => visibleNodeTypes.has(node.type));
|
|
246
242
|
const visibleNodeIds = new Set(visibleNodes.map((node) => node.id));
|
|
247
243
|
const nodesByType = new Map<NodeType, typeof visibleNodes>(
|
|
@@ -278,7 +274,7 @@ export function GraphView(props: GraphViewProps) {
|
|
|
278
274
|
const isPage = node.type === "page";
|
|
279
275
|
const screenshot = isPage ? (node.meta?.screenshot as string | undefined) : undefined;
|
|
280
276
|
const description = node.meta?.description as string | undefined;
|
|
281
|
-
const isDarkText =
|
|
277
|
+
const isDarkText = false;
|
|
282
278
|
|
|
283
279
|
const nodeType = isPage
|
|
284
280
|
? "pageNode"
|
|
@@ -308,7 +304,7 @@ export function GraphView(props: GraphViewProps) {
|
|
|
308
304
|
border: `2px ${borderStyle} ${borderColor}`,
|
|
309
305
|
padding: "10px 14px",
|
|
310
306
|
background: NODE_COLOR[node.type],
|
|
311
|
-
color:
|
|
307
|
+
color: "#ffffff",
|
|
312
308
|
fontSize: 12,
|
|
313
309
|
fontWeight: 600,
|
|
314
310
|
fontFamily: "'Inter', -apple-system, sans-serif",
|