next-arch-map 0.1.30 → 0.1.31

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,11 +1,12 @@
1
1
  import path from "node:path";
2
2
  import ts from "typescript";
3
- import { buildDbNode, buildEdgeKey, buildEndpointNode, buildHandlerNode, ensureNode, getEndpointRouteFromFile, getExistingDirectories, getSourceFile, isIgnoredSourceFile, isRouteHandlerFile, resolveLocalModulePath, resolveProjectRoot, walkDirectory, } from "../utils.js";
3
+ import { buildDbNode, buildEdgeKey, buildEndpointNode, buildHandlerNode, ensureNode, getEndpointRouteFromFile, getExistingDirectories, getSourceFile, isIgnoredSourceFile, isRouteHandlerFile, loadPathAliases, resolveLocalModulePath, resolveProjectRoot, walkDirectory, } from "../utils.js";
4
4
  const DEFAULT_API_DIRS = ["app", "src/app", "app/api", "src/app/api", "src/server", "src/api"];
5
5
  const DEFAULT_DB_CLIENT_IDENTIFIERS = ["prisma"];
6
6
  const ROUTE_METHOD_EXPORTS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
7
7
  export async function analyzeEndpointsToDb(options) {
8
8
  const projectRoot = resolveProjectRoot(options.projectRoot);
9
+ const aliases = loadPathAliases(projectRoot);
9
10
  const scanRoots = getExistingDirectories(projectRoot, options.apiDirs ?? DEFAULT_API_DIRS);
10
11
  const dbClientIdentifiers = new Set(options.dbClientIdentifiers ?? DEFAULT_DB_CLIENT_IDENTIFIERS);
11
12
  const nodes = [];
@@ -25,7 +26,7 @@ export async function analyzeEndpointsToDb(options) {
25
26
  seenRouteFiles.add(filePath);
26
27
  try {
27
28
  const endpointPath = getEndpointRouteFromFile(scanRoot, filePath);
28
- const { availableMethods, dbUsageByModel } = analyzeEndpoint(filePath, projectRoot, moduleCache, sourceFileCache, dbClientIdentifiers);
29
+ const { availableMethods, dbUsageByModel } = analyzeEndpoint(filePath, projectRoot, aliases, moduleCache, sourceFileCache, dbClientIdentifiers);
29
30
  const endpointNode = ensureNode(nodes, nodeIds, buildEndpointNode(endpointPath, filePath));
30
31
  const handlerMethods = availableMethods.size > 0 ? [...availableMethods] : [undefined];
31
32
  for (const methodName of handlerMethods) {
@@ -70,9 +71,10 @@ export async function analyzeEndpointsToDb(options) {
70
71
  left.to.localeCompare(right.to)),
71
72
  };
72
73
  }
73
- function analyzeEndpoint(routeFilePath, projectRoot, moduleCache, sourceFileCache, dbClientIdentifiers) {
74
+ function analyzeEndpoint(routeFilePath, projectRoot, aliases, moduleCache, sourceFileCache, dbClientIdentifiers) {
74
75
  const state = {
75
76
  projectRoot,
77
+ aliases,
76
78
  moduleCache,
77
79
  sourceFileCache,
78
80
  visitedDeclarationKeys: new Set(),
@@ -273,7 +275,7 @@ function getModuleInfo(filePath, state) {
273
275
  continue;
274
276
  }
275
277
  if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
276
- const resolvedImportPath = resolveLocalModulePath(filePath, statement.moduleSpecifier.text, state.projectRoot);
278
+ const resolvedImportPath = resolveLocalModulePath(filePath, statement.moduleSpecifier.text, state.projectRoot, state.aliases);
277
279
  if (!resolvedImportPath || !statement.importClause) {
278
280
  continue;
279
281
  }
@@ -298,7 +300,7 @@ function getModuleInfo(filePath, state) {
298
300
  statement.exportClause &&
299
301
  ts.isNamedExports(statement.exportClause)) {
300
302
  const resolvedModulePath = statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier)
301
- ? resolveLocalModulePath(filePath, statement.moduleSpecifier.text, state.projectRoot)
303
+ ? resolveLocalModulePath(filePath, statement.moduleSpecifier.text, state.projectRoot, state.aliases)
302
304
  : null;
303
305
  for (const element of statement.exportClause.elements) {
304
306
  const exportName = element.name.text;
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import ts from "typescript";
3
- import { buildActionNode, buildEdgeKey, buildEndpointNode, buildPageNode, buildServiceNode, collectStringConstants, ensureNode, getExistingDirectories, getPageRouteFromFile, getSourceFile, getStringLiteralValue, isIgnoredSourceFile, isPageFile, resolveLocalModulePath, resolveProjectRoot, walkDirectory, } from "../utils.js";
3
+ import { buildActionNode, buildEdgeKey, buildEndpointNode, buildPageNode, buildServiceNode, collectStringConstants, ensureNode, getExistingDirectories, getPageRouteFromFile, getSourceFile, getStringLiteralValue, isIgnoredSourceFile, isPageFile, loadPathAliases, 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"];
@@ -8,6 +8,7 @@ const DEFAULT_HTTP_CLIENT_METHODS = ["get", "post", "put", "patch", "delete", "h
8
8
  const DEFAULT_SDK_CLIENT_IDENTIFIERS = ["supabase"];
9
9
  export async function analyzePagesToEndpoints(options) {
10
10
  const projectRoot = resolveProjectRoot(options.projectRoot);
11
+ const aliases = loadPathAliases(projectRoot);
11
12
  const appDirs = getExistingDirectories(projectRoot, options.appDirs ?? DEFAULT_APP_DIRS);
12
13
  if (appDirs.length === 0) {
13
14
  throw new Error("Could not find an app/ or src/app/ directory.");
@@ -44,7 +45,7 @@ export async function analyzePagesToEndpoints(options) {
44
45
  }
45
46
  // For page files, follow imports transitively to find HTTP calls
46
47
  // in hooks, utilities, and other local modules.
47
- const httpCalls = collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers, sourceFileCache);
48
+ const httpCalls = collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers, sourceFileCache, aliases);
48
49
  const route = getPageRouteFromFile(appDir, filePath);
49
50
  ensureNode(nodes, nodeIds, buildPageNode(route, filePath));
50
51
  for (const call of httpCalls) {
@@ -152,7 +153,7 @@ function collectImportSpecifiers(sourceFile) {
152
153
  }
153
154
  return specifiers;
154
155
  }
155
- function collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers, sourceFileCache, visited) {
156
+ function collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers, sourceFileCache, aliases, visited) {
156
157
  const resolvedPath = path.resolve(filePath);
157
158
  const seen = visited ?? new Set();
158
159
  if (seen.has(resolvedPath)) {
@@ -165,11 +166,11 @@ function collectHttpCallsTransitively(filePath, projectRoot, httpClientIdentifie
165
166
  }
166
167
  const calls = collectHttpCalls(sourceFile, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers);
167
168
  for (const specifier of collectImportSpecifiers(sourceFile)) {
168
- const resolved = resolveLocalModulePath(filePath, specifier, projectRoot);
169
+ const resolved = resolveLocalModulePath(filePath, specifier, projectRoot, aliases);
169
170
  if (!resolved) {
170
171
  continue;
171
172
  }
172
- const transitiveCalls = collectHttpCallsTransitively(resolved, projectRoot, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers, sourceFileCache, seen);
173
+ const transitiveCalls = collectHttpCallsTransitively(resolved, projectRoot, httpClientIdentifiers, httpClientMethods, sdkClientIdentifiers, sourceFileCache, aliases, seen);
173
174
  calls.push(...transitiveCalls);
174
175
  }
175
176
  return calls;
@@ -1,18 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  const MODEL_BLOCK_PATTERN = /^model\s+(\w+)\s*\{/;
4
- const ENUM_BLOCK_PATTERN = /^enum\s+(\w+)\s*\{/;
5
- const SCALAR_TYPES = new Set([
6
- "String",
7
- "Int",
8
- "Float",
9
- "Boolean",
10
- "DateTime",
11
- "Json",
12
- "Bytes",
13
- "Decimal",
14
- "BigInt",
15
- ]);
16
4
  export async function analyzePrismaSchema(projectRoot) {
17
5
  const schemaPath = path.join(projectRoot, "prisma", "schema.prisma");
18
6
  if (!fs.existsSync(schemaPath)) {
@@ -26,7 +14,6 @@ export async function analyzePrismaSchema(projectRoot) {
26
14
  return { nodes: [], edges: [] };
27
15
  }
28
16
  const models = parseModels(content);
29
- const enumNames = parseEnumNames(content);
30
17
  const nodes = [];
31
18
  const edges = [];
32
19
  const edgeKeys = new Set();
@@ -78,22 +65,10 @@ export async function analyzePrismaSchema(projectRoot) {
78
65
  edges: edges.sort((a, b) => a.kind.localeCompare(b.kind) || a.from.localeCompare(b.from) || a.to.localeCompare(b.to)),
79
66
  };
80
67
  }
81
- function parseEnumNames(content) {
82
- const names = new Set();
83
- const lines = content.split("\n");
84
- for (const line of lines) {
85
- const match = ENUM_BLOCK_PATTERN.exec(line.trim());
86
- if (match) {
87
- names.add(match[1]);
88
- }
89
- }
90
- return names;
91
- }
92
68
  function parseModels(content) {
93
69
  const models = [];
94
70
  const lines = content.split("\n");
95
71
  const modelNames = new Set();
96
- const enumNames = parseEnumNames(content);
97
72
  // First pass: collect all model names
98
73
  for (const line of lines) {
99
74
  const match = MODEL_BLOCK_PATTERN.exec(line.trim());
@@ -129,7 +104,7 @@ function parseModels(content) {
129
104
  if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("@@")) {
130
105
  continue;
131
106
  }
132
- const field = parseField(trimmed, modelNames, enumNames);
107
+ const field = parseField(trimmed, modelNames);
133
108
  if (!field) {
134
109
  continue;
135
110
  }
@@ -145,7 +120,7 @@ function parseModels(content) {
145
120
  }
146
121
  return models;
147
122
  }
148
- function parseField(line, modelNames, enumNames) {
123
+ function parseField(line, modelNames) {
149
124
  // Tokenize: split on whitespace but respect parentheses content
150
125
  const tokens = tokenizeLine(line);
151
126
  if (tokens.length < 2) {
package/dist/serve.js CHANGED
@@ -140,9 +140,14 @@ export async function serve(options) {
140
140
  }
141
141
  });
142
142
  const server = http.createServer((req, res) => {
143
- res.setHeader("Access-Control-Allow-Origin", "*");
143
+ const origin = req.headers.origin ?? "";
144
+ const isLocalOrigin = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/.test(origin);
145
+ if (isLocalOrigin) {
146
+ res.setHeader("Access-Control-Allow-Origin", origin);
147
+ }
144
148
  res.setHeader("Access-Control-Allow-Methods", "GET, OPTIONS");
145
149
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
150
+ res.setHeader("Vary", "Origin");
146
151
  if (req.method === "OPTIONS") {
147
152
  res.statusCode = 204;
148
153
  res.end();
@@ -217,7 +222,7 @@ export async function serve(options) {
217
222
  });
218
223
  await new Promise((resolve, reject) => {
219
224
  server.once("error", reject);
220
- server.listen(port, () => {
225
+ server.listen(port, "127.0.0.1", () => {
221
226
  server.off("error", reject);
222
227
  console.log(`next-arch-map serve listening on http://localhost:${port}`);
223
228
  resolve();
package/dist/utils.d.ts CHANGED
@@ -13,7 +13,12 @@ export declare function getPageRouteFromFile(appDir: string, filePath: string):
13
13
  export declare function getEndpointRouteFromFile(scanRoot: string, filePath: string): string;
14
14
  export declare function getScriptKind(filePath: string): ts.ScriptKind;
15
15
  export declare function getSourceFile(filePath: string, sourceFileCache?: Map<string, ts.SourceFile>): ts.SourceFile | null;
16
- export declare function resolveLocalModulePath(importerFilePath: string, specifier: string, projectRoot: string): string | null;
16
+ export type PathAlias = {
17
+ prefix: string;
18
+ replacement: string;
19
+ };
20
+ export declare function loadPathAliases(projectRoot: string): PathAlias[];
21
+ export declare function resolveLocalModulePath(importerFilePath: string, specifier: string, projectRoot: string, aliases?: PathAlias[]): string | null;
17
22
  export declare function ensureDirectory(directoryPath: string): void;
18
23
  export declare function readJsonFile<T>(filePath: string): T;
19
24
  export declare function writeJsonFile(filePath: string, value: unknown): void;
package/dist/utils.js CHANGED
@@ -126,12 +126,62 @@ export function getSourceFile(filePath, sourceFileCache) {
126
126
  return null;
127
127
  }
128
128
  }
129
- export function resolveLocalModulePath(importerFilePath, specifier, projectRoot) {
129
+ export function loadPathAliases(projectRoot) {
130
+ for (const configName of ["tsconfig.json", "jsconfig.json"]) {
131
+ const configPath = path.join(projectRoot, configName);
132
+ if (!fileExists(configPath)) {
133
+ continue;
134
+ }
135
+ try {
136
+ const raw = fs.readFileSync(configPath, "utf8");
137
+ // Strip single-line comments (tsconfig allows them) before parsing
138
+ const stripped = raw.replace(/\/\/.*$/gm, "");
139
+ const config = JSON.parse(stripped);
140
+ const baseUrl = config.compilerOptions?.baseUrl ?? ".";
141
+ const paths = config.compilerOptions?.paths;
142
+ if (!paths) {
143
+ break;
144
+ }
145
+ const aliases = [];
146
+ for (const [pattern, targets] of Object.entries(paths)) {
147
+ if (!pattern.endsWith("/*") || targets.length === 0) {
148
+ continue;
149
+ }
150
+ const target = targets[0];
151
+ if (!target.endsWith("/*")) {
152
+ continue;
153
+ }
154
+ const prefix = pattern.slice(0, -1); // "@/*" → "@/"
155
+ const targetDir = target.slice(0, -2); // "./src/*" → "./src"
156
+ const replacement = path.resolve(projectRoot, baseUrl, targetDir);
157
+ aliases.push({ prefix, replacement });
158
+ }
159
+ // Sort longest prefix first so more specific aliases match first
160
+ aliases.sort((a, b) => b.prefix.length - a.prefix.length);
161
+ return aliases;
162
+ }
163
+ catch {
164
+ break;
165
+ }
166
+ }
167
+ // Fallback: default @/ → src/ for projects without tsconfig paths
168
+ return [{ prefix: "@/", replacement: path.join(projectRoot, "src") }];
169
+ }
170
+ export function resolveLocalModulePath(importerFilePath, specifier, projectRoot, aliases) {
130
171
  let basePath = null;
131
172
  if (specifier.startsWith("./") || specifier.startsWith("../")) {
132
173
  basePath = path.resolve(path.dirname(importerFilePath), specifier);
133
174
  }
175
+ else if (aliases) {
176
+ for (const alias of aliases) {
177
+ if (specifier.startsWith(alias.prefix)) {
178
+ basePath = path.join(alias.replacement, specifier.slice(alias.prefix.length));
179
+ break;
180
+ }
181
+ }
182
+ }
134
183
  else if (specifier.startsWith("@/")) {
184
+ // Legacy fallback when no aliases provided
135
185
  basePath = path.join(projectRoot, "src", specifier.slice(2));
136
186
  }
137
187
  if (!basePath) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "next-arch-map",
3
- "version": "0.1.30",
3
+ "version": "0.1.31",
4
4
  "description": "Static analyzer that builds a multi-layer architecture graph for Next.js-style apps.",
5
5
  "type": "module",
6
6
  "bin": {