@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
@@ -29,10 +29,10 @@ var importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
29
29
  // src/eventcatalog.ts
30
30
  var import_commander = require("commander");
31
31
  var import_node_child_process2 = require("child_process");
32
- var import_node_path15 = require("path");
32
+ var import_node_path17 = require("path");
33
33
  var import_node_http = __toESM(require("http"), 1);
34
34
  var import_fs3 = __toESM(require("fs"), 1);
35
- var import_node_path16 = __toESM(require("path"), 1);
35
+ var import_node_path18 = __toESM(require("path"), 1);
36
36
  var import_node_url = require("url");
37
37
 
38
38
  // src/generate.js
@@ -144,7 +144,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
144
144
  var import_picocolors = __toESM(require("picocolors"), 1);
145
145
 
146
146
  // package.json
147
- var version = "4.7.3";
147
+ var version = "4.7.5";
148
148
 
149
149
  // src/constants.ts
150
150
  var VERSION = version;
@@ -183,6 +183,29 @@ var logger = {
183
183
  warning: (message, tag = "warn") => {
184
184
  console.log(formatMessage(tag, message, import_picocolors.default.yellow));
185
185
  },
186
+ line: (message = "") => {
187
+ console.log(message);
188
+ },
189
+ diagnostic: (severity, message, rule, attributes = []) => {
190
+ const isError = severity === "error";
191
+ const color = isError ? import_picocolors.default.red : import_picocolors.default.yellow;
192
+ const icon = isError ? "\u2716" : "\u26A0";
193
+ const label = `${icon} ${severity.padEnd(7)} ${message}`;
194
+ const ruleSpacing = " ".repeat(Math.max(2, 72 - label.length));
195
+ console.log(` ${color(icon)} ${color(severity.padEnd(7))} ${message}${ruleSpacing}${import_picocolors.default.gray(rule)}`);
196
+ for (const attribute of attributes) {
197
+ console.log(` - ${import_picocolors.default.dim(`${attribute.label}:`)} ${attribute.value}`);
198
+ }
199
+ },
200
+ diagnosticSummary: (errors, warnings) => {
201
+ const total = errors + warnings;
202
+ const color = errors > 0 ? import_picocolors.default.red : import_picocolors.default.yellow;
203
+ const icon = errors > 0 ? "\u2716" : "\u26A0";
204
+ const problems = `${total} problem${total === 1 ? "" : "s"}`;
205
+ const errorCount = `${errors} error${errors === 1 ? "" : "s"}`;
206
+ const warningCount = `${warnings} warning${warnings === 1 ? "" : "s"}`;
207
+ console.log(color(`${icon} ${problems} (${errorCount}, ${warningCount})`));
208
+ },
186
209
  dim: (message) => {
187
210
  console.log(import_picocolors.default.dim(message));
188
211
  }
@@ -1310,22 +1333,22 @@ function getAvroTypeName(type) {
1310
1333
  function walkAvroRecord(schema, prefix, fields) {
1311
1334
  if (!schema.fields || !Array.isArray(schema.fields)) return;
1312
1335
  for (const field of schema.fields) {
1313
- const path16 = prefix ? `${prefix}.${field.name}` : field.name;
1336
+ const path18 = prefix ? `${prefix}.${field.name}` : field.name;
1314
1337
  const isOptional = Array.isArray(field.type) && field.type.includes("null");
1315
1338
  const typeName = getAvroTypeName(field.type);
1316
1339
  fields.push({
1317
- path: path16,
1340
+ path: path18,
1318
1341
  type: typeName,
1319
1342
  description: field.doc || "",
1320
1343
  required: !isOptional
1321
1344
  });
1322
1345
  const innerType = Array.isArray(field.type) ? field.type.find((t) => typeof t === "object" && t.type === "record") : typeof field.type === "object" && field.type.type === "record" ? field.type : null;
1323
1346
  if (innerType) {
1324
- walkAvroRecord(innerType, path16, fields);
1347
+ walkAvroRecord(innerType, path18, fields);
1325
1348
  }
1326
1349
  const arrayType = Array.isArray(field.type) ? field.type.find((t) => typeof t === "object" && t.type === "array") : typeof field.type === "object" && field.type.type === "array" ? field.type : null;
1327
1350
  if (arrayType && typeof arrayType.items === "object" && arrayType.items.type === "record") {
1328
- walkAvroRecord(arrayType.items, `${path16}[]`, fields);
1351
+ walkAvroRecord(arrayType.items, `${path18}[]`, fields);
1329
1352
  }
1330
1353
  }
1331
1354
  }
@@ -1356,32 +1379,32 @@ function walkJsonSchema(node, prefix, requiredList, rootSchema, fields) {
1356
1379
  }
1357
1380
  if (!node.properties) return;
1358
1381
  for (const [name, prop] of Object.entries(node.properties)) {
1359
- const path16 = prefix ? `${prefix}.${name}` : name;
1382
+ const path18 = prefix ? `${prefix}.${name}` : name;
1360
1383
  const isRequired = requiredList.includes(name);
1361
1384
  if (prop.$ref) {
1362
1385
  const resolved = resolveLocalRef(prop.$ref, rootSchema);
1363
1386
  if (resolved) {
1364
1387
  const rawRefType = resolved.type || "object";
1365
1388
  const type2 = Array.isArray(rawRefType) ? [...rawRefType].sort().join(" | ") : rawRefType;
1366
- fields.push({ path: path16, type: type2, description: resolved.description || "", required: isRequired });
1389
+ fields.push({ path: path18, type: type2, description: resolved.description || "", required: isRequired });
1367
1390
  if (resolved.properties) {
1368
- walkJsonSchema(resolved, path16, resolved.required || [], rootSchema, fields);
1391
+ walkJsonSchema(resolved, path18, resolved.required || [], rootSchema, fields);
1369
1392
  }
1370
1393
  } else {
1371
- fields.push({ path: path16, type: "$ref", description: "", required: isRequired });
1394
+ fields.push({ path: path18, type: "$ref", description: "", required: isRequired });
1372
1395
  }
1373
1396
  continue;
1374
1397
  }
1375
1398
  const rawType = prop.type || (prop.enum ? "enum" : prop.$ref ? "$ref" : "object");
1376
1399
  const typeList = Array.isArray(rawType) ? rawType : [rawType];
1377
1400
  const type = Array.isArray(rawType) ? [...rawType].sort().join(" | ") : rawType;
1378
- fields.push({ path: path16, type, description: prop.description || "", required: isRequired });
1401
+ fields.push({ path: path18, type, description: prop.description || "", required: isRequired });
1379
1402
  if (typeList.includes("object") && prop.properties) {
1380
- walkJsonSchema(prop, path16, prop.required || [], rootSchema, fields);
1403
+ walkJsonSchema(prop, path18, prop.required || [], rootSchema, fields);
1381
1404
  }
1382
1405
  if (typeList.includes("array") && prop.items) {
1383
1406
  if (prop.items.type === "object" && prop.items.properties) {
1384
- walkJsonSchema(prop.items, `${path16}[]`, prop.items.required || [], rootSchema, fields);
1407
+ walkJsonSchema(prop.items, `${path18}[]`, prop.items.required || [], rootSchema, fields);
1385
1408
  }
1386
1409
  }
1387
1410
  }
@@ -1900,10 +1923,10 @@ var createAstroDevLineFilter = () => {
1900
1923
  };
1901
1924
 
1902
1925
  // src/federation/federate.ts
1903
- var import_node_crypto3 = require("crypto");
1904
- var import_promises7 = __toESM(require("fs/promises"), 1);
1905
- var import_node_path14 = __toESM(require("path"), 1);
1906
- var import_sdk2 = __toESM(require("@eventcatalog/sdk"), 1);
1926
+ var import_node_crypto4 = require("crypto");
1927
+ var import_promises9 = __toESM(require("fs/promises"), 1);
1928
+ var import_node_path16 = __toESM(require("path"), 1);
1929
+ var import_sdk3 = __toESM(require("@eventcatalog/sdk"), 1);
1907
1930
 
1908
1931
  // src/federation/content-cache.ts
1909
1932
  var import_node_crypto = require("crypto");
@@ -1946,125 +1969,272 @@ var createFederationContentCache = (projectDirectory, options = {}) => {
1946
1969
  };
1947
1970
  };
1948
1971
 
1949
- // src/federation/github-source-provider.ts
1950
- var import_node_child_process = require("child_process");
1951
- var import_promises5 = __toESM(require("fs/promises"), 1);
1952
- var import_node_os4 = __toESM(require("os"), 1);
1953
- var import_node_path12 = __toESM(require("path"), 1);
1954
- var import_node_util = require("util");
1955
- var import_sdk = __toESM(require("@eventcatalog/sdk"), 1);
1956
- var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
1957
- var parseGitHubSource = (source) => {
1958
- const match = /^github:([^/]+)\/(.+)$/.exec(source.source);
1959
- if (!match) throw new Error(`Unsupported federation source "${source.source}". Expected github:owner/repository.`);
1960
- return { owner: match[1], repository: match[2] };
1961
- };
1962
- var assertSafeCatalogPath = (source) => {
1963
- const catalogPath = source.path ?? ".";
1964
- const normalized = import_node_path12.default.posix.normalize(catalogPath.replaceAll("\\", "/"));
1965
- if (catalogPath.includes("\\") || import_node_path12.default.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
1966
- throw new Error(`Catalog path "${catalogPath}" escapes source "${source.id}"`);
1967
- }
1968
- };
1969
- var encodePath = (value) => value.split("/").filter(Boolean).map(encodeURIComponent).join("/");
1970
- var rawUrl = (source, ref, filePath) => {
1971
- const { owner, repository } = parseGitHubSource(source);
1972
- return `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/${encodeURIComponent(
1973
- ref
1974
- )}/${encodePath(filePath)}`;
1975
- };
1976
- var contentsApiUrl = (source, ref, filePath) => {
1977
- const { owner, repository } = parseGitHubSource(source);
1978
- return `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/contents/${encodePath(
1979
- filePath
1980
- )}?ref=${encodeURIComponent(ref)}`;
1981
- };
1982
- var fetchBytes = async (source, ref, filePath, fetcher, token) => {
1983
- const url = token ? contentsApiUrl(source, ref, filePath) : rawUrl(source, ref, filePath);
1984
- const response = token ? await fetcher(url, {
1985
- headers: {
1986
- Accept: "application/vnd.github.raw+json",
1987
- Authorization: `Bearer ${token}`,
1988
- "X-GitHub-Api-Version": "2026-03-10"
1972
+ // src/federation/diagnostics.ts
1973
+ var federationRuleDefaults = {
1974
+ "federation/duplicate-source": "error",
1975
+ "federation/type-collision": "error",
1976
+ "federation/pointer-type-mismatch": "error",
1977
+ "federation/facet-disagreement": "error",
1978
+ "federation/asset-collision": "warn",
1979
+ "federation/missing-resource": "warn",
1980
+ "federation/unresolved-version": "warn"
1981
+ };
1982
+ var resolveFederationRules = (rules = {}) => {
1983
+ for (const [rule, level] of Object.entries(rules)) {
1984
+ if (!Object.hasOwn(federationRuleDefaults, rule)) throw new Error(`Unknown federation rule "${rule}".`);
1985
+ if (level !== "off" && level !== "warn" && level !== "error") {
1986
+ throw new Error(`Invalid level "${String(level)}" for federation rule "${rule}". Expected off, warn, or error.`);
1987
+ }
1988
+ }
1989
+ return { ...federationRuleDefaults, ...rules };
1990
+ };
1991
+ var getExternalResource = (external) => `${external.id}${external.version === void 0 ? "" : `@${external.version}`}`;
1992
+ var getVersionedResource = (id, version2) => `${id}${version2 == null ? "" : `@${version2}`}`;
1993
+ var getDocumentedResourceType = (type) => {
1994
+ const usesAn = type === "adr" || type === "agent" || type === "entity" || type === "event";
1995
+ return `documented as ${usesAn ? "an" : "a"} ${type}`;
1996
+ };
1997
+ var getConflictDiagnostic = (conflict, graph) => {
1998
+ const catalogs = { label: "catalogs", value: conflict.sources.join(", ") };
1999
+ switch (conflict.kind) {
2000
+ case "duplicate-source":
2001
+ return {
2002
+ severity: "error",
2003
+ message: "Resource has multiple owners",
2004
+ rule: "federation/duplicate-source",
2005
+ attributes: [
2006
+ { label: "resource", value: conflict.id },
2007
+ catalogs,
2008
+ { label: "resolution", value: "assign a single owning catalog" }
2009
+ ]
2010
+ };
2011
+ case "type-collision": {
2012
+ const types = [
2013
+ ...new Map(
2014
+ graph.entities.filter((entity) => entity.id === conflict.id).map(
2015
+ (entity) => [
2016
+ `${entity.resolvedFrom.source}:${entity.type}`,
2017
+ { label: entity.resolvedFrom.source, value: getDocumentedResourceType(entity.type) }
2018
+ ]
2019
+ )
2020
+ ).values()
2021
+ ];
2022
+ return {
2023
+ severity: "error",
2024
+ message: "Resource ID has conflicting types",
2025
+ rule: "federation/type-collision",
2026
+ attributes: [{ label: "resource", value: conflict.id }, ...types.length > 0 ? types : [catalogs]]
2027
+ };
1989
2028
  }
1990
- }) : await fetcher(url);
1991
- if (response.status === 404) return void 0;
1992
- if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
1993
- return Buffer.from(await response.arrayBuffer());
1994
- };
1995
- var getGitEnvironment = (token) => {
1996
- if (!token) return process.env;
1997
- const inheritedCount = Number.parseInt(process.env.GIT_CONFIG_COUNT ?? "0", 10);
1998
- const configIndex = Number.isInteger(inheritedCount) && inheritedCount >= 0 ? inheritedCount : 0;
1999
- return {
2000
- ...process.env,
2001
- GIT_CONFIG_COUNT: String(configIndex + 1),
2002
- [`GIT_CONFIG_KEY_${configIndex}`]: "http.https://github.com/.extraheader",
2003
- [`GIT_CONFIG_VALUE_${configIndex}`]: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`
2004
- };
2005
- };
2006
- var createCheckout = (executeFile, token) => async (source, ref, callback) => {
2007
- const { owner, repository } = parseGitHubSource(source);
2008
- const catalogPath = import_node_path12.default.posix.normalize(source.path ?? ".");
2009
- const directory = await import_promises5.default.mkdtemp(import_node_path12.default.join(import_node_os4.default.tmpdir(), "eventcatalog-federation-"));
2010
- const env = getGitEnvironment(token);
2011
- try {
2012
- const git = (args) => executeFile("git", args, { cwd: directory, env, encoding: "utf8" });
2013
- await git(["init", "--quiet"]);
2014
- await git(["remote", "add", "origin", `https://github.com/${owner}/${repository}.git`]);
2015
- if (catalogPath !== ".") {
2016
- await git(["sparse-checkout", "init", "--cone"]);
2017
- await git(["sparse-checkout", "set", catalogPath]);
2029
+ case "pointer-type-mismatch": {
2030
+ const typeMismatch = /^Expected (.+) but found (.+)$/.exec(conflict.detail ?? "");
2031
+ return {
2032
+ severity: "error",
2033
+ message: "Reference type does not match resource",
2034
+ rule: "federation/pointer-type-mismatch",
2035
+ attributes: [
2036
+ { label: "resource", value: conflict.id },
2037
+ ...typeMismatch ? [
2038
+ { label: "expected type", value: typeMismatch[1] },
2039
+ { label: "actual type", value: typeMismatch[2] }
2040
+ ] : conflict.detail ? [{ label: "detail", value: conflict.detail }] : [],
2041
+ catalogs
2042
+ ]
2043
+ };
2018
2044
  }
2019
- await git(["fetch", "--quiet", "--depth", "1", "origin", ref]);
2020
- await git(["checkout", "--quiet", "--detach", "FETCH_HEAD"]);
2021
- return await callback(directory);
2022
- } finally {
2023
- await import_promises5.default.rm(directory, { recursive: true, force: true });
2045
+ case "facet-disagreement":
2046
+ return {
2047
+ severity: "error",
2048
+ message: "Catalogs disagree about this resource",
2049
+ rule: "federation/facet-disagreement",
2050
+ attributes: [
2051
+ { label: "resource", value: conflict.id },
2052
+ ...conflict.detail ? [{ label: "detail", value: conflict.detail }] : [],
2053
+ catalogs
2054
+ ]
2055
+ };
2024
2056
  }
2025
2057
  };
2026
- var generateIndex = async (source, ref, checkout, executeFile) => checkout(source, ref, async (directory) => {
2027
- const { stdout } = await executeFile("git", ["rev-parse", "HEAD"], { cwd: directory, encoding: "utf8" });
2028
- const commit = stdout.trim();
2029
- const catalogDirectory = import_node_path12.default.resolve(directory, source.path ?? ".");
2030
- const relativeCatalogDirectory = import_node_path12.default.relative(directory, catalogDirectory);
2031
- if (relativeCatalogDirectory.startsWith("..") || import_node_path12.default.isAbsolute(relativeCatalogDirectory)) {
2032
- throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
2058
+ var getFederationDiagnostics = (graph, rules = {}) => {
2059
+ const resolvedRules = resolveFederationRules(rules);
2060
+ const diagnostics = graph.conflicts.map((conflict) => getConflictDiagnostic(conflict, graph));
2061
+ for (const external of graph.externals) {
2062
+ const resource = getExternalResource(external);
2063
+ for (const referencedBy of external.referencedBy) {
2064
+ const referrer = graph.entities.find((entity) => entity.id === referencedBy);
2065
+ diagnostics.push({
2066
+ severity: "warning",
2067
+ message: "Referenced EventCatalog resource does not exist",
2068
+ rule: "federation/missing-resource",
2069
+ attributes: [
2070
+ { label: "source catalog", value: referrer?.resolvedFrom.source ?? "unknown" },
2071
+ { label: "referenced by", value: referencedBy },
2072
+ { label: "missing resource", value: resource }
2073
+ ]
2074
+ });
2075
+ }
2033
2076
  }
2034
- const index = await (0, import_sdk.default)(catalogDirectory).buildIndex({ source: source.id, commit });
2035
- return { bytes: Buffer.from(JSON.stringify(index)), index, commit, generated: true };
2036
- });
2037
- var fetchPublishedIndex = async (source, ref, fetcher, token) => {
2038
- const indexPath = import_node_path12.default.posix.join(source.path ?? ".", "catalog.index.json");
2039
- const bytes = await fetchBytes(source, ref, indexPath, fetcher, token);
2040
- if (!bytes) return void 0;
2041
- const index = (0, import_sdk.parseIndex)(JSON.parse(bytes.toString("utf8")));
2042
- if (index.source !== source.id) {
2043
- throw new Error(`Published index source "${index.source}" does not match configured id "${source.id}"`);
2077
+ for (const edge of graph.edges.filter((edge2) => edge2.status === "unresolved")) {
2078
+ const availableVersions = [
2079
+ ...new Set(graph.entities.filter((entity) => entity.id === edge.to).map((entity) => entity.version ?? "unversioned"))
2080
+ ].sort((left, right) => left.localeCompare(right, void 0, { numeric: true }));
2081
+ diagnostics.push({
2082
+ severity: "warning",
2083
+ message: "Referenced EventCatalog resource version does not exist",
2084
+ rule: "federation/unresolved-version",
2085
+ attributes: [
2086
+ { label: "source catalog", value: edge.fromResolvedFrom.source },
2087
+ { label: "referenced by", value: getVersionedResource(edge.from, edge.fromVersion) },
2088
+ { label: "resource", value: edge.to },
2089
+ { label: "requested version", value: edge.pointer ?? "unspecified" },
2090
+ { label: "available versions", value: availableVersions.join(", ") }
2091
+ ]
2092
+ });
2044
2093
  }
2045
- return { bytes, index, commit: index.commit, generated: false };
2094
+ for (const warning of graph.warnings) {
2095
+ diagnostics.push({
2096
+ severity: "warning",
2097
+ message: "Asset collision",
2098
+ rule: "federation/asset-collision",
2099
+ attributes: [
2100
+ { label: "asset", value: warning.path },
2101
+ { label: "sources", value: warning.sources.join(", ") },
2102
+ { label: "winner", value: warning.winner },
2103
+ { label: "resolution", value: "last configured source wins" }
2104
+ ]
2105
+ });
2106
+ }
2107
+ return diagnostics.flatMap((diagnostic) => {
2108
+ const level = resolvedRules[diagnostic.rule];
2109
+ if (level === "off") return [];
2110
+ return [{ ...diagnostic, severity: level === "error" ? "error" : "warning" }];
2111
+ }).sort(
2112
+ (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 ?? "")
2113
+ );
2046
2114
  };
2047
- var createGitHubSourceProvider = (options = {}) => {
2048
- const fetcher = options.fetch ?? ((url, init) => fetch(url, init));
2049
- const executeFile = options.execFile ?? ((file, args, execOptions) => execFileAsync(file, args, execOptions));
2050
- const configuredToken = options.token ?? process.env.EVENTCATALOG_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN;
2051
- const token = configuredToken?.trim() || void 0;
2052
- const checkout = options.checkout ?? createCheckout(executeFile, token);
2115
+ var getFederationDiagnosticCounts = (diagnostics) => diagnostics.reduce(
2116
+ (counts, diagnostic) => ({
2117
+ errors: counts.errors + (diagnostic.severity === "error" ? 1 : 0),
2118
+ warnings: counts.warnings + (diagnostic.severity === "warning" ? 1 : 0)
2119
+ }),
2120
+ { errors: 0, warnings: 0 }
2121
+ );
2122
+ var getVisibleFederationDiagnostics = (diagnostics, verbose) => diagnostics.filter((diagnostic) => verbose || diagnostic.severity === "error");
2123
+
2124
+ // src/federation/output-transaction.ts
2125
+ var import_promises5 = __toESM(require("fs/promises"), 1);
2126
+ var import_node_path12 = __toESM(require("path"), 1);
2127
+ var getSafePublicPath = (publicDirectory, relativePath) => {
2128
+ const normalizedPath = import_node_path12.default.posix.normalize(relativePath);
2129
+ const unsafe = relativePath.length === 0 || relativePath.includes("\0") || relativePath.includes("\\") || import_node_path12.default.posix.isAbsolute(relativePath) || /^[a-zA-Z]:\//.test(relativePath) || normalizedPath !== relativePath || normalizedPath === ".." || normalizedPath.startsWith("../");
2130
+ if (unsafe) return void 0;
2131
+ const destinationPath = import_node_path12.default.resolve(publicDirectory, relativePath);
2132
+ const relativeDestinationPath = import_node_path12.default.relative(publicDirectory, destinationPath);
2133
+ return relativeDestinationPath !== "" && relativeDestinationPath !== ".." && !relativeDestinationPath.startsWith(`..${import_node_path12.default.sep}`) && !import_node_path12.default.isAbsolute(relativeDestinationPath) ? destinationPath : void 0;
2134
+ };
2135
+ var beginFederationOutputTransaction = async (projectDirectory, outDir, relativePublicPaths) => {
2136
+ const transactionDirectory = await import_promises5.default.mkdtemp(import_node_path12.default.join(projectDirectory, ".eventcatalog-federation-transaction-"));
2137
+ const previousOutDir = import_node_path12.default.join(transactionDirectory, "federated");
2138
+ const publicBackupDirectory = import_node_path12.default.join(transactionDirectory, "public");
2139
+ const publicDirectory = import_node_path12.default.resolve(projectDirectory, "public");
2140
+ const publicSnapshots = [];
2141
+ const missingPublicDirectories = /* @__PURE__ */ new Set();
2142
+ let hadPreviousOutDir = false;
2143
+ try {
2144
+ for (const relativePath of new Set(relativePublicPaths)) {
2145
+ const destinationPath = getSafePublicPath(publicDirectory, relativePath);
2146
+ if (!destinationPath) continue;
2147
+ try {
2148
+ const stat = await import_promises5.default.lstat(destinationPath);
2149
+ if (!stat.isFile()) continue;
2150
+ const backupPath = import_node_path12.default.join(publicBackupDirectory, `${publicSnapshots.length}`);
2151
+ await import_promises5.default.mkdir(import_node_path12.default.dirname(backupPath), { recursive: true });
2152
+ await import_promises5.default.copyFile(destinationPath, backupPath);
2153
+ publicSnapshots.push({ backupPath, destinationPath, state: "file" });
2154
+ } catch (error) {
2155
+ const code = error.code;
2156
+ if (code === "ENOTDIR") continue;
2157
+ if (code !== "ENOENT") throw error;
2158
+ publicSnapshots.push({ destinationPath, state: "missing" });
2159
+ let directory = import_node_path12.default.dirname(destinationPath);
2160
+ while (directory !== publicDirectory) {
2161
+ try {
2162
+ await import_promises5.default.lstat(directory);
2163
+ break;
2164
+ } catch (directoryError) {
2165
+ const directoryCode = directoryError.code;
2166
+ if (directoryCode === "ENOTDIR") break;
2167
+ if (directoryCode !== "ENOENT") throw directoryError;
2168
+ missingPublicDirectories.add(directory);
2169
+ directory = import_node_path12.default.dirname(directory);
2170
+ }
2171
+ }
2172
+ }
2173
+ }
2174
+ try {
2175
+ await import_promises5.default.rename(outDir, previousOutDir);
2176
+ hadPreviousOutDir = true;
2177
+ } catch (error) {
2178
+ if (error.code !== "ENOENT") throw error;
2179
+ }
2180
+ } catch (error) {
2181
+ await import_promises5.default.rm(transactionDirectory, { recursive: true, force: true });
2182
+ throw error;
2183
+ }
2053
2184
  return {
2054
- async resolve(source) {
2055
- assertSafeCatalogPath(source);
2056
- const ref = source.ref ?? "main";
2057
- return await fetchPublishedIndex(source, ref, fetcher, token) ?? generateIndex(source, ref, checkout, executeFile);
2185
+ commit: async () => {
2186
+ await import_promises5.default.rm(transactionDirectory, { recursive: true, force: true }).catch(() => void 0);
2058
2187
  },
2059
- async fetchContent({ source, commit, path: artifactPath }) {
2060
- assertSafeCatalogPath(source);
2061
- const catalogPath = import_node_path12.default.posix.join(source.path ?? ".", artifactPath);
2062
- const content = await fetchBytes(source, commit, catalogPath, fetcher, token);
2063
- if (!content) throw new Error(`Federated artifact not found for "${source.id}": ${artifactPath}`);
2064
- return content;
2188
+ rollback: async () => {
2189
+ const rollbackErrors = [];
2190
+ const attempt = async (operation) => {
2191
+ try {
2192
+ await operation();
2193
+ } catch (error) {
2194
+ rollbackErrors.push(error);
2195
+ }
2196
+ };
2197
+ await attempt(() => import_promises5.default.rm(outDir, { recursive: true, force: true }));
2198
+ if (hadPreviousOutDir) await attempt(() => import_promises5.default.rename(previousOutDir, outDir));
2199
+ for (const snapshot of publicSnapshots) {
2200
+ if (snapshot.state === "missing") {
2201
+ await attempt(() => import_promises5.default.rm(snapshot.destinationPath, { force: true }));
2202
+ continue;
2203
+ }
2204
+ await attempt(async () => {
2205
+ await import_promises5.default.mkdir(import_node_path12.default.dirname(snapshot.destinationPath), { recursive: true });
2206
+ await import_promises5.default.copyFile(snapshot.backupPath, snapshot.destinationPath);
2207
+ });
2208
+ }
2209
+ for (const directory of [...missingPublicDirectories].sort((left, right) => right.length - left.length)) {
2210
+ await attempt(async () => {
2211
+ try {
2212
+ await import_promises5.default.rmdir(directory);
2213
+ } catch (error) {
2214
+ if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) throw error;
2215
+ }
2216
+ });
2217
+ }
2218
+ await attempt(() => import_promises5.default.rm(transactionDirectory, { recursive: true, force: true }));
2219
+ if (rollbackErrors.length > 0) throw new AggregateError(rollbackErrors, "Failed to restore the previous federation output");
2065
2220
  }
2066
2221
  };
2067
2222
  };
2223
+ var withFederationOutputTransaction = async (projectDirectory, outDir, relativePublicPaths, update) => {
2224
+ const transaction = await beginFederationOutputTransaction(projectDirectory, outDir, relativePublicPaths);
2225
+ try {
2226
+ const result = await update();
2227
+ await transaction.commit();
2228
+ return result;
2229
+ } catch (error) {
2230
+ try {
2231
+ await transaction.rollback();
2232
+ } catch (rollbackError) {
2233
+ throw new AggregateError([error, rollbackError], "Federation failed and the previous output could not be restored");
2234
+ }
2235
+ throw error;
2236
+ }
2237
+ };
2068
2238
 
2069
2239
  // src/federation/public-assets.ts
2070
2240
  var import_node_crypto2 = require("crypto");
@@ -2196,6 +2366,232 @@ var composePublicAssets = async ({
2196
2366
  return { files, copied, skipped, overwritten, removed };
2197
2367
  };
2198
2368
 
2369
+ // src/federation/filesystem-source-provider.ts
2370
+ var import_node_crypto3 = require("crypto");
2371
+ var import_promises7 = __toESM(require("fs/promises"), 1);
2372
+ var import_node_path14 = __toESM(require("path"), 1);
2373
+ var import_sdk = __toESM(require("@eventcatalog/sdk"), 1);
2374
+ var FILESYSTEM_SOURCE_PREFIX = "file:";
2375
+ var isWithinDirectory = (directory, target) => {
2376
+ const relativePath = import_node_path14.default.relative(directory, target);
2377
+ return relativePath === "" || !relativePath.startsWith(`..${import_node_path14.default.sep}`) && relativePath !== ".." && !import_node_path14.default.isAbsolute(relativePath);
2378
+ };
2379
+ var assertPortableRelativePath = (filePath, label, allowRoot = true) => {
2380
+ const portablePath = filePath.replaceAll("\\", "/");
2381
+ const normalizedPath = import_node_path14.default.posix.normalize(portablePath);
2382
+ const isUnsafe = filePath.includes("\\") || filePath.includes("\0") || import_node_path14.default.posix.isAbsolute(normalizedPath) || /^[a-zA-Z]:\//.test(normalizedPath) || normalizedPath === ".." || normalizedPath.startsWith("../") || !allowRoot && (normalizedPath === "." || normalizedPath === "");
2383
+ if (isUnsafe) throw new Error(`${label} "${filePath}" escapes its filesystem source`);
2384
+ return normalizedPath;
2385
+ };
2386
+ var getSourceRoot = (projectDirectory, source) => {
2387
+ if (!source.source.startsWith(FILESYSTEM_SOURCE_PREFIX)) {
2388
+ throw new Error(`Unsupported federation source "${source.source}". Expected file:path/to/catalog.`);
2389
+ }
2390
+ const locator = source.source.slice(FILESYSTEM_SOURCE_PREFIX.length);
2391
+ if (!locator.trim()) throw new Error(`Filesystem federation source "${source.id}" requires a path after "file:".`);
2392
+ return import_node_path14.default.resolve(projectDirectory, locator);
2393
+ };
2394
+ var getCatalogDirectory = async (projectDirectory, source) => {
2395
+ if (source.ref) throw new Error(`Filesystem federation source "${source.id}" does not support "ref".`);
2396
+ const sourceRoot = getSourceRoot(projectDirectory, source);
2397
+ const catalogPath = assertPortableRelativePath(source.path ?? ".", "Catalog path");
2398
+ const catalogDirectory = import_node_path14.default.resolve(sourceRoot, ...catalogPath.split("/"));
2399
+ if (!isWithinDirectory(sourceRoot, catalogDirectory)) {
2400
+ throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
2401
+ }
2402
+ try {
2403
+ const [realSourceRoot, realCatalogDirectory] = await Promise.all([import_promises7.default.realpath(sourceRoot), import_promises7.default.realpath(catalogDirectory)]);
2404
+ if (!isWithinDirectory(realSourceRoot, realCatalogDirectory)) {
2405
+ throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
2406
+ }
2407
+ if (!(await import_promises7.default.stat(realCatalogDirectory)).isDirectory()) {
2408
+ throw new Error(`Filesystem federation source "${source.id}" is not a directory: ${catalogDirectory}`);
2409
+ }
2410
+ return realCatalogDirectory;
2411
+ } catch (error) {
2412
+ if (error.code === "ENOENT") {
2413
+ throw new Error(`Filesystem federation source "${source.id}" does not exist: ${catalogDirectory}`, { cause: error });
2414
+ }
2415
+ throw error;
2416
+ }
2417
+ };
2418
+ var getArtifactPath = async (catalogDirectory, source, artifactPath) => {
2419
+ const normalizedPath = assertPortableRelativePath(artifactPath, "Federated artifact path", false);
2420
+ const filePath = import_node_path14.default.resolve(catalogDirectory, ...normalizedPath.split("/"));
2421
+ if (!isWithinDirectory(catalogDirectory, filePath)) {
2422
+ throw new Error(`Federated artifact path "${artifactPath}" escapes source "${source.id}"`);
2423
+ }
2424
+ try {
2425
+ const realFilePath = await import_promises7.default.realpath(filePath);
2426
+ if (!isWithinDirectory(catalogDirectory, realFilePath)) {
2427
+ throw new Error(`Federated artifact path "${artifactPath}" escapes source "${source.id}"`);
2428
+ }
2429
+ return realFilePath;
2430
+ } catch (error) {
2431
+ if (error.code === "ENOENT") {
2432
+ throw new Error(`Federated artifact not found for "${source.id}": ${artifactPath}`, { cause: error });
2433
+ }
2434
+ throw error;
2435
+ }
2436
+ };
2437
+ var createFileSystemSourceProvider = (projectDirectory) => ({
2438
+ async resolve(source) {
2439
+ const catalogDirectory = await getCatalogDirectory(projectDirectory, source);
2440
+ const localIndex = await (0, import_sdk.default)(catalogDirectory).buildIndex({
2441
+ source: source.id,
2442
+ commit: "local",
2443
+ includeFederated: false
2444
+ });
2445
+ const snapshot = (0, import_node_crypto3.createHash)("sha256").update(JSON.stringify(localIndex)).digest("hex").slice(0, 12);
2446
+ const index = { ...localIndex, commit: `local:${snapshot}` };
2447
+ const bytes = Buffer.from(JSON.stringify(index));
2448
+ return { bytes, index, commit: index.commit, generated: true };
2449
+ },
2450
+ async fetchContent({ source, path: artifactPath }) {
2451
+ const catalogDirectory = await getCatalogDirectory(projectDirectory, source);
2452
+ return import_promises7.default.readFile(await getArtifactPath(catalogDirectory, source, artifactPath));
2453
+ }
2454
+ });
2455
+
2456
+ // src/federation/github-source-provider.ts
2457
+ var import_node_child_process = require("child_process");
2458
+ var import_promises8 = __toESM(require("fs/promises"), 1);
2459
+ var import_node_os4 = __toESM(require("os"), 1);
2460
+ var import_node_path15 = __toESM(require("path"), 1);
2461
+ var import_node_util = require("util");
2462
+ var import_sdk2 = __toESM(require("@eventcatalog/sdk"), 1);
2463
+ var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
2464
+ var parseGitHubSource = (source) => {
2465
+ const match = /^github:([^/]+)\/(.+)$/.exec(source.source);
2466
+ if (!match) throw new Error(`Unsupported federation source "${source.source}". Expected github:owner/repository.`);
2467
+ return { owner: match[1], repository: match[2] };
2468
+ };
2469
+ var assertSafeCatalogPath = (source) => {
2470
+ const catalogPath = source.path ?? ".";
2471
+ const normalized = import_node_path15.default.posix.normalize(catalogPath.replaceAll("\\", "/"));
2472
+ if (catalogPath.includes("\\") || import_node_path15.default.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
2473
+ throw new Error(`Catalog path "${catalogPath}" escapes source "${source.id}"`);
2474
+ }
2475
+ };
2476
+ var encodePath = (value) => value.split("/").filter(Boolean).map(encodeURIComponent).join("/");
2477
+ var rawUrl = (source, ref, filePath) => {
2478
+ const { owner, repository } = parseGitHubSource(source);
2479
+ return `https://raw.githubusercontent.com/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/${encodeURIComponent(
2480
+ ref
2481
+ )}/${encodePath(filePath)}`;
2482
+ };
2483
+ var contentsApiUrl = (source, ref, filePath) => {
2484
+ const { owner, repository } = parseGitHubSource(source);
2485
+ return `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repository)}/contents/${encodePath(
2486
+ filePath
2487
+ )}?ref=${encodeURIComponent(ref)}`;
2488
+ };
2489
+ var fetchBytes = async (source, ref, filePath, fetcher, token) => {
2490
+ const url = token ? contentsApiUrl(source, ref, filePath) : rawUrl(source, ref, filePath);
2491
+ const response = token ? await fetcher(url, {
2492
+ headers: {
2493
+ Accept: "application/vnd.github.raw+json",
2494
+ Authorization: `Bearer ${token}`,
2495
+ "X-GitHub-Api-Version": "2026-03-10"
2496
+ }
2497
+ }) : await fetcher(url);
2498
+ if (response.status === 404) return void 0;
2499
+ if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
2500
+ return Buffer.from(await response.arrayBuffer());
2501
+ };
2502
+ var getGitEnvironment = (token) => {
2503
+ if (!token) return process.env;
2504
+ const inheritedCount = Number.parseInt(process.env.GIT_CONFIG_COUNT ?? "0", 10);
2505
+ const configIndex = Number.isInteger(inheritedCount) && inheritedCount >= 0 ? inheritedCount : 0;
2506
+ return {
2507
+ ...process.env,
2508
+ GIT_CONFIG_COUNT: String(configIndex + 1),
2509
+ [`GIT_CONFIG_KEY_${configIndex}`]: "http.https://github.com/.extraheader",
2510
+ [`GIT_CONFIG_VALUE_${configIndex}`]: `AUTHORIZATION: basic ${Buffer.from(`x-access-token:${token}`).toString("base64")}`
2511
+ };
2512
+ };
2513
+ var createCheckout = (executeFile, token) => async (source, ref, callback) => {
2514
+ const { owner, repository } = parseGitHubSource(source);
2515
+ const catalogPath = import_node_path15.default.posix.normalize(source.path ?? ".");
2516
+ const directory = await import_promises8.default.mkdtemp(import_node_path15.default.join(import_node_os4.default.tmpdir(), "eventcatalog-federation-"));
2517
+ const env = getGitEnvironment(token);
2518
+ try {
2519
+ const git = (args) => executeFile("git", args, { cwd: directory, env, encoding: "utf8" });
2520
+ await git(["init", "--quiet"]);
2521
+ await git(["remote", "add", "origin", `https://github.com/${owner}/${repository}.git`]);
2522
+ if (catalogPath !== ".") {
2523
+ await git(["sparse-checkout", "init", "--cone"]);
2524
+ await git(["sparse-checkout", "set", catalogPath]);
2525
+ }
2526
+ await git(["fetch", "--quiet", "--depth", "1", "origin", ref]);
2527
+ await git(["checkout", "--quiet", "--detach", "FETCH_HEAD"]);
2528
+ return await callback(directory);
2529
+ } finally {
2530
+ await import_promises8.default.rm(directory, { recursive: true, force: true });
2531
+ }
2532
+ };
2533
+ var generateIndex = async (source, ref, checkout, executeFile) => checkout(source, ref, async (directory) => {
2534
+ const { stdout } = await executeFile("git", ["rev-parse", "HEAD"], { cwd: directory, encoding: "utf8" });
2535
+ const commit = stdout.trim();
2536
+ const catalogDirectory = import_node_path15.default.resolve(directory, source.path ?? ".");
2537
+ const relativeCatalogDirectory = import_node_path15.default.relative(directory, catalogDirectory);
2538
+ if (relativeCatalogDirectory.startsWith("..") || import_node_path15.default.isAbsolute(relativeCatalogDirectory)) {
2539
+ throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
2540
+ }
2541
+ const index = await (0, import_sdk2.default)(catalogDirectory).buildIndex({ source: source.id, commit });
2542
+ return { bytes: Buffer.from(JSON.stringify(index)), index, commit, generated: true };
2543
+ });
2544
+ var fetchPublishedIndex = async (source, ref, fetcher, token) => {
2545
+ const indexPath = import_node_path15.default.posix.join(source.path ?? ".", "catalog.index.json");
2546
+ const bytes = await fetchBytes(source, ref, indexPath, fetcher, token);
2547
+ if (!bytes) return void 0;
2548
+ const index = (0, import_sdk2.parseIndex)(JSON.parse(bytes.toString("utf8")));
2549
+ if (index.source !== source.id) {
2550
+ throw new Error(`Published index source "${index.source}" does not match configured id "${source.id}"`);
2551
+ }
2552
+ return { bytes, index, commit: index.commit, generated: false };
2553
+ };
2554
+ var createGitHubSourceProvider = (options = {}) => {
2555
+ const fetcher = options.fetch ?? ((url, init) => fetch(url, init));
2556
+ const executeFile = options.execFile ?? ((file, args, execOptions) => execFileAsync(file, args, execOptions));
2557
+ const configuredToken = options.token ?? process.env.EVENTCATALOG_GITHUB_TOKEN ?? process.env.GITHUB_TOKEN;
2558
+ const token = configuredToken?.trim() || void 0;
2559
+ const checkout = options.checkout ?? createCheckout(executeFile, token);
2560
+ return {
2561
+ async resolve(source) {
2562
+ assertSafeCatalogPath(source);
2563
+ const ref = source.ref ?? "main";
2564
+ return await fetchPublishedIndex(source, ref, fetcher, token) ?? generateIndex(source, ref, checkout, executeFile);
2565
+ },
2566
+ async fetchContent({ source, commit, path: artifactPath }) {
2567
+ assertSafeCatalogPath(source);
2568
+ const catalogPath = import_node_path15.default.posix.join(source.path ?? ".", artifactPath);
2569
+ const content = await fetchBytes(source, commit, catalogPath, fetcher, token);
2570
+ if (!content) throw new Error(`Federated artifact not found for "${source.id}": ${artifactPath}`);
2571
+ return content;
2572
+ }
2573
+ };
2574
+ };
2575
+
2576
+ // src/federation/source-provider.ts
2577
+ var createFederationSourceProvider = (projectDirectory, providers = {}) => {
2578
+ const github = providers.github ?? createGitHubSourceProvider();
2579
+ const filesystem = providers.filesystem ?? createFileSystemSourceProvider(projectDirectory);
2580
+ const getProvider = (source) => {
2581
+ if (source.source.startsWith("github:")) return github;
2582
+ if (source.source.startsWith("file:")) return filesystem;
2583
+ throw new Error(`Unsupported federation source "${source.source}" for "${source.id}". Supported protocols: github:, file:.`);
2584
+ };
2585
+ return {
2586
+ async resolve(source) {
2587
+ return getProvider(source).resolve(source);
2588
+ },
2589
+ async fetchContent(request) {
2590
+ return getProvider(request.source).fetchContent(request);
2591
+ }
2592
+ };
2593
+ };
2594
+
2199
2595
  // src/federation/federate.ts
2200
2596
  var FederationConflictError = class extends Error {
2201
2597
  conflicts;
@@ -2207,6 +2603,16 @@ ${details}`);
2207
2603
  this.conflicts = conflicts;
2208
2604
  }
2209
2605
  };
2606
+ var FederationDiagnosticError = class extends Error {
2607
+ diagnostics;
2608
+ constructor(diagnostics) {
2609
+ const details = diagnostics.map((diagnostic) => `${diagnostic.rule}: ${diagnostic.message}`).join("\n");
2610
+ super(`Federation diagnostics prevent hydration:
2611
+ ${details}`);
2612
+ this.name = "FederationDiagnosticError";
2613
+ this.diagnostics = diagnostics;
2614
+ }
2615
+ };
2210
2616
  var validateSources = (sources) => {
2211
2617
  const ids = /* @__PURE__ */ new Set();
2212
2618
  for (const source of sources) {
@@ -2215,19 +2621,46 @@ var validateSources = (sources) => {
2215
2621
  ids.add(source.id);
2216
2622
  }
2217
2623
  };
2624
+ var createDiagnosticGraph = (graph, ownershipGraph, remoteSourceIds, localSourceId) => {
2625
+ const edges = ownershipGraph.edges.filter((edge) => remoteSourceIds.has(edge.fromResolvedFrom.source));
2626
+ const externalsByPointer = /* @__PURE__ */ new Map();
2627
+ for (const edge of edges.filter((edge2) => edge2.status === "external")) {
2628
+ const version2 = edge.pointer ?? void 0;
2629
+ const key = `${edge.to}@${version2 ?? ""}`;
2630
+ const external = externalsByPointer.get(key) ?? {
2631
+ id: edge.to,
2632
+ ...version2 === void 0 ? {} : { version: version2 },
2633
+ referencedBy: []
2634
+ };
2635
+ if (!external.referencedBy.includes(edge.from)) external.referencedBy.push(edge.from);
2636
+ externalsByPointer.set(key, external);
2637
+ }
2638
+ return {
2639
+ ...graph,
2640
+ entities: [
2641
+ ...graph.entities,
2642
+ ...ownershipGraph.entities.filter(
2643
+ (entity) => entity.resolvedFrom.source === localSourceId && entity.resolvedFrom.commit === "local"
2644
+ )
2645
+ ],
2646
+ edges,
2647
+ conflicts: ownershipGraph.conflicts,
2648
+ externals: [...externalsByPointer.values()]
2649
+ };
2650
+ };
2218
2651
  var writeLock = async (lockPath, lock) => {
2219
2652
  const temporaryPath = `${lockPath}.tmp-${process.pid}`;
2220
2653
  try {
2221
- await import_promises7.default.writeFile(temporaryPath, `${JSON.stringify(lock, null, 2)}
2654
+ await import_promises9.default.writeFile(temporaryPath, `${JSON.stringify(lock, null, 2)}
2222
2655
  `, "utf8");
2223
- await import_promises7.default.rename(temporaryPath, lockPath);
2656
+ await import_promises9.default.rename(temporaryPath, lockPath);
2224
2657
  } finally {
2225
- await import_promises7.default.rm(temporaryPath, { force: true });
2658
+ await import_promises9.default.rm(temporaryPath, { force: true });
2226
2659
  }
2227
2660
  };
2228
2661
  var readLock = async (lockPath) => {
2229
2662
  try {
2230
- return JSON.parse(await import_promises7.default.readFile(lockPath, "utf8"));
2663
+ return JSON.parse(await import_promises9.default.readFile(lockPath, "utf8"));
2231
2664
  } catch (error) {
2232
2665
  if (error.code === "ENOENT") return void 0;
2233
2666
  throw new Error(`Cannot read federation lock at "${lockPath}"`, { cause: error });
@@ -2235,7 +2668,7 @@ var readLock = async (lockPath) => {
2235
2668
  };
2236
2669
  var pathExists2 = async (filePath) => {
2237
2670
  try {
2238
- await import_promises7.default.access(filePath);
2671
+ await import_promises9.default.access(filePath);
2239
2672
  return true;
2240
2673
  } catch (error) {
2241
2674
  if (error.code === "ENOENT") return false;
@@ -2243,20 +2676,27 @@ var pathExists2 = async (filePath) => {
2243
2676
  }
2244
2677
  };
2245
2678
  var cleanupPreviousFederation = async (projectDirectory, onProgress) => {
2246
- const outDir = import_node_path14.default.join(projectDirectory, "federated");
2247
- const lockPath = import_node_path14.default.join(projectDirectory, "eventcatalog.lock");
2679
+ const outDir = import_node_path16.default.join(projectDirectory, "federated");
2680
+ const lockPath = import_node_path16.default.join(projectDirectory, "eventcatalog.lock");
2248
2681
  const previousLock = await readLock(lockPath);
2249
2682
  const hadFederatedOutput = await pathExists2(outDir);
2250
2683
  const hadLock = previousLock !== void 0;
2251
2684
  if (!hadFederatedOutput && !hadLock) return;
2252
- await import_promises7.default.rm(outDir, { recursive: true, force: true });
2253
- const publicResult = await composePublicAssets({
2685
+ const publicResult = await withFederationOutputTransaction(
2254
2686
  projectDirectory,
2255
- federatedDirectory: outDir,
2256
- assets: [],
2257
- previousFiles: previousLock?.publicFiles
2258
- });
2259
- await import_promises7.default.rm(lockPath, { force: true });
2687
+ outDir,
2688
+ Object.keys(previousLock?.publicFiles ?? {}),
2689
+ async () => {
2690
+ const result = await composePublicAssets({
2691
+ projectDirectory,
2692
+ federatedDirectory: outDir,
2693
+ assets: [],
2694
+ previousFiles: previousLock?.publicFiles
2695
+ });
2696
+ await import_promises9.default.rm(lockPath, { force: true });
2697
+ return result;
2698
+ }
2699
+ );
2260
2700
  onProgress?.({
2261
2701
  type: "cleanup:complete",
2262
2702
  federated: hadFederatedOutput,
@@ -2278,8 +2718,9 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2278
2718
  );
2279
2719
  }
2280
2720
  validateSources(sources);
2721
+ const rules = resolveFederationRules(config.federation?.rules);
2281
2722
  if (options.useCache === false) options.onProgress?.({ type: "cache:disabled" });
2282
- const provider = options.provider ?? createGitHubSourceProvider();
2723
+ const provider = options.provider ?? createFederationSourceProvider(projectDirectory);
2283
2724
  const resolvedSources = [];
2284
2725
  for (const [index, source] of sources.entries()) {
2285
2726
  const current = index + 1;
@@ -2301,13 +2742,13 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2301
2742
  throw new Error(`Failed to federate source "${source.id}": ${message}`, { cause: error });
2302
2743
  }
2303
2744
  }
2304
- const outDir = import_node_path14.default.join(projectDirectory, "federated");
2305
- const lockPath = import_node_path14.default.join(projectDirectory, "eventcatalog.lock");
2745
+ const outDir = import_node_path16.default.join(projectDirectory, "federated");
2746
+ const lockPath = import_node_path16.default.join(projectDirectory, "eventcatalog.lock");
2306
2747
  const previousLock = await readLock(lockPath);
2307
2748
  const resources = resolvedSources.reduce((total, source) => total + source.resolved.index.resources.length, 0);
2308
2749
  const remoteIndexes = resolvedSources.map(({ resolved }) => resolved.index);
2309
2750
  options.onProgress?.({ type: "local:start" });
2310
- const localIndex = await (0, import_sdk2.default)(projectDirectory).buildIndex({
2751
+ const localIndex = await (0, import_sdk3.default)(projectDirectory).buildIndex({
2311
2752
  source: config.cId,
2312
2753
  commit: "local",
2313
2754
  hashContent: false,
@@ -2315,61 +2756,80 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2315
2756
  });
2316
2757
  options.onProgress?.({ type: "local:complete", resources: localIndex.resources.length });
2317
2758
  options.onProgress?.({ type: "resolving", resources, localResources: localIndex.resources.length });
2318
- const ownershipGraph = (0, import_sdk2.resolve)([localIndex, ...remoteIndexes]);
2319
- if (ownershipGraph.conflicts.length > 0) {
2320
- options.onProgress?.({ type: "resolved", graph: ownershipGraph });
2321
- throw new FederationConflictError(ownershipGraph.conflicts);
2322
- }
2323
- const graph = (0, import_sdk2.resolve)(remoteIndexes);
2324
- options.onProgress?.({ type: "resolved", graph });
2325
- const sourcesById = new Map(sources.map((source) => [source.id, source]));
2326
- options.onProgress?.({ type: "hydrating", outDir });
2327
- let hydratedFiles = 0;
2328
- let cachedFiles = 0;
2329
- const hydrateResult = await (0, import_sdk2.hydrate)(graph, {
2759
+ const ownershipGraph = (0, import_sdk3.resolve)([localIndex, ...remoteIndexes]);
2760
+ const graph = (0, import_sdk3.resolve)(remoteIndexes);
2761
+ const diagnosticGraph = createDiagnosticGraph(
2762
+ graph,
2763
+ ownershipGraph,
2764
+ new Set(remoteIndexes.map((index) => index.source)),
2765
+ localIndex.source
2766
+ );
2767
+ const diagnostics = getFederationDiagnostics(diagnosticGraph, rules);
2768
+ options.onProgress?.({ type: "resolved", graph, diagnostics });
2769
+ const blockingConflicts = ownershipGraph.conflicts.filter((conflict) => rules[`federation/${conflict.kind}`] === "error");
2770
+ if (blockingConflicts.length > 0) throw new FederationConflictError(blockingConflicts);
2771
+ const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
2772
+ if (blockingDiagnostics.length > 0) throw new FederationDiagnosticError(blockingDiagnostics);
2773
+ const publicPaths = [
2774
+ ...Object.keys(previousLock?.publicFiles ?? {}),
2775
+ ...graph.assets.filter((asset) => asset.path.startsWith("public/")).map((asset) => asset.path.slice("public/".length))
2776
+ ];
2777
+ const { hydrateResult, publicResult } = await withFederationOutputTransaction(
2778
+ projectDirectory,
2330
2779
  outDir,
2331
- cache: createFederationContentCache(projectDirectory, {
2332
- read: options.useCache !== false,
2333
- onHit: () => {
2334
- cachedFiles += 1;
2335
- options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
2336
- }
2337
- }),
2338
- modes: Object.fromEntries(sources.map((source) => [source.id, source.mode ?? "hydrate"])),
2339
- fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
2340
- const source = sourcesById.get(sourceId);
2341
- if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
2342
- const content = await provider.fetchContent({ source, commit, path: artifactPath });
2343
- hydratedFiles += 1;
2344
- options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
2345
- return content;
2780
+ publicPaths,
2781
+ async () => {
2782
+ const sourcesById = new Map(sources.map((source) => [source.id, source]));
2783
+ options.onProgress?.({ type: "hydrating", outDir });
2784
+ let hydratedFiles = 0;
2785
+ let cachedFiles = 0;
2786
+ const hydrateResult2 = await (0, import_sdk3.hydrate)(graph, {
2787
+ outDir,
2788
+ cache: createFederationContentCache(projectDirectory, {
2789
+ read: options.useCache !== false,
2790
+ onHit: () => {
2791
+ cachedFiles += 1;
2792
+ options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
2793
+ }
2794
+ }),
2795
+ fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
2796
+ const source = sourcesById.get(sourceId);
2797
+ if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
2798
+ const content = await provider.fetchContent({ source, commit, path: artifactPath });
2799
+ hydratedFiles += 1;
2800
+ options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
2801
+ return content;
2802
+ }
2803
+ });
2804
+ const publicResult2 = await composePublicAssets({
2805
+ projectDirectory,
2806
+ federatedDirectory: outDir,
2807
+ assets: graph.assets,
2808
+ previousFiles: previousLock?.publicFiles,
2809
+ collisionPaths: new Set(
2810
+ graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
2811
+ )
2812
+ });
2813
+ options.onProgress?.({ type: "public:complete", result: publicResult2 });
2814
+ const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
2815
+ await writeLock(lockPath, {
2816
+ lockVersion: 1,
2817
+ sources: resolvedSources.map(({ config: source, resolved }) => ({
2818
+ id: source.id,
2819
+ digest: `sha256:${(0, import_node_crypto4.createHash)("sha256").update(resolved.bytes).digest("hex")}`,
2820
+ commit: resolved.commit,
2821
+ resolvedAt
2822
+ })).sort((left, right) => left.id.localeCompare(right.id)),
2823
+ publicFiles: publicResult2.files
2824
+ });
2825
+ return { hydrateResult: hydrateResult2, publicResult: publicResult2 };
2346
2826
  }
2347
- });
2348
- const publicResult = await composePublicAssets({
2349
- projectDirectory,
2350
- federatedDirectory: outDir,
2351
- assets: graph.assets,
2352
- previousFiles: previousLock?.publicFiles,
2353
- collisionPaths: new Set(
2354
- graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
2355
- )
2356
- });
2357
- options.onProgress?.({ type: "public:complete", result: publicResult });
2358
- const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
2359
- await writeLock(lockPath, {
2360
- lockVersion: 1,
2361
- sources: resolvedSources.map(({ config: source, resolved }) => ({
2362
- id: source.id,
2363
- digest: `sha256:${(0, import_node_crypto3.createHash)("sha256").update(resolved.bytes).digest("hex")}`,
2364
- commit: resolved.commit,
2365
- resolvedAt
2366
- })).sort((left, right) => left.id.localeCompare(right.id)),
2367
- publicFiles: publicResult.files
2368
- });
2827
+ );
2369
2828
  const result = {
2370
2829
  sources: sources.length,
2371
2830
  resources,
2372
2831
  graph,
2832
+ diagnostics,
2373
2833
  hydrate: hydrateResult,
2374
2834
  public: publicResult,
2375
2835
  outDir,
@@ -2381,14 +2841,14 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2381
2841
 
2382
2842
  // src/eventcatalog.ts
2383
2843
  var import_license = require("@eventcatalog/license");
2384
- var currentDir = import_node_path16.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
2844
+ var currentDir = import_node_path18.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
2385
2845
  var program = new import_commander.Command().version(VERSION);
2386
- var dir = import_node_path16.default.resolve(process.env.PROJECT_DIR || process.cwd());
2387
- var core = import_node_path16.default.resolve(process.env.CATALOG_DIR || (0, import_node_path15.join)(dir, ".eventcatalog-core"));
2388
- var eventCatalogDir = import_node_path16.default.resolve((0, import_node_path15.join)(currentDir, "../eventcatalog/"));
2846
+ var dir = import_node_path18.default.resolve(process.env.PROJECT_DIR || process.cwd());
2847
+ var core = import_node_path18.default.resolve(process.env.CATALOG_DIR || (0, import_node_path17.join)(dir, ".eventcatalog-core"));
2848
+ var eventCatalogDir = import_node_path18.default.resolve((0, import_node_path17.join)(currentDir, "../eventcatalog/"));
2389
2849
  var getInstalledEventCatalogVersion = () => {
2390
2850
  try {
2391
- const pkg = import_fs3.default.readFileSync((0, import_node_path15.join)(dir, "package.json"), "utf8");
2851
+ const pkg = import_fs3.default.readFileSync((0, import_node_path17.join)(dir, "package.json"), "utf8");
2392
2852
  const json = JSON.parse(pkg);
2393
2853
  return json.dependencies["@eventcatalog/core"];
2394
2854
  } catch (error) {
@@ -2454,12 +2914,12 @@ var startDevPrewarm = ({
2454
2914
  var buildDevSearchIndex = async ({ config }) => {
2455
2915
  const result = await buildSearchIndex({
2456
2916
  projectDir: dir,
2457
- outDir: import_node_path16.default.join(core, "public"),
2458
- searchOutputPath: import_node_path16.default.join(core, "public", "pagefind"),
2917
+ outDir: import_node_path18.default.join(core, "public"),
2918
+ searchOutputPath: import_node_path18.default.join(core, "public", "pagefind"),
2459
2919
  config,
2460
2920
  isServer: false
2461
2921
  });
2462
- logger.info(`Indexed ${result.records} page(s) into ${import_node_path16.default.relative(core, result.outputPath)}`, "search");
2922
+ logger.info(`Indexed ${result.records} page(s) into ${import_node_path18.default.relative(core, result.outputPath)}`, "search");
2463
2923
  };
2464
2924
  var warnIfIndexedSearchUsesAuth = async () => {
2465
2925
  if (!await isAuthEnabled()) {
@@ -2574,12 +3034,12 @@ var copyCore = () => {
2574
3034
  import_fs3.default.cpSync(eventCatalogDir, core, {
2575
3035
  recursive: true,
2576
3036
  filter: (src) => {
2577
- const relativePath = import_node_path16.default.relative(eventCatalogDir, src);
2578
- const pathParts = relativePath.split(import_node_path16.default.sep);
3037
+ const relativePath = import_node_path18.default.relative(eventCatalogDir, src);
3038
+ const pathParts = relativePath.split(import_node_path18.default.sep);
2579
3039
  return !pathParts.some((part) => [".astro", "dist", "node_modules"].includes(part));
2580
3040
  }
2581
3041
  });
2582
- const coreNodeModules = import_node_path16.default.join(core, "node_modules");
3042
+ const coreNodeModules = import_node_path18.default.join(core, "node_modules");
2583
3043
  const installedCoreNodeModules = resolveInstalledCoreNodeModules(currentDir);
2584
3044
  linkCoreNodeModules({ coreNodeModules, installedCoreNodeModules });
2585
3045
  };
@@ -2638,8 +3098,8 @@ program.command("dev").description("Run development server of EventCatalog").opt
2638
3098
  logger.info("Setting up EventCatalog...", "eventcatalog");
2639
3099
  const isServer = await isOutputServer();
2640
3100
  logger.info(isServer ? "EventCatalog is running in Server Mode" : "EventCatalog is running in Static Mode", "config");
2641
- if (import_fs3.default.existsSync(import_node_path16.default.join(dir, ".env"))) {
2642
- import_dotenv.default.config({ path: import_node_path16.default.join(dir, ".env") });
3101
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3102
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2643
3103
  }
2644
3104
  if (options.debug) {
2645
3105
  logger.info("Debug mode enabled", "debug");
@@ -2719,8 +3179,8 @@ program.command("build").description("Run build of EventCatalog").action(async (
2719
3179
  logger.info("Building EventCatalog...", "build");
2720
3180
  const isServer = await isOutputServer();
2721
3181
  logger.info(isServer ? "EventCatalog is running in Server Mode" : "EventCatalog is running in Static Mode", "config");
2722
- if (import_fs3.default.existsSync(import_node_path16.default.join(dir, ".env"))) {
2723
- import_dotenv.default.config({ path: import_node_path16.default.join(dir, ".env") });
3182
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3183
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2724
3184
  }
2725
3185
  await verifyRequiredFieldsAreInCatalogConfigFile(dir);
2726
3186
  copyCore();
@@ -2770,7 +3230,7 @@ program.command("build").description("Run build of EventCatalog").action(async (
2770
3230
  if (await isIndexedSearchEnabled()) {
2771
3231
  await warnIfIndexedSearchUsesAuth();
2772
3232
  const config = await getEventCatalogConfigFile(dir);
2773
- const outDir = import_node_path16.default.resolve(dir, await getProjectOutDir());
3233
+ const outDir = import_node_path18.default.resolve(dir, await getProjectOutDir());
2774
3234
  logger.info("Building indexed search...", "search");
2775
3235
  const result = await buildSearchIndex({
2776
3236
  projectDir: dir,
@@ -2778,7 +3238,7 @@ program.command("build").description("Run build of EventCatalog").action(async (
2778
3238
  config,
2779
3239
  isServer
2780
3240
  });
2781
- logger.info(`Indexed ${result.records} page(s) into ${import_node_path16.default.relative(dir, result.outputPath)}`, "search");
3241
+ logger.info(`Indexed ${result.records} page(s) into ${import_node_path18.default.relative(dir, result.outputPath)}`, "search");
2782
3242
  }
2783
3243
  });
2784
3244
  var previewCatalog = async ({
@@ -2805,7 +3265,7 @@ var startServerCatalog = async ({
2805
3265
  isEventCatalogStarter = false,
2806
3266
  isEventCatalogScale = false
2807
3267
  }) => {
2808
- const serverEntryPath = import_node_path16.default.join(dir, "dist", "server", "entry.mjs");
3268
+ const serverEntryPath = import_node_path18.default.join(dir, "dist", "server", "entry.mjs");
2809
3269
  await runCommandWithFilteredOutput({
2810
3270
  command: `node "${serverEntryPath}"`,
2811
3271
  cwd: core,
@@ -2822,8 +3282,8 @@ var startServerCatalog = async ({
2822
3282
  program.command("preview").description("Serves the contents of your eventcatalog build directory").action(async (options, command) => {
2823
3283
  logger.welcome();
2824
3284
  logger.info("Starting preview of your build...", "preview");
2825
- if (import_fs3.default.existsSync(import_node_path16.default.join(dir, ".env"))) {
2826
- import_dotenv.default.config({ path: import_node_path16.default.join(dir, ".env") });
3285
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3286
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2827
3287
  }
2828
3288
  const canEmbedPages = await (0, import_license.isFeatureEnabled)(
2829
3289
  "@eventcatalog/backstage-plugin-eventcatalog",
@@ -2841,8 +3301,8 @@ program.command("preview").description("Serves the contents of your eventcatalog
2841
3301
  program.command("start").description("Serves the contents of your eventcatalog build directory").action(async (options, command) => {
2842
3302
  logger.welcome();
2843
3303
  logger.info("Starting preview of your build...", "preview");
2844
- if (import_fs3.default.existsSync(import_node_path16.default.join(dir, ".env"))) {
2845
- import_dotenv.default.config({ path: import_node_path16.default.join(dir, ".env") });
3304
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3305
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2846
3306
  }
2847
3307
  const canEmbedPages = await (0, import_license.isFeatureEnabled)(
2848
3308
  "@eventcatalog/backstage-plugin-eventcatalog",
@@ -2869,26 +3329,26 @@ program.command("start").description("Serves the contents of your eventcatalog b
2869
3329
  program.command("export").description("Export your EventCatalog using the SDK dumpCatalog function").option("--include-markdown", "Include markdown content in the export", false).action(async (options) => {
2870
3330
  logger.welcome();
2871
3331
  logger.info("Exporting EventCatalog...", "export");
2872
- if (import_fs3.default.existsSync(import_node_path16.default.join(dir, ".env"))) {
2873
- import_dotenv.default.config({ path: import_node_path16.default.join(dir, ".env") });
3332
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3333
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2874
3334
  }
2875
3335
  const { default: initSDK } = await import("@eventcatalog/sdk");
2876
3336
  const sdk = initSDK(dir);
2877
3337
  const catalog = await sdk.dumpCatalog({ includeMarkdown: options.includeMarkdown });
2878
- const exportsDir = import_node_path16.default.join(dir, "exports");
3338
+ const exportsDir = import_node_path18.default.join(dir, "exports");
2879
3339
  ensureDir(exportsDir);
2880
3340
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2881
- const exportFile = import_node_path16.default.join(exportsDir, `catalog-${date}.json`);
3341
+ const exportFile = import_node_path18.default.join(exportsDir, `catalog-${date}.json`);
2882
3342
  import_fs3.default.writeFileSync(exportFile, JSON.stringify(catalog, null, 2), "utf-8");
2883
3343
  logger.info(`Catalog exported to ${exportFile}`, "export");
2884
3344
  });
2885
3345
  program.command("generate [siteDir]").description("Start the generator scripts.").action(async () => {
2886
- if (import_fs3.default.existsSync(import_node_path16.default.join(dir, ".env"))) {
2887
- import_dotenv.default.config({ path: import_node_path16.default.join(dir, ".env") });
3346
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3347
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2888
3348
  }
2889
3349
  await generate(dir);
2890
3350
  });
2891
- var reportFederationProgress = (event) => {
3351
+ var reportFederationProgress = (event, verbose = false) => {
2892
3352
  switch (event.type) {
2893
3353
  case "configured":
2894
3354
  if (event.sources > 0)
@@ -2928,38 +3388,36 @@ var reportFederationProgress = (event) => {
2928
3388
  );
2929
3389
  return;
2930
3390
  case "resolved":
2931
- if (event.graph.conflicts.length > 0) {
2932
- logger.error(
2933
- `${event.graph.conflicts.length} ownership conflict${event.graph.conflicts.length === 1 ? "" : "s"} found`,
3391
+ const counts = getFederationDiagnosticCounts(event.diagnostics);
3392
+ const showDiagnosticDetails = verbose || counts.errors > 0;
3393
+ if (counts.errors === 0) {
3394
+ logger.success(
3395
+ `Graph resolved: ${event.graph.entities.length} remote resources, ${event.graph.edges.length} relationships`,
2934
3396
  "federation"
2935
3397
  );
2936
- for (const conflict of event.graph.conflicts.slice(0, 10)) {
2937
- logger.error(`${conflict.id}: ${conflict.sources.join(", ")}`, "federation");
2938
- }
2939
- if (event.graph.conflicts.length > 10) {
2940
- logger.error(`...and ${event.graph.conflicts.length - 10} more`, "federation");
2941
- }
2942
- return;
2943
3398
  }
2944
- logger.success(
2945
- `Graph resolved: ${event.graph.entities.length} remote resources, ${event.graph.edges.length} relationships`,
2946
- "federation"
2947
- );
2948
- if (event.graph.warnings.length > 0) {
3399
+ if (counts.errors + counts.warnings === 0) return;
3400
+ if (showDiagnosticDetails) {
3401
+ const diagnostics = getVisibleFederationDiagnostics(event.diagnostics, verbose);
3402
+ logger.info("Federation diagnostics", "federation");
3403
+ logger.line();
3404
+ for (const diagnostic of diagnostics) {
3405
+ logger.diagnostic(diagnostic.severity, diagnostic.message, diagnostic.rule, diagnostic.attributes);
3406
+ logger.line();
3407
+ }
3408
+ logger.diagnosticSummary(counts.errors, counts.warnings);
3409
+ } else {
2949
3410
  logger.warning(
2950
- `${event.graph.warnings.length} federation warning${event.graph.warnings.length === 1 ? "" : "s"}`,
3411
+ `${counts.warnings} problem${counts.warnings === 1 ? "" : "s"} (0 errors, ${counts.warnings} warning${counts.warnings === 1 ? "" : "s"})`,
2951
3412
  "federation"
2952
3413
  );
2953
3414
  }
2954
- if (event.graph.externals.length > 0) {
2955
- logger.warning(
2956
- `${event.graph.externals.length} reference${event.graph.externals.length === 1 ? "" : "s"} remain external`,
2957
- "federation"
2958
- );
3415
+ if (!verbose && counts.warnings > 0) {
3416
+ logger.info("Run with --verbose to see warning details.", "federation");
2959
3417
  }
2960
3418
  return;
2961
3419
  case "hydrating":
2962
- logger.info(`Hydrating federated content into ${import_node_path16.default.relative(dir, event.outDir)}/...`, "federation");
3420
+ logger.info(`Hydrating federated content into ${import_node_path18.default.relative(dir, event.outDir)}/...`, "federation");
2963
3421
  return;
2964
3422
  case "hydrate:cache":
2965
3423
  if (event.files === 1 || event.files % 25 === 0) {
@@ -2976,25 +3434,19 @@ var reportFederationProgress = (event) => {
2976
3434
  `Public assets: ${event.result.copied} copied, ${event.result.skipped} preserved from the main catalog, ${event.result.removed} stale removed`,
2977
3435
  "federation"
2978
3436
  );
2979
- if (event.result.overwritten > 0) {
2980
- logger.warning(
2981
- `${event.result.overwritten} public asset conflict${event.result.overwritten === 1 ? "" : "s"} resolved using the last configured source`,
2982
- "federation"
2983
- );
2984
- }
2985
3437
  return;
2986
3438
  case "complete":
2987
3439
  logger.success(
2988
3440
  `Federation complete: ${event.result.sources} sources, ${event.result.resources} remote resources, ${event.result.hydrate.written} files written (${event.result.hydrate.fetched} downloaded, ${event.result.hydrate.written - event.result.hydrate.fetched} cached)`,
2989
3441
  "federation"
2990
3442
  );
2991
- logger.info(`Pinned source commits in ${import_node_path16.default.relative(dir, event.result.lockPath)}`, "federation");
3443
+ logger.info(`Recorded resolved source state in ${import_node_path18.default.relative(dir, event.result.lockPath)}`, "federation");
2992
3444
  }
2993
3445
  };
2994
- program.command("federate").description("Fetch, resolve, and hydrate the catalogs configured in federation.sources.").option("--no-cache", "Download all federation content and refresh the cache.").action(async (commandOptions) => {
3446
+ program.command("federate").description("Fetch, resolve, and hydrate the catalogs configured in federation.sources.").option("--no-cache", "Download all federation content and refresh the cache.").option("-v, --verbose", "Show detailed federation diagnostics.").action(async (commandOptions) => {
2995
3447
  logger.welcome();
2996
- if (import_fs3.default.existsSync(import_node_path16.default.join(dir, ".env"))) {
2997
- import_dotenv.default.config({ path: import_node_path16.default.join(dir, ".env") });
3448
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3449
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2998
3450
  }
2999
3451
  logger.info("Starting federation...", "federation");
3000
3452
  let cleanedPreviousOutput = false;
@@ -3003,7 +3455,7 @@ program.command("federate").description("Fetch, resolve, and hydrate the catalog
3003
3455
  isFederationEnabled: import_license.isEventCatalogScaleEnabled,
3004
3456
  onProgress: (event) => {
3005
3457
  if (event.type === "cleanup:complete") cleanedPreviousOutput = true;
3006
- reportFederationProgress(event);
3458
+ reportFederationProgress(event, commandOptions.verbose);
3007
3459
  }
3008
3460
  });
3009
3461
  if (!result && !cleanedPreviousOutput) logger.warning("No federation sources configured; nothing to do.", "federation");
@@ -3017,6 +3469,6 @@ program.addHelpText(
3017
3469
  `
3018
3470
  );
3019
3471
  program.parseAsync().then(() => process.exit(0)).catch((err) => {
3020
- if (!(err instanceof FederationConflictError)) console.error(err);
3472
+ if (!(err instanceof FederationConflictError) && !(err instanceof FederationDiagnosticError)) console.error(err);
3021
3473
  process.exit(1);
3022
3474
  });