@eventcatalog/core 4.7.3 → 4.7.5

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 (48) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-SMDTRRED.js → chunk-2BYTIH4L.js} +1 -1
  6. package/dist/chunk-A2RZR3U4.js +29 -0
  7. package/dist/{chunk-XXJX3WWF.js → chunk-EUZT4RKY.js} +1 -1
  8. package/dist/chunk-GYYYAUJD.js +276 -0
  9. package/dist/chunk-JJPB6EOZ.js +159 -0
  10. package/dist/{chunk-GINIJSFI.js → chunk-QJRY6Q7S.js} +1 -1
  11. package/dist/{chunk-IALEXWSE.js → chunk-QQPH6RCB.js} +24 -1
  12. package/dist/chunk-SDZQJTJW.js +90 -0
  13. package/dist/chunk-WRNH5C3U.js +118 -0
  14. package/dist/{chunk-V4FAVGI7.js → chunk-ZBPMCEXS.js} +1 -1
  15. package/dist/constants.cjs +1 -1
  16. package/dist/constants.js +1 -1
  17. package/dist/eventcatalog.cjs +707 -255
  18. package/dist/eventcatalog.config.d.cts +12 -6
  19. package/dist/eventcatalog.config.d.ts +12 -6
  20. package/dist/eventcatalog.js +42 -42
  21. package/dist/federation/diagnostics.cjs +187 -0
  22. package/dist/federation/diagnostics.d.cts +23 -0
  23. package/dist/federation/diagnostics.d.ts +23 -0
  24. package/dist/federation/diagnostics.js +14 -0
  25. package/dist/federation/federate.cjs +605 -174
  26. package/dist/federation/federate.d.cts +8 -1
  27. package/dist/federation/federate.d.ts +8 -1
  28. package/dist/federation/federate.js +8 -2
  29. package/dist/federation/filesystem-source-provider.cjs +124 -0
  30. package/dist/federation/filesystem-source-provider.d.cts +7 -0
  31. package/dist/federation/filesystem-source-provider.d.ts +7 -0
  32. package/dist/federation/filesystem-source-provider.js +6 -0
  33. package/dist/federation/output-transaction.cjs +152 -0
  34. package/dist/federation/output-transaction.d.cts +3 -0
  35. package/dist/federation/output-transaction.d.ts +3 -0
  36. package/dist/federation/output-transaction.js +6 -0
  37. package/dist/federation/source-provider.cjs +265 -0
  38. package/dist/federation/source-provider.d.cts +11 -0
  39. package/dist/federation/source-provider.d.ts +11 -0
  40. package/dist/federation/source-provider.js +8 -0
  41. package/dist/generate.cjs +24 -1
  42. package/dist/generate.js +3 -3
  43. package/dist/utils/cli-logger.cjs +24 -1
  44. package/dist/utils/cli-logger.d.cts +6 -0
  45. package/dist/utils/cli-logger.d.ts +6 -0
  46. package/dist/utils/cli-logger.js +2 -2
  47. package/package.json +3 -3
  48. package/dist/chunk-ZR6AH5Z2.js +0 -204
@@ -31,13 +31,14 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var federate_exports = {};
32
32
  __export(federate_exports, {
33
33
  FederationConflictError: () => FederationConflictError,
34
+ FederationDiagnosticError: () => FederationDiagnosticError,
34
35
  federateCatalog: () => federateCatalog
35
36
  });
36
37
  module.exports = __toCommonJS(federate_exports);
37
- var import_node_crypto3 = require("crypto");
38
- var import_promises5 = __toESM(require("fs/promises"), 1);
39
- var import_node_path5 = __toESM(require("path"), 1);
40
- var import_sdk2 = __toESM(require("@eventcatalog/sdk"), 1);
38
+ var import_node_crypto4 = require("crypto");
39
+ var import_promises7 = __toESM(require("fs/promises"), 1);
40
+ var import_node_path7 = __toESM(require("path"), 1);
41
+ var import_sdk3 = __toESM(require("@eventcatalog/sdk"), 1);
41
42
 
42
43
  // src/eventcatalog-config-file-utils.js
43
44
  var import_promises = require("fs/promises");
@@ -149,125 +150,264 @@ var createFederationContentCache = (projectDirectory, options = {}) => {
149
150
  };
150
151
  };
151
152
 
