@cmflow/atlas 3.4.0-beta.5 → 3.4.0-beta.7

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.
@@ -0,0 +1,792 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { pathToFileURL } from "node:url";
4
+ import { Node, Project, SyntaxKind } from "ts-morph";
5
+ import { spinner } from "@clack/prompts";
6
+ import { AsyncLocalStorage } from "node:async_hooks";
7
+ import { globby } from "globby";
8
+ import { minimatch } from "minimatch";
9
+ import { Worker } from "node:worker_threads";
10
+ //#region src/utils/config.ts
11
+ let _config;
12
+ function setUserConfig(config) {
13
+ _config = config;
14
+ }
15
+ function getUserConfig() {
16
+ if (!_config) throw new Error("Atlas config not loaded. Run Atlas from a directory containing atlas.config.ts or pass --config.");
17
+ return _config;
18
+ }
19
+ function getTsconfigAliases(repoRoot) {
20
+ const tsconfigPath = path.join(repoRoot, "tsconfig.json");
21
+ if (!fs.existsSync(tsconfigPath)) return {};
22
+ const paths = new Project({
23
+ tsConfigFilePath: tsconfigPath,
24
+ skipAddingFilesFromTsConfig: true
25
+ }).getCompilerOptions().paths ?? {};
26
+ return Object.fromEntries(Object.entries(paths).flatMap(([alias, targets]) => {
27
+ const target = targets?.[0];
28
+ if (!target) return [];
29
+ return [[alias.replace(/\/\*$/, ""), target.replace(/^\.\//, "").replace(/\/\*$/, "")]];
30
+ }));
31
+ }
32
+ async function loadAtlasConfig(configPath, projectRoot) {
33
+ const filePath = path.resolve(configPath);
34
+ try {
35
+ let mod;
36
+ if (filePath.endsWith(".ts")) {
37
+ const { tsImport } = await import("tsx/esm/api");
38
+ mod = await tsImport(filePath, import.meta.url);
39
+ } else mod = await import(pathToFileURL(filePath).href);
40
+ const config = mod.default ?? mod;
41
+ const repoRoot = projectRoot || config.repoRoot || process.cwd();
42
+ return {
43
+ ...config,
44
+ repoRoot,
45
+ cwd: repoRoot,
46
+ resolver: { alias: {
47
+ ...getTsconfigAliases(repoRoot),
48
+ ...config.resolver?.alias
49
+ } }
50
+ };
51
+ } catch (error) {
52
+ throw new Error(`Unable to load Atlas configuration at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
53
+ }
54
+ }
55
+ //#endregion
56
+ //#region src/services/taskProgressService.ts
57
+ function formatDuration(durationMs) {
58
+ if (durationMs < 1e3) return `${durationMs}ms`;
59
+ return `${(durationMs / 1e3).toFixed(1)}s`;
60
+ }
61
+ var TaskProgressService = class {
62
+ #storage = new AsyncLocalStorage();
63
+ attach(sink, task) {
64
+ return this.#storage.run(sink, task);
65
+ }
66
+ log(message) {
67
+ this.#storage.getStore()?.log(message);
68
+ }
69
+ report(message) {
70
+ const sink = this.#storage.getStore();
71
+ (sink?.report || sink?.log)?.(message);
72
+ }
73
+ createStepProgress(classify = (message) => ({
74
+ id: message,
75
+ title: message
76
+ })) {
77
+ const progress = spinner();
78
+ let active;
79
+ const finishActive = (label) => {
80
+ if (!active) return;
81
+ const duration = formatDuration(Date.now() - active.startedAt);
82
+ progress.stop(`${label || active.completedTitle || `${active.title} completed`} (${duration})`);
83
+ active = void 0;
84
+ };
85
+ const start = (step) => {
86
+ if (active?.id === step.id) {
87
+ progress.message(step.detail ? `${step.title}: ${step.detail}` : step.title);
88
+ return;
89
+ }
90
+ finishActive();
91
+ active = {
92
+ id: step.id,
93
+ title: step.title,
94
+ completedTitle: step.completedTitle,
95
+ startedAt: Date.now()
96
+ };
97
+ progress.start(step.detail ? `${step.title}: ${step.detail}` : step.title);
98
+ };
99
+ const execute = (task) => this.attach({
100
+ log: (message) => progress.message(active ? `${active.title}: ${message}` : message),
101
+ report: (message) => start(classify(message))
102
+ }, task);
103
+ return {
104
+ report(message) {
105
+ start(classify(message));
106
+ },
107
+ start,
108
+ execute,
109
+ run: async (step, task) => {
110
+ start(step);
111
+ await new Promise((resolve) => setImmediate(resolve));
112
+ const result = await execute(task);
113
+ finishActive();
114
+ return result;
115
+ },
116
+ finish(label) {
117
+ finishActive(label);
118
+ },
119
+ fail(label) {
120
+ if (!active) return;
121
+ const duration = formatDuration(Date.now() - active.startedAt);
122
+ progress.stop(`${label} (${duration})`);
123
+ active = void 0;
124
+ }
125
+ };
126
+ }
127
+ };
128
+ const taskProgressService = new TaskProgressService();
129
+ //#endregion
130
+ //#region src/services/analysisFileService.ts
131
+ function shouldKeepAnalysisFile(filePath) {
132
+ const normalizedProjectPath = filePath.replaceAll(path.sep, "/").replace(/^.*?(app\/)/, "app/");
133
+ return !getUserConfig().analysis.excluded.some((pattern) => minimatch(normalizedProjectPath, pattern, { dot: true }));
134
+ }
135
+ function filterAnalysisFiles(filePaths) {
136
+ return filePaths.filter(shouldKeepAnalysisFile);
137
+ }
138
+ //#endregion
139
+ //#region src/utils/isKnowBackendType.ts
140
+ function isKnownBackendType(value) {
141
+ return getUserConfig().analysis.backends.includes(value);
142
+ }
143
+ //#endregion
144
+ //#region src/services/backendSourceService.ts
145
+ function inferBackendNameFromFile(sourceFile) {
146
+ const declaredType = sourceFile.getFullText().match(/BackendTypes\.([A-Z0-9_]+)/)?.[1];
147
+ if (declaredType) return isKnownBackendType(declaredType) ? declaredType : null;
148
+ const parts = sourceFile.getFilePath().split(path.sep);
149
+ const backIndex = parts.lastIndexOf("back");
150
+ const infrastructureIndex = parts.lastIndexOf("_infra");
151
+ const candidate = backIndex >= 0 ? parts[backIndex + 1] : infrastructureIndex >= 0 ? parts[infrastructureIndex + 2] : void 0;
152
+ return candidate && isKnownBackendType(candidate.toUpperCase()) ? candidate.toUpperCase() : null;
153
+ }
154
+ //#endregion
155
+ //#region src/utils/resolveAliasPath.ts
156
+ function resolveAliasPath(moduleSpecifier, aliases, rootPath) {
157
+ for (const [alias, target] of Object.entries(aliases)) {
158
+ if (moduleSpecifier !== alias && !moduleSpecifier.startsWith(`${alias}/`)) continue;
159
+ const modulePath = moduleSpecifier.slice(alias.length).replace(/^\//, "");
160
+ return path.join(rootPath, target, modulePath);
161
+ }
162
+ return null;
163
+ }
164
+ //#endregion
165
+ //#region src/utils/tryResolveWithExtensions.ts
166
+ function tryResolveWithExtensions(basePath) {
167
+ const ext = path.extname(basePath);
168
+ const withoutExt = ext ? basePath.slice(0, -ext.length) : basePath;
169
+ const candidates = [
170
+ basePath,
171
+ ext === ".js" ? `${withoutExt}.ts` : null,
172
+ ext === ".ts" ? `${withoutExt}.js` : null,
173
+ ext === ".mjs" ? `${withoutExt}.mts` : null,
174
+ ext === ".mts" ? `${withoutExt}.mjs` : null,
175
+ `${basePath}.ts`,
176
+ `${basePath}.js`,
177
+ `${basePath}.mts`,
178
+ `${basePath}.mjs`,
179
+ path.join(withoutExt, "index.ts"),
180
+ path.join(withoutExt, "index.js"),
181
+ path.join(basePath, "index.ts"),
182
+ path.join(basePath, "index.js")
183
+ ].filter((candidate) => Boolean(candidate));
184
+ for (const candidate of candidates) try {
185
+ const normalized = path.normalize(candidate);
186
+ if (fs.existsSync(normalized)) return normalized;
187
+ } catch {
188
+ continue;
189
+ }
190
+ return null;
191
+ }
192
+ //#endregion
193
+ //#region src/utils/resolveModulePath.ts
194
+ function resolveModulePath(sourceFilePath, moduleSpecifier, aliases, rootPath) {
195
+ if (moduleSpecifier.startsWith(".")) return tryResolveWithExtensions(path.resolve(path.dirname(sourceFilePath), moduleSpecifier));
196
+ const aliased = resolveAliasPath(moduleSpecifier, aliases, rootPath);
197
+ return aliased ? tryResolveWithExtensions(aliased) : null;
198
+ }
199
+ //#endregion
200
+ //#region src/services/routeBackendTopologyService.ts
201
+ const backendTopologyWeights = {
202
+ local_call: 1,
203
+ imported_call: 2,
204
+ local_callback: 2,
205
+ imported_callback: 3
206
+ };
207
+ function normalizeSourcePath(cwd, filePath) {
208
+ const relative = path.relative(cwd, filePath);
209
+ return relative.startsWith("..") ? filePath : relative.replaceAll(path.sep, "/");
210
+ }
211
+ function isFilePath(filePath) {
212
+ try {
213
+ return fs.statSync(filePath).isFile();
214
+ } catch {
215
+ return false;
216
+ }
217
+ }
218
+ function callableLine(callable) {
219
+ return callable.declaration?.getStartLineNumber() || 1;
220
+ }
221
+ function callableKey(callable) {
222
+ return `${callable.sourceFile.getFilePath()}:${callable.symbol}:${callableLine(callable)}`;
223
+ }
224
+ function isCallableVariable(declaration) {
225
+ const initializer = declaration.getInitializer();
226
+ return Boolean(initializer && (Node.isArrowFunction(initializer) || Node.isFunctionExpression(initializer) || Node.isCallExpression(initializer) || Node.isNewExpression(initializer)));
227
+ }
228
+ function findLocalCallable(sourceFile, symbol) {
229
+ const functionDeclaration = sourceFile.getFunctions().find((item) => item.getName() === symbol);
230
+ if (functionDeclaration) return functionDeclaration;
231
+ const variableDeclaration = sourceFile.getVariableDeclarations().find((item) => item.getName() === symbol && isCallableVariable(item));
232
+ if (variableDeclaration) return variableDeclaration;
233
+ return sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration).find((item) => item.getName() === symbol);
234
+ }
235
+ function resolveSourceFile(project, owner, moduleSpecifier, resolvePath) {
236
+ const resolvedPath = resolvePath(owner.getFilePath(), moduleSpecifier);
237
+ if (!resolvedPath) return void 0;
238
+ const existing = project.getSourceFile(resolvedPath);
239
+ if (existing) return existing;
240
+ return isFilePath(resolvedPath) ? project.addSourceFileAtPathIfExists(resolvedPath) : void 0;
241
+ }
242
+ function resolveExportedCallable(project, sourceFile, symbol, resolvePath, seen = /* @__PURE__ */ new Set()) {
243
+ const key = `${sourceFile.getFilePath()}:${symbol}`;
244
+ if (seen.has(key)) return void 0;
245
+ seen.add(key);
246
+ const local = findLocalCallable(sourceFile, symbol);
247
+ if (local) return {
248
+ declaration: local,
249
+ sourceFile,
250
+ symbol,
251
+ imported: true
252
+ };
253
+ for (const exportDeclaration of sourceFile.getExportDeclarations()) {
254
+ const moduleSpecifier = exportDeclaration.getModuleSpecifierValue();
255
+ if (!moduleSpecifier) continue;
256
+ const namedExport = exportDeclaration.getNamedExports().find((item) => (item.getAliasNode()?.getText() || item.getName()) === symbol);
257
+ if (exportDeclaration.getNamedExports().length && !namedExport) continue;
258
+ const targetFile = resolveSourceFile(project, sourceFile, moduleSpecifier, resolvePath);
259
+ if (!targetFile) continue;
260
+ const resolved = resolveExportedCallable(project, targetFile, namedExport?.getName() || symbol, resolvePath, seen);
261
+ if (resolved) return resolved;
262
+ }
263
+ }
264
+ function resolveConstructedMember(project, sourceFile, variableName, memberName, resolvePath) {
265
+ const initializer = sourceFile.getVariableDeclaration(variableName)?.getInitializer();
266
+ if (!initializer || !Node.isNewExpression(initializer)) return void 0;
267
+ const constructorName = initializer.getExpression().getText();
268
+ const localMethod = sourceFile.getClass(constructorName)?.getInstanceMethod(memberName);
269
+ if (localMethod) return {
270
+ declaration: localMethod,
271
+ sourceFile,
272
+ symbol: memberName,
273
+ imported: true
274
+ };
275
+ for (const importDeclaration of sourceFile.getImportDeclarations()) {
276
+ const namedImport = importDeclaration.getNamedImports().find((item) => (item.getAliasNode()?.getText() || item.getName()) === constructorName);
277
+ if (!namedImport) continue;
278
+ const targetFile = resolveSourceFile(project, sourceFile, importDeclaration.getModuleSpecifierValue(), resolvePath);
279
+ const method = targetFile?.getClass(namedImport.getName())?.getInstanceMethod(memberName);
280
+ if (targetFile && method) return {
281
+ declaration: method,
282
+ sourceFile: targetFile,
283
+ symbol: memberName,
284
+ imported: true
285
+ };
286
+ }
287
+ }
288
+ function resolveImportedReference(project, sourceFile, expressionText, resolvePath) {
289
+ const [root, member] = expressionText.split(".");
290
+ for (const importDeclaration of sourceFile.getImportDeclarations()) {
291
+ const targetFile = resolveSourceFile(project, sourceFile, importDeclaration.getModuleSpecifierValue(), resolvePath);
292
+ if (!targetFile) continue;
293
+ if (importDeclaration.getNamespaceImport()?.getText() === root && member) return resolveExportedCallable(project, targetFile, member, resolvePath) || {
294
+ sourceFile: targetFile,
295
+ symbol: expressionText,
296
+ imported: true
297
+ };
298
+ const namedImport = importDeclaration.getNamedImports().find((item) => (item.getAliasNode()?.getText() || item.getName()) === root);
299
+ if (namedImport) {
300
+ const importedSymbol = namedImport.getName();
301
+ if (member) {
302
+ if (inferBackendNameFromFile(targetFile)) return {
303
+ sourceFile: targetFile,
304
+ symbol: expressionText,
305
+ imported: true
306
+ };
307
+ const constructedMember = resolveConstructedMember(project, targetFile, importedSymbol, member, resolvePath);
308
+ if (constructedMember) return constructedMember;
309
+ }
310
+ return resolveExportedCallable(project, targetFile, member || importedSymbol, resolvePath) || {
311
+ sourceFile: targetFile,
312
+ symbol: expressionText,
313
+ imported: true
314
+ };
315
+ }
316
+ if (importDeclaration.getDefaultImport()?.getText() === root) return resolveExportedCallable(project, targetFile, member || "default", resolvePath) || {
317
+ sourceFile: targetFile,
318
+ symbol: expressionText,
319
+ imported: true
320
+ };
321
+ }
322
+ }
323
+ function resolveTypedPropertyMethod(project, sourceFile, expression, resolvePath) {
324
+ const receiver = expression.getExpression();
325
+ if (!Node.isPropertyAccessExpression(receiver) || receiver.getExpression().getText() !== "this") return;
326
+ const propertyType = expression.getFirstAncestorByKind(SyntaxKind.ClassDeclaration)?.getProperty(receiver.getName())?.getTypeNode()?.getText().match(/[A-Za-z_$][A-Za-z0-9_$]*/)?.[0];
327
+ if (!propertyType) return void 0;
328
+ for (const importDeclaration of sourceFile.getImportDeclarations()) {
329
+ const namedImport = importDeclaration.getNamedImports().find((item) => (item.getAliasNode()?.getText() || item.getName()) === propertyType);
330
+ if (!namedImport) continue;
331
+ const targetFile = resolveSourceFile(project, sourceFile, importDeclaration.getModuleSpecifierValue(), resolvePath);
332
+ if (!targetFile) continue;
333
+ const importedType = namedImport.getName();
334
+ const method = targetFile.getClasses().find((declaration) => declaration.getName() === importedType)?.getInstanceMethod(expression.getName());
335
+ if (method) return {
336
+ declaration: method,
337
+ sourceFile: targetFile,
338
+ symbol: expression.getText(),
339
+ imported: true
340
+ };
341
+ }
342
+ }
343
+ function resolveReference(project, sourceFile, expression, resolvePath) {
344
+ const expressionText = expression.getText();
345
+ const localSymbol = Node.isPropertyAccessExpression(expression) ? expression.getName() : Node.isIdentifier(expression) ? expression.getText() : void 0;
346
+ if (Node.isPropertyAccessExpression(expression)) {
347
+ const imported = resolveImportedReference(project, sourceFile, expressionText, resolvePath);
348
+ if (imported) return imported;
349
+ const typedPropertyMethod = resolveTypedPropertyMethod(project, sourceFile, expression, resolvePath);
350
+ if (typedPropertyMethod) return typedPropertyMethod;
351
+ }
352
+ if (localSymbol) {
353
+ const local = findLocalCallable(sourceFile, localSymbol);
354
+ if (local) return {
355
+ declaration: local,
356
+ sourceFile,
357
+ symbol: localSymbol,
358
+ imported: false
359
+ };
360
+ }
361
+ const imported = resolveImportedReference(project, sourceFile, expressionText, resolvePath);
362
+ if (imported) return imported;
363
+ const declaration = expression.getSymbol()?.getAliasedSymbol()?.getDeclarations()[0] || expression.getSymbol()?.getDeclarations()[0];
364
+ if (!declaration) return void 0;
365
+ const callable = Node.isFunctionDeclaration(declaration) || Node.isMethodDeclaration(declaration) || Node.isVariableDeclaration(declaration) ? declaration : void 0;
366
+ if (!callable) return void 0;
367
+ if (Node.isVariableDeclaration(callable) && !isCallableVariable(callable)) return void 0;
368
+ const targetFile = callable.getSourceFile();
369
+ return {
370
+ declaration: callable,
371
+ sourceFile: targetFile,
372
+ symbol: localSymbol || expressionText,
373
+ imported: targetFile.getFilePath() !== sourceFile.getFilePath()
374
+ };
375
+ }
376
+ function referenceExpressions(call) {
377
+ const references = [{
378
+ expression: call.getExpression(),
379
+ type: "call"
380
+ }];
381
+ for (const argument of call.getArguments()) if (Node.isIdentifier(argument) || Node.isPropertyAccessExpression(argument)) references.push({
382
+ expression: argument,
383
+ type: "callback"
384
+ });
385
+ return references;
386
+ }
387
+ function edgeWeight(type, imported) {
388
+ if (type === "callback") return imported ? backendTopologyWeights.imported_callback : backendTopologyWeights.local_callback;
389
+ return imported ? backendTopologyWeights.imported_call : backendTopologyWeights.local_call;
390
+ }
391
+ function callableCalls(callable) {
392
+ return callable.declaration?.getDescendantsOfKind(SyntaxKind.CallExpression) || [];
393
+ }
394
+ function mappingDirection(symbol, layer) {
395
+ const mapperNaming = getUserConfig().analysis.mapperNaming;
396
+ if (layer === "api") {
397
+ if (mapperNaming.inputPatterns.some((pattern) => pattern.test(symbol))) return "input";
398
+ if (mapperNaming.outputPatterns.some((pattern) => pattern.test(symbol))) return "output";
399
+ return;
400
+ }
401
+ if (mapperNaming.domainToBackendPattern.test(symbol)) return "input";
402
+ if (mapperNaming.domainNameFromToDomainPattern.test(symbol) || /(?:From(?:Back|Backend)ToDomain|ToDomain)$/i.test(symbol)) return "output";
403
+ }
404
+ function callableFromTopologyNode(cwd, project, node) {
405
+ const filePath = path.resolve(cwd, node.source);
406
+ const sourceFile = project.getSourceFile(filePath) || project.addSourceFileAtPathIfExists(filePath);
407
+ if (!sourceFile) return void 0;
408
+ const declaration = [
409
+ ...sourceFile.getFunctions(),
410
+ ...sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration),
411
+ ...sourceFile.getVariableDeclarations().filter(isCallableVariable)
412
+ ].find((item) => item.getStartLineNumber() === node.line);
413
+ return declaration ? {
414
+ declaration,
415
+ sourceFile,
416
+ symbol: node.symbol,
417
+ imported: true
418
+ } : void 0;
419
+ }
420
+ function collectBackendTopologyMappingContext(params) {
421
+ const handlerDeclaration = findLocalCallable(params.handlerFile, params.handlerName);
422
+ if (!handlerDeclaration) return [];
423
+ const handler = {
424
+ declaration: handlerDeclaration,
425
+ sourceFile: params.handlerFile,
426
+ symbol: params.handlerName,
427
+ imported: false
428
+ };
429
+ const contexts = [];
430
+ const visitedMapperCalls = /* @__PURE__ */ new Set();
431
+ const appendMapper = (callable, layer, direction, backendPath, backendType) => {
432
+ const key = [
433
+ callableKey(callable),
434
+ layer,
435
+ direction,
436
+ backendPath,
437
+ backendType
438
+ ].join(":");
439
+ if (visitedMapperCalls.has(key)) return;
440
+ visitedMapperCalls.add(key);
441
+ contexts.push({
442
+ layer,
443
+ direction,
444
+ symbol: callable.declaration?.getSymbol()?.getName() || callable.symbol.split(".").at(-1),
445
+ source: normalizeSourcePath(params.cwd, callable.sourceFile.getFilePath()),
446
+ line: callableLine(callable),
447
+ backend_path: backendPath,
448
+ backend_type: backendType
449
+ });
450
+ const expectedLayerPath = layer === "api" ? "app/_api/" : "app/_infra/back/";
451
+ for (const call of callableCalls(callable)) {
452
+ const dependency = resolveReference(params.project, callable.sourceFile, call.getExpression(), params.resolvePath);
453
+ if (!dependency?.declaration || !normalizeSourcePath(params.cwd, dependency.sourceFile.getFilePath()).includes(expectedLayerPath)) continue;
454
+ appendMapper(dependency, layer, direction, backendPath, backendType);
455
+ }
456
+ };
457
+ const appendMapperCalls = (callable, layer, backendPath, backendType) => {
458
+ for (const call of callableCalls(callable)) {
459
+ const resolved = resolveReference(params.project, callable.sourceFile, call.getExpression(), params.resolvePath);
460
+ if (!resolved?.declaration) continue;
461
+ const direction = mappingDirection(resolved.declaration.getSymbol()?.getName() || resolved.symbol.split(".").at(-1), layer);
462
+ if (!direction) continue;
463
+ appendMapper(resolved, layer, direction, backendPath, backendType);
464
+ }
465
+ };
466
+ for (const [backendPath, topologyPath] of params.backendPaths.entries()) {
467
+ appendMapperCalls(handler, "api", backendPath, topologyPath.backend_type);
468
+ for (const node of topologyPath.nodes) {
469
+ if (!node.source.includes("app/_infra/back/")) continue;
470
+ const callable = callableFromTopologyNode(params.cwd, params.project, node);
471
+ if (callable) appendMapperCalls(callable, "backend", backendPath, topologyPath.backend_type);
472
+ }
473
+ }
474
+ return [...new Map(contexts.map((context) => [[
475
+ context.layer,
476
+ context.direction,
477
+ context.symbol,
478
+ context.source,
479
+ context.line,
480
+ context.backend_path,
481
+ context.backend_type
482
+ ].join(":"), context])).values()];
483
+ }
484
+ function traceBackendPaths(params) {
485
+ const handlerDeclaration = findLocalCallable(params.handlerFile, params.handlerName);
486
+ if (!handlerDeclaration) throw new Error(`Handler declaration not found: ${params.handlerName} in ${params.handlerFile.getFilePath()}`);
487
+ const handler = {
488
+ declaration: handlerDeclaration,
489
+ sourceFile: params.handlerFile,
490
+ symbol: params.handlerName,
491
+ imported: false
492
+ };
493
+ const queue = [{
494
+ callable: handler,
495
+ weight: 0,
496
+ nodes: [{
497
+ symbol: handler.symbol,
498
+ source: normalizeSourcePath(params.cwd, handler.sourceFile.getFilePath()),
499
+ line: callableLine(handler),
500
+ depth: 0
501
+ }],
502
+ edges: [],
503
+ visited: /* @__PURE__ */ new Set([callableKey(handler)])
504
+ }];
505
+ const paths = [];
506
+ const shortestWeightByCallable = /* @__PURE__ */ new Map([[callableKey(handler), 0]]);
507
+ const shortestWeightByBackendBoundary = /* @__PURE__ */ new Map();
508
+ while (queue.length) {
509
+ queue.sort((left, right) => left.weight - right.weight);
510
+ const current = queue.shift();
511
+ for (const call of callableCalls(current.callable)) for (const reference of referenceExpressions(call)) {
512
+ const resolved = resolveReference(params.project, current.callable.sourceFile, reference.expression, params.resolvePath);
513
+ if (!resolved) continue;
514
+ const key = callableKey(resolved);
515
+ if (current.visited.has(key)) continue;
516
+ const weight = edgeWeight(reference.type, resolved.imported);
517
+ const totalWeight = current.weight + weight;
518
+ const backendType = inferBackendNameFromFile(resolved.sourceFile) || void 0;
519
+ const node = {
520
+ symbol: resolved.symbol,
521
+ source: normalizeSourcePath(params.cwd, resolved.sourceFile.getFilePath()),
522
+ line: callableLine(resolved),
523
+ depth: totalWeight,
524
+ backend_type: backendType
525
+ };
526
+ const edge = {
527
+ type: reference.type,
528
+ from: current.callable.symbol,
529
+ to: resolved.symbol,
530
+ source: normalizeSourcePath(params.cwd, current.callable.sourceFile.getFilePath()),
531
+ line: call.getStartLineNumber(),
532
+ weight
533
+ };
534
+ const nodes = [...current.nodes, node];
535
+ const edges = [...current.edges, edge];
536
+ if (backendType) {
537
+ const boundaryKey = `${backendType}:${node.source}:${node.symbol}:${node.line}`;
538
+ const shortestWeight = shortestWeightByBackendBoundary.get(boundaryKey);
539
+ if (shortestWeight !== void 0 && totalWeight > shortestWeight) continue;
540
+ shortestWeightByBackendBoundary.set(boundaryKey, totalWeight);
541
+ paths.push({
542
+ backend_type: backendType,
543
+ total_weight: totalWeight,
544
+ status: "resolved",
545
+ nodes,
546
+ edges
547
+ });
548
+ continue;
549
+ }
550
+ if (!resolved.declaration) continue;
551
+ const shortestWeight = shortestWeightByCallable.get(key);
552
+ if (shortestWeight !== void 0 && totalWeight >= shortestWeight) continue;
553
+ shortestWeightByCallable.set(key, totalWeight);
554
+ queue.push({
555
+ callable: resolved,
556
+ weight: totalWeight,
557
+ nodes,
558
+ edges,
559
+ visited: /* @__PURE__ */ new Set([...current.visited, key])
560
+ });
561
+ }
562
+ }
563
+ const unique = /* @__PURE__ */ new Map();
564
+ for (const topologyPath of paths.filter((item) => {
565
+ const terminalNode = item.nodes.at(-1);
566
+ const boundaryKey = `${item.backend_type}:${terminalNode.source}:${terminalNode.symbol}:${terminalNode.line}`;
567
+ return item.total_weight === shortestWeightByBackendBoundary.get(boundaryKey);
568
+ })) {
569
+ const key = `${topologyPath.backend_type}:${topologyPath.nodes.map((node) => `${node.source}:${node.symbol}:${node.line}`).join("->")}`;
570
+ const existing = unique.get(key);
571
+ if (!existing || topologyPath.total_weight < existing.total_weight) unique.set(key, topologyPath);
572
+ }
573
+ return [...unique.values()].sort((left, right) => left.total_weight - right.total_weight || left.backend_type.localeCompare(right.backend_type));
574
+ }
575
+ function stringProperty(object, name) {
576
+ const property = object.getProperty(name);
577
+ if (!property || !Node.isPropertyAssignment(property)) return void 0;
578
+ const initializer = property.getInitializer();
579
+ return initializer && (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) ? initializer.getLiteralValue() : void 0;
580
+ }
581
+ function handlerProperty(object) {
582
+ const property = object.getProperty("handler");
583
+ if (!property || !Node.isPropertyAssignment(property)) return void 0;
584
+ const initializer = property.getInitializer();
585
+ return initializer && (Node.isIdentifier(initializer) || Node.isPropertyAccessExpression(initializer)) ? initializer.getText() : void 0;
586
+ }
587
+ function extractRouteDeclarations(sourceFile) {
588
+ const initializer = sourceFile.getVariableDeclaration("routes")?.getInitializer();
589
+ let expression = initializer;
590
+ if (initializer && (Node.isAsExpression(initializer) || Node.isSatisfiesExpression(initializer))) expression = initializer.getExpression();
591
+ if (!expression || !Node.isArrayLiteralExpression(expression)) return [];
592
+ return expression.getElements().flatMap((element) => {
593
+ if (!Node.isObjectLiteralExpression(element)) return [];
594
+ const method = stringProperty(element, "method");
595
+ const routePath = stringProperty(element, "path");
596
+ const handlerRef = handlerProperty(element);
597
+ return method && routePath && handlerRef ? [{
598
+ method: method.toUpperCase(),
599
+ path: routePath,
600
+ sourceFile,
601
+ handlerRef
602
+ }] : [];
603
+ });
604
+ }
605
+ function resolveHandler(project, route, resolvePath) {
606
+ const resolved = resolveImportedReference(project, route.sourceFile, route.handlerRef, resolvePath);
607
+ if (resolved) return resolved;
608
+ const local = findLocalCallable(route.sourceFile, route.handlerRef);
609
+ return local ? {
610
+ declaration: local,
611
+ sourceFile: route.sourceFile,
612
+ symbol: route.handlerRef,
613
+ imported: false
614
+ } : void 0;
615
+ }
616
+ async function collectBackendTopologyDependencyFiles(project, entryFilePath, resolvePath) {
617
+ const queue = [entryFilePath];
618
+ const visited = /* @__PURE__ */ new Set();
619
+ while (queue.length) {
620
+ const current = queue.shift();
621
+ if (visited.has(current)) continue;
622
+ visited.add(current);
623
+ const sourceFile = project.getSourceFile(current) || (isFilePath(current) ? project.addSourceFileAtPathIfExists(current) : void 0);
624
+ if (!sourceFile) continue;
625
+ const moduleSpecifiers = [...sourceFile.getImportDeclarations().map((item) => item.getModuleSpecifierValue()), ...sourceFile.getExportDeclarations().map((item) => item.getModuleSpecifierValue()).filter((value) => Boolean(value))];
626
+ for (const moduleSpecifier of moduleSpecifiers) {
627
+ const resolved = resolvePath(sourceFile.getFilePath(), moduleSpecifier);
628
+ if (resolved && !visited.has(resolved) && shouldKeepAnalysisFile(resolved)) queue.push(resolved);
629
+ }
630
+ }
631
+ return [...visited];
632
+ }
633
+ async function generateBackendTopologyArtifacts(params) {
634
+ const resolvePath = (sourceFilePath, moduleSpecifier) => resolveModulePath(sourceFilePath, moduleSpecifier, getUserConfig().resolver.alias, params.cwd);
635
+ const project = new Project({
636
+ skipAddingFilesFromTsConfig: true,
637
+ compilerOptions: {
638
+ allowJs: true,
639
+ checkJs: false,
640
+ target: 99,
641
+ module: 99
642
+ }
643
+ });
644
+ taskProgressService.report("Discovering route files");
645
+ const routeFiles = await globby(["app/_api/**/routes.@(js|ts)", "app/legacy/**/routes.@(js|ts)"], {
646
+ cwd: params.cwd,
647
+ absolute: true
648
+ });
649
+ taskProgressService.log(`${routeFiles.length} route file${routeFiles.length === 1 ? "" : "s"} discovered`);
650
+ taskProgressService.report("Extracting route declarations");
651
+ const routes = routeFiles.flatMap((routeFile) => extractRouteDeclarations(project.addSourceFileAtPath(routeFile))).filter((route) => {
652
+ if (params.routeSelector) return route.method === params.routeSelector.method && route.path === params.routeSelector.path;
653
+ return !params.routeSelectors || params.routeSelectors.some((selector) => route.method === selector.method && route.path === selector.path);
654
+ });
655
+ if (params.routeSelector && !routes.length) throw new Error(`Route not found: ${params.routeSelector.method} ${params.routeSelector.path}`);
656
+ params.onRoutesDiscovered?.(routes.length);
657
+ if (!params.onRoutesDiscovered) taskProgressService.log(`${routes.length} API route${routes.length === 1 ? "" : "s"} selected`);
658
+ const artifacts = [];
659
+ for (const [index, route] of routes.entries()) {
660
+ const routeProgress = (stage) => params.onRouteProgress?.({
661
+ current: index + 1,
662
+ total: routes.length,
663
+ route: {
664
+ method: route.method,
665
+ path: route.path
666
+ },
667
+ stage
668
+ });
669
+ if (!params.onRouteProgress) {
670
+ taskProgressService.report(`Tracing route ${index + 1}/${routes.length}: ${route.method} ${route.path}`);
671
+ await new Promise((resolve) => setImmediate(resolve));
672
+ }
673
+ const handler = resolveHandler(project, route, resolvePath);
674
+ if (!handler?.declaration) {
675
+ taskProgressService.log(`Warning: handler not resolved for ${route.method} ${route.path} (${route.handlerRef}); route skipped`);
676
+ routeProgress("completed");
677
+ continue;
678
+ }
679
+ routeProgress("collecting_dependencies");
680
+ if (!params.onRouteProgress) taskProgressService.log(`${route.method} ${route.path} ... Collecting dependencies`);
681
+ const dependencyFiles = await collectBackendTopologyDependencyFiles(project, handler.sourceFile.getFilePath(), resolvePath);
682
+ routeProgress("tracing_paths");
683
+ if (!params.onRouteProgress) taskProgressService.log(`${route.method} ${route.path} ... Tracing callable paths`);
684
+ const backendPaths = traceBackendPaths({
685
+ cwd: params.cwd,
686
+ project,
687
+ handlerFile: handler.sourceFile,
688
+ handlerName: handler.symbol,
689
+ resolvePath
690
+ });
691
+ const mappingContext = collectBackendTopologyMappingContext({
692
+ cwd: params.cwd,
693
+ project,
694
+ handlerFile: handler.sourceFile,
695
+ handlerName: handler.symbol,
696
+ backendPaths,
697
+ resolvePath
698
+ });
699
+ if (!params.onRouteProgress) taskProgressService.log(`${route.method} ${route.path} ... ${backendPaths.length} backend path${backendPaths.length === 1 ? "" : "s"}`);
700
+ const artifact = {
701
+ schema_version: 3,
702
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
703
+ route: {
704
+ method: route.method,
705
+ path: route.path,
706
+ source: normalizeSourcePath(params.cwd, route.sourceFile.getFilePath()),
707
+ handler: {
708
+ symbol: handler.symbol,
709
+ source: normalizeSourcePath(params.cwd, handler.sourceFile.getFilePath()),
710
+ line: callableLine(handler)
711
+ }
712
+ },
713
+ analysis_files: [.../* @__PURE__ */ new Set([...dependencyFiles.map((filePath) => normalizeSourcePath(params.cwd, filePath)), ...backendPaths.flatMap((backendPath) => backendPath.nodes.map((node) => node.source))])].sort(),
714
+ mapping_context: mappingContext,
715
+ weights: backendTopologyWeights,
716
+ backend_paths: backendPaths
717
+ };
718
+ artifacts.push(artifact);
719
+ await params.onArtifact?.(artifact);
720
+ routeProgress("completed");
721
+ }
722
+ return artifacts;
723
+ }
724
+ async function discoverBackendTopologyRouteSelectors(cwd) {
725
+ const project = new Project({
726
+ skipAddingFilesFromTsConfig: true,
727
+ compilerOptions: {
728
+ allowJs: true,
729
+ checkJs: false
730
+ }
731
+ });
732
+ return (await globby(["app/_api/**/routes.@(js|ts)", "app/legacy/**/routes.@(js|ts)"], {
733
+ cwd,
734
+ absolute: true
735
+ })).flatMap((routeFile) => extractRouteDeclarations(project.addSourceFileAtPath(routeFile))).map((route) => ({
736
+ method: route.method,
737
+ path: route.path
738
+ }));
739
+ }
740
+ async function generateBackendTopologyArtifactsInWorkers(params) {
741
+ const selectors = params.routeSelector ? [params.routeSelector] : await discoverBackendTopologyRouteSelectors(params.cwd);
742
+ params.onRoutesDiscovered?.(selectors.length);
743
+ const workerCount = Math.min(params.workers || 2, selectors.length);
744
+ const chunks = Array.from({ length: workerCount }, () => []);
745
+ selectors.forEach((selector, index) => chunks[index % workerCount].push(selector));
746
+ const artifacts = [];
747
+ let completed = 0;
748
+ let writeQueue = Promise.resolve();
749
+ const workers = [];
750
+ try {
751
+ await Promise.all(chunks.map((routeSelectors) => new Promise((resolve, reject) => {
752
+ const workerFile = import.meta.url.endsWith(".ts") ? "../workers/routeBackendTopologyWorker.ts" : "./workers/routeBackendTopologyWorker.mjs";
753
+ const worker = new Worker(new URL(workerFile, import.meta.url), {
754
+ workerData: {
755
+ cwd: params.cwd,
756
+ routeSelectors
757
+ },
758
+ execArgv: process.execArgv
759
+ });
760
+ workers.push(worker);
761
+ worker.on("message", (message) => {
762
+ if (message.type === "error") {
763
+ reject(new Error(message.message));
764
+ return;
765
+ }
766
+ writeQueue = writeQueue.then(async () => {
767
+ if (message.type === "artifact") {
768
+ await params.onArtifact(message.artifact);
769
+ artifacts.push(message.artifact);
770
+ return;
771
+ }
772
+ completed += 1;
773
+ params.onRouteProgress?.({
774
+ current: completed,
775
+ total: selectors.length,
776
+ route: message.route,
777
+ stage: "completed"
778
+ });
779
+ }).catch(reject);
780
+ });
781
+ worker.once("error", reject);
782
+ worker.once("exit", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`Topology worker exited with code ${code}`)));
783
+ })));
784
+ await writeQueue;
785
+ return artifacts;
786
+ } catch (error) {
787
+ await Promise.all(workers.map((worker) => worker.terminate().catch(() => void 0)));
788
+ throw error;
789
+ }
790
+ }
791
+ //#endregion
792
+ export { isKnownBackendType as a, taskProgressService as c, setUserConfig as d, inferBackendNameFromFile as i, getUserConfig as l, generateBackendTopologyArtifactsInWorkers as n, filterAnalysisFiles as o, resolveModulePath as r, shouldKeepAnalysisFile as s, generateBackendTopologyArtifacts as t, loadAtlasConfig as u };