@eventcatalog/core 4.7.4 → 4.7.6

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 (59) hide show
  1. package/LICENSE +95 -0
  2. package/README.md +3 -2
  3. package/dist/analytics/analytics.cjs +1 -1
  4. package/dist/analytics/analytics.js +2 -2
  5. package/dist/analytics/log-build.cjs +1 -1
  6. package/dist/analytics/log-build.js +3 -3
  7. package/dist/{chunk-7QTCSRAC.js → chunk-6BBLQLGL.js} +1 -1
  8. package/dist/chunk-DDALKGTF.js +7 -0
  9. package/dist/chunk-JJPB6EOZ.js +159 -0
  10. package/dist/chunk-MLIR3NL4.js +279 -0
  11. package/dist/{chunk-ZXVQXBTT.js → chunk-MNAPKH63.js} +24 -1
  12. package/dist/{chunk-TLWM7WRZ.js → chunk-Q534DWPZ.js} +1 -1
  13. package/dist/{chunk-ZIABX6SP.js → chunk-RZAJNR7O.js} +1 -1
  14. package/dist/chunk-WRNH5C3U.js +118 -0
  15. package/dist/{chunk-FDXJIZ74.js → chunk-YXANZO6Y.js} +1 -1
  16. package/dist/constants.cjs +1 -1
  17. package/dist/constants.js +1 -1
  18. package/dist/eventcatalog.cjs +560 -211
  19. package/dist/eventcatalog.config.d.cts +9 -3
  20. package/dist/eventcatalog.config.d.ts +9 -3
  21. package/dist/eventcatalog.js +40 -42
  22. package/dist/federation/content-cache.d.cts +5 -0
  23. package/dist/federation/content-cache.d.ts +5 -0
  24. package/dist/federation/diagnostics.cjs +187 -0
  25. package/dist/federation/diagnostics.d.cts +28 -0
  26. package/dist/federation/diagnostics.d.ts +28 -0
  27. package/dist/federation/diagnostics.js +14 -0
  28. package/dist/federation/entitlement.cjs +31 -0
  29. package/dist/federation/entitlement.d.cts +12 -0
  30. package/dist/federation/entitlement.d.ts +12 -0
  31. package/dist/federation/entitlement.js +6 -0
  32. package/dist/federation/federate.cjs +447 -118
  33. package/dist/federation/federate.d.cts +13 -2
  34. package/dist/federation/federate.d.ts +13 -2
  35. package/dist/federation/federate.js +9 -4
  36. package/dist/federation/filesystem-source-provider.d.cts +5 -0
  37. package/dist/federation/filesystem-source-provider.d.ts +5 -0
  38. package/dist/federation/github-source-provider.d.cts +5 -0
  39. package/dist/federation/github-source-provider.d.ts +5 -0
  40. package/dist/federation/output-transaction.cjs +152 -0
  41. package/dist/federation/output-transaction.d.cts +7 -0
  42. package/dist/federation/output-transaction.d.ts +7 -0
  43. package/dist/federation/output-transaction.js +6 -0
  44. package/dist/federation/public-assets.d.cts +5 -0
  45. package/dist/federation/public-assets.d.ts +5 -0
  46. package/dist/federation/source-provider.d.cts +5 -0
  47. package/dist/federation/source-provider.d.ts +5 -0
  48. package/dist/federation/source-provider.js +2 -2
  49. package/dist/federation/types.d.cts +5 -0
  50. package/dist/federation/types.d.ts +5 -0
  51. package/dist/generate.cjs +24 -1
  52. package/dist/generate.js +3 -3
  53. package/dist/utils/cli-logger.cjs +24 -1
  54. package/dist/utils/cli-logger.d.cts +6 -0
  55. package/dist/utils/cli-logger.d.ts +6 -0
  56. package/dist/utils/cli-logger.js +2 -2
  57. package/package.json +4 -4
  58. package/dist/chunk-RIUHZZRA.js +0 -204
  59. package/dist/{chunk-A2RZR3U4.js → chunk-6K7XZAYI.js} +3 -3
@@ -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_path16 = 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_path17 = __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.4";
147
+ var version = "4.7.6";
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
  }
@@ -389,7 +412,7 @@ var reportCloudResourceInventory = async (configFile, resourceCounts) => {
389
412
  })
390
413
  });
391
414
  };
392
- var main = async (projectDir, { isEventCatalogStarterEnabled: isEventCatalogStarterEnabled2, isEventCatalogScaleEnabled: isEventCatalogScaleEnabled2, isBackstagePluginEnabled }) => {
415
+ var main = async (projectDir, { isEventCatalogStarterEnabled: isEventCatalogStarterEnabled2, isEventCatalogScaleEnabled: isEventCatalogScaleEnabled3, isBackstagePluginEnabled }) => {
393
416
  try {
394
417
  await verifyRequiredFieldsAreInCatalogConfigFile(projectDir);
395
418
  const configFile = await getEventCatalogConfigFile(projectDir);
@@ -398,7 +421,7 @@ var main = async (projectDir, { isEventCatalogStarterEnabled: isEventCatalogStar
398
421
  if (isEventCatalogStarterEnabled2) {
399
422
  generatorNames.push("@eventcatalog/eventcatalog-starter");
400
423
  }
401
- if (isEventCatalogScaleEnabled2) {
424
+ if (isEventCatalogScaleEnabled3) {
402
425
  generatorNames.push("@eventcatalog/eventcatalog-scale");
403
426
  }
404
427
  if (isBackstagePluginEnabled) {
@@ -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 path17 = 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: path17,
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, path17, 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, `${path17}[]`, 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 path17 = 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: path17, 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, path17, resolved.required || [], rootSchema, fields);
1391
+ walkJsonSchema(resolved, path18, resolved.required || [], rootSchema, fields);
1369
1392
  }
1370
1393
  } else {
1371
- fields.push({ path: path17, 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: path17, 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, path17, 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, `${path17}[]`, prop.items.required || [], rootSchema, fields);
1407
+ walkJsonSchema(prop.items, `${path18}[]`, prop.items.required || [], rootSchema, fields);
1385
1408
  }
1386
1409
  }
1387
1410
  }
@@ -1901,8 +1924,8 @@ var createAstroDevLineFilter = () => {
1901
1924
 
1902
1925
  // src/federation/federate.ts
1903
1926
  var import_node_crypto4 = require("crypto");
1904
- var import_promises8 = __toESM(require("fs/promises"), 1);
1905
- var import_node_path15 = __toESM(require("path"), 1);
1927
+ var import_promises9 = __toESM(require("fs/promises"), 1);
1928
+ var import_node_path16 = __toESM(require("path"), 1);
1906
1929
  var import_sdk3 = __toESM(require("@eventcatalog/sdk"), 1);
1907
1930
 
1908
1931
  // src/federation/content-cache.ts
@@ -1946,28 +1969,299 @@ var createFederationContentCache = (projectDirectory, options = {}) => {
1946
1969
  };
1947
1970
  };
1948
1971
 
1949
- // src/federation/public-assets.ts
1950
- var import_node_crypto2 = require("crypto");
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
+ };
2028
+ }
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
+ };
2044
+ }
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
+ };
2056
+ }
2057
+ };
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
+ }
2076
+ }
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
+ });
2093
+ }
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
+ );
2114
+ };
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/entitlement.ts
2125
+ var import_license = require("@eventcatalog/license");
2126
+ var isFederationEnabled = () => (0, import_license.isEventCatalogScaleEnabled)();
2127
+
2128
+ // src/federation/output-transaction.ts
1951
2129
  var import_promises5 = __toESM(require("fs/promises"), 1);
1952
2130
  var import_node_path12 = __toESM(require("path"), 1);
