@cmflow/atlas 3.4.0-beta.8 → 3.4.0

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