@eventcatalog/core 4.7.4 → 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 (37) 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-FDXJIZ74.js → chunk-2BYTIH4L.js} +1 -1
  6. package/dist/{chunk-7QTCSRAC.js → chunk-EUZT4RKY.js} +1 -1
  7. package/dist/{chunk-RIUHZZRA.js → chunk-GYYYAUJD.js} +128 -56
  8. package/dist/chunk-JJPB6EOZ.js +159 -0
  9. package/dist/{chunk-ZIABX6SP.js → chunk-QJRY6Q7S.js} +1 -1
  10. package/dist/{chunk-ZXVQXBTT.js → chunk-QQPH6RCB.js} +24 -1
  11. package/dist/chunk-WRNH5C3U.js +118 -0
  12. package/dist/{chunk-TLWM7WRZ.js → chunk-ZBPMCEXS.js} +1 -1
  13. package/dist/constants.cjs +1 -1
  14. package/dist/constants.js +1 -1
  15. package/dist/eventcatalog.cjs +540 -194
  16. package/dist/eventcatalog.config.d.cts +9 -3
  17. package/dist/eventcatalog.config.d.ts +9 -3
  18. package/dist/eventcatalog.js +37 -39
  19. package/dist/federation/diagnostics.cjs +187 -0
  20. package/dist/federation/diagnostics.d.cts +23 -0
  21. package/dist/federation/diagnostics.d.ts +23 -0
  22. package/dist/federation/diagnostics.js +14 -0
  23. package/dist/federation/federate.cjs +442 -117
  24. package/dist/federation/federate.d.cts +8 -1
  25. package/dist/federation/federate.d.ts +8 -1
  26. package/dist/federation/federate.js +6 -2
  27. package/dist/federation/output-transaction.cjs +152 -0
  28. package/dist/federation/output-transaction.d.cts +3 -0
  29. package/dist/federation/output-transaction.d.ts +3 -0
  30. package/dist/federation/output-transaction.js +6 -0
  31. package/dist/generate.cjs +24 -1
  32. package/dist/generate.js +3 -3
  33. package/dist/utils/cli-logger.cjs +24 -1
  34. package/dist/utils/cli-logger.d.cts +6 -0
  35. package/dist/utils/cli-logger.d.ts +6 -0
  36. package/dist/utils/cli-logger.js +2 -2
  37. package/package.json +3 -3
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(analytics_exports);
36
36
  var import_os = __toESM(require("os"), 1);
37
37
 
38
38
  // package.json
39
- var version = "4.7.4";
39
+ var version = "4.7.5";
40
40
 
41
41
  // src/constants.ts
42
42
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "../chunk-FDXJIZ74.js";
4
- import "../chunk-TLWM7WRZ.js";
3
+ } from "../chunk-2BYTIH4L.js";
4
+ import "../chunk-ZBPMCEXS.js";
5
5
  export {
6
6
  raiseEvent
7
7
  };
@@ -140,7 +140,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
140
140
  var import_os = __toESM(require("os"), 1);
141
141
 
142
142
  // package.json
143
- var version = "4.7.4";
143
+ var version = "4.7.5";
144
144
 
145
145
  // src/constants.ts
146
146
  var VERSION = version;
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  log_build_default
3
- } from "../chunk-ZIABX6SP.js";
4
- import "../chunk-FDXJIZ74.js";
3
+ } from "../chunk-QJRY6Q7S.js";
4
+ import "../chunk-2BYTIH4L.js";
5
5
  import "../chunk-K2XIENVT.js";
6
- import "../chunk-TLWM7WRZ.js";
6
+ import "../chunk-ZBPMCEXS.js";
7
7
  import "../chunk-6QENHZZP.js";