2131
+ var getSafePublicPath = (publicDirectory, relativePath) => {
2132
+ const normalizedPath = import_node_path12.default.posix.normalize(relativePath);
2133
+ 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("../");
2134
+ if (unsafe) return void 0;
2135
+ const destinationPath = import_node_path12.default.resolve(publicDirectory, relativePath);
2136
+ const relativeDestinationPath = import_node_path12.default.relative(publicDirectory, destinationPath);
2137
+ return relativeDestinationPath !== "" && relativeDestinationPath !== ".." && !relativeDestinationPath.startsWith(`..${import_node_path12.default.sep}`) && !import_node_path12.default.isAbsolute(relativeDestinationPath) ? destinationPath : void 0;
2138
+ };
2139
+ var beginFederationOutputTransaction = async (projectDirectory, outDir, relativePublicPaths) => {
2140
+ const transactionDirectory = await import_promises5.default.mkdtemp(import_node_path12.default.join(projectDirectory, ".eventcatalog-federation-transaction-"));
2141
+ const previousOutDir = import_node_path12.default.join(transactionDirectory, "federated");
2142
+ const publicBackupDirectory = import_node_path12.default.join(transactionDirectory, "public");
2143
+ const publicDirectory = import_node_path12.default.resolve(projectDirectory, "public");
2144
+ const publicSnapshots = [];
2145
+ const missingPublicDirectories = /* @__PURE__ */ new Set();
2146
+ let hadPreviousOutDir = false;
2147
+ try {
2148
+ for (const relativePath of new Set(relativePublicPaths)) {
2149
+ const destinationPath = getSafePublicPath(publicDirectory, relativePath);
2150
+ if (!destinationPath) continue;
2151
+ try {
2152
+ const stat = await import_promises5.default.lstat(destinationPath);
2153
+ if (!stat.isFile()) continue;
2154
+ const backupPath = import_node_path12.default.join(publicBackupDirectory, `${publicSnapshots.length}`);
2155
+ await import_promises5.default.mkdir(import_node_path12.default.dirname(backupPath), { recursive: true });
2156
+ await import_promises5.default.copyFile(destinationPath, backupPath);
2157
+ publicSnapshots.push({ backupPath, destinationPath, state: "file" });
2158
+ } catch (error) {
2159
+ const code = error.code;
2160
+ if (code === "ENOTDIR") continue;
2161
+ if (code !== "ENOENT") throw error;
2162
+ publicSnapshots.push({ destinationPath, state: "missing" });
2163
+ let directory = import_node_path12.default.dirname(destinationPath);
2164
+ while (directory !== publicDirectory) {
2165
+ try {
2166
+ await import_promises5.default.lstat(directory);
2167
+ break;
2168
+ } catch (directoryError) {
2169
+ const directoryCode = directoryError.code;
2170
+ if (directoryCode === "ENOTDIR") break;
2171
+ if (directoryCode !== "ENOENT") throw directoryError;
2172
+ missingPublicDirectories.add(directory);
2173
+ directory = import_node_path12.default.dirname(directory);
2174
+ }
2175
+ }
2176
+ }
2177
+ }
2178
+ try {
2179
+ await import_promises5.default.rename(outDir, previousOutDir);
2180
+ hadPreviousOutDir = true;
2181
+ } catch (error) {
2182
+ if (error.code !== "ENOENT") throw error;
2183
+ }
2184
+ } catch (error) {
2185
+ await import_promises5.default.rm(transactionDirectory, { recursive: true, force: true });
2186
+ throw error;
2187
+ }
2188
+ return {
2189
+ commit: async () => {
2190
+ await import_promises5.default.rm(transactionDirectory, { recursive: true, force: true }).catch(() => void 0);
2191
+ },
2192
+ rollback: async () => {
2193
+ const rollbackErrors = [];
2194
+ const attempt = async (operation) => {
2195
+ try {
2196
+ await operation();
2197
+ } catch (error) {
2198
+ rollbackErrors.push(error);
2199
+ }
2200
+ };
2201
+ await attempt(() => import_promises5.default.rm(outDir, { recursive: true, force: true }));
2202
+ if (hadPreviousOutDir) await attempt(() => import_promises5.default.rename(previousOutDir, outDir));
2203
+ for (const snapshot of publicSnapshots) {
2204
+ if (snapshot.state === "missing") {
2205
+ await attempt(() => import_promises5.default.rm(snapshot.destinationPath, { force: true }));
2206
+ continue;
2207
+ }
2208
+ await attempt(async () => {
2209
+ await import_promises5.default.mkdir(import_node_path12.default.dirname(snapshot.destinationPath), { recursive: true });
2210
+ await import_promises5.default.copyFile(snapshot.backupPath, snapshot.destinationPath);
2211
+ });
2212
+ }
2213
+ for (const directory of [...missingPublicDirectories].sort((left, right) => right.length - left.length)) {
2214
+ await attempt(async () => {
2215
+ try {
2216
+ await import_promises5.default.rmdir(directory);
2217
+ } catch (error) {
2218
+ if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) throw error;
2219
+ }
2220
+ });
2221
+ }
2222
+ await attempt(() => import_promises5.default.rm(transactionDirectory, { recursive: true, force: true }));
2223
+ if (rollbackErrors.length > 0) throw new AggregateError(rollbackErrors, "Failed to restore the previous federation output");
2224
+ }
2225
+ };
2226
+ };
2227
+ var withFederationOutputTransaction = async (projectDirectory, outDir, relativePublicPaths, update) => {
2228
+ const transaction = await beginFederationOutputTransaction(projectDirectory, outDir, relativePublicPaths);
2229
+ try {
2230
+ const result = await update();
2231
+ await transaction.commit();
2232
+ return result;
2233
+ } catch (error) {
2234
+ try {
2235
+ await transaction.rollback();
2236
+ } catch (rollbackError) {
2237
+ throw new AggregateError([error, rollbackError], "Federation failed and the previous output could not be restored");
2238
+ }
2239
+ throw error;
2240
+ }
2241
+ };
2242
+
2243
+ // src/federation/public-assets.ts
2244
+ var import_node_crypto2 = require("crypto");
2245
+ var import_promises6 = __toESM(require("fs/promises"), 1);
2246
+ var import_node_path13 = __toESM(require("path"), 1);
1953
2247
  var getContentHash2 = (content) => `sha256:${(0, import_node_crypto2.createHash)("sha256").update(content).digest("hex")}`;
1954
2248
  var getFileHash = async (filePath) => {
1955
2249
  try {
1956
- const stat = await import_promises5.default.lstat(filePath);
1957
- return stat.isFile() ? getContentHash2(await import_promises5.default.readFile(filePath)) : void 0;
2250
+ const stat = await import_promises6.default.lstat(filePath);
2251
+ return stat.isFile() ? getContentHash2(await import_promises6.default.readFile(filePath)) : void 0;
1958
2252
  } catch (error) {
1959
2253
  if (error.code === "ENOENT") return void 0;
1960
2254
  throw error;
1961
2255
  }
1962
2256
  };
