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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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) {
|