@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 CHANGED
@@ -50,10 +50,12 @@ The configuration imports `defineConfig` from Atlas:
50
50
  import { defineConfig } from "@cmflow/atlas";
51
51
 
52
52
  export default defineConfig({
53
- backends: ["ICC", "CMS", "CMS_B2C"],
54
53
  openapiUrl: "https://api.example.com/openapi.json",
55
54
  openapiTimeoutMs: 60_000,
56
- directusUrl: "https://cms.api.clubmed"
55
+ directusUrl: "https://cms.api.clubmed",
56
+ analysis: {
57
+ backends: ["ICC", "CMS", "CMS_B2C"]
58
+ }
57
59
  });
58
60
  ```
59
61
 
@@ -117,7 +119,7 @@ Generate the topology first, then the route catalogue:
117
119
 
118
120
  ```bash
119
121
  atlas --project-root /path/to/api generate:graph
120
- atlas --project-root /path/to/api generate:catalogue --output .tmp/datasource-catalogue
122
+ atlas --project-root /path/to/api generate:catalog --output .tmp/datasource-catalogue
121
123
  ```
122
124
 
123
125
  Each route produces a YAML document and a `.graph.yaml` topology artifact. The catalogue requires a valid graph and leaves ambiguous fields marked `needs_review`.
@@ -150,7 +152,7 @@ Without `--write`, the command performs a dry run. Use `clean-orphans` to inspec
150
152
  ```text
151
153
  atlas init Create atlas.config.ts
152
154
  atlas generate:graph Trace API routes to backends
153
- atlas generate:catalogue [route] Generate route review documents
155
+ atlas generate:catalog [route] Generate route review documents
154
156
  atlas generate:test Check configured coverage baselines
155
157
  atlas needs-review Rank unresolved routes
156
158
  atlas infer [route|directory] Optionally enrich unresolved mappings
@@ -161,7 +163,7 @@ atlas report:changed Report coverage for changed routes
161
163
 
162
164
  ## CI
163
165
 
164
- The recommended static CI flow is `generate:graph`, `generate:catalogue`, then `push`. Do not run optional inference in CI. Supply `DIRECTUS_URL` and `DIRECTUS_TOKEN` through the CI secret context; skip the push if either value is absent.
166
+ The recommended static CI flow is `generate:graph`, `generate:catalog`, then `push`. Do not run optional inference in CI. Supply `DIRECTUS_URL` and `DIRECTUS_TOKEN` through the CI secret context; skip the push if either value is absent.
165
167
 
166
168
  ## Development
167
169
 
@@ -1,156 +1,23 @@
1
1
  #!/usr/bin/env node
2
+ import { a as isKnownBackendType, c as taskProgressService, d as setUserConfig, i as inferBackendNameFromFile, l as getUserConfig, n as generateBackendTopologyArtifactsInWorkers, o as filterAnalysisFiles, r as resolveModulePath, s as shouldKeepAnalysisFile, u as loadAtlasConfig } from "../routeBackendTopologyService-D_aIIBbX.mjs";
2
3
  import { Command, InvalidArgumentError, Option } from "commander";
3
4
  import path from "node:path";
4
- import fs from "node:fs";
5
- import { pathToFileURL } from "node:url";
6
5
  import { Node, Project, SyntaxKind } from "ts-morph";
7
- import { cancel, intro, log, note, outro, progress, spinner } from "@clack/prompts";
8
- import fs$1 from "node:fs/promises";
6
+ import { cancel, intro, log, note, outro, progress } from "@clack/prompts";
7
+ import fs from "node:fs/promises";
9
8
  import { createDirectus, createItem, deleteItem, readItems, rest, staticToken, updateItem } from "@directus/sdk";
10
- import { AsyncLocalStorage } from "node:async_hooks";
11
9
  import { globby } from "globby";
12
- import { minimatch } from "minimatch";
13
10
  import { createHash } from "node:crypto";
14
11
  import { parse, stringify } from "yaml";
15
- import { Worker } from "node:worker_threads";
16
12
  import { NoObjectGeneratedError, Output, generateText } from "ai";
17
13
  import { z } from "zod";