1963
2257
  var getSafePath = (directory, relativePath) => {
1964
- const normalizedPath = import_node_path12.default.posix.normalize(relativePath);
1965
- 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("../");
2258
+ const normalizedPath = import_node_path13.default.posix.normalize(relativePath);
2259
+ const unsafe = relativePath.length === 0 || relativePath.includes("\0") || relativePath.includes("\\") || import_node_path13.default.posix.isAbsolute(relativePath) || /^[a-zA-Z]:\//.test(relativePath) || normalizedPath !== relativePath || normalizedPath === ".." || normalizedPath.startsWith("../");
1966
2260
  if (unsafe) return void 0;
1967
- const resolvedDirectory = import_node_path12.default.resolve(directory);
1968
- const resolvedPath = import_node_path12.default.resolve(resolvedDirectory, relativePath);
1969
- const relativeResolvedPath = import_node_path12.default.relative(resolvedDirectory, resolvedPath);
1970
- if (relativeResolvedPath === "" || relativeResolvedPath === ".." || relativeResolvedPath.startsWith(`..${import_node_path12.default.sep}`) || import_node_path12.default.isAbsolute(relativeResolvedPath)) {
2261
+ const resolvedDirectory = import_node_path13.default.resolve(directory);
2262
+ const resolvedPath = import_node_path13.default.resolve(resolvedDirectory, relativePath);
2263
+ const relativeResolvedPath = import_node_path13.default.relative(resolvedDirectory, resolvedPath);
2264
+ if (relativeResolvedPath === "" || relativeResolvedPath === ".." || relativeResolvedPath.startsWith(`..${import_node_path13.default.sep}`) || import_node_path13.default.isAbsolute(relativeResolvedPath)) {
1971
2265
  return void 0;
1972
2266
  }
1973
2267
  return resolvedPath;
@@ -1975,23 +2269,23 @@ var getSafePath = (directory, relativePath) => {
1975
2269
  var listFiles = async (directory, relativeDirectory = "") => {
1976
2270
  let entries;
1977
2271
  try {
1978
- entries = await import_promises5.default.readdir(import_node_path12.default.join(directory, relativeDirectory), { withFileTypes: true });
2272
+ entries = await import_promises6.default.readdir(import_node_path13.default.join(directory, relativeDirectory), { withFileTypes: true });
1979
2273
  } catch (error) {
1980
2274
  if (error.code === "ENOENT") return [];
1981
2275
  throw error;
1982
2276
  }
1983
2277
  const files = await Promise.all(
1984
2278
  entries.map(async (entry) => {
1985
- const relativePath = import_node_path12.default.join(relativeDirectory, entry.name);
2279
+ const relativePath = import_node_path13.default.join(relativeDirectory, entry.name);
1986
2280
  if (entry.isDirectory()) return listFiles(directory, relativePath);
1987
- return entry.isFile() ? [relativePath.split(import_node_path12.default.sep).join("/")] : [];
2281
+ return entry.isFile() ? [relativePath.split(import_node_path13.default.sep).join("/")] : [];
1988
2282
  })
1989
2283
  );
1990
2284
  return files.flat().sort();
1991
2285
  };
1992
2286
  var pathExists = async (filePath) => {
1993
2287
  try {
1994
- await import_promises5.default.lstat(filePath);
2288
+ await import_promises6.default.lstat(filePath);
1995
2289
  return true;
1996
2290
  } catch (error) {
1997
2291
  if (error.code === "ENOENT") return false;
@@ -1999,27 +2293,27 @@ var pathExists = async (filePath) => {
1999
2293
  }
2000
2294
  };
2001
2295
  var hasBlockingParent = async (publicDirectory, destinationPath) => {
2002
- let currentPath = import_node_path12.default.dirname(destinationPath);
2296
+ let currentPath = import_node_path13.default.dirname(destinationPath);
2003
2297
  while (currentPath !== publicDirectory) {
2004
2298
  try {
2005
- if (!(await import_promises5.default.lstat(currentPath)).isDirectory()) return true;
2299
+ if (!(await import_promises6.default.lstat(currentPath)).isDirectory()) return true;
2006
2300
  } catch (error) {
2007
2301
  if (error.code !== "ENOENT") throw error;
2008
2302
  }
2009
- currentPath = import_node_path12.default.dirname(currentPath);
2303
+ currentPath = import_node_path13.default.dirname(currentPath);
2010
2304
  }
2011
2305
  return false;
2012
2306
  };
2013
2307
  var pruneEmptyDirectories = async (publicDirectory, filePath) => {
2014
- let currentPath = import_node_path12.default.dirname(filePath);
2308
+ let currentPath = import_node_path13.default.dirname(filePath);
2015
2309
  while (currentPath !== publicDirectory) {
2016
2310
  try {
2017
- await import_promises5.default.rmdir(currentPath);
2311
+ await import_promises6.default.rmdir(currentPath);
2018
2312
  } catch (error) {
2019
2313
  if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) throw error;
2020
2314
  if (error.code === "ENOTEMPTY") return;
2021
2315
  }
2022
- currentPath = import_node_path12.default.dirname(currentPath);
2316
+ currentPath = import_node_path13.default.dirname(currentPath);
2023
2317
  }
2024
2318
  };
2025
2319
  var composePublicAssets = async ({
@@ -2029,8 +2323,8 @@ var composePublicAssets = async ({
2029
2323
  previousFiles = {},
2030
2324
  collisionPaths = /* @__PURE__ */ new Set()
2031
2325
  }) => {
2032
- const publicDirectory = import_node_path12.default.resolve(projectDirectory, "public");
2033
- const federatedPublicDirectory = import_node_path12.default.resolve(federatedDirectory, "public");
2326
+ const publicDirectory = import_node_path13.default.resolve(projectDirectory, "public");
2327
+ const federatedPublicDirectory = import_node_path13.default.resolve(federatedDirectory, "public");
2034
2328
  const sourceFiles = await listFiles(federatedPublicDirectory);
2035
2329
  const sourceFileSet = new Set(sourceFiles);
2036
2330
  const managedFiles = /* @__PURE__ */ new Set();
@@ -2043,7 +2337,7 @@ var composePublicAssets = async ({
2043
2337
  if (sourceFileSet.has(relativePath)) continue;
2044
2338
  const destinationPath = getSafePath(publicDirectory, relativePath);
2045
2339
  if (!destinationPath) continue;
2046
- await import_promises5.default.rm(destinationPath, { force: true });
2340
+ await import_promises6.default.rm(destinationPath, { force: true });
2047
2341
  await pruneEmptyDirectories(publicDirectory, destinationPath);
2048
2342
  removed += 1;
2049
2343
  }
@@ -2065,31 +2359,31 @@ var composePublicAssets = async ({
2065
2359
  }
2066
2360
  const asset = publicAssetsByPath.get(relativePath);
2067
2361
  if (!asset) throw new Error(`Cannot identify the source of federated public asset "${relativePath}"`);
2068
- const content = await import_promises5.default.readFile(sourcePath);
2069
- await import_promises5.default.mkdir(import_node_path12.default.dirname(destinationPath), { recursive: true });
2070
- await import_promises5.default.writeFile(destinationPath, content);
2362
+ const content = await import_promises6.default.readFile(sourcePath);
2363
+ await import_promises6.default.mkdir(import_node_path13.default.dirname(destinationPath), { recursive: true });
2364
+ await import_promises6.default.writeFile(destinationPath, content);
2071
2365
  files[relativePath] = { source: asset.resolvedFrom.source, hash: getContentHash2(content) };
2072
2366
  copied += 1;
2073
2367
  if (collisionPaths.has(`public/${relativePath}`)) overwritten += 1;
2074
2368
  }
2075
- await import_promises5.default.rm(federatedPublicDirectory, { recursive: true, force: true });
2369
+ await import_promises6.default.rm(federatedPublicDirectory, { recursive: true, force: true });
2076
2370
  return { files, copied, skipped, overwritten, removed };
2077
2371
  };
2078
2372
 
2079
2373
  // src/federation/filesystem-source-provider.ts
2080
2374
  var import_node_crypto3 = require("crypto");
2081
- var import_promises6 = __toESM(require("fs/promises"), 1);
2082
- var import_node_path13 = __toESM(require("path"), 1);
2375
+ var import_promises7 = __toESM(require("fs/promises"), 1);
2376
+ var import_node_path14 = __toESM(require("path"), 1);
2083
2377
  var import_sdk = __toESM(require("@eventcatalog/sdk"), 1);
2084
2378
  var FILESYSTEM_SOURCE_PREFIX = "file:";
2085
2379
  var isWithinDirectory = (directory, target) => {
2086
- const relativePath = import_node_path13.default.relative(directory, target);
2087
- return relativePath === "" || !relativePath.startsWith(`..${import_node_path13.default.sep}`) && relativePath !== ".." && !import_node_path13.default.isAbsolute(relativePath);
2380
+ const relativePath = import_node_path14.default.relative(directory, target);
2381
+ return relativePath === "" || !relativePath.startsWith(`..${import_node_path14.default.sep}`) && relativePath !== ".." && !import_node_path14.default.isAbsolute(relativePath);
2088
2382
  };
2089
2383
  var assertPortableRelativePath = (filePath, label, allowRoot = true) => {
2090
2384
  const portablePath = filePath.replaceAll("\\", "/");
2091
- const normalizedPath = import_node_path13.default.posix.normalize(portablePath);
2092
- const isUnsafe = filePath.includes("\\") || filePath.includes("\0") || import_node_path13.default.posix.isAbsolute(normalizedPath) || /^[a-zA-Z]:\//.test(normalizedPath) || normalizedPath === ".." || normalizedPath.startsWith("../") || !allowRoot && (normalizedPath === "." || normalizedPath === "");
2385
+ const normalizedPath = import_node_path14.default.posix.normalize(portablePath);
2386
+ 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 === "");
2093
2387
  if (isUnsafe) throw new Error(`${label} "${filePath}" escapes its filesystem source`);
2094
2388
  return normalizedPath;
2095
2389
  };
@@ -2099,22 +2393,22 @@ var getSourceRoot = (projectDirectory, source) => {
2099
2393
  }
2100
2394
  const locator = source.source.slice(FILESYSTEM_SOURCE_PREFIX.length);
2101
2395
  if (!locator.trim()) throw new Error(`Filesystem federation source "${source.id}" requires a path after "file:".`);
2102
- return import_node_path13.default.resolve(projectDirectory, locator);
2396
+ return import_node_path14.default.resolve(projectDirectory, locator);
2103
2397
  };
2104
2398
  var getCatalogDirectory = async (projectDirectory, source) => {
2105
2399
  if (source.ref) throw new Error(`Filesystem federation source "${source.id}" does not support "ref".`);
2106
2400
  const sourceRoot = getSourceRoot(projectDirectory, source);
2107
2401
  const catalogPath = assertPortableRelativePath(source.path ?? ".", "Catalog path");
2108
- const catalogDirectory = import_node_path13.default.resolve(sourceRoot, ...catalogPath.split("/"));
2402
+ const catalogDirectory = import_node_path14.default.resolve(sourceRoot, ...catalogPath.split("/"));
2109
2403
  if (!isWithinDirectory(sourceRoot, catalogDirectory)) {
2110
2404
  throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
2111
2405
  }
2112
2406
  try {
2113
- const [realSourceRoot, realCatalogDirectory] = await Promise.all([import_promises6.default.realpath(sourceRoot), import_promises6.default.realpath(catalogDirectory)]);
2407
+ const [realSourceRoot, realCatalogDirectory] = await Promise.all([import_promises7.default.realpath(sourceRoot), import_promises7.default.realpath(catalogDirectory)]);
2114
2408
  if (!isWithinDirectory(realSourceRoot, realCatalogDirectory)) {
2115
2409
  throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
2116
2410
  }
2117
- if (!(await import_promises6.default.stat(realCatalogDirectory)).isDirectory()) {
2411
+ if (!(await import_promises7.default.stat(realCatalogDirectory)).isDirectory()) {
2118
2412
  throw new Error(`Filesystem federation source "${source.id}" is not a directory: ${catalogDirectory}`);
2119
2413
  }
2120
2414
  return realCatalogDirectory;
@@ -2127,12 +2421,12 @@ var getCatalogDirectory = async (projectDirectory, source) => {
2127
2421
  };
2128
2422
  var getArtifactPath = async (catalogDirectory, source, artifactPath) => {
2129
2423
  const normalizedPath = assertPortableRelativePath(artifactPath, "Federated artifact path", false);
2130
- const filePath = import_node_path13.default.resolve(catalogDirectory, ...normalizedPath.split("/"));
2424
+ const filePath = import_node_path14.default.resolve(catalogDirectory, ...normalizedPath.split("/"));
2131
2425
  if (!isWithinDirectory(catalogDirectory, filePath)) {
2132
2426
  throw new Error(`Federated artifact path "${artifactPath}" escapes source "${source.id}"`);
2133
2427
  }
2134
2428
  try {
2135
- const realFilePath = await import_promises6.default.realpath(filePath);
2429
+ const realFilePath = await import_promises7.default.realpath(filePath);
2136
2430
  if (!isWithinDirectory(catalogDirectory, realFilePath)) {
2137
2431
  throw new Error(`Federated artifact path "${artifactPath}" escapes source "${source.id}"`);
2138
2432
  }
@@ -2159,15 +2453,15 @@ var createFileSystemSourceProvider = (projectDirectory) => ({
2159
2453
  },
2160
2454
  async fetchContent({ source, path: artifactPath }) {
2161
2455
  const catalogDirectory = await getCatalogDirectory(projectDirectory, source);
2162
- return import_promises6.default.readFile(await getArtifactPath(catalogDirectory, source, artifactPath));
2456
+ return import_promises7.default.readFile(await getArtifactPath(catalogDirectory, source, artifactPath));
2163
2457
  }
2164
2458
  });
2165
2459
 
2166
2460
  // src/federation/github-source-provider.ts
2167
2461
  var import_node_child_process = require("child_process");
2168
- var import_promises7 = __toESM(require("fs/promises"), 1);
2462
+ var import_promises8 = __toESM(require("fs/promises"), 1);
2169
2463
  var import_node_os4 = __toESM(require("os"), 1);
2170
- var import_node_path14 = __toESM(require("path"), 1);
2464
+ var import_node_path15 = __toESM(require("path"), 1);
2171
2465
  var import_node_util = require("util");
2172
2466
  var import_sdk2 = __toESM(require("@eventcatalog/sdk"), 1);
2173
2467
  var execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
@@ -2178,8 +2472,8 @@ var parseGitHubSource = (source) => {
2178
2472
  };
2179
2473
  var assertSafeCatalogPath = (source) => {
2180
2474
  const catalogPath = source.path ?? ".";
2181
- const normalized = import_node_path14.default.posix.normalize(catalogPath.replaceAll("\\", "/"));
2182
- if (catalogPath.includes("\\") || import_node_path14.default.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
2475
+ const normalized = import_node_path15.default.posix.normalize(catalogPath.replaceAll("\\", "/"));
2476
+ if (catalogPath.includes("\\") || import_node_path15.default.posix.isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
2183
2477
  throw new Error(`Catalog path "${catalogPath}" escapes source "${source.id}"`);
2184
2478
  }
2185
2479
  };
@@ -2222,8 +2516,8 @@ var getGitEnvironment = (token) => {
2222
2516
  };
2223
2517
  var createCheckout = (executeFile, token) => async (source, ref, callback) => {
2224
2518
  const { owner, repository } = parseGitHubSource(source);
2225
- const catalogPath = import_node_path14.default.posix.normalize(source.path ?? ".");
2226
- const directory = await import_promises7.default.mkdtemp(import_node_path14.default.join(import_node_os4.default.tmpdir(), "eventcatalog-federation-"));
2519
+ const catalogPath = import_node_path15.default.posix.normalize(source.path ?? ".");
2520
+ const directory = await import_promises8.default.mkdtemp(import_node_path15.default.join(import_node_os4.default.tmpdir(), "eventcatalog-federation-"));
2227
2521
  const env = getGitEnvironment(token);
2228
2522
  try {
2229
2523
  const git = (args) => executeFile("git", args, { cwd: directory, env, encoding: "utf8" });
@@ -2237,22 +2531,22 @@ var createCheckout = (executeFile, token) => async (source, ref, callback) => {
2237
2531
  await git(["checkout", "--quiet", "--detach", "FETCH_HEAD"]);
2238
2532
  return await callback(directory);
2239
2533
  } finally {
2240
- await import_promises7.default.rm(directory, { recursive: true, force: true });
2534
+ await import_promises8.default.rm(directory, { recursive: true, force: true });
2241
2535
  }
2242
2536
  };
2243
2537
  var generateIndex = async (source, ref, checkout, executeFile) => checkout(source, ref, async (directory) => {
2244
2538
  const { stdout } = await executeFile("git", ["rev-parse", "HEAD"], { cwd: directory, encoding: "utf8" });
2245
2539
  const commit = stdout.trim();
2246
- const catalogDirectory = import_node_path14.default.resolve(directory, source.path ?? ".");
2247
- const relativeCatalogDirectory = import_node_path14.default.relative(directory, catalogDirectory);
2248
- if (relativeCatalogDirectory.startsWith("..") || import_node_path14.default.isAbsolute(relativeCatalogDirectory)) {
2540
+ const catalogDirectory = import_node_path15.default.resolve(directory, source.path ?? ".");
2541
+ const relativeCatalogDirectory = import_node_path15.default.relative(directory, catalogDirectory);
2542
+ if (relativeCatalogDirectory.startsWith("..") || import_node_path15.default.isAbsolute(relativeCatalogDirectory)) {
2249
2543
  throw new Error(`Catalog path "${source.path}" escapes source "${source.id}"`);
2250
2544
  }
2251
2545
  const index = await (0, import_sdk2.default)(catalogDirectory).buildIndex({ source: source.id, commit });
2252
2546
  return { bytes: Buffer.from(JSON.stringify(index)), index, commit, generated: true };
2253
2547
  });
2254
2548
  var fetchPublishedIndex = async (source, ref, fetcher, token) => {
2255
- const indexPath = import_node_path14.default.posix.join(source.path ?? ".", "catalog.index.json");
2549
+ const indexPath = import_node_path15.default.posix.join(source.path ?? ".", "catalog.index.json");
2256
2550
  const bytes = await fetchBytes(source, ref, indexPath, fetcher, token);
2257
2551
  if (!bytes) return void 0;
2258
2552
  const index = (0, import_sdk2.parseIndex)(JSON.parse(bytes.toString("utf8")));
@@ -2275,7 +2569,7 @@ var createGitHubSourceProvider = (options = {}) => {
2275
2569
  },
2276
2570
  async fetchContent({ source, commit, path: artifactPath }) {
2277
2571
  assertSafeCatalogPath(source);
2278
- const catalogPath = import_node_path14.default.posix.join(source.path ?? ".", artifactPath);
2572
+ const catalogPath = import_node_path15.default.posix.join(source.path ?? ".", artifactPath);
2279
2573
  const content = await fetchBytes(source, commit, catalogPath, fetcher, token);
2280
2574
  if (!content) throw new Error(`Federated artifact not found for "${source.id}": ${artifactPath}`);
2281
2575
  return content;
@@ -2313,6 +2607,16 @@ ${details}`);
2313
2607
  this.conflicts = conflicts;
2314
2608
  }
2315
2609
  };
2610
+ var FederationDiagnosticError = class extends Error {
2611
+ diagnostics;
2612
+ constructor(diagnostics) {
2613
+ const details = diagnostics.map((diagnostic) => `${diagnostic.rule}: ${diagnostic.message}`).join("\n");
2614
+ super(`Federation diagnostics prevent hydration:
2615
+ ${details}`);
2616
+ this.name = "FederationDiagnosticError";
2617
+ this.diagnostics = diagnostics;
2618
+ }
2619
+ };
2316
2620
  var validateSources = (sources) => {
2317
2621
  const ids = /* @__PURE__ */ new Set();
2318
2622
  for (const source of sources) {
@@ -2321,19 +2625,46 @@ var validateSources = (sources) => {
2321
2625
  ids.add(source.id);
2322
2626
  }
2323
2627
  };
2628
+ var createDiagnosticGraph = (graph, ownershipGraph, remoteSourceIds, localSourceId) => {
2629
+ const edges = ownershipGraph.edges.filter((edge) => remoteSourceIds.has(edge.fromResolvedFrom.source));
2630
+ const externalsByPointer = /* @__PURE__ */ new Map();
2631
+ for (const edge of edges.filter((edge2) => edge2.status === "external")) {
2632
+ const version2 = edge.pointer ?? void 0;
2633
+ const key = `${edge.to}@${version2 ?? ""}`;
2634
+ const external = externalsByPointer.get(key) ?? {
2635
+ id: edge.to,
2636
+ ...version2 === void 0 ? {} : { version: version2 },
2637
+ referencedBy: []
2638
+ };
2639
+ if (!external.referencedBy.includes(edge.from)) external.referencedBy.push(edge.from);
2640
+ externalsByPointer.set(key, external);
2641
+ }
2642
+ return {
2643
+ ...graph,
2644
+ entities: [
2645
+ ...graph.entities,
2646
+ ...ownershipGraph.entities.filter(
2647
+ (entity) => entity.resolvedFrom.source === localSourceId && entity.resolvedFrom.commit === "local"
2648
+ )
2649
+ ],
2650
+ edges,
2651
+ conflicts: ownershipGraph.conflicts,
2652
+ externals: [...externalsByPointer.values()]
2653
+ };
2654
+ };
2324
2655
  var writeLock = async (lockPath, lock) => {
2325
2656
  const temporaryPath = `${lockPath}.tmp-${process.pid}`;
2326
2657
  try {
2327
- await import_promises8.default.writeFile(temporaryPath, `${JSON.stringify(lock, null, 2)}
2658
+ await import_promises9.default.writeFile(temporaryPath, `${JSON.stringify(lock, null, 2)}
2328
2659
  `, "utf8");
2329
- await import_promises8.default.rename(temporaryPath, lockPath);
2660
+ await import_promises9.default.rename(temporaryPath, lockPath);
2330
2661
  } finally {
2331
- await import_promises8.default.rm(temporaryPath, { force: true });
2662
+ await import_promises9.default.rm(temporaryPath, { force: true });
2332
2663
  }
2333
2664
  };
2334
2665
  var readLock = async (lockPath) => {
2335
2666
  try {
2336
- return JSON.parse(await import_promises8.default.readFile(lockPath, "utf8"));
2667
+ return JSON.parse(await import_promises9.default.readFile(lockPath, "utf8"));
2337
2668
  } catch (error) {
2338
2669
  if (error.code === "ENOENT") return void 0;
2339
2670
  throw new Error(`Cannot read federation lock at "${lockPath}"`, { cause: error });
@@ -2341,7 +2672,7 @@ var readLock = async (lockPath) => {
2341
2672
  };
2342
2673
  var pathExists2 = async (filePath) => {
2343
2674
  try {
2344
- await import_promises8.default.access(filePath);
2675
+ await import_promises9.default.access(filePath);
2345
2676
  return true;
2346
2677
  } catch (error) {
2347
2678
  if (error.code === "ENOENT") return false;
@@ -2349,20 +2680,27 @@ var pathExists2 = async (filePath) => {
2349
2680
  }
2350
2681
  };
2351
2682
  var cleanupPreviousFederation = async (projectDirectory, onProgress) => {
2352
- const outDir = import_node_path15.default.join(projectDirectory, "federated");
2353
- const lockPath = import_node_path15.default.join(projectDirectory, "eventcatalog.lock");
2683
+ const outDir = import_node_path16.default.join(projectDirectory, "federated");
2684
+ const lockPath = import_node_path16.default.join(projectDirectory, "eventcatalog.lock");
2354
2685
  const previousLock = await readLock(lockPath);
2355
2686
  const hadFederatedOutput = await pathExists2(outDir);
2356
2687
  const hadLock = previousLock !== void 0;
2357
2688
  if (!hadFederatedOutput && !hadLock) return;
2358
- await import_promises8.default.rm(outDir, { recursive: true, force: true });
2359
- const publicResult = await composePublicAssets({
2689
+ const publicResult = await withFederationOutputTransaction(
2360
2690
  projectDirectory,
2361
- federatedDirectory: outDir,
2362
- assets: [],
2363
- previousFiles: previousLock?.publicFiles
2364
- });
2365
- await import_promises8.default.rm(lockPath, { force: true });
2691
+ outDir,
2692
+ Object.keys(previousLock?.publicFiles ?? {}),
2693
+ async () => {
2694
+ const result = await composePublicAssets({
2695
+ projectDirectory,
2696
+ federatedDirectory: outDir,
2697
+ assets: [],
2698
+ previousFiles: previousLock?.publicFiles
2699
+ });
2700
+ await import_promises9.default.rm(lockPath, { force: true });
2701
+ return result;
2702
+ }
2703
+ );
2366
2704
  onProgress?.({
2367
2705
  type: "cleanup:complete",
2368
2706
  federated: hadFederatedOutput,
@@ -2378,12 +2716,13 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2378
2716
  await cleanupPreviousFederation(projectDirectory, options.onProgress);
2379
2717
  return null;
2380
2718
  }
2381
- if (options.isFederationEnabled && !await options.isFederationEnabled()) {
2719
+ if (!await isFederationEnabled()) {
2382
2720
  throw new Error(
2383
2721
  "Cannot federate catalogs: EventCatalog federation is an Enterprise feature. Visit https://www.eventcatalog.dev/pricing to enable federation."
2384
2722
  );
2385
2723
  }
2386
2724
  validateSources(sources);
2725
+ const rules = resolveFederationRules(config.federation?.rules);
2387
2726
  if (options.useCache === false) options.onProgress?.({ type: "cache:disabled" });
2388
2727
  const provider = options.provider ?? createFederationSourceProvider(projectDirectory);
2389
2728
  const resolvedSources = [];
@@ -2407,8 +2746,8 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2407
2746
  throw new Error(`Failed to federate source "${source.id}": ${message}`, { cause: error });
2408
2747
  }
2409
2748
  }
2410
- const outDir = import_node_path15.default.join(projectDirectory, "federated");
2411
- const lockPath = import_node_path15.default.join(projectDirectory, "eventcatalog.lock");
2749
+ const outDir = import_node_path16.default.join(projectDirectory, "federated");
2750
+ const lockPath = import_node_path16.default.join(projectDirectory, "eventcatalog.lock");
2412
2751
  const previousLock = await readLock(lockPath);
2413
2752
  const resources = resolvedSources.reduce((total, source) => total + source.resolved.index.resources.length, 0);
2414
2753
  const remoteIndexes = resolvedSources.map(({ resolved }) => resolved.index);
@@ -2422,60 +2761,79 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2422
2761
  options.onProgress?.({ type: "local:complete", resources: localIndex.resources.length });
2423
2762
  options.onProgress?.({ type: "resolving", resources, localResources: localIndex.resources.length });
2424
2763
  const ownershipGraph = (0, import_sdk3.resolve)([localIndex, ...remoteIndexes]);
2425
- if (ownershipGraph.conflicts.length > 0) {
2426
- options.onProgress?.({ type: "resolved", graph: ownershipGraph });
2427
- throw new FederationConflictError(ownershipGraph.conflicts);
2428
- }
2429
2764
  const graph = (0, import_sdk3.resolve)(remoteIndexes);
2430
- options.onProgress?.({ type: "resolved", graph });
2431
- const sourcesById = new Map(sources.map((source) => [source.id, source]));
2432
- options.onProgress?.({ type: "hydrating", outDir });
2433
- let hydratedFiles = 0;
2434
- let cachedFiles = 0;
2435
- const hydrateResult = await (0, import_sdk3.hydrate)(graph, {
2765
+ const diagnosticGraph = createDiagnosticGraph(
2766
+ graph,
2767
+ ownershipGraph,
2768
+ new Set(remoteIndexes.map((index) => index.source)),
2769
+ localIndex.source
2770
+ );
2771
+ const diagnostics = getFederationDiagnostics(diagnosticGraph, rules);
2772
+ options.onProgress?.({ type: "resolved", graph, diagnostics });
2773
+ const blockingConflicts = ownershipGraph.conflicts.filter((conflict) => rules[`federation/${conflict.kind}`] === "error");
2774
+ if (blockingConflicts.length > 0) throw new FederationConflictError(blockingConflicts);
2775
+ const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
2776
+ if (blockingDiagnostics.length > 0) throw new FederationDiagnosticError(blockingDiagnostics);
2777
+ const publicPaths = [
2778
+ ...Object.keys(previousLock?.publicFiles ?? {}),
2779
+ ...graph.assets.filter((asset) => asset.path.startsWith("public/")).map((asset) => asset.path.slice("public/".length))
2780
+ ];
2781
+ const { hydrateResult, publicResult } = await withFederationOutputTransaction(
2782
+ projectDirectory,
2436
2783
  outDir,
2437
- cache: createFederationContentCache(projectDirectory, {
2438
- read: options.useCache !== false,
2439
- onHit: () => {
2440
- cachedFiles += 1;
2441
- options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
2442
- }
2443
- }),
2444
- modes: Object.fromEntries(sources.map((source) => [source.id, source.mode ?? "hydrate"])),
2445
- fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
2446
- const source = sourcesById.get(sourceId);
2447
- if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
2448
- const content = await provider.fetchContent({ source, commit, path: artifactPath });
2449
- hydratedFiles += 1;
2450
- options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
2451
- return content;
2784
+ publicPaths,
2785
+ async () => {
2786
+ const sourcesById = new Map(sources.map((source) => [source.id, source]));
2787
+ options.onProgress?.({ type: "hydrating", outDir });
2788
+ let hydratedFiles = 0;
2789
+ let cachedFiles = 0;
2790
+ const hydrateResult2 = await (0, import_sdk3.hydrate)(graph, {
2791
+ outDir,
2792
+ cache: createFederationContentCache(projectDirectory, {
2793
+ read: options.useCache !== false,
2794
+ onHit: () => {
2795
+ cachedFiles += 1;
2796
+ options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
2797
+ }
2798
+ }),
2799
+ fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
2800
+ const source = sourcesById.get(sourceId);
2801
+ if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
2802
+ const content = await provider.fetchContent({ source, commit, path: artifactPath });
2803
+ hydratedFiles += 1;
2804
+ options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
2805
+ return content;
2806
+ }
2807
+ });
2808
+ const publicResult2 = await composePublicAssets({
2809
+ projectDirectory,
2810
+ federatedDirectory: outDir,
2811
+ assets: graph.assets,
2812
+ previousFiles: previousLock?.publicFiles,
2813
+ collisionPaths: new Set(
2814
+ graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
2815
+ )
2816
+ });
2817
+ options.onProgress?.({ type: "public:complete", result: publicResult2 });
2818
+ const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
2819
+ await writeLock(lockPath, {
2820
+ lockVersion: 1,
2821
+ sources: resolvedSources.map(({ config: source, resolved }) => ({
2822
+ id: source.id,
2823
+ digest: `sha256:${(0, import_node_crypto4.createHash)("sha256").update(resolved.bytes).digest("hex")}`,
2824
+ commit: resolved.commit,
2825
+ resolvedAt
2826
+ })).sort((left, right) => left.id.localeCompare(right.id)),
2827
+ publicFiles: publicResult2.files
2828
+ });
2829
+ return { hydrateResult: hydrateResult2, publicResult: publicResult2 };
2452
2830
  }
2453
- });
2454
- const publicResult = await composePublicAssets({
2455
- projectDirectory,
2456
- federatedDirectory: outDir,
2457
- assets: graph.assets,
2458
- previousFiles: previousLock?.publicFiles,
2459
- collisionPaths: new Set(
2460
- graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
2461
- )
2462
- });
2463
- options.onProgress?.({ type: "public:complete", result: publicResult });
2464
- const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
2465
- await writeLock(lockPath, {
2466
- lockVersion: 1,
2467
- sources: resolvedSources.map(({ config: source, resolved }) => ({
2468
- id: source.id,
2469
- digest: `sha256:${(0, import_node_crypto4.createHash)("sha256").update(resolved.bytes).digest("hex")}`,
2470
- commit: resolved.commit,
2471
- resolvedAt
2472
- })).sort((left, right) => left.id.localeCompare(right.id)),
2473
- publicFiles: publicResult.files
2474
- });
2831
+ );
2475
2832
  const result = {
2476
2833
  sources: sources.length,
2477
2834
  resources,
2478
2835
  graph,
2836
+ diagnostics,
2479
2837
  hydrate: hydrateResult,
2480
2838
  public: publicResult,
2481
2839
  outDir,
@@ -2486,15 +2844,15 @@ var federateCatalog = async (projectDirectory, options = {}) => {
2486
2844
  };
2487
2845
 
2488
2846
  // src/eventcatalog.ts
2489
- var import_license = require("@eventcatalog/license");
2490
- var currentDir = import_node_path17.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
2847
+ var import_license2 = require("@eventcatalog/license");
2848
+ var currentDir = import_node_path18.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
2491
2849
  var program = new import_commander.Command().version(VERSION);
2492
- var dir = import_node_path17.default.resolve(process.env.PROJECT_DIR || process.cwd());
2493
- var core = import_node_path17.default.resolve(process.env.CATALOG_DIR || (0, import_node_path16.join)(dir, ".eventcatalog-core"));
2494
- var eventCatalogDir = import_node_path17.default.resolve((0, import_node_path16.join)(currentDir, "../eventcatalog/"));
2850
+ var dir = import_node_path18.default.resolve(process.env.PROJECT_DIR || process.cwd());
2851
+ var core = import_node_path18.default.resolve(process.env.CATALOG_DIR || (0, import_node_path17.join)(dir, ".eventcatalog-core"));
2852
+ var eventCatalogDir = import_node_path18.default.resolve((0, import_node_path17.join)(currentDir, "../eventcatalog/"));
2495
2853
  var getInstalledEventCatalogVersion = () => {
2496
2854
  try {
2497
- const pkg = import_fs3.default.readFileSync((0, import_node_path16.join)(dir, "package.json"), "utf8");
2855
+ const pkg = import_fs3.default.readFileSync((0, import_node_path17.join)(dir, "package.json"), "utf8");
2498
2856
  const json = JSON.parse(pkg);
2499
2857
  return json.dependencies["@eventcatalog/core"];
2500
2858
  } catch (error) {
@@ -2560,12 +2918,12 @@ var startDevPrewarm = ({
2560
2918
  var buildDevSearchIndex = async ({ config }) => {
2561
2919
  const result = await buildSearchIndex({
2562
2920
  projectDir: dir,
2563
- outDir: import_node_path17.default.join(core, "public"),
2564
- searchOutputPath: import_node_path17.default.join(core, "public", "pagefind"),
2921
+ outDir: import_node_path18.default.join(core, "public"),
2922
+ searchOutputPath: import_node_path18.default.join(core, "public", "pagefind"),
2565
2923
  config,
2566
2924
  isServer: false
2567
2925
  });
2568
- logger.info(`Indexed ${result.records} page(s) into ${import_node_path17.default.relative(core, result.outputPath)}`, "search");
2926
+ logger.info(`Indexed ${result.records} page(s) into ${import_node_path18.default.relative(core, result.outputPath)}`, "search");
2569
2927
  };
2570
2928
  var warnIfIndexedSearchUsesAuth = async () => {
2571
2929
  if (!await isAuthEnabled()) {
@@ -2680,12 +3038,12 @@ var copyCore = () => {
2680
3038
  import_fs3.default.cpSync(eventCatalogDir, core, {
2681
3039
  recursive: true,
2682
3040
  filter: (src) => {
2683
- const relativePath = import_node_path17.default.relative(eventCatalogDir, src);
2684
- const pathParts = relativePath.split(import_node_path17.default.sep);
3041
+ const relativePath = import_node_path18.default.relative(eventCatalogDir, src);
3042
+ const pathParts = relativePath.split(import_node_path18.default.sep);
2685
3043
  return !pathParts.some((part) => [".astro", "dist", "node_modules"].includes(part));
2686
3044
  }
2687
3045
  });
2688
- const coreNodeModules = import_node_path17.default.join(core, "node_modules");
3046
+ const coreNodeModules = import_node_path18.default.join(core, "node_modules");
2689
3047
  const installedCoreNodeModules = resolveInstalledCoreNodeModules(currentDir);
2690
3048
  linkCoreNodeModules({ coreNodeModules, installedCoreNodeModules });
2691
3049
  };
@@ -2744,8 +3102,8 @@ program.command("dev").description("Run development server of EventCatalog").opt
2744
3102
  logger.info("Setting up EventCatalog...", "eventcatalog");
2745
3103
  const isServer = await isOutputServer();
2746
3104
  logger.info(isServer ? "EventCatalog is running in Server Mode" : "EventCatalog is running in Static Mode", "config");
2747
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2748
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3105
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3106
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2749
3107
  }
2750
3108
  if (options.debug) {
2751
3109
  logger.info("Debug mode enabled", "debug");
@@ -2759,12 +3117,12 @@ program.command("dev").description("Run development server of EventCatalog").opt
2759
3117
  await runMigrations(dir);
2760
3118
  await catalogToAstro(dir, core);
2761
3119
  const config = await getEventCatalogConfigFile(dir);
2762
- const canEmbedPages = await (0, import_license.isFeatureEnabled)(
3120
+ const canEmbedPages = await (0, import_license2.isFeatureEnabled)(
2763
3121
  "@eventcatalog/backstage-plugin-eventcatalog",
2764
3122
  process.env.EVENTCATALOG_LICENSE_KEY_BACKSTAGE
2765
3123
  );
2766
- const isEventCatalogStarter = await (0, import_license.isEventCatalogStarterEnabled)();
2767
- const isEventCatalogScale = await (0, import_license.isEventCatalogScaleEnabled)();
3124
+ const isEventCatalogStarter = await (0, import_license2.isEventCatalogStarterEnabled)();
3125
+ const isEventCatalogScale = await (0, import_license2.isEventCatalogScaleEnabled)();
2768
3126
  if (isServer) {
2769
3127
  try {
2770
3128
  logger.info("Building fields index...", "fields");
@@ -2825,17 +3183,17 @@ program.command("build").description("Run build of EventCatalog").action(async (
2825
3183
  logger.info("Building EventCatalog...", "build");
2826
3184
  const isServer = await isOutputServer();
2827
3185
  logger.info(isServer ? "EventCatalog is running in Server Mode" : "EventCatalog is running in Static Mode", "config");
2828
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2829
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3186
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3187
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2830
3188
  }
2831
3189
  await verifyRequiredFieldsAreInCatalogConfigFile(dir);
2832
3190
  copyCore();
2833
- const isBackstagePluginEnabled = await (0, import_license.isFeatureEnabled)(
3191
+ const isBackstagePluginEnabled = await (0, import_license2.isFeatureEnabled)(
2834
3192
  "@eventcatalog/backstage-plugin-eventcatalog",
2835
3193
  process.env.EVENTCATALOG_LICENSE_KEY_BACKSTAGE
2836
3194
  );
2837
- const isEventCatalogStarter = await (0, import_license.isEventCatalogStarterEnabled)();
2838
- const isEventCatalogScale = await (0, import_license.isEventCatalogScaleEnabled)();
3195
+ const isEventCatalogStarter = await (0, import_license2.isEventCatalogStarterEnabled)();
3196
+ const isEventCatalogScale = await (0, import_license2.isEventCatalogScaleEnabled)();
2839
3197
  const canEmbedPages = isBackstagePluginEnabled || isEventCatalogScale;
2840
3198
  await log_build_default(dir, {
2841
3199
  isEventCatalogStarterEnabled: isEventCatalogStarter,
@@ -2876,7 +3234,7 @@ program.command("build").description("Run build of EventCatalog").action(async (
2876
3234
  if (await isIndexedSearchEnabled()) {
2877
3235
  await warnIfIndexedSearchUsesAuth();
2878
3236
  const config = await getEventCatalogConfigFile(dir);
2879
- const outDir = import_node_path17.default.resolve(dir, await getProjectOutDir());
3237
+ const outDir = import_node_path18.default.resolve(dir, await getProjectOutDir());
2880
3238
  logger.info("Building indexed search...", "search");
2881
3239
  const result = await buildSearchIndex({
2882
3240
  projectDir: dir,
@@ -2884,7 +3242,7 @@ program.command("build").description("Run build of EventCatalog").action(async (
2884
3242
  config,
2885
3243
  isServer
2886
3244
  });
2887
- logger.info(`Indexed ${result.records} page(s) into ${import_node_path17.default.relative(dir, result.outputPath)}`, "search");
3245
+ logger.info(`Indexed ${result.records} page(s) into ${import_node_path18.default.relative(dir, result.outputPath)}`, "search");
2888
3246
  }
2889
3247
  });
2890
3248
  var previewCatalog = async ({
@@ -2911,7 +3269,7 @@ var startServerCatalog = async ({
2911
3269
  isEventCatalogStarter = false,
2912
3270
  isEventCatalogScale = false
2913
3271
  }) => {
2914
- const serverEntryPath = import_node_path17.default.join(dir, "dist", "server", "entry.mjs");
3272
+ const serverEntryPath = import_node_path18.default.join(dir, "dist", "server", "entry.mjs");
2915
3273
  await runCommandWithFilteredOutput({
2916
3274
  command: `node "${serverEntryPath}"`,
2917
3275
  cwd: core,
@@ -2928,15 +3286,15 @@ var startServerCatalog = async ({
2928
3286
  program.command("preview").description("Serves the contents of your eventcatalog build directory").action(async (options, command) => {
2929
3287
  logger.welcome();
2930
3288
  logger.info("Starting preview of your build...", "preview");
2931
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2932
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3289
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3290
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2933
3291
  }
2934
- const canEmbedPages = await (0, import_license.isFeatureEnabled)(
3292
+ const canEmbedPages = await (0, import_license2.isFeatureEnabled)(
2935
3293
  "@eventcatalog/backstage-plugin-eventcatalog",
2936
3294
  process.env.EVENTCATALOG_LICENSE_KEY_BACKSTAGE
2937
3295
  );
2938
- const isEventCatalogStarter = await (0, import_license.isEventCatalogStarterEnabled)();
2939
- const isEventCatalogScale = await (0, import_license.isEventCatalogScaleEnabled)();
3296
+ const isEventCatalogStarter = await (0, import_license2.isEventCatalogStarterEnabled)();
3297
+ const isEventCatalogScale = await (0, import_license2.isEventCatalogScaleEnabled)();
2940
3298
  await previewCatalog({
2941
3299
  command,
2942
3300
  canEmbedPages: canEmbedPages || isEventCatalogScale,
@@ -2947,15 +3305,15 @@ program.command("preview").description("Serves the contents of your eventcatalog
2947
3305
  program.command("start").description("Serves the contents of your eventcatalog build directory").action(async (options, command) => {
2948
3306
  logger.welcome();
2949
3307
  logger.info("Starting preview of your build...", "preview");
2950
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2951
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3308
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3309
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2952
3310
  }
2953
- const canEmbedPages = await (0, import_license.isFeatureEnabled)(
3311
+ const canEmbedPages = await (0, import_license2.isFeatureEnabled)(
2954
3312
  "@eventcatalog/backstage-plugin-eventcatalog",
2955
3313
  process.env.EVENTCATALOG_LICENSE_KEY_BACKSTAGE
2956
3314
  );
2957
- const isEventCatalogStarter = await (0, import_license.isEventCatalogStarterEnabled)();
2958
- const isEventCatalogScale = await (0, import_license.isEventCatalogScaleEnabled)();
3315
+ const isEventCatalogStarter = await (0, import_license2.isEventCatalogStarterEnabled)();
3316
+ const isEventCatalogScale = await (0, import_license2.isEventCatalogScaleEnabled)();
2959
3317
  const isServerOutput = await isOutputServer();
2960
3318
  if (isServerOutput) {
2961
3319
  await startServerCatalog({
@@ -2975,26 +3333,26 @@ program.command("start").description("Serves the contents of your eventcatalog b
2975
3333
  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) => {
2976
3334
  logger.welcome();
2977
3335
  logger.info("Exporting EventCatalog...", "export");
2978
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2979
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3336
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3337
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2980
3338
  }
2981
3339
  const { default: initSDK } = await import("@eventcatalog/sdk");
2982
3340
  const sdk = initSDK(dir);
2983
3341
  const catalog = await sdk.dumpCatalog({ includeMarkdown: options.includeMarkdown });
2984
- const exportsDir = import_node_path17.default.join(dir, "exports");
3342
+ const exportsDir = import_node_path18.default.join(dir, "exports");
2985
3343
  ensureDir(exportsDir);
2986
3344
  const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
2987
- const exportFile = import_node_path17.default.join(exportsDir, `catalog-${date}.json`);
3345
+ const exportFile = import_node_path18.default.join(exportsDir, `catalog-${date}.json`);
2988
3346
  import_fs3.default.writeFileSync(exportFile, JSON.stringify(catalog, null, 2), "utf-8");
2989
3347
  logger.info(`Catalog exported to ${exportFile}`, "export");
2990
3348
  });
2991
3349
  program.command("generate [siteDir]").description("Start the generator scripts.").action(async () => {
2992
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
2993
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3350
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3351
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
2994
3352
  }
2995
3353
  await generate(dir);
2996
3354
  });
2997
- var reportFederationProgress = (event) => {
3355
+ var reportFederationProgress = (event, verbose = false) => {
2998
3356
  switch (event.type) {
2999
3357
  case "configured":
3000
3358
  if (event.sources > 0)
@@ -3034,38 +3392,36 @@ var reportFederationProgress = (event) => {
3034
3392
  );
3035
3393
  return;
3036
3394
  case "resolved":
3037
- if (event.graph.conflicts.length > 0) {
3038
- logger.error(
3039
- `${event.graph.conflicts.length} ownership conflict${event.graph.conflicts.length === 1 ? "" : "s"} found`,
3395
+ const counts = getFederationDiagnosticCounts(event.diagnostics);
3396
+ const showDiagnosticDetails = verbose || counts.errors > 0;
3397
+ if (counts.errors === 0) {
3398
+ logger.success(
3399
+ `Graph resolved: ${event.graph.entities.length} remote resources, ${event.graph.edges.length} relationships`,
3040
3400
  "federation"
3041
3401
  );
3042
- for (const conflict of event.graph.conflicts.slice(0, 10)) {
3043
- logger.error(`${conflict.id}: ${conflict.sources.join(", ")}`, "federation");
3044
- }
3045
- if (event.graph.conflicts.length > 10) {
3046
- logger.error(`...and ${event.graph.conflicts.length - 10} more`, "federation");
3047
- }
3048
- return;
3049
3402
  }
3050
- logger.success(
3051
- `Graph resolved: ${event.graph.entities.length} remote resources, ${event.graph.edges.length} relationships`,
3052
- "federation"
3053
- );
3054
- if (event.graph.warnings.length > 0) {
3403
+ if (counts.errors + counts.warnings === 0) return;
3404
+ if (showDiagnosticDetails) {
3405
+ const diagnostics = getVisibleFederationDiagnostics(event.diagnostics, verbose);
3406
+ logger.info("Federation diagnostics", "federation");
3407
+ logger.line();
3408
+ for (const diagnostic of diagnostics) {
3409
+ logger.diagnostic(diagnostic.severity, diagnostic.message, diagnostic.rule, diagnostic.attributes);
3410
+ logger.line();
3411
+ }
3412
+ logger.diagnosticSummary(counts.errors, counts.warnings);
3413
+ } else {
3055
3414
  logger.warning(
3056
- `${event.graph.warnings.length} federation warning${event.graph.warnings.length === 1 ? "" : "s"}`,
3415
+ `${counts.warnings} problem${counts.warnings === 1 ? "" : "s"} (0 errors, ${counts.warnings} warning${counts.warnings === 1 ? "" : "s"})`,
3057
3416
  "federation"
3058
3417
  );
3059
3418
  }
3060
- if (event.graph.externals.length > 0) {
3061
- logger.warning(
3062
- `${event.graph.externals.length} reference${event.graph.externals.length === 1 ? "" : "s"} remain external`,
3063
- "federation"
3064
- );
3419
+ if (!verbose && counts.warnings > 0) {
3420
+ logger.info("Run with --verbose to see warning details.", "federation");
3065
3421
  }
3066
3422
  return;
3067
3423
  case "hydrating":
3068
- logger.info(`Hydrating federated content into ${import_node_path17.default.relative(dir, event.outDir)}/...`, "federation");
3424
+ logger.info(`Hydrating federated content into ${import_node_path18.default.relative(dir, event.outDir)}/...`, "federation");
3069
3425
  return;
3070
3426
  case "hydrate:cache":
3071
3427
  if (event.files === 1 || event.files % 25 === 0) {
@@ -3082,34 +3438,27 @@ var reportFederationProgress = (event) => {
3082
3438
  `Public assets: ${event.result.copied} copied, ${event.result.skipped} preserved from the main catalog, ${event.result.removed} stale removed`,
3083
3439
  "federation"
3084
3440
  );
3085
- if (event.result.overwritten > 0) {
3086
- logger.warning(
3087
- `${event.result.overwritten} public asset conflict${event.result.overwritten === 1 ? "" : "s"} resolved using the last configured source`,
3088
- "federation"
3089
- );
3090
- }
3091
3441
  return;
3092
3442
  case "complete":
3093
3443
  logger.success(
3094
3444
  `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)`,
3095
3445
  "federation"
3096
3446
  );
3097
- logger.info(`Pinned source commits in ${import_node_path17.default.relative(dir, event.result.lockPath)}`, "federation");
3447
+ logger.info(`Recorded resolved source state in ${import_node_path18.default.relative(dir, event.result.lockPath)}`, "federation");
3098
3448
  }
3099
3449
  };
3100
- 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) => {
3450
+ 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) => {
3101
3451
  logger.welcome();
3102
- if (import_fs3.default.existsSync(import_node_path17.default.join(dir, ".env"))) {
3103
- import_dotenv.default.config({ path: import_node_path17.default.join(dir, ".env") });
3452
+ if (import_fs3.default.existsSync(import_node_path18.default.join(dir, ".env"))) {
3453
+ import_dotenv.default.config({ path: import_node_path18.default.join(dir, ".env") });
3104
3454
  }
3105
3455
  logger.info("Starting federation...", "federation");
3106
3456
  let cleanedPreviousOutput = false;
3107
3457
  const result = await federateCatalog(dir, {
3108
3458
  useCache: commandOptions.cache,
3109
- isFederationEnabled: import_license.isEventCatalogScaleEnabled,
3110
3459
  onProgress: (event) => {
3111
3460
  if (event.type === "cleanup:complete") cleanedPreviousOutput = true;
3112
- reportFederationProgress(event);
3461
+ reportFederationProgress(event, commandOptions.verbose);
3113
3462
  }
3114
3463
  });
3115
3464
  if (!result && !cleanedPreviousOutput) logger.warning("No federation sources configured; nothing to do.", "federation");
@@ -3123,6 +3472,6 @@ program.addHelpText(
3123
3472
  `
3124
3473
  );
3125
3474
  program.parseAsync().then(() => process.exit(0)).catch((err) => {
3126
- if (!(err instanceof FederationConflictError)) console.error(err);
3475
+ if (!(err instanceof FederationConflictError) && !(err instanceof FederationDiagnosticError)) console.error(err);
3127
3476
  process.exit(1);
3128
3477
  });