8
8
  export {
9
9
  log_build_default as default
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-TLWM7WRZ.js";
3
+ } from "./chunk-ZBPMCEXS.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import os from "os";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-ZXVQXBTT.js";
3
+ } from "./chunk-QQPH6RCB.js";
4
4
  import {
5
5
  cleanup,
6
6
  getEventCatalogConfigFile
@@ -1,3 +1,9 @@
1
+ import {
2
+ withFederationOutputTransaction
3
+ } from "./chunk-WRNH5C3U.js";
4
+ import {
5
+ composePublicAssets
6
+ } from "./chunk-5EPNFDT5.js";
1
7
  import {
2
8
  createFederationSourceProvider
3
9
  } from "./chunk-A2RZR3U4.js";
@@ -5,8 +11,9 @@ import {
5
11
  createFederationContentCache
6
12
  } from "./chunk-R7Z5ALYI.js";
7
13
  import {
8
- composePublicAssets
9
- } from "./chunk-5EPNFDT5.js";
14
+ getFederationDiagnostics,
15
+ resolveFederationRules
16
+ } from "./chunk-JJPB6EOZ.js";
10
17
  import {
11
18
  getEventCatalogConfigFile
12
19
  } from "./chunk-6QENHZZP.js";
@@ -26,6 +33,16 @@ ${details}`);
26
33
  this.conflicts = conflicts;
27
34
  }
28
35
  };
36
+ var FederationDiagnosticError = class extends Error {
37
+ diagnostics;
38
+ constructor(diagnostics) {
39
+ const details = diagnostics.map((diagnostic) => `${diagnostic.rule}: ${diagnostic.message}`).join("\n");
40
+ super(`Federation diagnostics prevent hydration:
41
+ ${details}`);
42
+ this.name = "FederationDiagnosticError";
43
+ this.diagnostics = diagnostics;
44
+ }
45
+ };
29
46
  var validateSources = (sources) => {
30
47
  const ids = /* @__PURE__ */ new Set();
31
48
  for (const source of sources) {
@@ -34,6 +51,33 @@ var validateSources = (sources) => {
34
51
  ids.add(source.id);
35
52
  }
36
53
  };
54
+ var createDiagnosticGraph = (graph, ownershipGraph, remoteSourceIds, localSourceId) => {
55
+ const edges = ownershipGraph.edges.filter((edge) => remoteSourceIds.has(edge.fromResolvedFrom.source));
56
+ const externalsByPointer = /* @__PURE__ */ new Map();
57
+ for (const edge of edges.filter((edge2) => edge2.status === "external")) {
58
+ const version = edge.pointer ?? void 0;
59
+ const key = `${edge.to}@${version ?? ""}`;
60
+ const external = externalsByPointer.get(key) ?? {
61
+ id: edge.to,
62
+ ...version === void 0 ? {} : { version },
63
+ referencedBy: []
64
+ };
65
+ if (!external.referencedBy.includes(edge.from)) external.referencedBy.push(edge.from);
66
+ externalsByPointer.set(key, external);
67
+ }
68
+ return {
69
+ ...graph,
70
+ entities: [
71
+ ...graph.entities,
72
+ ...ownershipGraph.entities.filter(
73
+ (entity) => entity.resolvedFrom.source === localSourceId && entity.resolvedFrom.commit === "local"
74
+ )
75
+ ],
76
+ edges,
77
+ conflicts: ownershipGraph.conflicts,
78
+ externals: [...externalsByPointer.values()]
79
+ };
80
+ };
37
81
  var writeLock = async (lockPath, lock) => {
38
82
  const temporaryPath = `${lockPath}.tmp-${process.pid}`;
39
83
  try {
@@ -68,14 +112,21 @@ var cleanupPreviousFederation = async (projectDirectory, onProgress) => {
68
112
  const hadFederatedOutput = await pathExists(outDir);
69
113
  const hadLock = previousLock !== void 0;
70
114
  if (!hadFederatedOutput && !hadLock) return;
71
- await fs.rm(outDir, { recursive: true, force: true });
72
- const publicResult = await composePublicAssets({
115
+ const publicResult = await withFederationOutputTransaction(
73
116
  projectDirectory,
74
- federatedDirectory: outDir,
75
- assets: [],
76
- previousFiles: previousLock?.publicFiles
77
- });
78
- await fs.rm(lockPath, { force: true });
117
+ outDir,
118
+ Object.keys(previousLock?.publicFiles ?? {}),
119
+ async () => {
120
+ const result = await composePublicAssets({
121
+ projectDirectory,
122
+ federatedDirectory: outDir,
123
+ assets: [],
124
+ previousFiles: previousLock?.publicFiles
125
+ });
126
+ await fs.rm(lockPath, { force: true });
127
+ return result;
128
+ }
129
+ );
79
130
  onProgress?.({
80
131
  type: "cleanup:complete",
81
132
  federated: hadFederatedOutput,
@@ -97,6 +148,7 @@ var federateCatalog = async (projectDirectory, options = {}) => {
97
148
  );
98
149
  }
99
150
  validateSources(sources);
151
+ const rules = resolveFederationRules(config.federation?.rules);
100
152
  if (options.useCache === false) options.onProgress?.({ type: "cache:disabled" });
101
153
  const provider = options.provider ?? createFederationSourceProvider(projectDirectory);
102
154
  const resolvedSources = [];
@@ -135,60 +187,79 @@ var federateCatalog = async (projectDirectory, options = {}) => {
135
187
  options.onProgress?.({ type: "local:complete", resources: localIndex.resources.length });
136
188
  options.onProgress?.({ type: "resolving", resources, localResources: localIndex.resources.length });
137
189
  const ownershipGraph = resolve([localIndex, ...remoteIndexes]);
138
- if (ownershipGraph.conflicts.length > 0) {
139
- options.onProgress?.({ type: "resolved", graph: ownershipGraph });
140
- throw new FederationConflictError(ownershipGraph.conflicts);
141
- }
142
190
  const graph = resolve(remoteIndexes);
143
- options.onProgress?.({ type: "resolved", graph });
144
- const sourcesById = new Map(sources.map((source) => [source.id, source]));
145
- options.onProgress?.({ type: "hydrating", outDir });
146
- let hydratedFiles = 0;
147
- let cachedFiles = 0;
148
- const hydrateResult = await hydrate(graph, {
191
+ const diagnosticGraph = createDiagnosticGraph(
192
+ graph,
193
+ ownershipGraph,
194
+ new Set(remoteIndexes.map((index) => index.source)),
195
+ localIndex.source
196
+ );
197
+ const diagnostics = getFederationDiagnostics(diagnosticGraph, rules);
198
+ options.onProgress?.({ type: "resolved", graph, diagnostics });
199
+ const blockingConflicts = ownershipGraph.conflicts.filter((conflict) => rules[`federation/${conflict.kind}`] === "error");
200
+ if (blockingConflicts.length > 0) throw new FederationConflictError(blockingConflicts);
201
+ const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
202
+ if (blockingDiagnostics.length > 0) throw new FederationDiagnosticError(blockingDiagnostics);
203
+ const publicPaths = [
204
+ ...Object.keys(previousLock?.publicFiles ?? {}),
205
+ ...graph.assets.filter((asset) => asset.path.startsWith("public/")).map((asset) => asset.path.slice("public/".length))
206
+ ];
207
+ const { hydrateResult, publicResult } = await withFederationOutputTransaction(
208
+ projectDirectory,
149
209
  outDir,
150
- cache: createFederationContentCache(projectDirectory, {
151
- read: options.useCache !== false,
152
- onHit: () => {
153
- cachedFiles += 1;
154
- options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
155
- }
156
- }),
157
- modes: Object.fromEntries(sources.map((source) => [source.id, source.mode ?? "hydrate"])),
158
- fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
159
- const source = sourcesById.get(sourceId);
160
- if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
161
- const content = await provider.fetchContent({ source, commit, path: artifactPath });
162
- hydratedFiles += 1;
163
- options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
164
- return content;
210
+ publicPaths,
211
+ async () => {
212
+ const sourcesById = new Map(sources.map((source) => [source.id, source]));
213
+ options.onProgress?.({ type: "hydrating", outDir });
214
+ let hydratedFiles = 0;
215
+ let cachedFiles = 0;
216
+ const hydrateResult2 = await hydrate(graph, {
217
+ outDir,
218
+ cache: createFederationContentCache(projectDirectory, {
219
+ read: options.useCache !== false,
220
+ onHit: () => {
221
+ cachedFiles += 1;
222
+ options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
223
+ }
224
+ }),
225
+ fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
226
+ const source = sourcesById.get(sourceId);
227
+ if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
228
+ const content = await provider.fetchContent({ source, commit, path: artifactPath });
229
+ hydratedFiles += 1;
230
+ options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
231
+ return content;
232
+ }
233
+ });
234
+ const publicResult2 = await composePublicAssets({
235
+ projectDirectory,
236
+ federatedDirectory: outDir,
237
+ assets: graph.assets,
238
+ previousFiles: previousLock?.publicFiles,
239
+ collisionPaths: new Set(
240
+ graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
241
+ )
242
+ });
243
+ options.onProgress?.({ type: "public:complete", result: publicResult2 });
244
+ const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
245
+ await writeLock(lockPath, {
246
+ lockVersion: 1,
247
+ sources: resolvedSources.map(({ config: source, resolved }) => ({
248
+ id: source.id,
249
+ digest: `sha256:${createHash("sha256").update(resolved.bytes).digest("hex")}`,
250
+ commit: resolved.commit,
251
+ resolvedAt
252
+ })).sort((left, right) => left.id.localeCompare(right.id)),
253
+ publicFiles: publicResult2.files
254
+ });
255
+ return { hydrateResult: hydrateResult2, publicResult: publicResult2 };
165
256
  }
166
- });
167
- const publicResult = await composePublicAssets({
168
- projectDirectory,
169
- federatedDirectory: outDir,
170
- assets: graph.assets,
171
- previousFiles: previousLock?.publicFiles,
172
- collisionPaths: new Set(
173
- graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
174
- )
175
- });
176
- options.onProgress?.({ type: "public:complete", result: publicResult });
177
- const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
178
- await writeLock(lockPath, {
179
- lockVersion: 1,
180
- sources: resolvedSources.map(({ config: source, resolved }) => ({
181
- id: source.id,
182
- digest: `sha256:${createHash("sha256").update(resolved.bytes).digest("hex")}`,
183
- commit: resolved.commit,
184
- resolvedAt
185
- })).sort((left, right) => left.id.localeCompare(right.id)),
186
- publicFiles: publicResult.files
187
- });
257
+ );
188
258
  const result = {
189
259
  sources: sources.length,
190
260
  resources,
191
261
  graph,
262
+ diagnostics,
192
263
  hydrate: hydrateResult,
193
264
  public: publicResult,
194
265
  outDir,
@@ -200,5 +271,6 @@ var federateCatalog = async (projectDirectory, options = {}) => {
200
271
 
201
272
  export {
202
273
  FederationConflictError,
274
+ FederationDiagnosticError,
203
275
  federateCatalog
204
276
  };
@@ -0,0 +1,159 @@
1
+ // src/federation/diagnostics.ts
2
+ var federationRuleDefaults = {
3
+ "federation/duplicate-source": "error",
4
+ "federation/type-collision": "error",
5
+ "federation/pointer-type-mismatch": "error",
6
+ "federation/facet-disagreement": "error",
7
+ "federation/asset-collision": "warn",
8
+ "federation/missing-resource": "warn",
9
+ "federation/unresolved-version": "warn"
10
+ };
11
+ var resolveFederationRules = (rules = {}) => {
12
+ for (const [rule, level] of Object.entries(rules)) {
13
+ if (!Object.hasOwn(federationRuleDefaults, rule)) throw new Error(`Unknown federation rule "${rule}".`);
14
+ if (level !== "off" && level !== "warn" && level !== "error") {
15
+ throw new Error(`Invalid level "${String(level)}" for federation rule "${rule}". Expected off, warn, or error.`);
16
+ }
17
+ }
18
+ return { ...federationRuleDefaults, ...rules };
19
+ };
20
+ var getExternalResource = (external) => `${external.id}${external.version === void 0 ? "" : `@${external.version}`}`;
21
+ var getVersionedResource = (id, version) => `${id}${version == null ? "" : `@${version}`}`;
22
+ var getDocumentedResourceType = (type) => {
23
+ const usesAn = type === "adr" || type === "agent" || type === "entity" || type === "event";
24
+ return `documented as ${usesAn ? "an" : "a"} ${type}`;
25
+ };
26
+ var getConflictDiagnostic = (conflict, graph) => {
27
+ const catalogs = { label: "catalogs", value: conflict.sources.join(", ") };
28
+ switch (conflict.kind) {
29
+ case "duplicate-source":
30
+ return {
31
+ severity: "error",
32
+ message: "Resource has multiple owners",
33
+ rule: "federation/duplicate-source",
34
+ attributes: [
35
+ { label: "resource", value: conflict.id },
36
+ catalogs,
37
+ { label: "resolution", value: "assign a single owning catalog" }
38
+ ]
39
+ };
40
+ case "type-collision": {
41
+ const types = [
42
+ ...new Map(
43
+ graph.entities.filter((entity) => entity.id === conflict.id).map(
44
+ (entity) => [
45
+ `${entity.resolvedFrom.source}:${entity.type}`,
46
+ { label: entity.resolvedFrom.source, value: getDocumentedResourceType(entity.type) }
47
+ ]
48
+ )
49
+ ).values()
50
+ ];
51
+ return {
52
+ severity: "error",
53
+ message: "Resource ID has conflicting types",
54
+ rule: "federation/type-collision",
55
+ attributes: [{ label: "resource", value: conflict.id }, ...types.length > 0 ? types : [catalogs]]
56
+ };
57
+ }
58
+ case "pointer-type-mismatch": {
59
+ const typeMismatch = /^Expected (.+) but found (.+)$/.exec(conflict.detail ?? "");
60
+ return {
61
+ severity: "error",
62
+ message: "Reference type does not match resource",
63
+ rule: "federation/pointer-type-mismatch",
64
+ attributes: [
65
+ { label: "resource", value: conflict.id },
66
+ ...typeMismatch ? [
67
+ { label: "expected type", value: typeMismatch[1] },
68
+ { label: "actual type", value: typeMismatch[2] }
69
+ ] : conflict.detail ? [{ label: "detail", value: conflict.detail }] : [],
70
+ catalogs
71
+ ]
72
+ };
73
+ }
74
+ case "facet-disagreement":
75
+ return {
76
+ severity: "error",
77
+ message: "Catalogs disagree about this resource",
78
+ rule: "federation/facet-disagreement",
79
+ attributes: [
80
+ { label: "resource", value: conflict.id },
81
+ ...conflict.detail ? [{ label: "detail", value: conflict.detail }] : [],
82
+ catalogs
83
+ ]
84
+ };
85
+ }
86
+ };
87
+ var getFederationDiagnostics = (graph, rules = {}) => {
88
+ const resolvedRules = resolveFederationRules(rules);
89
+ const diagnostics = graph.conflicts.map((conflict) => getConflictDiagnostic(conflict, graph));
90
+ for (const external of graph.externals) {
91
+ const resource = getExternalResource(external);
92
+ for (const referencedBy of external.referencedBy) {
93
+ const referrer = graph.entities.find((entity) => entity.id === referencedBy);
94
+ diagnostics.push({
95
+ severity: "warning",
96
+ message: "Referenced EventCatalog resource does not exist",
97
+ rule: "federation/missing-resource",
98
+ attributes: [
99
+ { label: "source catalog", value: referrer?.resolvedFrom.source ?? "unknown" },
100
+ { label: "referenced by", value: referencedBy },
101
+ { label: "missing resource", value: resource }
102
+ ]
103
+ });
104
+ }
105
+ }
106
+ for (const edge of graph.edges.filter((edge2) => edge2.status === "unresolved")) {
107
+ const availableVersions = [
108
+ ...new Set(graph.entities.filter((entity) => entity.id === edge.to).map((entity) => entity.version ?? "unversioned"))
109
+ ].sort((left, right) => left.localeCompare(right, void 0, { numeric: true }));
110
+ diagnostics.push({
111
+ severity: "warning",
112
+ message: "Referenced EventCatalog resource version does not exist",
113
+ rule: "federation/unresolved-version",
114
+ attributes: [
115
+ { label: "source catalog", value: edge.fromResolvedFrom.source },
116
+ { label: "referenced by", value: getVersionedResource(edge.from, edge.fromVersion) },
117
+ { label: "resource", value: edge.to },
118
+ { label: "requested version", value: edge.pointer ?? "unspecified" },
119
+ { label: "available versions", value: availableVersions.join(", ") }
120
+ ]
121
+ });
122
+ }
123
+ for (const warning of graph.warnings) {
124
+ diagnostics.push({
125
+ severity: "warning",
126
+ message: "Asset collision",
127
+ rule: "federation/asset-collision",
128
+ attributes: [
129
+ { label: "asset", value: warning.path },
130
+ { label: "sources", value: warning.sources.join(", ") },
131
+ { label: "winner", value: warning.winner },
132
+ { label: "resolution", value: "last configured source wins" }
133
+ ]
134
+ });
135
+ }
136
+ return diagnostics.flatMap((diagnostic) => {
137
+ const level = resolvedRules[diagnostic.rule];
138
+ if (level === "off") return [];
139
+ return [{ ...diagnostic, severity: level === "error" ? "error" : "warning" }];
140
+ }).sort(
141
+ (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 ?? "")
142
+ );
143
+ };
144
+ var getFederationDiagnosticCounts = (diagnostics) => diagnostics.reduce(
145
+ (counts, diagnostic) => ({
146
+ errors: counts.errors + (diagnostic.severity === "error" ? 1 : 0),
147
+ warnings: counts.warnings + (diagnostic.severity === "warning" ? 1 : 0)
148
+ }),
149
+ { errors: 0, warnings: 0 }
150
+ );
151
+ var getVisibleFederationDiagnostics = (diagnostics, verbose) => diagnostics.filter((diagnostic) => verbose || diagnostic.severity === "error");
152
+
153
+ export {
154
+ federationRuleDefaults,
155
+ resolveFederationRules,
156
+ getFederationDiagnostics,
157
+ getFederationDiagnosticCounts,
158
+ getVisibleFederationDiagnostics
159
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-FDXJIZ74.js";
3
+ } from "./chunk-2BYTIH4L.js";
4
4
  import {
5
5
  countResources,
6
6
  serializeCounts
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-TLWM7WRZ.js";
3
+ } from "./chunk-ZBPMCEXS.js";
4
4
 
5
5
  // src/utils/cli-logger.ts
6
6
  import pc from "picocolors";
@@ -37,6 +37,29 @@ var logger = {
37
37
  warning: (message, tag = "warn") => {
38
38
  console.log(formatMessage(tag, message, pc.yellow));
39
39
  },
40
+ line: (message = "") => {
41
+ console.log(message);
42
+ },
43
+ diagnostic: (severity, message, rule, attributes = []) => {
44
+ const isError = severity === "error";
45
+ const color = isError ? pc.red : pc.yellow;
46
+ const icon = isError ? "\u2716" : "\u26A0";
47
+ const label = `${icon} ${severity.padEnd(7)} ${message}`;
48
+ const ruleSpacing = " ".repeat(Math.max(2, 72 - label.length));
49
+ console.log(` ${color(icon)} ${color(severity.padEnd(7))} ${message}${ruleSpacing}${pc.gray(rule)}`);
50
+ for (const attribute of attributes) {
51
+ console.log(` - ${pc.dim(`${attribute.label}:`)} ${attribute.value}`);
52
+ }
53
+ },
54
+ diagnosticSummary: (errors, warnings) => {
55
+ const total = errors + warnings;
56
+ const color = errors > 0 ? pc.red : pc.yellow;
57
+ const icon = errors > 0 ? "\u2716" : "\u26A0";
58
+ const problems = `${total} problem${total === 1 ? "" : "s"}`;
59
+ const errorCount = `${errors} error${errors === 1 ? "" : "s"}`;
60
+ const warningCount = `${warnings} warning${warnings === 1 ? "" : "s"}`;
61
+ console.log(color(`${icon} ${problems} (${errorCount}, ${warningCount})`));
62
+ },
40
63
  dim: (message) => {
41
64
  console.log(pc.dim(message));
42
65
  }
@@ -0,0 +1,118 @@
1
+ // src/federation/output-transaction.ts
2
+ import fs from "fs/promises";
3
+ import path from "path";
4
+ var getSafePublicPath = (publicDirectory, relativePath) => {
5
+ const normalizedPath = path.posix.normalize(relativePath);
6
+ const unsafe = relativePath.length === 0 || relativePath.includes("\0") || relativePath.includes("\\") || path.posix.isAbsolute(relativePath) || /^[a-zA-Z]:\//.test(relativePath) || normalizedPath !== relativePath || normalizedPath === ".." || normalizedPath.startsWith("../");
7
+ if (unsafe) return void 0;
8
+ const destinationPath = path.resolve(publicDirectory, relativePath);
9
+ const relativeDestinationPath = path.relative(publicDirectory, destinationPath);
10
+ return relativeDestinationPath !== "" && relativeDestinationPath !== ".." && !relativeDestinationPath.startsWith(`..${path.sep}`) && !path.isAbsolute(relativeDestinationPath) ? destinationPath : void 0;
11
+ };
12
+ var beginFederationOutputTransaction = async (projectDirectory, outDir, relativePublicPaths) => {
13
+ const transactionDirectory = await fs.mkdtemp(path.join(projectDirectory, ".eventcatalog-federation-transaction-"));
14
+ const previousOutDir = path.join(transactionDirectory, "federated");
15
+ const publicBackupDirectory = path.join(transactionDirectory, "public");
16
+ const publicDirectory = path.resolve(projectDirectory, "public");
17
+ const publicSnapshots = [];
18
+ const missingPublicDirectories = /* @__PURE__ */ new Set();
19
+ let hadPreviousOutDir = false;
20
+ try {
21
+ for (const relativePath of new Set(relativePublicPaths)) {
22
+ const destinationPath = getSafePublicPath(publicDirectory, relativePath);
23
+ if (!destinationPath) continue;
24
+ try {
25
+ const stat = await fs.lstat(destinationPath);
26
+ if (!stat.isFile()) continue;
27
+ const backupPath = path.join(publicBackupDirectory, `${publicSnapshots.length}`);
28
+ await fs.mkdir(path.dirname(backupPath), { recursive: true });
29
+ await fs.copyFile(destinationPath, backupPath);
30
+ publicSnapshots.push({ backupPath, destinationPath, state: "file" });
31
+ } catch (error) {
32
+ const code = error.code;
33
+ if (code === "ENOTDIR") continue;
34
+ if (code !== "ENOENT") throw error;
35
+ publicSnapshots.push({ destinationPath, state: "missing" });
36
+ let directory = path.dirname(destinationPath);
37
+ while (directory !== publicDirectory) {
38
+ try {
39
+ await fs.lstat(directory);
40
+ break;
41
+ } catch (directoryError) {
42
+ const directoryCode = directoryError.code;
43
+ if (directoryCode === "ENOTDIR") break;
44
+ if (directoryCode !== "ENOENT") throw directoryError;
45
+ missingPublicDirectories.add(directory);
46
+ directory = path.dirname(directory);
47
+ }
48
+ }
49
+ }
50
+ }
51
+ try {
52
+ await fs.rename(outDir, previousOutDir);
53
+ hadPreviousOutDir = true;
54
+ } catch (error) {
55
+ if (error.code !== "ENOENT") throw error;
56
+ }
57
+ } catch (error) {
58
+ await fs.rm(transactionDirectory, { recursive: true, force: true });
59
+ throw error;
60
+ }
61
+ return {
62
+ commit: async () => {
63
+ await fs.rm(transactionDirectory, { recursive: true, force: true }).catch(() => void 0);
64
+ },
65
+ rollback: async () => {
66
+ const rollbackErrors = [];
67
+ const attempt = async (operation) => {
68
+ try {
69
+ await operation();
70
+ } catch (error) {
71
+ rollbackErrors.push(error);
72
+ }
73
+ };
74
+ await attempt(() => fs.rm(outDir, { recursive: true, force: true }));
75
+ if (hadPreviousOutDir) await attempt(() => fs.rename(previousOutDir, outDir));
76
+ for (const snapshot of publicSnapshots) {
77
+ if (snapshot.state === "missing") {
78
+ await attempt(() => fs.rm(snapshot.destinationPath, { force: true }));
79
+ continue;
80
+ }
81
+ await attempt(async () => {
82
+ await fs.mkdir(path.dirname(snapshot.destinationPath), { recursive: true });
83
+ await fs.copyFile(snapshot.backupPath, snapshot.destinationPath);
84
+ });
85
+ }
86
+ for (const directory of [...missingPublicDirectories].sort((left, right) => right.length - left.length)) {
87
+ await attempt(async () => {
88
+ try {
89
+ await fs.rmdir(directory);
90
+ } catch (error) {
91
+ if (!["ENOENT", "ENOTEMPTY"].includes(error.code ?? "")) throw error;
92
+ }
93
+ });
94
+ }
95
+ await attempt(() => fs.rm(transactionDirectory, { recursive: true, force: true }));
96
+ if (rollbackErrors.length > 0) throw new AggregateError(rollbackErrors, "Failed to restore the previous federation output");
97
+ }
98
+ };
99
+ };
100
+ var withFederationOutputTransaction = async (projectDirectory, outDir, relativePublicPaths, update) => {
101
+ const transaction = await beginFederationOutputTransaction(projectDirectory, outDir, relativePublicPaths);
102
+ try {
103
+ const result = await update();
104
+ await transaction.commit();
105
+ return result;
106
+ } catch (error) {
107
+ try {
108
+ await transaction.rollback();
109
+ } catch (rollbackError) {
110
+ throw new AggregateError([error, rollbackError], "Federation failed and the previous output could not be restored");
111
+ }
112
+ throw error;
113
+ }
114
+ };
115
+
116
+ export {
117
+ withFederationOutputTransaction
118
+ };
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "4.7.4";
2
+ var version = "4.7.5";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -25,7 +25,7 @@ __export(constants_exports, {
25
25
  module.exports = __toCommonJS(constants_exports);
26
26
 
27
27
  // package.json
28
- var version = "4.7.4";
28
+ var version = "4.7.5";
29
29
 
30
30
  // src/constants.ts
31
31
  var VERSION = version;
package/dist/constants.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-TLWM7WRZ.js";
3
+ } from "./chunk-ZBPMCEXS.js";
4
4
  export {
5
5
  VERSION
6
6
  };