18
14
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
19
- //#region src/utils/config.ts
20
- let _config;
21
- function setUserConfig(config) {
22
- _config = config;
23
- }
24
- function getUserConfig() {
25
- if (!_config) throw new Error("Atlas config not loaded. Run Atlas from a directory containing atlas.config.ts or pass --config.");
26
- return _config;
27
- }
28
- function getTsconfigAliases(repoRoot) {
29
- const tsconfigPath = path.join(repoRoot, "tsconfig.json");
30
- if (!fs.existsSync(tsconfigPath)) return {};
31
- const paths = new Project({
32
- tsConfigFilePath: tsconfigPath,
33
- skipAddingFilesFromTsConfig: true
34
- }).getCompilerOptions().paths ?? {};
35
- return Object.fromEntries(Object.entries(paths).flatMap(([alias, targets]) => {
36
- const target = targets?.[0];
37
- if (!target) return [];
38
- return [[alias.replace(/\/\*$/, ""), target.replace(/^\.\//, "").replace(/\/\*$/, "")]];
39
- }));
40
- }
41
- async function loadAtlasConfig(configPath, projectRoot) {
42
- const filePath = path.resolve(configPath);
43
- try {
44
- let mod;
45
- if (filePath.endsWith(".ts")) {
46
- const { tsImport } = await import("tsx/esm/api");
47
- mod = await tsImport(filePath, import.meta.url);
48
- } else mod = await import(pathToFileURL(filePath).href);
49
- const config = mod.default ?? mod;
50
- const repoRoot = projectRoot || config.repoRoot || process.cwd();
51
- return {
52
- ...config,
53
- repoRoot,
54
- cwd: repoRoot,
55
- resolver: { alias: {
56
- ...getTsconfigAliases(repoRoot),
57
- ...config.resolver?.alias
58
- } }
59
- };
60
- } catch (error) {
61
- throw new Error(`Unable to load Atlas configuration at ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
62
- }
63
- }
64
- //#endregion
65
- //#region src/services/taskProgressService.ts
66
- function formatDuration(durationMs) {
67
- if (durationMs < 1e3) return `${durationMs}ms`;
68
- return `${(durationMs / 1e3).toFixed(1)}s`;
69
- }
70
- var TaskProgressService = class {
71
- #storage = new AsyncLocalStorage();
72
- attach(sink, task) {
73
- return this.#storage.run(sink, task);
74
- }
75
- log(message) {
76
- this.#storage.getStore()?.log(message);
77
- }
78
- report(message) {
79
- const sink = this.#storage.getStore();
80
- (sink?.report || sink?.log)?.(message);
81
- }
82
- createStepProgress(classify = (message) => ({
83
- id: message,
84
- title: message
85
- })) {
86
- const progress = spinner();
87
- let active;
88
- const finishActive = (label) => {
89
- if (!active) return;
90
- const duration = formatDuration(Date.now() - active.startedAt);
91
- progress.stop(`${label || active.completedTitle || `${active.title} completed`} (${duration})`);
92
- active = void 0;
93
- };
94
- const start = (step) => {
95
- if (active?.id === step.id) {
96
- progress.message(step.detail ? `${step.title}: ${step.detail}` : step.title);
97
- return;
98
- }
99
- finishActive();
100
- active = {
101
- id: step.id,
102
- title: step.title,
103
- completedTitle: step.completedTitle,
104
- startedAt: Date.now()
105
- };
106
- progress.start(step.detail ? `${step.title}: ${step.detail}` : step.title);
107
- };
108
- const execute = (task) => this.attach({
109
- log: (message) => progress.message(active ? `${active.title}: ${message}` : message),
110
- report: (message) => start(classify(message))
111
- }, task);
112
- return {
113
- report(message) {
114
- start(classify(message));
115
- },
116
- start,
117
- execute,
118
- run: async (step, task) => {
119
- start(step);
120
- await new Promise((resolve) => setImmediate(resolve));
121
- const result = await execute(task);
122
- finishActive();
123
- return result;
124
- },
125
- finish(label) {
126
- finishActive(label);
127
- },
128
- fail(label) {
129
- if (!active) return;
130
- const duration = formatDuration(Date.now() - active.startedAt);
131
- progress.stop(`${label} (${duration})`);
132
- active = void 0;
133
- }
134
- };
135
- }
136
- };
137
- const taskProgressService = new TaskProgressService();
138
- //#endregion
139
15
  //#region src/utils/catalogueStats.ts
140
16
  function calculateNeedsReviewPercentage(inputProperties, outputProperties, needsReview) {
141
17
  const properties = inputProperties + outputProperties;
142
18
  return properties ? Number((needsReview / properties * 100).toFixed(2)) : 0;
143
19
  }
144
20
  //#endregion
145
- //#region src/services/analysisFileService.ts
146
- function shouldKeepAnalysisFile(filePath) {
147
- const normalizedProjectPath = filePath.replaceAll(path.sep, "/").replace(/^.*?(app\/)/, "app/");
148
- return !getUserConfig().analysis.excluded.some((pattern) => minimatch(normalizedProjectPath, pattern, { dot: true }));
149
- }
150
- function filterAnalysisFiles(filePaths) {
151
- return filePaths.filter(shouldKeepAnalysisFile);
152
- }
153
- //#endregion
154
21
  //#region src/services/useCaseAnalysisService.ts
155
22
  function normalizeCodeText(value) {
156
23
  return value.replace(/\s+/g, " ").replace(/;$/, "").trim();
@@ -391,25 +258,6 @@ function extractOpenApiOutputProperties(operation, swagger) {
391
258
  return [...map.values()];
392
259
  }
393
260
  //#endregion
394
- //#region src/services/backendTypeService.ts
395
- function getBackendTypes() {
396
- return Object.fromEntries(getUserConfig().backends.map((backend) => [backend, backend]));
397
- }
398
- function isKnownBackendType(value) {
399
- return Object.hasOwn(getBackendTypes(), value);
400
- }
401
- //#endregion
402
- //#region src/services/backendSourceService.ts
403
- function inferBackendNameFromFile(sourceFile) {
404
- const declaredType = sourceFile.getFullText().match(/BackendTypes\.([A-Z0-9_]+)/)?.[1];
405
- if (declaredType) return isKnownBackendType(declaredType) ? declaredType : null;
406
- const parts = sourceFile.getFilePath().split(path.sep);
407
- const backIndex = parts.lastIndexOf("back");
408
- const infrastructureIndex = parts.lastIndexOf("_infra");
409
- const candidate = backIndex >= 0 ? parts[backIndex + 1] : infrastructureIndex >= 0 ? parts[infrastructureIndex + 2] : void 0;
410
- return candidate && isKnownBackendType(candidate.toUpperCase()) ? candidate.toUpperCase() : null;
411
- }
412
- //#endregion
413
261
  //#region src/services/backendRouteExtractionService.ts
414
262
  function looksLikeBackendRoute(value) {
415
263
  return value.startsWith("/") || /^https?:\/\//.test(value) || /^graphql$/i.test(value);
@@ -460,51 +308,6 @@ function normalizeDescription(description) {
460
308
  return description?.trim() || "";
461
309
  }
462
310
  //#endregion
463
- //#region src/utils/resolveAliasPath.ts
464
- function resolveAliasPath(moduleSpecifier, aliases, rootPath) {
465
- for (const [alias, target] of Object.entries(aliases)) {
466
- if (moduleSpecifier !== alias && !moduleSpecifier.startsWith(`${alias}/`)) continue;
467
- const modulePath = moduleSpecifier.slice(alias.length).replace(/^\//, "");
468
- return path.join(rootPath, target, modulePath);
469
- }
470
- return null;
471
- }
472
- //#endregion
473
- //#region src/utils/tryResolveWithExtensions.ts
474
- function tryResolveWithExtensions(basePath) {
475
- const ext = path.extname(basePath);
476
- const withoutExt = ext ? basePath.slice(0, -ext.length) : basePath;
477
- const candidates = [
478
- basePath,
479
- ext === ".js" ? `${withoutExt}.ts` : null,
480
- ext === ".ts" ? `${withoutExt}.js` : null,
481
- ext === ".mjs" ? `${withoutExt}.mts` : null,
482
- ext === ".mts" ? `${withoutExt}.mjs` : null,
483
- `${basePath}.ts`,
484
- `${basePath}.js`,
485
- `${basePath}.mts`,
486
- `${basePath}.mjs`,
487
- path.join(withoutExt, "index.ts"),
488
- path.join(withoutExt, "index.js"),
489
- path.join(basePath, "index.ts"),
490
- path.join(basePath, "index.js")
491
- ].filter((candidate) => Boolean(candidate));
492
- for (const candidate of candidates) try {
493
- const normalized = path.normalize(candidate);
494
- if (fs.existsSync(normalized)) return normalized;
495
- } catch {
496
- continue;
497
- }
498
- return null;
499
- }
500
- //#endregion
501
- //#region src/utils/resolveModulePath.ts
502
- function resolveModulePath(sourceFilePath, moduleSpecifier, aliases, rootPath) {
503
- if (moduleSpecifier.startsWith(".")) return tryResolveWithExtensions(path.resolve(path.dirname(sourceFilePath), moduleSpecifier));
504
- const aliased = resolveAliasPath(moduleSpecifier, aliases, rootPath);
505
- return aliased ? tryResolveWithExtensions(aliased) : null;
506
- }
507
- //#endregion
508
311
  //#region src/utils/dedupeByKey.ts
509
312
  function dedupeByKey(items, keyFn) {
510
313
  const map = /* @__PURE__ */ new Map();
@@ -1446,11 +1249,12 @@ async function analyzeCodebaseRouteContracts(params) {
1446
1249
  }) : void 0;
1447
1250
  if (routeAnalysisScope && !topologyScope) continue;
1448
1251
  taskProgressService.log(topologyScope ? `${routeLabel} ... Loading dependencies from backend graph` : `${routeLabel} ... Collecting dependencies`);
1449
- const analysisFiles = filterAnalysisFiles(dedupeByKey(await (topologyScope ? topologyScope.analysisFiles : handler.handlerFile ? dependencyFilesByHandler.get(handler.handlerFile) || (() => {
1252
+ const dependencyFiles = topologyScope ? topologyScope.analysisFiles : handler.handlerFile ? dependencyFilesByHandler.get(handler.handlerFile) || (() => {
1450
1253
  const files = collectDependencyFiles(project, handler.handlerFile, routeLabel, resolveCachedModulePath);
1451
1254
  dependencyFilesByHandler.set(handler.handlerFile, files);
1452
1255
  return files;
1453
- })() : [routeDeclaration.file]), (value) => value));
1256
+ })() : [routeDeclaration.file];
1257
+ const analysisFiles = filterAnalysisFiles(dedupeByKey(await dependencyFiles, (value) => value));
1454
1258
  taskProgressService.log(`${routeLabel} ... Extracting mappings from ${analysisFiles.length} relevant files`);
1455
1259
  await yieldToEventLoop();
1456
1260
  const backendSourceFiles = analysisFiles.filter((file) => file.includes(`${path.sep}app${path.sep}_infra${path.sep}back${path.sep}`)).map((file) => project.getSourceFile(file) || project.addSourceFileAtPath(file)).filter((sourceFile) => {
@@ -2359,7 +2163,7 @@ async function pushCatalogueToDirectus(catalogue, options) {
2359
2163
  }
2360
2164
  }
2361
2165
  }
2362
- if (options.outputPath) await fs$1.appendFile(options.outputPath, "", "utf8");
2166
+ if (options.outputPath) await fs.appendFile(options.outputPath, "", "utf8");
2363
2167
  const removedOrphanedLinks = await cleanupOrphanedPropertyLinks(client);
2364
2168
  taskProgressService.report("Finalizing Directus synchronization");
2365
2169
  if (warnings.size) {
@@ -2555,12 +2359,12 @@ async function writeRouteReviewDocument(catalogue, outputDir, repoRoot, routeKey
2555
2359
  if (!route) throw new Error(`Unable to write review document for unknown route key ${routeKey}`);
2556
2360
  const document = relativizeRouteReviewDocument(buildRouteReviewDocument(catalogue, route.key), repoRoot);
2557
2361
  const filePath = path.join(outputDir, buildRouteReviewRelativePath(route.method, route.path));
2558
- await fs$1.mkdir(path.dirname(filePath), { recursive: true });
2559
- await fs$1.writeFile(filePath, `${stringify(document)}\n`, "utf8");
2362
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
2363
+ await fs.writeFile(filePath, `${stringify(document)}\n`, "utf8");
2560
2364
  return filePath;
2561
2365
  }
2562
2366
  async function writeRouteReviewDocuments(catalogue, outputDir, repoRoot) {
2563
- await fs$1.mkdir(outputDir, { recursive: true });
2367
+ await fs.mkdir(outputDir, { recursive: true });
2564
2368
  for (const [index, route] of catalogue.routes.entries()) {
2565
2369
  taskProgressService.log(`[${index + 1}/${catalogue.routes.length}] - ${route.method} ${route.path} ...`);
2566
2370
  await writeRouteReviewDocument(catalogue, outputDir, repoRoot, route.key);
@@ -2568,8 +2372,8 @@ async function writeRouteReviewDocuments(catalogue, outputDir, repoRoot) {
2568
2372
  return outputDir;
2569
2373
  }
2570
2374
  async function prepareCatalogueOutputDirectory(outputDir) {
2571
- await fs$1.mkdir(outputDir, { recursive: true });
2572
- await Promise.all(["catalogue.yaml", "catalogue.yml"].map((fileName) => fs$1.rm(path.join(outputDir, fileName), { force: true })));
2375
+ await fs.mkdir(outputDir, { recursive: true });
2376
+ await Promise.all(["catalogue.yaml", "catalogue.yml"].map((fileName) => fs.rm(path.join(outputDir, fileName), { force: true })));
2573
2377
  }
2574
2378
  async function writeCatalogueArtifacts(catalogue, outputDir, repoRoot = process.cwd()) {
2575
2379
  await prepareCatalogueOutputDirectory(outputDir);
@@ -2747,7 +2551,7 @@ async function readCatalogueFromDirectory(inputDir) {
2747
2551
  if (!files.length) throw new Error(`No route YAML documents found in ${inputDir}`);
2748
2552
  return {
2749
2553
  catalogue: mergeRouteReviewDocuments(await Promise.all(files.sort().map(async (file) => {
2750
- const parsed = parse(await fs$1.readFile(file, "utf8"));
2554
+ const parsed = parse(await fs.readFile(file, "utf8"));
2751
2555
  if (parsed?.schema_version !== 1 || !parsed?.route?.method || !parsed?.route?.path) throw new Error(`Invalid route YAML document: ${file}`);
2752
2556
  return parsed;
2753
2557
  }))),
@@ -2941,8 +2745,8 @@ function buildBackendTopologyRelativePath(method, routePath) {
2941
2745
  }
2942
2746
  async function writeBackendTopologyArtifact(artifact, outputDir) {
2943
2747
  const filePath = path.join(outputDir, buildBackendTopologyRelativePath(artifact.route.method, artifact.route.path));
2944
- await fs$1.mkdir(path.dirname(filePath), { recursive: true });
2945
- await fs$1.writeFile(filePath, `${stringify(artifact, { aliasDuplicateObjects: false })}\n`, "utf8");
2748
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
2749
+ await fs.writeFile(filePath, `${stringify(artifact, { aliasDuplicateObjects: false })}\n`, "utf8");
2946
2750
  return filePath;
2947
2751
  }
2948
2752
  function isRecord(value) {
@@ -2961,7 +2765,7 @@ async function loadBackendTopologyAnalysisScope(outputDir, cwd, method, routePat
2961
2765
  const filePath = path.join(outputDir, buildBackendTopologyRelativePath(method, routePath));
2962
2766
  let content;
2963
2767
  try {
2964
- content = await fs$1.readFile(filePath, "utf8");
2768
+ content = await fs.readFile(filePath, "utf8");
2965
2769
  } catch (error) {
2966
2770
  if (isRecord(error) && error.code === "ENOENT") throw new Error(`Backend graph artifact not found for ${method} ${routePath}: ${filePath}. Run generate:graph first.`);
2967
2771
  throw error;
@@ -2982,7 +2786,7 @@ async function loadBackendTopologyAnalysisScope(outputDir, cwd, method, routePat
2982
2786
  const missingFiles = [];
2983
2787
  await Promise.all(analysisFiles.map(async (analysisFile) => {
2984
2788
  try {
2985
- await fs$1.access(analysisFile);
2789
+ await fs.access(analysisFile);
2986
2790
  } catch {
2987
2791
  missingFiles.push(path.relative(cwd, analysisFile));
2988
2792
  }
@@ -3082,7 +2886,7 @@ async function generateHandler(cwd, routeArgument, options) {
3082
2886
  process.exit(1);
3083
2887
  }
3084
2888
  }
3085
- var generate_default = (program) => void program.command("generate:catalogue").argument("[route]", "Route selector in the format \"GET /v1/products\"").option("-o, --output <directory>", "Directory where catalogue artifacts are generated", ".tmp/datasource-catalogue").option("--openapi-url <url>", "OpenAPI document URL").option("--profile", "Display elapsed time by generation stage").description("Analyze the codebase and generate one YAML review document per route").action((routeArgument, options) => {
2889
+ var generate_default = (program) => void program.command("generate:catalog").argument("[route]", "Route selector in the format \"GET /v1/products\"").option("-o, --output <directory>", "Directory where catalogue artifacts are generated", ".tmp/datasource-catalogue").option("--openapi-url <url>", "OpenAPI document URL").option("--profile", "Display elapsed time by generation stage").description("Analyze the codebase and generate one YAML review document per route").action((routeArgument, options) => {
3086
2890
  const config = getUserConfig();
3087
2891
  return generateHandler(config.cwd, routeArgument, {
3088
2892
  ...options,
@@ -3090,104 +2894,6 @@ var generate_default = (program) => void program.command("generate:catalogue").a
3090
2894
  });
3091
2895
  });
3092
2896
  //#endregion
3093
- //#region src/services/routeBackendTopologyService.ts
3094
- function stringProperty(object, name) {
3095
- const property = object.getProperty(name);
3096
- if (!property || !Node.isPropertyAssignment(property)) return void 0;
3097
- const initializer = property.getInitializer();
3098
- return initializer && (Node.isStringLiteral(initializer) || Node.isNoSubstitutionTemplateLiteral(initializer)) ? initializer.getLiteralValue() : void 0;
3099
- }
3100
- function handlerProperty(object) {
3101
- const property = object.getProperty("handler");
3102
- if (!property || !Node.isPropertyAssignment(property)) return void 0;
3103
- const initializer = property.getInitializer();
3104
- return initializer && (Node.isIdentifier(initializer) || Node.isPropertyAccessExpression(initializer)) ? initializer.getText() : void 0;
3105
- }
3106
- function extractRouteDeclarations(sourceFile) {
3107
- const initializer = sourceFile.getVariableDeclaration("routes")?.getInitializer();
3108
- let expression = initializer;
3109
- if (initializer && (Node.isAsExpression(initializer) || Node.isSatisfiesExpression(initializer))) expression = initializer.getExpression();
3110
- if (!expression || !Node.isArrayLiteralExpression(expression)) return [];
3111
- return expression.getElements().flatMap((element) => {
3112
- if (!Node.isObjectLiteralExpression(element)) return [];
3113
- const method = stringProperty(element, "method");
3114
- const routePath = stringProperty(element, "path");
3115
- const handlerRef = handlerProperty(element);
3116
- return method && routePath && handlerRef ? [{
3117
- method: method.toUpperCase(),
3118
- path: routePath,
3119
- sourceFile,
3120
- handlerRef
3121
- }] : [];
3122
- });
3123
- }
3124
- async function discoverBackendTopologyRouteSelectors(cwd) {
3125
- const project = new Project({
3126
- skipAddingFilesFromTsConfig: true,
3127
- compilerOptions: {
3128
- allowJs: true,
3129
- checkJs: false
3130
- }
3131
- });
3132
- return (await globby(["app/_api/**/routes.@(js|ts)", "app/legacy/**/routes.@(js|ts)"], {
3133
- cwd,
3134
- absolute: true
3135
- })).flatMap((routeFile) => extractRouteDeclarations(project.addSourceFileAtPath(routeFile))).map((route) => ({
3136
- method: route.method,
3137
- path: route.path
3138
- }));
3139
- }
3140
- async function generateBackendTopologyArtifactsInWorkers(params) {
3141
- const selectors = params.routeSelector ? [params.routeSelector] : await discoverBackendTopologyRouteSelectors(params.cwd);
3142
- params.onRoutesDiscovered?.(selectors.length);
3143
- const workerCount = Math.min(params.workers || 2, selectors.length);
3144
- const chunks = Array.from({ length: workerCount }, () => []);
3145
- selectors.forEach((selector, index) => chunks[index % workerCount].push(selector));
3146
- const artifacts = [];
3147
- let completed = 0;
3148
- let writeQueue = Promise.resolve();
3149
- const workers = [];
3150
- try {
3151
- await Promise.all(chunks.map((routeSelectors) => new Promise((resolve, reject) => {
3152
- const worker = new Worker(new URL("./routeBackendTopologyWorker.ts", import.meta.url), {
3153
- workerData: {
3154
- cwd: params.cwd,
3155
- routeSelectors
3156
- },
3157
- execArgv: process.execArgv
3158
- });
3159
- workers.push(worker);
3160
- worker.on("message", (message) => {
3161
- if (message.type === "error") {
3162
- reject(new Error(message.message));
3163
- return;
3164
- }
3165
- writeQueue = writeQueue.then(async () => {
3166
- if (message.type === "artifact") {
3167
- await params.onArtifact(message.artifact);
3168
- artifacts.push(message.artifact);
3169
- return;
3170
- }
3171
- completed += 1;
3172
- params.onRouteProgress?.({
3173
- current: completed,
3174
- total: selectors.length,
3175
- route: message.route,
3176
- stage: "completed"
3177
- });
3178
- }).catch(reject);
3179
- });
3180
- worker.once("error", reject);
3181
- worker.once("exit", (code) => code === 0 ? resolve() : reject(/* @__PURE__ */ new Error(`Topology worker exited with code ${code}`)));
3182
- })));
3183
- await writeQueue;
3184
- return artifacts;
3185
- } catch (error) {
3186
- await Promise.all(workers.map((worker) => worker.terminate().catch(() => void 0)));
3187
- throw error;
3188
- }
3189
- }
3190
- //#endregion
3191
2897
  //#region src/commands/generateGraph.ts
3192
2898
  function parseWorkerCount$1(value) {
3193
2899
  const workers = Number(value);
@@ -3442,7 +3148,7 @@ async function readSourceExcerpts(document) {
3442
3148
  const excerpts = [];
3443
3149
  let totalCharacters = 0;
3444
3150
  for (const location of uniqueLocations) try {
3445
- const lines = (await fs$1.readFile(location.sourceFile, "utf8")).split("\n");
3151
+ const lines = (await fs.readFile(location.sourceFile, "utf8")).split("\n");
3446
3152
  const startLine = Math.max(1, location.line - EXCERPT_RADIUS);
3447
3153
  const endLine = Math.min(lines.length, location.line + EXCERPT_RADIUS);
3448
3154
  const code = lines.slice(startLine - 1, endLine).map((line, index) => `${String(startLine + index).padStart(5)} | ${line}`).join("\n");
@@ -4169,7 +3875,6 @@ const configTemplate = `import { defineConfig } from "@cmflow/atlas";
4169
3875
 
4170
3876
  export default defineConfig({
4171
3877
  repoRoot: process.cwd(),
4172
- backends: [],
4173
3878
  openapiUrl: "https://api.example.com/openapi.json",
4174
3879
  openapiTimeoutMs: 60_000,
4175
3880
  directusUrl: "https://cms.api.clubmed",
@@ -4181,6 +3886,7 @@ export default defineConfig({
4181
3886
  workers: 2
4182
3887
  },
4183
3888
  analysis: {
3889
+ backends: [],
4184
3890
  excluded: [],
4185
3891
  transversalInputs: [],
4186
3892
  ignoredOutputs: [],
@@ -4208,18 +3914,18 @@ var init_default = (program, datasourceCommand) => void program.command("init").
4208
3914
  const configDirectory = path.dirname(configPath);
4209
3915
  intro("Initialize datasource configuration");
4210
3916
  try {
4211
- await fs$1.mkdir(configDirectory, { recursive: true });
3917
+ await fs.mkdir(configDirectory, { recursive: true });
4212
3918
  if (!options.force) {
4213
3919
  let exists = false;
4214
3920
  try {
4215
- await fs$1.access(configPath);
3921
+ await fs.access(configPath);
4216
3922
  exists = true;
4217
3923
  } catch (error) {
4218
3924
  if (error.code !== "ENOENT") throw error;
4219
3925
  }
4220
3926
  if (exists) throw new Error(`${configPath} already exists. Use --force to overwrite it.`);
4221
3927
  }
4222
- await fs$1.writeFile(configPath, configTemplate, "utf8");
3928
+ await fs.writeFile(configPath, configTemplate, "utf8");
4223
3929
  outro(`Created ${configPath}`);
4224
3930
  } catch (error) {
4225
3931
  cancel(error instanceof Error ? error.message : String(error));
@@ -4352,7 +4058,7 @@ function collectImpactedRoutes(params) {
4352
4058
  return params.graphs.filter((graph) => graph.analysisFiles.some((analysisFile) => changedFiles.has(normalizeFilePath(analysisFile)))).sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
4353
4059
  }
4354
4060
  async function readChangedFiles(filePath) {
4355
- return (await fs$1.readFile(filePath, "utf8")).split(/\r?\n/).map((line) => normalizeFilePath(line.trim())).filter(Boolean);
4061
+ return (await fs.readFile(filePath, "utf8")).split(/\r?\n/).map((line) => normalizeFilePath(line.trim())).filter(Boolean);
4356
4062
  }
4357
4063
  async function readRouteGraphs(outputDirectory) {
4358
4064
  const graphFiles = await globby("**/*.graph.yaml", {
@@ -4360,7 +4066,7 @@ async function readRouteGraphs(outputDirectory) {
4360
4066
  absolute: true
4361
4067
  });
4362
4068
  const graphs = await Promise.all(graphFiles.map(async (graphFile) => {
4363
- const document = parse(await fs$1.readFile(graphFile, "utf8"));
4069
+ const document = parse(await fs.readFile(graphFile, "utf8"));
4364
4070
  if (!isGraphDocument(document)) throw new Error(`Invalid graph document: ${graphFile}`);
4365
4071
  return {
4366
4072
  method: document.route.method,
@@ -4478,8 +4184,8 @@ async function reportChangedHandler(cwd, options) {
4478
4184
  routes: coverages
4479
4185
  });
4480
4186
  const reportPath = path.resolve(cwd, options.report);
4481
- await fs$1.mkdir(path.dirname(reportPath), { recursive: true });
4482
- await fs$1.writeFile(reportPath, `${report}\n`, "utf8");
4187
+ await fs.mkdir(path.dirname(reportPath), { recursive: true });
4188
+ await fs.writeFile(reportPath, `${report}\n`, "utf8");
4483
4189
  note(report, "Datasource mapping report");
4484
4190
  }
4485
4191
  var reportChanged_default = (program) => void program.command("report:changed").requiredOption("--changed-files <file>", "Newline-delimited list of changed repository files").option("-o, --output <directory>", "Directory containing generated graph artifacts", ".tmp/datasource-catalogue").option("--report <file>", "Markdown report output path", ".tmp/datasource-catalogue/changed-routes-report.md").description("Report deterministic mapping coverage for routes impacted by changed files").action(async (options) => {
@@ -4499,7 +4205,8 @@ const program = new Command();
4499
4205
  program.version("3.4.0-alpha.1");
4500
4206
  program.name("atlas").description("Manage the API-to-backend mapping catalogue").option("--project-root <path>", "Root directory of the API project to analyze").option("-c, --config <path>", "Path to atlas.config.ts", path.resolve(process.cwd(), "atlas.config.ts")).hook("preAction", async (_thisCommand, actionCommand) => {
4501
4207
  if (actionCommand.name() === "init") return;
4502
- setUserConfig(await loadAtlasConfig(program.opts().config, program.opts().projectRoot));
4208
+ const config = await loadAtlasConfig(program.opts().config, program.opts().projectRoot);
4209
+ setUserConfig(config);
4503
4210
  });
4504
4211
  init_default(program, program);
4505
4212
  generate_default(program);
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { r as defineExpressionRule, t as UserConfig } from "./types-DcvdOZ2j.mjs";
1
+ import { r as defineExpressionRule, t as UserConfig } from "./types-3y34Gf8R.mjs";
2
2
  //#region src/utils/defineConfig.d.ts
3
3
  declare function defineConfig(config: UserConfig): UserConfig;
4
4
  //#endregion