@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.
- package/LICENSE +95 -0
- package/README.md +3 -2
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-7QTCSRAC.js → chunk-6BBLQLGL.js} +1 -1
- package/dist/chunk-DDALKGTF.js +7 -0
- package/dist/chunk-JJPB6EOZ.js +159 -0
- package/dist/chunk-MLIR3NL4.js +279 -0
- package/dist/{chunk-ZXVQXBTT.js → chunk-MNAPKH63.js} +24 -1
- package/dist/{chunk-TLWM7WRZ.js → chunk-Q534DWPZ.js} +1 -1
- package/dist/{chunk-ZIABX6SP.js → chunk-RZAJNR7O.js} +1 -1
- package/dist/chunk-WRNH5C3U.js +118 -0
- package/dist/{chunk-FDXJIZ74.js → chunk-YXANZO6Y.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +560 -211
- package/dist/eventcatalog.config.d.cts +9 -3
- package/dist/eventcatalog.config.d.ts +9 -3
- package/dist/eventcatalog.js +40 -42
- package/dist/federation/content-cache.d.cts +5 -0
- package/dist/federation/content-cache.d.ts +5 -0
- package/dist/federation/diagnostics.cjs +187 -0
- package/dist/federation/diagnostics.d.cts +28 -0
- package/dist/federation/diagnostics.d.ts +28 -0
- package/dist/federation/diagnostics.js +14 -0
- package/dist/federation/entitlement.cjs +31 -0
- package/dist/federation/entitlement.d.cts +12 -0
- package/dist/federation/entitlement.d.ts +12 -0
- package/dist/federation/entitlement.js +6 -0
- package/dist/federation/federate.cjs +447 -118
- package/dist/federation/federate.d.cts +13 -2
- package/dist/federation/federate.d.ts +13 -2
- package/dist/federation/federate.js +9 -4
- package/dist/federation/filesystem-source-provider.d.cts +5 -0
- package/dist/federation/filesystem-source-provider.d.ts +5 -0
- package/dist/federation/github-source-provider.d.cts +5 -0
- package/dist/federation/github-source-provider.d.ts +5 -0
- package/dist/federation/output-transaction.cjs +152 -0
- package/dist/federation/output-transaction.d.cts +7 -0
- package/dist/federation/output-transaction.d.ts +7 -0
- package/dist/federation/output-transaction.js +6 -0
- package/dist/federation/public-assets.d.cts +5 -0
- package/dist/federation/public-assets.d.ts +5 -0
- package/dist/federation/source-provider.d.cts +5 -0
- package/dist/federation/source-provider.d.ts +5 -0
- package/dist/federation/source-provider.js +2 -2
- package/dist/federation/types.d.cts +5 -0
- package/dist/federation/types.d.ts +5 -0
- package/dist/generate.cjs +24 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +24 -1
- package/dist/utils/cli-logger.d.cts +6 -0
- package/dist/utils/cli-logger.d.ts +6 -0
- package/dist/utils/cli-logger.js +2 -2
- package/package.json +4 -4
- package/dist/chunk-RIUHZZRA.js +0 -204
- package/dist/{chunk-A2RZR3U4.js → chunk-6K7XZAYI.js} +3 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
EventCatalog Core is distributed under a mixed-license model.
|
|
2
|
+
|
|
3
|
+
The EventCatalog Commercial License below applies to these source paths:
|
|
4
|
+
|
|
5
|
+
- `eventcatalog/src/enterprise/**`
|
|
6
|
+
- `src/federation/**`
|
|
7
|
+
|
|
8
|
+
The EventCatalog Commercial License also applies to any portion of a compiled,
|
|
9
|
+
bundled, copied, minified, or otherwise generated artifact derived from those
|
|
10
|
+
paths, regardless of its output location, including artifacts under `dist/**`
|
|
11
|
+
and `bin/**`.
|
|
12
|
+
|
|
13
|
+
The MIT License below applies to all remaining portions of this package.
|
|
14
|
+
|
|
15
|
+
MIT License
|
|
16
|
+
|
|
17
|
+
Copyright (c) 2022-2026 boyney123
|
|
18
|
+
|
|
19
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
20
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
21
|
+
in the Software without restriction, including without limitation the rights
|
|
22
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
23
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
24
|
+
furnished to do so, subject to the following conditions:
|
|
25
|
+
|
|
26
|
+
The above copyright notice and this permission notice shall be included in all
|
|
27
|
+
copies or substantial portions of the Software.
|
|
28
|
+
|
|
29
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
30
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
31
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
32
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
33
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
34
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
35
|
+
SOFTWARE.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
The EventCatalog Commercial License (the "Commercial License")
|
|
40
|
+
Copyright (c) 2022-2026 EventCatalog Ltd
|
|
41
|
+
|
|
42
|
+
With regard to the EventCatalog Software:
|
|
43
|
+
|
|
44
|
+
This software and associated documentation files (the "Software") may only be
|
|
45
|
+
used in production, if you (and any entity that you represent) have agreed to,
|
|
46
|
+
and are in compliance with, the EventCatalog Terms of Service available
|
|
47
|
+
at https://www.eventcatalog.dev/terms, or other agreements governing the use of
|
|
48
|
+
the Software, as mutually agreed by you and EventCatalog Ltd ("EventCatalog"),
|
|
49
|
+
and otherwise have a valid EventCatalog Starter, Scale, or Enterprise
|
|
50
|
+
subscription ("Commercial Subscription").
|
|
51
|
+
|
|
52
|
+
Subject to the foregoing sentence, you are free to modify this Software and
|
|
53
|
+
publish patches to the Software. You agree that EventCatalog and/or its
|
|
54
|
+
licensors (as applicable) retain all right, title and interest in and to all
|
|
55
|
+
such modifications and/or patches, and all such modifications and/or patches
|
|
56
|
+
may only be used, copied, modified, displayed, distributed, or otherwise
|
|
57
|
+
exploited with a valid Commercial Subscription.
|
|
58
|
+
|
|
59
|
+
A 14-day free trial of premium features is available at
|
|
60
|
+
https://eventcatalog.cloud/ to help you evaluate the Software before purchasing
|
|
61
|
+
a Commercial Subscription.
|
|
62
|
+
|
|
63
|
+
Notwithstanding the foregoing, you may copy and modify the Software for
|
|
64
|
+
development and testing purposes, without requiring a subscription. You agree
|
|
65
|
+
that EventCatalog and/or its licensors (as applicable) retain all right, title
|
|
66
|
+
and interest in and to all such modifications. You are not granted any other
|
|
67
|
+
rights beyond what is expressly stated herein. Subject to the foregoing, it is
|
|
68
|
+
forbidden to copy, merge, publish, distribute, sublicense, and/or sell the
|
|
69
|
+
Software.
|
|
70
|
+
|
|
71
|
+
You may not move, change, disable, or circumvent the license key functionality
|
|
72
|
+
in the Software, and you may not remove or obscure any functionality in the
|
|
73
|
+
Software that is protected by the license key.
|
|
74
|
+
|
|
75
|
+
This Software is source-available for the purposes of transparency and
|
|
76
|
+
auditability. Source availability does not constitute a grant of rights to use
|
|
77
|
+
the Software in production without a valid Commercial Subscription.
|
|
78
|
+
|
|
79
|
+
This Commercial License applies only to the part of this Software that is not
|
|
80
|
+
distributed under the MIT license. Any part of this Software distributed under
|
|
81
|
+
the MIT license is copyrighted under the MIT license. The full text of this
|
|
82
|
+
Commercial License shall be included in all copies or substantial portions of
|
|
83
|
+
the Software.
|
|
84
|
+
|
|
85
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
86
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
87
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
88
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
89
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
90
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
91
|
+
SOFTWARE.
|
|
92
|
+
|
|
93
|
+
For all third party components incorporated into the EventCatalog Software,
|
|
94
|
+
those components are licensed under the original license provided by the owner
|
|
95
|
+
of the applicable component.
|
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
<div align="center">
|
|
36
36
|
|
|
37
37
|
[](https://github.com/event-catalog/eventcatalog/actions/workflows/verify-build.yml)
|
|
38
|
-
[](
|
|
38
|
+
[](./LICENSE)
|
|
39
39
|
[](https://badge.fury.io/js/@eventcatalog/core)
|
|
40
40
|
|
|
41
41
|
</div>
|
|
@@ -205,4 +205,5 @@ This project follows the [all-contributors](https://github.com/all-contributors/
|
|
|
205
205
|
|
|
206
206
|
# License
|
|
207
207
|
|
|
208
|
-
MIT
|
|
208
|
+
EventCatalog Core uses a mixed-license model. Most files are licensed under MIT; the Enterprise and Federation paths
|
|
209
|
+
identified in [LICENSE](./LICENSE) use the EventCatalog Commercial License.
|
|
@@ -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.
|
|
143
|
+
var version = "4.7.6";
|
|
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-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-RZAJNR7O.js";
|
|
4
|
+
import "../chunk-YXANZO6Y.js";
|
|
5
5
|
import "../chunk-K2XIENVT.js";
|
|
6
|
-
import "../chunk-
|
|
6
|
+
import "../chunk-Q534DWPZ.js";
|
|
7
7
|
import "../chunk-6QENHZZP.js";
|
|
8
8
|
export {
|
|
9
9
|
log_build_default as default
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import {
|
|
2
|
+
withFederationOutputTransaction
|
|
3
|
+
} from "./chunk-WRNH5C3U.js";
|
|
4
|
+
import {
|
|
5
|
+
composePublicAssets
|
|
6
|
+
} from "./chunk-5EPNFDT5.js";
|
|
7
|
+
import {
|
|
8
|
+
createFederationSourceProvider
|
|
9
|
+
} from "./chunk-6K7XZAYI.js";
|
|
10
|
+
import {
|
|
11
|
+
createFederationContentCache
|
|
12
|
+
} from "./chunk-R7Z5ALYI.js";
|
|
13
|
+
import {
|
|
14
|
+
getFederationDiagnostics,
|
|
15
|
+
resolveFederationRules
|
|
16
|
+
} from "./chunk-JJPB6EOZ.js";
|
|
17
|
+
import {
|
|
18
|
+
isFederationEnabled
|
|
19
|
+
} from "./chunk-DDALKGTF.js";
|
|
20
|
+
import {
|
|
21
|
+
getEventCatalogConfigFile
|
|
22
|
+
} from "./chunk-6QENHZZP.js";
|
|
23
|
+
|
|
24
|
+
// src/federation/federate.ts
|
|
25
|
+
import { createHash } from "crypto";
|
|
26
|
+
import fs from "fs/promises";
|
|
27
|
+
import path from "path";
|
|
28
|
+
import createSDK, { hydrate, resolve } from "@eventcatalog/sdk";
|
|
29
|
+
var FederationConflictError = class extends Error {
|
|
30
|
+
conflicts;
|
|
31
|
+
constructor(conflicts) {
|
|
32
|
+
const details = conflicts.map((conflict) => `${conflict.id}: ${conflict.sources.join(", ")}`).join("\n");
|
|
33
|
+
super(`Federation conflicts prevent hydration:
|
|
34
|
+
${details}`);
|
|
35
|
+
this.name = "FederationConflictError";
|
|
36
|
+
this.conflicts = conflicts;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var FederationDiagnosticError = class extends Error {
|
|
40
|
+
diagnostics;
|
|
41
|
+
constructor(diagnostics) {
|
|
42
|
+
const details = diagnostics.map((diagnostic) => `${diagnostic.rule}: ${diagnostic.message}`).join("\n");
|
|
43
|
+
super(`Federation diagnostics prevent hydration:
|
|
44
|
+
${details}`);
|
|
45
|
+
this.name = "FederationDiagnosticError";
|
|
46
|
+
this.diagnostics = diagnostics;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var validateSources = (sources) => {
|
|
50
|
+
const ids = /* @__PURE__ */ new Set();
|
|
51
|
+
for (const source of sources) {
|
|
52
|
+
if (!source.id?.trim()) throw new Error("Every federation source requires a stable id.");
|
|
53
|
+
if (ids.has(source.id)) throw new Error(`Federation source id "${source.id}" is configured more than once.`);
|
|
54
|
+
ids.add(source.id);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
var createDiagnosticGraph = (graph, ownershipGraph, remoteSourceIds, localSourceId) => {
|
|
58
|
+
const edges = ownershipGraph.edges.filter((edge) => remoteSourceIds.has(edge.fromResolvedFrom.source));
|
|
59
|
+
const externalsByPointer = /* @__PURE__ */ new Map();
|
|
60
|
+
for (const edge of edges.filter((edge2) => edge2.status === "external")) {
|
|
61
|
+
const version = edge.pointer ?? void 0;
|
|
62
|
+
const key = `${edge.to}@${version ?? ""}`;
|
|
63
|
+
const external = externalsByPointer.get(key) ?? {
|
|
64
|
+
id: edge.to,
|
|
65
|
+
...version === void 0 ? {} : { version },
|
|
66
|
+
referencedBy: []
|
|
67
|
+
};
|
|
68
|
+
if (!external.referencedBy.includes(edge.from)) external.referencedBy.push(edge.from);
|
|
69
|
+
externalsByPointer.set(key, external);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
...graph,
|
|
73
|
+
entities: [
|
|
74
|
+
...graph.entities,
|
|
75
|
+
...ownershipGraph.entities.filter(
|
|
76
|
+
(entity) => entity.resolvedFrom.source === localSourceId && entity.resolvedFrom.commit === "local"
|
|
77
|
+
)
|
|
78
|
+
],
|
|
79
|
+
edges,
|
|
80
|
+
conflicts: ownershipGraph.conflicts,
|
|
81
|
+
externals: [...externalsByPointer.values()]
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
var writeLock = async (lockPath, lock) => {
|
|
85
|
+
const temporaryPath = `${lockPath}.tmp-${process.pid}`;
|
|
86
|
+
try {
|
|
87
|
+
await fs.writeFile(temporaryPath, `${JSON.stringify(lock, null, 2)}
|
|
88
|
+
`, "utf8");
|
|
89
|
+
await fs.rename(temporaryPath, lockPath);
|
|
90
|
+
} finally {
|
|
91
|
+
await fs.rm(temporaryPath, { force: true });
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
var readLock = async (lockPath) => {
|
|
95
|
+
try {
|
|
96
|
+
return JSON.parse(await fs.readFile(lockPath, "utf8"));
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error.code === "ENOENT") return void 0;
|
|
99
|
+
throw new Error(`Cannot read federation lock at "${lockPath}"`, { cause: error });
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var pathExists = async (filePath) => {
|
|
103
|
+
try {
|
|
104
|
+
await fs.access(filePath);
|
|
105
|
+
return true;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (error.code === "ENOENT") return false;
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
var cleanupPreviousFederation = async (projectDirectory, onProgress) => {
|
|
112
|
+
const outDir = path.join(projectDirectory, "federated");
|
|
113
|
+
const lockPath = path.join(projectDirectory, "eventcatalog.lock");
|
|
114
|
+
const previousLock = await readLock(lockPath);
|
|
115
|
+
const hadFederatedOutput = await pathExists(outDir);
|
|
116
|
+
const hadLock = previousLock !== void 0;
|
|
117
|
+
if (!hadFederatedOutput && !hadLock) return;
|
|
118
|
+
const publicResult = await withFederationOutputTransaction(
|
|
119
|
+
projectDirectory,
|
|
120
|
+
outDir,
|
|
121
|
+
Object.keys(previousLock?.publicFiles ?? {}),
|
|
122
|
+
async () => {
|
|
123
|
+
const result = await composePublicAssets({
|
|
124
|
+
projectDirectory,
|
|
125
|
+
federatedDirectory: outDir,
|
|
126
|
+
assets: [],
|
|
127
|
+
previousFiles: previousLock?.publicFiles
|
|
128
|
+
});
|
|
129
|
+
await fs.rm(lockPath, { force: true });
|
|
130
|
+
return result;
|
|
131
|
+
}
|
|
132
|
+
);
|
|
133
|
+
onProgress?.({
|
|
134
|
+
type: "cleanup:complete",
|
|
135
|
+
federated: hadFederatedOutput,
|
|
136
|
+
publicFiles: publicResult.removed,
|
|
137
|
+
lock: hadLock
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
var federateCatalog = async (projectDirectory, options = {}) => {
|
|
141
|
+
const config = await getEventCatalogConfigFile(projectDirectory);
|
|
142
|
+
const sources = config.federation?.sources ?? [];
|
|
143
|
+
options.onProgress?.({ type: "configured", sources: sources.length });
|
|
144
|
+
if (sources.length === 0) {
|
|
145
|
+
await cleanupPreviousFederation(projectDirectory, options.onProgress);
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (!await isFederationEnabled()) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"Cannot federate catalogs: EventCatalog federation is an Enterprise feature. Visit https://www.eventcatalog.dev/pricing to enable federation."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
validateSources(sources);
|
|
154
|
+
const rules = resolveFederationRules(config.federation?.rules);
|
|
155
|
+
if (options.useCache === false) options.onProgress?.({ type: "cache:disabled" });
|
|
156
|
+
const provider = options.provider ?? createFederationSourceProvider(projectDirectory);
|
|
157
|
+
const resolvedSources = [];
|
|
158
|
+
for (const [index, source] of sources.entries()) {
|
|
159
|
+
const current = index + 1;
|
|
160
|
+
options.onProgress?.({ type: "source:start", source, current, total: sources.length });
|
|
161
|
+
try {
|
|
162
|
+
const resolved = await provider.resolve(source);
|
|
163
|
+
resolvedSources.push({ config: source, resolved });
|
|
164
|
+
options.onProgress?.({
|
|
165
|
+
type: "source:complete",
|
|
166
|
+
source,
|
|
167
|
+
current,
|
|
168
|
+
total: sources.length,
|
|
169
|
+
commit: resolved.commit,
|
|
170
|
+
resources: resolved.index.resources.length,
|
|
171
|
+
generated: resolved.generated
|
|
172
|
+
});
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
175
|
+
throw new Error(`Failed to federate source "${source.id}": ${message}`, { cause: error });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const outDir = path.join(projectDirectory, "federated");
|
|
179
|
+
const lockPath = path.join(projectDirectory, "eventcatalog.lock");
|
|
180
|
+
const previousLock = await readLock(lockPath);
|
|
181
|
+
const resources = resolvedSources.reduce((total, source) => total + source.resolved.index.resources.length, 0);
|
|
182
|
+
const remoteIndexes = resolvedSources.map(({ resolved }) => resolved.index);
|
|
183
|
+
options.onProgress?.({ type: "local:start" });
|
|
184
|
+
const localIndex = await createSDK(projectDirectory).buildIndex({
|
|
185
|
+
source: config.cId,
|
|
186
|
+
commit: "local",
|
|
187
|
+
hashContent: false,
|
|
188
|
+
includeFederated: false
|
|
189
|
+
});
|
|
190
|
+
options.onProgress?.({ type: "local:complete", resources: localIndex.resources.length });
|
|
191
|
+
options.onProgress?.({ type: "resolving", resources, localResources: localIndex.resources.length });
|
|
192
|
+
const ownershipGraph = resolve([localIndex, ...remoteIndexes]);
|
|
193
|
+
const graph = resolve(remoteIndexes);
|
|
194
|
+
const diagnosticGraph = createDiagnosticGraph(
|
|
195
|
+
graph,
|
|
196
|
+
ownershipGraph,
|
|
197
|
+
new Set(remoteIndexes.map((index) => index.source)),
|
|
198
|
+
localIndex.source
|
|
199
|
+
);
|
|
200
|
+
const diagnostics = getFederationDiagnostics(diagnosticGraph, rules);
|
|
201
|
+
options.onProgress?.({ type: "resolved", graph, diagnostics });
|
|
202
|
+
const blockingConflicts = ownershipGraph.conflicts.filter((conflict) => rules[`federation/${conflict.kind}`] === "error");
|
|
203
|
+
if (blockingConflicts.length > 0) throw new FederationConflictError(blockingConflicts);
|
|
204
|
+
const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.severity === "error");
|
|
205
|
+
if (blockingDiagnostics.length > 0) throw new FederationDiagnosticError(blockingDiagnostics);
|
|
206
|
+
const publicPaths = [
|
|
207
|
+
...Object.keys(previousLock?.publicFiles ?? {}),
|
|
208
|
+
...graph.assets.filter((asset) => asset.path.startsWith("public/")).map((asset) => asset.path.slice("public/".length))
|
|
209
|
+
];
|
|
210
|
+
const { hydrateResult, publicResult } = await withFederationOutputTransaction(
|
|
211
|
+
projectDirectory,
|
|
212
|
+
outDir,
|
|
213
|
+
publicPaths,
|
|
214
|
+
async () => {
|
|
215
|
+
const sourcesById = new Map(sources.map((source) => [source.id, source]));
|
|
216
|
+
options.onProgress?.({ type: "hydrating", outDir });
|
|
217
|
+
let hydratedFiles = 0;
|
|
218
|
+
let cachedFiles = 0;
|
|
219
|
+
const hydrateResult2 = await hydrate(graph, {
|
|
220
|
+
outDir,
|
|
221
|
+
cache: createFederationContentCache(projectDirectory, {
|
|
222
|
+
read: options.useCache !== false,
|
|
223
|
+
onHit: () => {
|
|
224
|
+
cachedFiles += 1;
|
|
225
|
+
options.onProgress?.({ type: "hydrate:cache", files: cachedFiles });
|
|
226
|
+
}
|
|
227
|
+
}),
|
|
228
|
+
fetch: async ({ source: sourceId, commit, path: artifactPath }) => {
|
|
229
|
+
const source = sourcesById.get(sourceId);
|
|
230
|
+
if (!source) throw new Error(`Cannot fetch content for unconfigured source "${sourceId}"`);
|
|
231
|
+
const content = await provider.fetchContent({ source, commit, path: artifactPath });
|
|
232
|
+
hydratedFiles += 1;
|
|
233
|
+
options.onProgress?.({ type: "hydrate:file", files: hydratedFiles, source: sourceId, path: artifactPath });
|
|
234
|
+
return content;
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
const publicResult2 = await composePublicAssets({
|
|
238
|
+
projectDirectory,
|
|
239
|
+
federatedDirectory: outDir,
|
|
240
|
+
assets: graph.assets,
|
|
241
|
+
previousFiles: previousLock?.publicFiles,
|
|
242
|
+
collisionPaths: new Set(
|
|
243
|
+
graph.warnings.filter((warning) => warning.kind === "asset-collision").map((warning) => warning.path)
|
|
244
|
+
)
|
|
245
|
+
});
|
|
246
|
+
options.onProgress?.({ type: "public:complete", result: publicResult2 });
|
|
247
|
+
const resolvedAt = (options.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
|
|
248
|
+
await writeLock(lockPath, {
|
|
249
|
+
lockVersion: 1,
|
|
250
|
+
sources: resolvedSources.map(({ config: source, resolved }) => ({
|
|
251
|
+
id: source.id,
|
|
252
|
+
digest: `sha256:${createHash("sha256").update(resolved.bytes).digest("hex")}`,
|
|
253
|
+
commit: resolved.commit,
|
|
254
|
+
resolvedAt
|
|
255
|
+
})).sort((left, right) => left.id.localeCompare(right.id)),
|
|
256
|
+
publicFiles: publicResult2.files
|
|
257
|
+
});
|
|
258
|
+
return { hydrateResult: hydrateResult2, publicResult: publicResult2 };
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
const result = {
|
|
262
|
+
sources: sources.length,
|
|
263
|
+
resources,
|
|
264
|
+
graph,
|
|
265
|
+
diagnostics,
|
|
266
|
+
hydrate: hydrateResult,
|
|
267
|
+
public: publicResult,
|
|
268
|
+
outDir,
|
|
269
|
+
lockPath
|
|
270
|
+
};
|
|
271
|
+
options.onProgress?.({ type: "complete", result });
|
|
272
|
+
return result;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
export {
|
|
276
|
+
FederationConflictError,
|
|
277
|
+
FederationDiagnosticError,
|
|
278
|
+
federateCatalog
|
|
279
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-Q534DWPZ.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
|
}
|