@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.
- package/README.md +7 -5
- package/dist/bin/atlas.mjs +29 -322
- package/dist/index.d.mts +1 -1
- package/dist/routeBackendTopologyService-D_aIIBbX.mjs +792 -0
- package/dist/rules/cleanObjectRule.d.mts +1 -1
- package/dist/rules/cmsI18nFieldRule.d.mts +1 -1
- package/dist/rules/dateConversionRule.d.mts +1 -1
- package/dist/rules/lodashGetRule.d.mts +1 -1
- package/dist/rules/mappingUtilityRule.d.mts +1 -1
- package/dist/rules/memberGetFieldRule.d.mts +1 -1
- package/dist/rules/quableI18nFieldRule.d.mts +1 -1
- package/dist/{types-DcvdOZ2j.d.mts → types-3y34Gf8R.d.mts} +3 -3
- package/dist/workers/routeBackendTopologyWorker.mjs +29 -0
- package/package.json +1 -1
|
@@ -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 };
|