152
- // src/federation/github-source-provider.ts
153
- var import_node_child_process = require("child_process");
154
- var import_promises3 = __toESM(require("fs/promises"), 1);
155
- var import_node_os2 = __toESM(require("os"), 1);
156
- var import_node_path3 = __toESM(require("path"), 1);
157
- var import_node_util = require("util");
158
- var import_sdk = __toESM(require("@eventcatalog/sdk"), 1);
159
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
160
- var parseGitHubSource = (source) => {
161
- const match = /^github:([^/]+)\/(.+)$/.exec(source.source);
162
- if (!match) throw new Error(`Unsupported federation source "${source.source}". Expected github:owner/repository.`);
163
- return { owner: match[1], repository: match[2] };
153
+ // src/federation/diagnostics.ts
154
+ var federationRuleDefaults = {
155
+ "federation/duplicate-source": "error",
156
+ "federation/type-collision": "error",
157
+ "federation/pointer-type-mismatch": "error",
158
+ "federation/facet-disagreement": "error",
159
+ "federation/asset-collision": "warn",
160
+ "federation/missing-resource": "warn",
161
+ "federation/unresolved-version": "warn"
164
162
  };
165
- var assertSafeCatalogPath = (source) => {
166
- const catalogPath = source.path ?? ".";
167
- const normalized = import_node_path3.default.posix.normalize(catalogPath.replaceAll("\\", "/"));
168
- if (catalogPath.includes("\\") || import_node_path3.default.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
169
- throw new Error(`Catalog path "${catalogPath}" escapes source "${source.id}"`);
163
+ var resolveFederationRules = (rules = {}) => {
164
+ for (const [rule, level] of Object.entries(rules)) {
165
+ if (!Object.hasOwn(federationRuleDefaults, rule)) throw new Error(`Unknown federation rule "${rule}".`);
166
+ if (level !== "off" && level !== "warn" && level !== "error") {
167
+ throw new Error(`Invalid level "${String(level)}" for federation rule "${rule}". Expected off, warn, or error.`);
168
+ }
170
169
  }
170
+ return { ...federationRuleDefaults, ...rules };
171
171
  };
172
- var encodePath = (value) => value.split("/").filter(Boolean).map(encodeURIComponent).join("/");
173
- var rawUrl = (source, ref, filePath) => {
174
- const { owner, repository } = parseGitHubSource(source);
175
- return `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/${encodeURIComponent(
176
- ref
177
- )}/${encodePath(filePath)}`;
172
+ var getExternalResource = (external) => `${external.id}${external.version === void 0 ? "" : `@${external.version}`}`;
173
+ var getVersionedResource = (id, version) => `${id}${version == null ? "" : `@${version}`}`;
174
+ var getDocumentedResourceType = (type) => {
175
+ const usesAn = type === "adr" || type === "agent" || type === "entity" || type === "event";
176
+ return `documented as ${usesAn ? "an" : "a"} ${type}`;
178
177
  };
179
- var contentsApiUrl = (source, ref, filePath) => {
180
- const { owner, repository } = parseGitHubSource(source);
181
- return `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/contents/${encodePath(
182
- filePath
183
- )}?ref=${encodeURIComponent(ref)}`;
184
- };
185
- var fetchBytes = async (source, ref, filePath, fetcher, token) => {
186
- const url = token ? contentsApiUrl(source, ref, filePath) : rawUrl(source, ref, filePath);
187
- const response = token ? await fetcher(url, {
188
- headers: {
189
- Accept: "application/vnd.github.raw+json",
190
- Authorization: `Bearer ${token}`,
191
- "X-GitHub-Api-Version": "2026-03-10"
178
+ var getConflictDiagnostic = (conflict, graph) => {
179
+ const catalogs = { label: "catalogs", value: conflict.sources.join(", ") };
180
+ switch (conflict.kind) {
181
+ case "duplicate-source":
182
+ return {
183
+ severity: "error",
184
+ message: "Resource has multiple owners",
185
+ rule: "federation/duplicate-source",
186
+ attributes: [
187
+ { label: "resource", value: conflict.id },
188
+ catalogs,
189
+ { label: "resolution", value: "assign a single owning catalog" }
190
+ ]
191
+ };
192
+ case "type-collision": {
193
+ const types = [
194
+ ...new Map(
195
+ graph.entities.filter((entity) => entity.id === conflict.id).map(
196
+ (entity) => [
197
+ `${entity.resolvedFrom.source}:${entity.type}`,
198
+ { label: entity.resolvedFrom.source, value: getDocumentedResourceType(entity.type) }
199
+ ]
200
+ )
201
+ ).values()
202
+ ];
203
+ return {
204
+ severity: "error",
205
+ message: "Resource ID has conflicting types",
206
+ rule: "federation/type-collision",
207
+ attributes: [{ label: "resource", value: conflict.id }, ...types.length > 0 ? types : [catalogs]]
208
+ };
192
209
  }
193
- }) : await fetcher(url);
194
- if (response.status === 404) return void 0;
195
- if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
196
- return Buffer.from(await response.arrayBuffer());
197
- };
198
- var getGitEnvironment = (token) => {
199
- if (!token) return process.env;
200
- const inheritedCount = Number.parseInt(process.env.GIT_CONFIG_COUNT ?? "0", 10);
201
- const configIndex = Number.isInteger(inheritedCount) && inheritedCount >= 0 ? inheritedCount : 0;
202
- return {
203
- ...process.env,
204
- GIT_CONFIG_COUNT: String(configIndex + 1),
205
- [`GIT_CONFIG_KEY_${configIndex}`]: "http.https://github.com/.extraheader",
206
- [`GIT_CONFIG_VALUE_${configIndex}`]: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`
207
- };
208
- };
209
- var createCheckout = (executeFile, token) => async (source, ref, callback) => {
210
- const { owner, repository } = parseGitHubSource(source);
211
- const catalogPath = import_node_path3.default.posix.normalize(source.path ?? ".");
212
- const directory = await import_promises3.default.mkdtemp(import_node_path3.default.join(import_node_os2.default.tmpdir(), "eventcatalog-federation-"));
213
- const env = getGitEnvironment(token);
214
- try {
215
- const git = (args) => executeFile("git", args, { cwd: directory, env, encoding: "utf8" });
216
- await git(["init", "--quiet"]);
217
- await git(["remote", "add", "origin", `https://github.com/${owner}/${repository}.git`]);
218
- if (catalogPath !== ".") {
219
- await git(["sparse-checkout", "init", "--cone"]);
220
- await git(["sparse-checkout", "set", catalogPath]);
210
+ case "pointer-type-mismatch": {
211
+ const typeMismatch = /^Expected (.+) but found (.+)$/.exec(conflict.detail ?? "");
212
+ return {
213
+ severity: "error",
214
+ message: "Reference type does not match resource",
215
+ rule: "federation/pointer-type-mismatch",
216
+ attributes: [
217
+ { label: "resource", value: conflict.id },
218
+ ...typeMismatch ? [
219
+ { label: "expected type", value: typeMismatch[1] },
220
+ { label: "actual type", value: typeMismatch[2] }
221
+ ] : conflict.detail ? [{ label: "detail", value: conflict.detail }] : [],
222
+ catalogs
223
+ ]
224
+ };
221
225
  }
222
- await git(["fetch", "--quiet", "--depth", "1", "origin", ref]);
223
- await git(["checkout", "--quiet", "--detach", "FETCH_HEAD"]);
224
- return await callback(directory);
225
- } finally {
226
- await import_promises3.default.rm(directory, { recursive: true, force: true });
226
+ case "facet-disagreement":
227
+ return {
228
+ severity: "error",
229
+ message: "Catalogs disagree about this resource",
230
+ rule: "federation/facet-disagreement",
231
+ attributes: [
232
+ { label: "resource", value: conflict.id },
233
+ ...conflict.detail ? [{ label: "detail", value: conflict.detail }] : [],
234
+ catalogs
235
+ ]
236
+ };
227
237
  }
228
238
  };
229
- var generateIndex = async (source, ref, checkout, executeFile) => checkout(source, ref, async (directory) => {
230
- const { stdout } = await executeFile("git", ["rev-parse", "HEAD"], { cwd: directory, encoding: "utf8" });
231
- const commit = stdout.trim();
232
- const catalogDirectory = import_node_path3.default.resolve(directory, source.path ?? ".");
233
- const relativeCatalogDirectory = import_node_path3.default.relative(directory, catalogDirectory);
234
- if (relativeCatalogDirectory.startsWith("..") || import_node_path3.default.isAbsolute(relativeCatalogDirectory)) {
235
- throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
239
+ var getFederationDiagnostics = (graph, rules = {}) => {
240
+ const resolvedRules = resolveFederationRules(rules);
241
+ const diagnostics = graph.conflicts.map((conflict) => getConflictDiagnostic(conflict, graph));
242
+ for (const external of graph.externals) {
243
+ const resource = getExternalResource(external);
244
+ for (const referencedBy of external.referencedBy) {
245
+ const referrer = graph.entities.find((entity) => entity.id === referencedBy);
246
+ diagnostics.push({
247
+ severity: "warning",
248
+ message: "Referenced EventCatalog resource does not exist",
249
+ rule: "federation/missing-resource",
250
+ attributes: [
251
+ { label: "source catalog", value: referrer?.resolvedFrom.source ?? "unknown" },
252
+ { label: "referenced by", value: referencedBy },
253
+ { label: "missing resource", value: resource }
254
+ ]
255
+ });
256
+ }
236
257
  }
237
- const index = await (0, import_sdk.default)(catalogDirectory).buildIndex({ source: source.id, commit });
238
- return { bytes: Buffer.from(JSON.stringify(index)), index, commit, generated: true };
239
- });
240
- var fetchPublishedIndex = async (source, ref, fetcher, token) => {
241
- const indexPath = import_node_path3.default.posix.join(source.path ?? ".", "catalog.index.json");
242
- const bytes = await fetchBytes(source, ref, indexPath, fetcher, token);
243
- if (!bytes) return void 0;
244
- const index = (0, import_sdk.parseIndex)(JSON.parse(bytes.toString("utf8")));
245
- if (index.source !== source.id) {
246
- throw new Error(`Published index source "${index.source}" does not match configured id "${source.id}"`);
258
+ for (const edge of graph.edges.filter((edge2) => edge2.status === "unresolved")) {
259
+ const availableVersions = [
260
+ ...new Set(graph.entities.filter((entity) => entity.id === edge.to).map((entity) => entity.version ?? "unversioned"))
261
+ ].sort((left, right) => left.localeCompare(right, void 0, { numeric: true }));
262
+ diagnostics.push({
263
+ severity: "warning",
264
+ message: "Referenced EventCatalog resource version does not exist",
265
+ rule: "federation/unresolved-version",
266
+ attributes: [
267
+ { label: "source catalog", value: edge.fromResolvedFrom.source },
268
+ { label: "referenced by", value: getVersionedResource(edge.from, edge.fromVersion) },
269
+ { label: "resource", value: edge.to },
270
+ { label: "requested version", value: edge.pointer ?? "unspecified" },
271
+ { label: "available versions", value: availableVersions.join(", ") }
272
+ ]
273
+ });
247
274
  }
248
- return { bytes, index, commit: index.commit, generated: false };
275
+ for (const warning of graph.warnings) {
276
+ diagnostics.push({
277
+ severity: "warning",
278
+ message: "Asset collision",
279
+ rule: "federation/asset-collision",
280
+ attributes: [
281
+ { label: "asset", value: warning.path },
282
+ { label: "sources", value: warning.sources.join(", ") },
283
+ { label: "winner", value: warning.winner },
284
+ { label: "resolution", value: "last configured source wins" }
285
+ ]
286
+ });
287
+ }
288
+ return diagnostics.flatMap((diagnostic) => {
289
+ const level = resolvedRules[diagnostic.rule];
290
+ if (level === "off") return [];
291
+ return [{ ...diagnostic, severity: level === "error" ? "error" : "warning" }];
292
+ }).sort(
293
+ (left, right) => (left.severity === right.severity ? 0 : left.severity === "error" ? -1 : 1) || left.rule.localeCompare(right.rule) || (left.attributes[0]?.value ?? "").localeCompare(right.attributes[0]?.value ?? "")
294
+ );
249
295
  };
250
- var createGitHubSourceProvider = (options = {}) => {
251
- const fetcher = options.fetch ?? ((url, init) => fetch(url, init));
252
- const executeFile = options.execFile ?? ((file, args, execOptions) => execFileAsync(file, args, execOptions));
253
- const configuredToken = options.token ?? process.env.EVENTCATALOG_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN;
254
- const token = configuredToken?.trim() || void 0;
255
- const checkout = options.checkout ?? createCheckout(executeFile, token);
296
+
297
+ // src/federation/output-transaction.ts
298
+ var import_promises3 = __toESM(require("fs/promises"), 1);
299
+ var import_node_path3 = __toESM(require("path"), 1);
300
+ var getSafePublicPath = (publicDirectory, relativePath) => {
301
+ const normalizedPath = import_node_path3.default.posix.normalize(relativePath);
302
+ const unsafe = relativePath.length === 0 || relativePath.includes("\0") || relativePath.includes("\\") || import_node_path3.default.posix.isAbsolute(relativePath) || /^[a-zA-Z]:\//.test(relativePath) || normalizedPath !== relativePath || normalizedPath === ".." || normalizedPath.startsWith("../");
303
+ if (unsafe) return void 0;
304
+ const destinationPath = import_node_path3.default.resolve(publicDirectory, relativePath);
305
+ const relativeDestinationPath = import_node_path3.default.relative(publicDirectory, destinationPath);
306
+ return relativeDestinationPath !== "" && relativeDestinationPath !== ".." && !relativeDestinationPath.startsWith(`..${import_node_path3.default.sep}`) && !import_node_path3.default.isAbsolute(relativeDestinationPath) ? destinationPath : void 0;
307
+ };
308
+ var beginFederationOutputTransaction = async (projectDirectory, outDir, relativePublicPaths) => {
309
+ const transactionDirectory = await import_promises3.default.mkdtemp(import_node_path3.default.join(projectDirectory, ".eventcatalog-federation-transaction-"));
310
+ const previousOutDir = import_node_path3.default.join(transactionDirectory, "federated");
311
+ const publicBackupDirectory = import_node_path3.default.join(transactionDirectory, "public");
312
+ const publicDirectory = import_node_path3.default.resolve(projectDirectory, "public");
313
+ const publicSnapshots = [];
314
+ const missingPublicDirectories = /* @__PURE__ */ new Set();
315
+ let hadPreviousOutDir = false;
316
+ try {
317
+ for (const relativePath of new Set(relativePublicPaths)) {
318
+ const destinationPath = getSafePublicPath(publicDirectory, relativePath);
319
+ if (!destinationPath) continue;
320
+ try {
321
+ const stat = await import_promises3.default.lstat(destinationPath);
322
+ if (!stat.isFile()) continue;
323
+ const backupPath = import_node_path3.default.join(publicBackupDirectory, `${publicSnapshots.length}`);
324
+ await import_promises3.default.mkdir(import_node_path3.default.dirname(backupPath), { recursive: true });
325
+ await import_promises3.default.copyFile(destinationPath, backupPath);
326
+ publicSnapshots.push({ backupPath, destinationPath, state: "file" });
327
+ } catch (error) {
328
+ const code = error.code;
329
+ if (code === "ENOTDIR") continue;
330
+ if (code !== "ENOENT") throw error;
331
+ publicSnapshots.push({ destinationPath, state: "missing" });
332
+ let directory = import_node_path3.default.dirname(destinationPath);
333
+ while (directory !== publicDirectory) {
334
+ try {
335
+ await import_promises3.default.lstat(directory);
336
+ break;
337
+ } catch (directoryError) {
338
+ const directoryCode = directoryError.code;
339
+ if (directoryCode === "ENOTDIR") break;
340
+ if (directoryCode !== "ENOENT") throw directoryError;
341
+ missingPublicDirectories.add(directory);
342
+ directory = import_node_path3.default.dirname(directory);
343
+ }
344
+ }
345
+ }
346
+ }
347
+ try {
348
+ await import_promises3.default.rename(outDir, previousOutDir);
349
+ hadPreviousOutDir = true;
350
+ } catch (error) {
351
+ if (error.code !== "ENOENT") throw error;
352
+ }
353
+ } catch (error) {
354
+ await import_promises3.default.rm(transactionDirectory, { recursive: true, force: true });
355
+ throw error;
356
+ }
256
357
  return {
257
- async resolve(source) {
258
- assertSafeCatalogPath(source);
259
- const ref = source.ref ?? "main";
260
- return await fetchPublishedIndex(source, ref, fetcher, token) ?? generateIndex(source, ref, checkout, executeFile);
358
+ commit: async () => {
359
+ await import_promises3.default.rm(transactionDirectory, { recursive: true, force: true }).catch(() => void 0);
261
360
  },
262
- async fetchContent({ source, commit, path: artifactPath }) {
263
- assertSafeCatalogPath(source);
264
- const catalogPath = import_node_path3.default.posix.join(source.path ?? ".", artifactPath);
265
- const content = await fetchBytes(source, commit, catalogPath, fetcher, token);
266
- if (!content) throw new Error(`Federated artifact not found for "${source.id}": ${artifactPath}`);
267
- return content;
361
+ rollback: async () => {
362
+ const rollbackErrors = [];
363
+ const attempt = async (operation) => {
364
+ try {
365
+ await operation();
366
+ } catch (error) {
367
+ rollbackErrors.push(error);
368
+ }
369
+ };
370
+ await attempt(() => import_promises3.default.rm(outDir, { recursive: true, force: true }));
371
+ if (hadPreviousOutDir) await attempt(() => import_promises3.default.rename(previousOutDir, outDir));
372
+ for (const snapshot of publicSnapshots) {
373
+ if (snapshot.state === "missing") {
374
+ await attempt(() => import_promises3.default.rm(snapshot.destinationPath, { force: true }));
375
+ continue;
376
+ }
377
+ await attempt(async () => {
378
+ await import_promises3.default.mkdir(import_node_path3.default.dirname(snapshot.destinationPath), { recursive: true });
379
+ await import_promises3.default.copyFile(snapshot.backupPath, snapshot.destinationPath);
380
+ });
381
+ }
382
+ for (const directory of [...missingPublicDirectories].sort((left, right) => right.length - left.length)) {
383
+ await attempt(async () => {
384
+ try {
385
+ await import_promises3.default.rmdir(directory);
386
+ } catch (error) {
387
+ if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) throw error;
388
+ }
389
+ });
390
+ }
391
+ await attempt(() => import_promises3.default.rm(transactionDirectory, { recursive: true, force: true }));
392
+ if (rollbackErrors.length > 0) throw new AggregateError(rollbackErrors, "Failed to restore the previous federation output");
268
393
  }
269
394
  };
270
395
  };
396
+ var withFederationOutputTransaction = async (projectDirectory, outDir, relativePublicPaths, update) => {
397
+ const transaction = await beginFederationOutputTransaction(projectDirectory, outDir, relativePublicPaths);
398
+ try {
399
+ const result = await update();
400
+ await transaction.commit();
401
+ return result;
402
+ } catch (error) {
403
+ try {
404
+ await transaction.rollback();
405
+ } catch (rollbackError) {
406
+ throw new AggregateError([error, rollbackError], "Federation failed and the previous output could not be restored");
407
+ }
408
+ throw error;
409
+ }
410
+ };
271
411
 
272
412
  // src/federation/public-assets.ts
273
413
  var import_node_crypto2 = require("crypto");
@@ -399,6 +539,232 @@ var composePublicAssets = async ({
399
539
  return { files, copied, skipped, overwritten, removed };
400
540
  };
401
541
 
542
+ // src/federation/filesystem-source-provider.ts
543
+ var import_node_crypto3 = require("crypto");
544
+ var import_promises5 = __toESM(require("fs/promises"), 1);
545
+ var import_node_path5 = __toESM(require("path"), 1);
546
+ var import_sdk = __toESM(require("@eventcatalog/sdk"), 1);
547
+ var FILESYSTEM_SOURCE_PREFIX = "file:";
548
+ var isWithinDirectory = (directory, target) => {
549
+ const relativePath = import_node_path5.default.relative(directory, target);
550
+ return relativePath === "" || !relativePath.startsWith(`..${import_node_path5.default.sep}`) && relativePath !== ".." && !import_node_path5.default.isAbsolute(relativePath);
551
+ };
552
+ var assertPortableRelativePath = (filePath, label, allowRoot = true) => {
553
+ const portablePath = filePath.replaceAll("\\", "/");
554
+ const normalizedPath = import_node_path5.default.posix.normalize(portablePath);
555
+ const isUnsafe = filePath.includes("\\") || filePath.includes("\0") || import_node_path5.default.posix.isAbsolute(normalizedPath) || /^[a-zA-Z]:\//.test(normalizedPath) || normalizedPath === ".." || normalizedPath.startsWith("../") || !allowRoot && (normalizedPath === "." || normalizedPath === "");
556
+ if (isUnsafe) throw new Error(`${label} "${filePath}" escapes its filesystem source`);
557
+ return normalizedPath;
558
+ };
559
+ var getSourceRoot = (projectDirectory, source) => {
560
+ if (!source.source.startsWith(FILESYSTEM_SOURCE_PREFIX)) {
561
+ throw new Error(`Unsupported federation source "${source.source}". Expected file:path/to/catalog.`);
562
+ }
563
+ const locator = source.source.slice(FILESYSTEM_SOURCE_PREFIX.length);
564
+ if (!locator.trim()) throw new Error(`Filesystem federation source "${source.id}" requires a path after "file:".`);
565
+ return import_node_path5.default.resolve(projectDirectory, locator);
566
+ };
567
+ var getCatalogDirectory = async (projectDirectory, source) => {
568
+ if (source.ref) throw new Error(`Filesystem federation source "${source.id}" does not support "ref".`);
569
+ const sourceRoot = getSourceRoot(projectDirectory, source);
570
+ const catalogPath = assertPortableRelativePath(source.path ?? ".", "Catalog path");
571
+ const catalogDirectory = import_node_path5.default.resolve(sourceRoot, ...catalogPath.split("/"));
572
+ if (!isWithinDirectory(sourceRoot, catalogDirectory)) {
573
+ throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
574
+ }
575
+ try {
576
+ const [realSourceRoot, realCatalogDirectory] = await Promise.all([import_promises5.default.realpath(sourceRoot), import_promises5.default.realpath(catalogDirectory)]);
577
+ if (!isWithinDirectory(realSourceRoot, realCatalogDirectory)) {
578
+ throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
579
+ }
580
+ if (!(await import_promises5.default.stat(realCatalogDirectory)).isDirectory()) {
581
+ throw new Error(`Filesystem federation source "${source.id}" is not a directory: ${catalogDirectory}`);
582
+ }
583
+ return realCatalogDirectory;
584
+ } catch (error) {
585
+ if (error.code === "ENOENT") {
586
+ throw new Error(`Filesystem federation source "${source.id}" does not exist: ${catalogDirectory}`, { cause: error });
587
+ }
588
+ throw error;
589
+ }
590
+ };
591
+ var getArtifactPath = async (catalogDirectory, source, artifactPath) => {
592
+ const normalizedPath = assertPortableRelativePath(artifactPath, "Federated artifact path", false);
593
+ const filePath = import_node_path5.default.resolve(catalogDirectory, ...normalizedPath.split("/"));
594
+ if (!isWithinDirectory(catalogDirectory, filePath)) {
595
+ throw new Error(`Federated artifact path "${artifactPath}" escapes source "${source.id}"`);
596
+ }
597
+ try {
598
+ const realFilePath = await import_promises5.default.realpath(filePath);
599
+ if (!isWithinDirectory(catalogDirectory, realFilePath)) {
600
+ throw new Error(`Federated artifact path "${artifactPath}" escapes source "${source.id}"`);
601
+ }
602
+ return realFilePath;
603
+ } catch (error) {
604
+ if (error.code === "ENOENT") {
605
+ throw new Error(`Federated artifact not found for "${source.id}": ${artifactPath}`, { cause: error });
606
+ }
607
+ throw error;
608
+ }
609
+ };
610
+ var createFileSystemSourceProvider = (projectDirectory) => ({
611
+ async resolve(source) {
612
+ const catalogDirectory = await getCatalogDirectory(projectDirectory, source);
613
+ const localIndex = await (0, import_sdk.default)(catalogDirectory).buildIndex({
614
+ source: source.id,
615
+ commit: "local",
616
+ includeFederated: false
617
+ });
618
+ const snapshot = (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(localIndex)).digest("hex").slice(0, 12);
619
+ const index = { ...localIndex, commit: `local:${snapshot}` };
620
+ const bytes = Buffer.from(JSON.stringify(index));
621
+ return { bytes, index, commit: index.commit, generated: true };
622
+ },
623
+ async fetchContent({ source, path: artifactPath }) {
624
+ const catalogDirectory = await getCatalogDirectory(projectDirectory, source);
625
+ return import_promises5.default.readFile(await getArtifactPath(catalogDirectory, source, artifactPath));
626
+ }
627
+ });
628
+
629
+ // src/federation/github-source-provider.ts
630
+ var import_node_child_process = require("child_process");
631
+ var import_promises6 = __toESM(require("fs/promises"), 1);
632
+ var import_node_os2 = __toESM(require("os"), 1);
633
+ var import_node_path6 = __toESM(require("path"), 1);
634
+ var import_node_util = require("util");
635
+ var import_sdk2 = __toESM(require("@eventcatalog/sdk"), 1);
636
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
637
+ var parseGitHubSource = (source) => {
638
+ const match = /^github:([^/]+)\/(.+)$/.exec(source.source);
639
+ if (!match) throw new Error(`Unsupported federation source "${source.source}". Expected github:owner/repository.`);
640
+ return { owner: match[1], repository: match[2] };
641
+ };
642
+ var assertSafeCatalogPath = (source) => {
643
+ const catalogPath = source.path ?? ".";
644
+ const normalized = import_node_path6.default.posix.normalize(catalogPath.replaceAll("\\", "/"));
645
+ if (catalogPath.includes("\\") || import_node_path6.default.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
646
+ throw new Error(`Catalog path "${catalogPath}" escapes source "${source.id}"`);
647
+ }
648
+ };
649
+ var encodePath = (value) => value.split("/").filter(Boolean).map(encodeURIComponent).join("/");
650
+ var rawUrl = (source, ref, filePath) => {
651
+ const { owner, repository } = parseGitHubSource(source);
652
+ return `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/${encodeURIComponent(
653
+ ref
654
+ )}/${encodePath(filePath)}`;
655
+ };
656
+ var contentsApiUrl = (source, ref, filePath) => {
657
+ const { owner, repository } = parseGitHubSource(source);
658
+ return `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/contents/${encodePath(
659
+ filePath
660
+ )}?ref=${encodeURIComponent(ref)}`;
661
+ };
662
+ var fetchBytes = async (source, ref, filePath, fetcher, token) => {
663
+ const url = token ? contentsApiUrl(source, ref, filePath) : rawUrl(source, ref, filePath);
664
+ const response = token ? await fetcher(url, {
665
+ headers: {
666
+ Accept: "application/vnd.github.raw+json",
667
+ Authorization: `Bearer ${token}`,
668
+ "X-GitHub-Api-Version": "2026-03-10"
669
+ }
670
+ }) : await fetcher(url);
671
+ if (response.status === 404) return void 0;
672
+ if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
673
+ return Buffer.from(await response.arrayBuffer());
674
+ };
675
+ var getGitEnvironment = (token) => {
676
+ if (!token) return process.env;
677
+ const inheritedCount = Number.parseInt(process.env.GIT_CONFIG_COUNT ?? "0", 10);
678
+ const configIndex = Number.isInteger(inheritedCount) && inheritedCount >= 0 ? inheritedCount : 0;
679
+ return {
680
+ ...process.env,
681
+ GIT_CONFIG_COUNT: String(configIndex + 1),
682
+ [`GIT_CONFIG_KEY_${configIndex}`]: "http.https://github.com/.extraheader",
683
+ [`GIT_CONFIG_VALUE_${configIndex}`]: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`
684
+ };
685
+ };
686
+ var createCheckout = (executeFile, token) => async (source, ref, callback) => {
687
+ const { owner, repository } = parseGitHubSource(source);
688
+ const catalogPath = import_node_path6.default.posix.normalize(source.path ?? ".");
689
+ const directory = await import_promises6.default.mkdtemp(import_node_path6.default.join(import_node_os2.default.tmpdir(), "eventcatalog-federation-"));
690
+ const env = getGitEnvironment(token);
691
+ try {
692
+ const git = (args) => executeFile("git", args, { cwd: directory, env, encoding: "utf8" });
693
+ await git(["init", "--quiet"]);
694
+ await git(["remote", "add", "origin", `https://github.com/${owner}/${repository}.git`]);
695
+ if (catalogPath !== ".") {
696
+ await git(["sparse-checkout", "init", "--cone"]);
697
+ await git(["sparse-checkout", "set", catalogPath]);
698
+ }
699
+ await git(["fetch", "--quiet", "--depth", "1", "origin", ref]);
700
+ await git(["checkout", "--quiet", "--detach", "FETCH_HEAD"]);
701
+ return await callback(directory);
702
+ } finally {
703
+ await import_promises6.default.rm(directory, { recursive: true, force: true });
704
+ }
705
+ };
706
+ var generateIndex = async (source, ref, checkout, executeFile) => checkout(source, ref, async (directory) => {
707
+ const { stdout } = await executeFile("git", ["rev-parse", "HEAD"], { cwd: directory, encoding: "utf8" });
708
+ const commit = stdout.trim();
709
+ const catalogDirectory = import_node_path6.default.resolve(directory, source.path ?? ".");
710
+ const relativeCatalogDirectory = import_node_path6.default.relative(directory, catalogDirectory);
711
+ if (relativeCatalogDirectory.startsWith("..") || import_node_path6.default.isAbsolute(relativeCatalogDirectory)) {
712
+ throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
713
+ }
714
+ const index = await (0, import_sdk2.default)(catalogDirectory).buildIndex({ source: source.id, commit });
715
+ return { bytes: Buffer.from(JSON.stringify(index)), index, commit, generated: true };
716
+ });
717
+ var fetchPublishedIndex = async (source, ref, fetcher, token) => {
718
+ const indexPath = import_node_path6.default.posix.join(source.path ?? ".", "catalog.index.json");
719
+ const bytes = await fetchBytes(source, ref, indexPath, fetcher, token);
720
+ if (!bytes) return void 0;
721
+ const index = (0, import_sdk2.parseIndex)(JSON.parse(bytes.toString("utf8")));
722
+ if (index.source !== source.id) {
723
+ throw new Error(`Published index source "${index.source}" does not match configured id "${source.id}"`);
724
+ }
725
+ return { bytes, index, commit: index.commit, generated: false };
726
+ };
727
+ var createGitHubSourceProvider = (options = {}) => {
728
+ const fetcher = options.fetch ?? ((url, init) => fetch(url, init));
729
+ const executeFile = options.execFile ?? ((file, args, execOptions) => execFileAsync(file, args, execOptions));
730
+ const configuredToken = options.token ?? process.env.EVENTCATALOG_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN;
731
+ const token = configuredToken?.trim() || void 0;
732
+ const checkout = options.checkout ?? createCheckout(executeFile, token);
733
+ return {
734
+ async resolve(source) {
735
+ assertSafeCatalogPath(source);
736
+ const ref = source.ref ?? "main";
737
+ return await fetchPublishedIndex(source, ref, fetcher, token) ?? generateIndex(source, ref, checkout, executeFile);
738
+ },
739
+ async fetchContent({ source, commit, path: artifactPath }) {
740
+ assertSafeCatalogPath(source);
741
+ const catalogPath = import_node_path6.default.posix.join(source.path ?? ".", artifactPath);
742
+ const content = await fetchBytes(source, commit, catalogPath, fetcher, token);
743
+ if (!content) throw new Error(`Federated artifact not found for "${source.id}": ${artifactPath}`);
744
+ return content;
745
+ }
746
+ };
747
+ };
748
+
749
+ // src/federation/source-provider.ts
750
+ var createFederationSourceProvider = (projectDirectory, providers = {}) => {
751
+ const github = providers.github ?? createGitHubSourceProvider();
752
+ const filesystem = providers.filesystem ?? createFileSystemSourceProvider(projectDirectory);
753
+ const getProvider = (source) => {
754
+ if (source.source.startsWith("github:")) return github;
755
+ if (source.source.startsWith("file:")) return filesystem;
756
+ throw new Error(`Unsupported federation source "${source.source}" for "${source.id}". Supported protocols: github:, file:.`);
757
+ };
758
+ return {
759
+ async resolve(source) {
760
+ return getProvider(source).resolve(source);
761
+ },
762
+ async fetchContent(request) {
763
+ return getProvider(request.source).fetchContent(request);
764
+ }
765
+ };
766
+ };
767
+
402
768
  // src/federation/federate.ts
403
769
  var FederationConflictError = class extends Error {
404
770
  conflicts;
@@ -410,6 +776,16 @@ ${details}`);
410
776
  this.conflicts = conflicts;
411
777
  }
412
778
  };
779
+ var FederationDiagnosticError = class extends Error {
780
+ diagnostics;
781
+ constructor(diagnostics) {
782
+ const details = diagnostics.map((diagnostic) => `${diagnostic.rule}: ${diagnostic.message}`).join("\n");
783
+ super(`Federation diagnostics prevent hydration:
784
+ ${details}`);
785
+ this.name = "FederationDiagnosticError";
786
+ this.diagnostics = diagnostics;
787
+ }
788
+ };
413
789
  var validateSources = (sources) => {
414
790
  const ids = /* @__PURE__ */ new Set();
415
791
  for (const source of sources) {
@@ -418,19 +794,46 @@ var validateSources = (sources) => {
418
794
  ids.add(source.id);
419
795
  }
420
796
  };
797
+ var createDiagnosticGraph = (graph, ownershipGraph, remoteSourceIds, localSourceId) => {
798
+ const edges = ownershipGraph.edges.filter((edge) => remoteSourceIds.has(edge.fromResolvedFrom.source));
799
+ const externalsByPointer = /* @__PURE__ */ new Map();
800
+ for (const edge of edges.filter((edge2) => edge2.status === "external")) {
801
+ const version = edge.pointer ?? void 0;
802
+ const key = `${edge.to}@${version ?? ""}`;
803
+ const external = externalsByPointer.get(key) ?? {
804
+ id: edge.to,
805
+ ...version === void 0 ? {} : { version },
806
+ referencedBy: []
807
+ };
808
+ if (!external.referencedBy.includes(edge.from)) external.referencedBy.push(edge.from);
809
+ externalsByPointer.set(key, external);
810
+ }
811
+ return {
812
+ ...graph,
813
+ entities: [
814
+ ...graph.entities,
815
+ ...ownershipGraph.entities.filter(
816
+ (entity) => entity.resolvedFrom.source === localSourceId && entity.resolvedFrom.commit === "local"
817
+ )
818
+ ],
819
+ edges,
820
+ conflicts: ownershipGraph.conflicts,
821
+ externals: [...externalsByPointer.values()]
822
+ };
823
+ };
421
824
  var writeLock = async (lockPath, lock) => {
422
825
  const temporaryPath = `${lockPath}.tmp-${process.pid}`;
423
826
  try {
424
- await import_promises5.default.writeFile(temporaryPath, `${JSON.stringify(lock, null, 2)}
827
+ await import_promises7.default.writeFile(temporaryPath, `${JSON.stringify(lock, null, 2)}
425
828
  `, "utf8");
426
- await import_promises5.default.rename(temporaryPath, lockPath);
829
+ await import_promises7.default.rename(temporaryPath, lockPath);
427
830
  } finally {
428
- await import_promises5.default.rm(temporaryPath, { force: true });
831
+ await import_promises7.default.rm(temporaryPath, { force: true });
429
832
  }
430
833
  };
431
834
  var readLock = async (lockPath) => {
432
835
  try {
433
- return JSON.parse(await import_promises5.default.readFile(lockPath, "utf8"));
836
+ return JSON.parse(await import_promises7.default.readFile(lockPath, "utf8"));
434
837
  } catch (error) {
435
838
  if (error.code === "ENOENT") return void 0;
436
839
  throw new Error(`Cannot read federation lock at "${lockPath}"`, { cause: error });
@@ -438,7 +841,7 @@ var readLock = async (lockPath) => {
438
841
  };
439
842
  var pathExists2 = async (filePath) => {
440
843
  try {
441
- await import_promises5.default.access(filePath);
844
+ await import_promises7.default.access(filePath);
442
845
  return true;
443
846
  } catch (error) {
444
847
  if (error.code === "ENOENT") return false;
@@ -446,20 +849,27 @@ var pathExists2 = async (filePath) => {
446
849
  }
447
850
  };
448
851
  var cleanupPreviousFederation = async (projectDirectory, onProgress) => {
449
- const outDir = import_node_path5.default.join(projectDirectory, "federated");
450
- const lockPath = import_node_path5.default.join(projectDirectory, "eventcatalog.lock");
852
+ const outDir = import_node_path7.default.join(projectDirectory, "federated");
853
+ const lockPath = import_node_path7.default.join(projectDirectory, "eventcatalog.lock");
451
854
  const previousLock = await readLock(lockPath);
452
855
  const hadFederatedOutput = await pathExists2(outDir);
453
856
  const hadLock = previousLock !== void 0;
454
857
  if (!hadFederatedOutput && !hadLock) return;
455
- await import_promises5.default.rm(outDir, { recursive: true, force: true });
456
- const publicResult = await composePublicAssets({
858
+ const publicResult = await withFederationOutputTransaction(
457
859
  projectDirectory,
458
- federatedDirectory: outDir,
459
- assets: [],
460
- previousFiles: previousLock?.publicFiles
461
- });
462
- await import_promises5.default.rm(lockPath, { force: true });
860
+ outDir,
861
+ Object.keys(previousLock?.publicFiles ?? {}),
862
+ async () => {
863
+ const result = await composePublicAssets({
864
+ projectDirectory,
865
+ federatedDirectory: outDir,
866
+ assets: [],
867
+ previousFiles: previousLock?.publicFiles
868
+ });
869
+ await import_promises7.default.rm(lockPath, { force: true });
870
+ return result;
871
+ }
872
+ );
463
873
  onProgress?.({
464
874
  type: "cleanup:complete",
465
875
  federated: hadFederatedOutput,
@@ -481,8 +891,9 @@ var federateCatalog = async (projectDirectory, options = {}) => {
481
891
  );
482
892
  }
483
893
  validateSources(sources);
894
+ const rules = resolveFederationRules(config.federation?.rules);
484
895
  if (options.useCache === false) options.onProgress?.({ type: "cache:disabled" });
485
- const provider = options.provider ?? createGitHubSourceProvider();
896
+ const provider = options.provider ?? createFederationSourceProvider(projectDirectory);
486
897
  const resolvedSources = [];
487
898
  for (const [index, source] of sources.entries()) {
488
899
  const current = index + 1;
@@ -504,13 +915,13 @@ var federateCatalog = async (projectDirectory, options = {}) => {
504
915
  throw new Error(`Failed to federate source "${source.id}": ${message}`, { cause: error });
505
916
  }
506
917
  }
507
- const outDir = import_node_path5.default.join(projectDirectory, "federated");
508
- const lockPath = import_node_path5.default.join(projectDirectory, "eventcatalog.lock");
918
+ const outDir = import_node_path7.default.join(projectDirectory, "federated");
919
+ const lockPath = import_node_path7.default.join(projectDirectory, "eventcatalog.lock");
509
920
  const previousLock = await readLock(lockPath);
510
921
  const resources = resolvedSources.reduce((total, source) => total + source.resolved.index.resources.length, 0);
511
922
  const remoteIndexes = resolvedSources.map(({ resolved }) => resolved.index);
512
923
  options.onProgress?.({ type: "local:start" });
513
- const localIndex = await (0, import_sdk2.default)(projectDirectory).buildIndex({
924
+ const localIndex = await (0, import_sdk3.default)(projectDirectory).buildIndex({
514
925
  source: config.cId,
515
926
  commit: "local",
516
927
  hashContent: false,
@@ -518,61 +929,80 @@ var federateCatalog = async (projectDirectory, options = {}) => {
518
929
  });
519
930
  options.onProgress?.({ type: "local:complete", resources: localIndex.resources.length });
520
931
  options.onProgress?.({ type: "resolving", resources, localResources: localIndex.resources.length });
521
- const ownershipGraph = (0, import_sdk2.resolve)([localIndex, ...remoteIndexes]);
522
- if (ownershipGraph.conflicts.length > 0) {
523
- options.onProgress?.({ type: "resolved", graph: ownershipGraph });
524
- throw new FederationConflictError(ownershipGraph.conflicts);
525
- }
526
- const graph = (0, import_sdk2.resolve)(remoteIndexes);
527
- options.onProgress?.({ type: "resolved", graph });
528
- const sourcesById = new Map(sources.map((source) => [source.id, source]));
529
- options.onProgress?.({ type: "hydrating", outDir });
530
- let hydratedFiles = 0;
531
- let cachedFiles = 0;
532
- const hydrateResult = await (0, import_sdk2.hydrate)(graph, {
932
+ const ownershipGraph = (0, import_sdk3.resolve)([localIndex, ...remoteIndexes]);
933
+ const graph = (0, import_sdk3.resolve)(remoteIndexes);
934
+ const diagnosticGraph = createDiagnosticGraph(
935
+ graph,
936
+ ownershipGraph,
937
+ new Set(remoteIndexes.map((index) => index.source)),
938
+ localIndex.source
939
+ );
940
+ const diagnostics = getFederationDiagnostics(diagnosticGraph, rules);
941
+ options.onProgress?.({ type: "resolved", graph, diagnostics });
942
+ const blockingConflicts = ownershipGraph.conflicts.filter((conflict) => rules[`federation/${conflict.kind}`] === "error");
943
+ if (blockingConflicts.length > 0) throw new FederationConflictError(blockingConflicts);
944
+ const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
945
+ if (blockingDiagnostics.length > 0) throw new FederationDiagnosticError(blockingDiagnostics);
946
+ const publicPaths = [
947
+ ...Object.keys(previousLock?.publicFiles ?? {}),
948
+ ...graph.assets.filter((asset) => asset.path.startsWith("public/")).map((asset) => asset.path.slice("public/".length))
949
+ ];
950
+ const { hydrateResult, publicResult } = await withFederationOutputTransaction(
951
+ projectDirectory,
533
952
  outDir,
534
- cache: createFederationContentCache(projectDirectory, {
535
- read: options.useCache !== false,
536
- onHit: () => {
537
- cachedFiles += 1;
538
- options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
539
- }
540
- }),
541
- modes: Object.fromEntries(sources.map((source) => [source.id, source.mode ?? "hydrate"])),
542
- fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
543
- const source = sourcesById.get(sourceId);
544
- if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
545
- const content = await provider.fetchContent({ source, commit, path: artifactPath });
546
- hydratedFiles += 1;
547
- options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
548
- return content;
953
+ publicPaths,
954
+ async () => {
955
+ const sourcesById = new Map(sources.map((source) => [source.id, source]));
956
+ options.onProgress?.({ type: "hydrating", outDir });
957
+ let hydratedFiles = 0;
958
+ let cachedFiles = 0;
959
+ const hydrateResult2 = await (0, import_sdk3.hydrate)(graph, {
960
+ outDir,
961
+ cache: createFederationContentCache(projectDirectory, {
962
+ read: options.useCache !== false,
963
+ onHit: () => {
964
+ cachedFiles += 1;
965
+ options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
966
+ }
967
+ }),
968
+ fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
969
+ const source = sourcesById.get(sourceId);
970
+ if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
971
+ const content = await provider.fetchContent({ source, commit, path: artifactPath });
972
+ hydratedFiles += 1;
973
+ options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
974
+ return content;
975
+ }
976
+ });
977
+ const publicResult2 = await composePublicAssets({
978
+ projectDirectory,
979
+ federatedDirectory: outDir,
980
+ assets: graph.assets,
981
+ previousFiles: previousLock?.publicFiles,
982
+ collisionPaths: new Set(
983
+ graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
984
+ )
985
+ });
986
+ options.onProgress?.({ type: "public:complete", result: publicResult2 });
987
+ const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
988
+ await writeLock(lockPath, {
989
+ lockVersion: 1,
990
+ sources: resolvedSources.map(({ config: source, resolved }) => ({
991
+ id: source.id,
992
+ digest: `sha256:${(0, import_node_crypto4.createHash)("sha256").update(resolved.bytes).digest("hex")}`,
993
+ commit: resolved.commit,
994
+ resolvedAt
995
+ })).sort((left, right) => left.id.localeCompare(right.id)),
996
+ publicFiles: publicResult2.files
997
+ });
998
+ return { hydrateResult: hydrateResult2, publicResult: publicResult2 };
549
999
  }
550
- });
551
- const publicResult = await composePublicAssets({
552
- projectDirectory,
553
- federatedDirectory: outDir,
554
- assets: graph.assets,
555
- previousFiles: previousLock?.publicFiles,
556
- collisionPaths: new Set(
557
- graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
558
- )
559
- });
560
- options.onProgress?.({ type: "public:complete", result: publicResult });
561
- const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
562
- await writeLock(lockPath, {
563
- lockVersion: 1,
564
- sources: resolvedSources.map(({ config: source, resolved }) => ({
565
- id: source.id,
566
- digest: `sha256:${(0, import_node_crypto3.createHash)("sha256").update(resolved.bytes).digest("hex")}`,
567
- commit: resolved.commit,
568
- resolvedAt
569
- })).sort((left, right) => left.id.localeCompare(right.id)),
570
- publicFiles: publicResult.files
571
- });
1000
+ );
572
1001
  const result = {
573
1002
  sources: sources.length,
574
1003
  resources,
575
1004
  graph,
1005
+ diagnostics,
576
1006
  hydrate: hydrateResult,
577
1007
  public: publicResult,
578
1008
  outDir,
@@ -584,5 +1014,6 @@ var federateCatalog = async (projectDirectory, options = {}) => {
584
1014
  // Annotate the CommonJS export names for ESM import in node:
585
1015
  0 && (module.exports = {
586
1016
  FederationConflictError,
1017
+ FederationDiagnosticError,
587
1018
  federateCatalog
588
1019
  });