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