@eventcatalog/core 5.0.0-beta.13 → 5.0.0-beta.14
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 +13 -12
- 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 +4 -4
- package/dist/{chunk-T75DOVFP.js → chunk-4KE3EUDJ.js} +1 -1
- package/dist/{chunk-BO2HHVZ3.js → chunk-I3XDOICA.js} +1 -1
- package/dist/{chunk-XA5W6DXX.js → chunk-OFK7U7BX.js} +1 -1
- package/dist/{chunk-JPT3QC5V.js → chunk-PI5YNZZZ.js} +1 -1
- package/dist/{chunk-PLTLW45Q.js → chunk-WY6M3RPT.js} +5 -5
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +10 -5
- package/dist/eventcatalog.js +23 -18
- package/dist/federation/federate.js +3 -3
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/eventcatalog/astro.config.mjs +13 -1
- package/eventcatalog/integrations/skip-startup-reload.mjs +31 -0
- package/eventcatalog/integrations/stable-dependency-order.mjs +29 -0
- package/eventcatalog/src/components/CatalogGraph/CatalogForceGraph.tsx +39 -0
- package/eventcatalog/src/components/Search/SearchDataLoader.astro +16 -28
- package/eventcatalog/src/components/SideNav/NestedSideBar/index.tsx +37 -76
- package/eventcatalog/src/components/SideNav/NestedSideBar/storage.ts +2 -0
- package/eventcatalog/src/components/SideNav/NestedSideBar/utils.ts +122 -1
- package/eventcatalog/src/components/SideNav/SideNav.astro +9 -1
- package/package.json +3 -3
- package/dist/{chunk-ZEY2XUJN.js → chunk-KHACCTW2.js} +4 -4
package/LICENSE
CHANGED
|
@@ -17,13 +17,13 @@ Licensed Work: EventCatalog version 5.0 or later, including pre-releases,
|
|
|
17
17
|
and the examples directory. The Licensed Work is (c) 2026
|
|
18
18
|
EventCatalog Ltd.
|
|
19
19
|
Additional Use Grant: You may make Production use of the Licensed Work if You
|
|
20
|
-
|
|
20
|
+
qualify for Free Use and Your use is not a Hosted
|
|
21
21
|
Offering. Sections 1 to 7 below form part of this grant.
|
|
22
22
|
Capitalised terms are defined in section 6.
|
|
23
23
|
|
|
24
|
-
1.
|
|
24
|
+
1. Free Use
|
|
25
25
|
|
|
26
|
-
You
|
|
26
|
+
You qualify for "Free Use" if You are:
|
|
27
27
|
|
|
28
28
|
(a) an individual using the Licensed Work for Your own
|
|
29
29
|
personal purposes, and not for or on behalf of any
|
|
@@ -47,9 +47,9 @@ Additional Use Grant: You may make Production use of the Licensed Work if You
|
|
|
47
47
|
|
|
48
48
|
2. Growing past the thresholds
|
|
49
49
|
|
|
50
|
-
An Organisation that
|
|
51
|
-
|
|
52
|
-
following to happen:
|
|
50
|
+
An Organisation that qualifies for Free Use under 1(b)
|
|
51
|
+
continues to qualify until ninety (90) days after the
|
|
52
|
+
first of the following to happen:
|
|
53
53
|
|
|
54
54
|
(a) the end of the first financial year in which its
|
|
55
55
|
Annual Revenue reaches USD 10,000,000; or
|
|
@@ -68,7 +68,7 @@ Additional Use Grant: You may make Production use of the Licensed Work if You
|
|
|
68
68
|
vendor), including building, customising, hosting or
|
|
69
69
|
distributing the Licensed Work or a catalog built with it
|
|
70
70
|
for that client, Your use for that client is permitted if
|
|
71
|
-
the client
|
|
71
|
+
the client qualifies for Free Use or holds a Commercial
|
|
72
72
|
License covering that use. You may rely on a client's
|
|
73
73
|
written statement of its status unless You know it is
|
|
74
74
|
wrong. Your own use of the Licensed Work for Your own
|
|
@@ -76,7 +76,7 @@ Additional Use Grant: You may make Production use of the Licensed Work if You
|
|
|
76
76
|
|
|
77
77
|
4. Coming into compliance
|
|
78
78
|
|
|
79
|
-
(a) If You stop
|
|
79
|
+
(a) If You stop qualifying for Free Use (for example
|
|
80
80
|
because Your Evaluation Period ends), or You become
|
|
81
81
|
aware (including by notice from the Licensor) that
|
|
82
82
|
Your Production use is not permitted by this grant,
|
|
@@ -219,7 +219,8 @@ Additional Use Grant: You may make Production use of the Licensed Work if You
|
|
|
219
219
|
Public Open Source Projects.
|
|
220
220
|
|
|
221
221
|
"Commercial License" means a written license from the
|
|
222
|
-
Licensor
|
|
222
|
+
Licensor, including one accepted online, permitting
|
|
223
|
+
Production use beyond this grant.
|
|
223
224
|
|
|
224
225
|
7. Everything else
|
|
225
226
|
|
|
@@ -230,9 +231,9 @@ Additional Use Grant: You may make Production use of the Licensed Work if You
|
|
|
230
231
|
Work. The content of Your own catalog (the documentation,
|
|
231
232
|
schemas, diagrams and configuration You create) is Yours
|
|
232
233
|
and is not part of the Licensed Work.
|
|
233
|
-
Change Date: Four years after the first public release
|
|
234
|
-
pre-release
|
|
235
|
-
|
|
234
|
+
Change Date: Four years after the first public release (including any
|
|
235
|
+
pre-release) in the same release series as the Licensed
|
|
236
|
+
Work.
|
|
236
237
|
|
|
237
238
|
A "release series" is every release that shares the same
|
|
238
239
|
major and minor release number, including its pre-releases
|
|
@@ -176,7 +176,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
176
176
|
var import_os = __toESM(require("os"), 1);
|
|
177
177
|
|
|
178
178
|
// package.json
|
|
179
|
-
var version = "5.0.0-beta.
|
|
179
|
+
var version = "5.0.0-beta.14";
|
|
180
180
|
|
|
181
181
|
// src/constants.ts
|
|
182
182
|
var VERSION = version;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
log_build_default
|
|
3
|
-
} from "../chunk-
|
|
4
|
-
import "../chunk-
|
|
3
|
+
} from "../chunk-WY6M3RPT.js";
|
|
4
|
+
import "../chunk-AWOO63ME.js";
|
|
5
5
|
import "../chunk-ZAZHAVKB.js";
|
|
6
|
+
import "../chunk-PI5YNZZZ.js";
|
|
6
7
|
import "../chunk-CPW6G62V.js";
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-BO2HHVZ3.js";
|
|
8
|
+
import "../chunk-I3XDOICA.js";
|
|
9
9
|
import "../chunk-RFIXKATC.js";
|
|
10
10
|
export {
|
|
11
11
|
log_build_default as default
|
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
} from "./chunk-
|
|
2
|
+
getLicenseAnalytics
|
|
3
|
+
} from "./chunk-AWOO63ME.js";
|
|
4
4
|
import {
|
|
5
5
|
countResources,
|
|
6
6
|
hashCatalogContent,
|
|
7
7
|
serializeCounts
|
|
8
8
|
} from "./chunk-ZAZHAVKB.js";
|
|
9
|
+
import {
|
|
10
|
+
raiseEvent
|
|
11
|
+
} from "./chunk-PI5YNZZZ.js";
|
|
9
12
|
import {
|
|
10
13
|
TELEMETRY_REQUEST_TIMEOUT_MS,
|
|
11
14
|
isTelemetryDisabled
|
|
12
15
|
} from "./chunk-CPW6G62V.js";
|
|
13
|
-
import {
|
|
14
|
-
getLicenseAnalytics
|
|
15
|
-
} from "./chunk-AWOO63ME.js";
|
|
16
16
|
import {
|
|
17
17
|
getEventCatalogConfigFile,
|
|
18
18
|
verifyRequiredFieldsAreInCatalogConfigFile
|
package/dist/constants.cjs
CHANGED
package/dist/constants.js
CHANGED
package/dist/eventcatalog.cjs
CHANGED
|
@@ -181,7 +181,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
|
|
|
181
181
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
182
182
|
|
|
183
183
|
// package.json
|
|
184
|
-
var version = "5.0.0-beta.
|
|
184
|
+
var version = "5.0.0-beta.14";
|
|
185
185
|
|
|
186
186
|
// src/constants.ts
|
|
187
187
|
var VERSION = version;
|
|
@@ -2980,7 +2980,7 @@ var resolveDevPort = async ({ projectDir }) => {
|
|
|
2980
2980
|
};
|
|
2981
2981
|
var startDevPrewarm = ({
|
|
2982
2982
|
port = 3e3,
|
|
2983
|
-
paths = ["/
|
|
2983
|
+
paths = ["/"],
|
|
2984
2984
|
retries = 80,
|
|
2985
2985
|
intervalMs = 250,
|
|
2986
2986
|
initialDelayMs = 500
|
|
@@ -2989,10 +2989,13 @@ var startDevPrewarm = ({
|
|
|
2989
2989
|
const hit = (requestPath) => new Promise((resolve2) => {
|
|
2990
2990
|
const req = import_node_http.default.get(
|
|
2991
2991
|
{
|
|
2992
|
-
|
|
2992
|
+
// Not 127.0.0.1: the dev server can listen on IPv6 only ([::1]), which refused
|
|
2993
|
+
// every prewarm request
|
|
2994
|
+
hostname: "localhost",
|
|
2993
2995
|
port,
|
|
2994
2996
|
path: requestPath,
|
|
2995
|
-
|
|
2997
|
+
// A refused connection fails at once; once connected, let the first render finish
|
|
2998
|
+
timeout: 3e4
|
|
2996
2999
|
},
|
|
2997
3000
|
(res) => {
|
|
2998
3001
|
res.resume();
|
|
@@ -3251,7 +3254,9 @@ program.command("dev").description("Run development server of EventCatalog").opt
|
|
|
3251
3254
|
});
|
|
3252
3255
|
startDevPrewarm({
|
|
3253
3256
|
port: prewarmPort,
|
|
3254
|
-
|
|
3257
|
+
// Renders the homepage before anyone opens it. Requests stop at the first response,
|
|
3258
|
+
// so a probe path like /ping first would leave the homepage cold.
|
|
3259
|
+
paths: ["/"]
|
|
3255
3260
|
});
|
|
3256
3261
|
}
|
|
3257
3262
|
await runCommandWithFilteredOutput({
|
package/dist/eventcatalog.js
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
log_build_default
|
|
3
|
-
} from "./chunk-PLTLW45Q.js";
|
|
4
|
-
import "./chunk-JPT3QC5V.js";
|
|
5
|
-
import "./chunk-ZAZHAVKB.js";
|
|
6
|
-
import "./chunk-CPW6G62V.js";
|
|
7
1
|
import {
|
|
8
2
|
runMigrations
|
|
9
3
|
} from "./chunk-XUAF2H54.js";
|
|
10
4
|
import "./chunk-CA4U2JP7.js";
|
|
5
|
+
import {
|
|
6
|
+
log_build_default
|
|
7
|
+
} from "./chunk-WY6M3RPT.js";
|
|
11
8
|
import {
|
|
12
9
|
getLicenseStatus,
|
|
13
10
|
printLicenseStatus
|
|
14
11
|
} from "./chunk-AWOO63ME.js";
|
|
12
|
+
import "./chunk-ZAZHAVKB.js";
|
|
15
13
|
import {
|
|
16
14
|
FederationConflictError,
|
|
17
15
|
FederationDiagnosticError,
|
|
18
16
|
federateCatalog
|
|
19
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-KHACCTW2.js";
|
|
18
|
+
import "./chunk-WRNH5C3U.js";
|
|
19
|
+
import "./chunk-5EPNFDT5.js";
|
|
20
|
+
import "./chunk-A2RZR3U4.js";
|
|
21
|
+
import "./chunk-R7Z5ALYI.js";
|
|
20
22
|
import {
|
|
21
23
|
getFederationDiagnosticCounts,
|
|
22
24
|
getVisibleFederationDiagnostics
|
|
23
25
|
} from "./chunk-JJPB6EOZ.js";
|
|
24
|
-
import "./chunk-WRNH5C3U.js";
|
|
25
|
-
import "./chunk-5EPNFDT5.js";
|
|
26
|
-
import "./chunk-A2RZR3U4.js";
|
|
27
26
|
import "./chunk-SDZQJTJW.js";
|
|
28
27
|
import "./chunk-352FGP6W.js";
|
|
29
28
|
import {
|
|
@@ -34,10 +33,10 @@ import {
|
|
|
34
33
|
} from "./chunk-MDWB5HLO.js";
|
|
35
34
|
import {
|
|
36
35
|
generate
|
|
37
|
-
} from "./chunk-
|
|
36
|
+
} from "./chunk-4KE3EUDJ.js";
|
|
38
37
|
import {
|
|
39
38
|
logger
|
|
40
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-OFK7U7BX.js";
|
|
41
40
|
import {
|
|
42
41
|
resolve_catalog_dependencies_default
|
|
43
42
|
} from "./chunk-36RYTYT5.js";
|
|
@@ -51,7 +50,8 @@ import {
|
|
|
51
50
|
import {
|
|
52
51
|
watch
|
|
53
52
|
} from "./chunk-U63GDPPY.js";
|
|
54
|
-
import "./chunk-
|
|
53
|
+
import "./chunk-PI5YNZZZ.js";
|
|
54
|
+
import "./chunk-CPW6G62V.js";
|
|
55
55
|
import {
|
|
56
56
|
getAstroConfigPath
|
|
57
57
|
} from "./chunk-VIJJ7JKH.js";
|
|
@@ -67,7 +67,7 @@ import {
|
|
|
67
67
|
} from "./chunk-LFQJXLHG.js";
|
|
68
68
|
import {
|
|
69
69
|
VERSION
|
|
70
|
-
} from "./chunk-
|
|
70
|
+
} from "./chunk-I3XDOICA.js";
|
|
71
71
|
import {
|
|
72
72
|
getEventCatalogConfigFile,
|
|
73
73
|
verifyRequiredFieldsAreInCatalogConfigFile
|
|
@@ -669,7 +669,7 @@ var resolveDevPort = async ({ projectDir }) => {
|
|
|
669
669
|
};
|
|
670
670
|
var startDevPrewarm = ({
|
|
671
671
|
port = 3e3,
|
|
672
|
-
paths = ["/
|
|
672
|
+
paths = ["/"],
|
|
673
673
|
retries = 80,
|
|
674
674
|
intervalMs = 250,
|
|
675
675
|
initialDelayMs = 500
|
|
@@ -678,10 +678,13 @@ var startDevPrewarm = ({
|
|
|
678
678
|
const hit = (requestPath) => new Promise((resolve) => {
|
|
679
679
|
const req = http.get(
|
|
680
680
|
{
|
|
681
|
-
|
|
681
|
+
// Not 127.0.0.1: the dev server can listen on IPv6 only ([::1]), which refused
|
|
682
|
+
// every prewarm request
|
|
683
|
+
hostname: "localhost",
|
|
682
684
|
port,
|
|
683
685
|
path: requestPath,
|
|
684
|
-
|
|
686
|
+
// A refused connection fails at once; once connected, let the first render finish
|
|
687
|
+
timeout: 3e4
|
|
685
688
|
},
|
|
686
689
|
(res) => {
|
|
687
690
|
res.resume();
|
|
@@ -940,7 +943,9 @@ program.command("dev").description("Run development server of EventCatalog").opt
|
|
|
940
943
|
});
|
|
941
944
|
startDevPrewarm({
|
|
942
945
|
port: prewarmPort,
|
|
943
|
-
|
|
946
|
+
// Renders the homepage before anyone opens it. Requests stop at the first response,
|
|
947
|
+
// so a probe path like /ping first would leave the homepage cold.
|
|
948
|
+
paths: ["/"]
|
|
944
949
|
});
|
|
945
950
|
}
|
|
946
951
|
await runCommandWithFilteredOutput({
|
|
@@ -2,14 +2,14 @@ import {
|
|
|
2
2
|
FederationConflictError,
|
|
3
3
|
FederationDiagnosticError,
|
|
4
4
|
federateCatalog
|
|
5
|
-
} from "../chunk-
|
|
6
|
-
import "../chunk-JJPB6EOZ.js";
|
|
5
|
+
} from "../chunk-KHACCTW2.js";
|
|
7
6
|
import "../chunk-WRNH5C3U.js";
|
|
8
7
|
import "../chunk-5EPNFDT5.js";
|
|
9
8
|
import "../chunk-A2RZR3U4.js";
|
|
9
|
+
import "../chunk-R7Z5ALYI.js";
|
|
10
|
+
import "../chunk-JJPB6EOZ.js";
|
|
10
11
|
import "../chunk-SDZQJTJW.js";
|
|
11
12
|
import "../chunk-352FGP6W.js";
|
|
12
|
-
import "../chunk-R7Z5ALYI.js";
|
|
13
13
|
import "../chunk-RFIXKATC.js";
|
|
14
14
|
export {
|
|
15
15
|
FederationConflictError,
|
package/dist/generate.cjs
CHANGED
|
@@ -112,7 +112,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
|
|
|
112
112
|
var import_picocolors = __toESM(require("picocolors"), 1);
|
|
113
113
|
|
|
114
114
|
// package.json
|
|
115
|
-
var version = "5.0.0-beta.
|
|
115
|
+
var version = "5.0.0-beta.14";
|
|
116
116
|
|
|
117
117
|
// src/constants.ts
|
|
118
118
|
var VERSION = version;
|
package/dist/generate.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generate
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import "./chunk-
|
|
5
|
-
import "./chunk-
|
|
3
|
+
} from "./chunk-4KE3EUDJ.js";
|
|
4
|
+
import "./chunk-OFK7U7BX.js";
|
|
5
|
+
import "./chunk-I3XDOICA.js";
|
|
6
6
|
import "./chunk-RFIXKATC.js";
|
|
7
7
|
export {
|
|
8
8
|
generate
|
package/dist/utils/cli-logger.js
CHANGED
|
@@ -35,6 +35,8 @@ import config from './src/utils/eventcatalog-config/source.ts';
|
|
|
35
35
|
import preprocessExpressiveCodeConfig from './integrations/expressive-code-config.mjs';
|
|
36
36
|
import catalogAssets from './integrations/catalog-assets.mjs';
|
|
37
37
|
import runtimeDependencies from './integrations/runtime-dependencies.mjs';
|
|
38
|
+
import stableDependencyOrder from './integrations/stable-dependency-order.mjs';
|
|
39
|
+
import skipStartupReload from './integrations/skip-startup-reload.mjs';
|
|
38
40
|
import { getRuntimePaths } from './integrations/runtime-paths.mjs';
|
|
39
41
|
import { collectClientDependencies } from './integrations/client-dependencies.mjs';
|
|
40
42
|
|
|
@@ -166,6 +168,8 @@ export default defineConfig({
|
|
|
166
168
|
},
|
|
167
169
|
plugins: [
|
|
168
170
|
runtimeDependencies(),
|
|
171
|
+
stableDependencyOrder(),
|
|
172
|
+
skipStartupReload(),
|
|
169
173
|
tailwindcss(),
|
|
170
174
|
...(await eventCatalogLikeC4(projectDirectory)),
|
|
171
175
|
...(config.trailingSlash === true ? [astroTrailingSlashEndpointFix()] : []),
|
|
@@ -224,7 +228,15 @@ export default defineConfig({
|
|
|
224
228
|
},
|
|
225
229
|
optimizeDeps: {
|
|
226
230
|
exclude: [],
|
|
227
|
-
include:
|
|
231
|
+
include: [
|
|
232
|
+
...collectClientDependencies(),
|
|
233
|
+
// <ClientRouter /> imports these from a virtual module the dependency scan can't follow,
|
|
234
|
+
// so without them every cold start re-bundles on the first page and reloads it
|
|
235
|
+
'astro/virtual-modules/transitions-events.js',
|
|
236
|
+
'astro/virtual-modules/transitions-router.js',
|
|
237
|
+
'astro/virtual-modules/transitions-swap-functions.js',
|
|
238
|
+
'astro/virtual-modules/transitions-types.js',
|
|
239
|
+
],
|
|
228
240
|
},
|
|
229
241
|
},
|
|
230
242
|
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Astro writes its content data store as the dev server starts, which sends the browser a
|
|
2
|
+
// full reload. With no browser connected yet, Vite holds that reload and hands it to the
|
|
3
|
+
// first page that connects, so the first page opened after every start loaded twice.
|
|
4
|
+
// A reload sent before any page has been served has nothing to refresh (a tab left open
|
|
5
|
+
// from the previous run reloads itself when it reconnects), so drop those. Once a page has
|
|
6
|
+
// been requested, reloads pass through as usual.
|
|
7
|
+
|
|
8
|
+
const isFullReload = (payload) => typeof payload === 'object' && payload?.type === 'full-reload';
|
|
9
|
+
|
|
10
|
+
export default function skipStartupReload() {
|
|
11
|
+
return {
|
|
12
|
+
name: 'eventcatalog:skip-startup-reload',
|
|
13
|
+
apply: 'serve',
|
|
14
|
+
configureServer(server) {
|
|
15
|
+
let pageRequested = false;
|
|
16
|
+
server.middlewares.use((req, _res, next) => {
|
|
17
|
+
// Browser navigations ask for HTML; the CLI's prewarm request doesn't
|
|
18
|
+
if (!pageRequested && req.headers.accept?.includes('text/html')) pageRequested = true;
|
|
19
|
+
next();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
for (const channel of new Set([server.ws, server.environments?.client?.hot].filter(Boolean))) {
|
|
23
|
+
const send = channel.send.bind(channel);
|
|
24
|
+
channel.send = (...args) => {
|
|
25
|
+
if (!pageRequested && isFullReload(args[0])) return;
|
|
26
|
+
return send(...args);
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Astro crawls framework packages with vitefu, which looks them up concurrently and lists
|
|
2
|
+
// each one in `resolve.external` / `resolve.noExternal` as its lookup finishes, so the
|
|
3
|
+
// order changes from one start to the next. Vite hashes these lists unsorted to decide
|
|
4
|
+
// whether its pre-bundled dependencies are still valid, so every `eventcatalog dev` saw a
|
|
5
|
+
// "changed" config and re-bundled all of them. Only membership matters in these lists, so
|
|
6
|
+
// sorting them keeps the hash (and the dependency cache) stable across starts.
|
|
7
|
+
|
|
8
|
+
const byName = (a, b) => {
|
|
9
|
+
const left = String(a);
|
|
10
|
+
const right = String(b);
|
|
11
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const sortResolveLists = (resolve) => {
|
|
15
|
+
if (Array.isArray(resolve?.external)) resolve.external.sort(byName);
|
|
16
|
+
if (Array.isArray(resolve?.noExternal)) resolve.noExternal.sort(byName);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export default function stableDependencyOrder() {
|
|
20
|
+
return {
|
|
21
|
+
name: 'eventcatalog:stable-dependency-order',
|
|
22
|
+
apply: 'serve',
|
|
23
|
+
configResolved(config) {
|
|
24
|
+
sortResolveLists(config.resolve);
|
|
25
|
+
sortResolveLists(config.ssr);
|
|
26
|
+
for (const environment of Object.values(config.environments ?? {})) sortResolveLists(environment.resolve);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -170,6 +170,16 @@ const nodeRadius = (degree: number, isHub = false) =>
|
|
|
170
170
|
/** Above this node count, expensive extras (collision force) are disabled. */
|
|
171
171
|
const LARGE_GRAPH_NODE_COUNT = 1500;
|
|
172
172
|
|
|
173
|
+
/**
|
|
174
|
+
* The first layout settles before it paints instead of animating into place for ~5s: ticks
|
|
175
|
+
* run in short slices across frames (no long tasks) until it settles, ~40ms of work for 100
|
|
176
|
+
* nodes. Big catalogs stop once ticking has taken the work limit, paint past their largest
|
|
177
|
+
* moves and animate the rest. The limit counts ticking time, not elapsed time, so a page busy
|
|
178
|
+
* hydrating other islands doesn't cut the settle short.
|
|
179
|
+
*/
|
|
180
|
+
const INITIAL_SETTLE_SLICE_MS = 12;
|
|
181
|
+
const INITIAL_SETTLE_WORK_LIMIT_MS = 400;
|
|
182
|
+
|
|
173
183
|
/** Sprite resolution for node icons (drawn scaled-down, so keep it crisp) */
|
|
174
184
|
const ICON_SPRITE_SIZE = 64;
|
|
175
185
|
|
|
@@ -617,6 +627,10 @@ const CatalogForceGraph = ({
|
|
|
617
627
|
// normal-sized, whatever the lens's usual hubs are
|
|
618
628
|
const isHub = (d: SimNode) => (focusKey ? d.key === focusKey : hubs.has(d.collection));
|
|
619
629
|
|
|
630
|
+
// Later layouts (lens, focus and filter changes) animate from the previous positions
|
|
631
|
+
const isFirstLayout = positionsRef.current.size === 0;
|
|
632
|
+
// Nothing paints until the first layout has settled (see INITIAL_SETTLE_SLICE_MS)
|
|
633
|
+
let settling = isFirstLayout;
|
|
620
634
|
const visibleNodes: SimNode[] = viewNodes
|
|
621
635
|
.filter((n) => !hiddenCollections.has(n.collection))
|
|
622
636
|
.map((n) => ({ ...n, degree: 0, ...positionsRef.current.get(n.key) }));
|
|
@@ -734,6 +748,7 @@ const CatalogForceGraph = ({
|
|
|
734
748
|
};
|
|
735
749
|
|
|
736
750
|
const draw = () => {
|
|
751
|
+
if (settling) return;
|
|
737
752
|
context.save();
|
|
738
753
|
context.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0);
|
|
739
754
|
context.clearRect(0, 0, width, height);
|
|
@@ -1014,6 +1029,29 @@ const CatalogForceGraph = ({
|
|
|
1014
1029
|
);
|
|
1015
1030
|
}
|
|
1016
1031
|
|
|
1032
|
+
// Settle the first layout before it paints rather than animating it into place
|
|
1033
|
+
let settleFrame: number | undefined;
|
|
1034
|
+
if (isFirstLayout) {
|
|
1035
|
+
simulation.stop();
|
|
1036
|
+
let workLeft = INITIAL_SETTLE_WORK_LIMIT_MS;
|
|
1037
|
+
const settle = () => {
|
|
1038
|
+
const sliceStart = performance.now();
|
|
1039
|
+
const sliceEnd = sliceStart + Math.min(INITIAL_SETTLE_SLICE_MS, workLeft);
|
|
1040
|
+
while (simulation.alpha() > simulation.alphaMin() && performance.now() < sliceEnd) simulation.tick();
|
|
1041
|
+
workLeft -= performance.now() - sliceStart;
|
|
1042
|
+
if (simulation.alpha() > simulation.alphaMin() && workLeft > 0) {
|
|
1043
|
+
settleFrame = requestAnimationFrame(settle);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
settling = false;
|
|
1047
|
+
// Settled: paint it as is (restarting would run one tick more than the animated
|
|
1048
|
+
// layout). Otherwise paint and animate whatever is left.
|
|
1049
|
+
if (simulation.alpha() <= simulation.alphaMin()) fitToView();
|
|
1050
|
+
else simulation.restart();
|
|
1051
|
+
};
|
|
1052
|
+
settleFrame = requestAnimationFrame(settle);
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1017
1055
|
const findNode = (canvasX: number, canvasY: number): SimNode | null => {
|
|
1018
1056
|
if (Number.isNaN(canvasX) || Number.isNaN(canvasY)) return null;
|
|
1019
1057
|
const x = transform.invertX(canvasX);
|
|
@@ -1219,6 +1257,7 @@ const CatalogForceGraph = ({
|
|
|
1219
1257
|
for (const node of visibleNodes) {
|
|
1220
1258
|
if (node.x !== undefined && node.y !== undefined) positionsRef.current.set(node.key, { x: node.x, y: node.y });
|
|
1221
1259
|
}
|
|
1260
|
+
if (settleFrame !== undefined) cancelAnimationFrame(settleFrame);
|
|
1222
1261
|
simulation.stop();
|
|
1223
1262
|
// The tooltip outlives this effect — never leave it showing stale content
|
|
1224
1263
|
tooltip.style.opacity = '0';
|
|
@@ -16,34 +16,22 @@
|
|
|
16
16
|
import { setSidebarData, sidebarStore } from '@stores/sidebar-store';
|
|
17
17
|
import { buildUrl } from '@utils/url-builder';
|
|
18
18
|
|
|
19
|
-
// Only fetch if we haven't already loaded the data
|
|
19
|
+
// Only fetch if we haven't already loaded the data. Fetched straight away rather than when
|
|
20
|
+
// the browser is idle: the sidebar's first paint comes from the server (see SideNav.astro),
|
|
21
|
+
// but drilling down and search wait on this data
|
|
20
22
|
if (!sidebarStore.get()) {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.then((data) => {
|
|
35
|
-
setSidebarData(data);
|
|
36
|
-
})
|
|
37
|
-
.catch((error) => {
|
|
38
|
-
console.error('Error loading sidebar data:', error);
|
|
39
|
-
});
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// Defer non-critical sidebar payload fetch until browser is idle
|
|
43
|
-
if (typeof window.requestIdleCallback === 'function') {
|
|
44
|
-
window.requestIdleCallback(loadSidebarData, { timeout: 1200 });
|
|
45
|
-
} else {
|
|
46
|
-
setTimeout(loadSidebarData, 300);
|
|
47
|
-
}
|
|
23
|
+
fetch(buildUrl('/api/sidebar-data.json', true))
|
|
24
|
+
.then((response) => {
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
throw new Error(`Failed to fetch sidebar data: ${response.status}`);
|
|
27
|
+
}
|
|
28
|
+
return response.json();
|
|
29
|
+
})
|
|
30
|
+
.then((data) => {
|
|
31
|
+
setSidebarData(data);
|
|
32
|
+
})
|
|
33
|
+
.catch((error) => {
|
|
34
|
+
console.error('Error loading sidebar data:', error);
|
|
35
|
+
});
|
|
48
36
|
}
|
|
49
37
|
</script>
|
|
@@ -21,57 +21,56 @@ import {
|
|
|
21
21
|
type FavoriteItem,
|
|
22
22
|
} from '@stores/favorites-store';
|
|
23
23
|
import {
|
|
24
|
+
buildNodeLookup,
|
|
24
25
|
canCollapseGroup,
|
|
25
26
|
findNodeKeyByUrl,
|
|
26
27
|
getBadgeClasses,
|
|
27
28
|
getDefaultCollapsedState,
|
|
28
29
|
hidesDefaultItemIcon,
|
|
29
30
|
isGroupCollapsed,
|
|
31
|
+
resolveNavRef,
|
|
32
|
+
rootLevel,
|
|
33
|
+
stripBasePath,
|
|
30
34
|
toggleGroupCollapsed,
|
|
35
|
+
type NavigationLevel,
|
|
36
|
+
type SidebarSnapshot,
|
|
31
37
|
} from './utils';
|
|
32
38
|
import { resolveIconUrl } from '@utils/icon';
|
|
33
39
|
|
|
34
40
|
const cn = (...classes: (string | false | undefined)[]) => classes.filter(Boolean).join(' ');
|
|
35
41
|
|
|
36
|
-
/**
|
|
37
|
-
* Strip the configured base path from a pathname so URL pattern matching works
|
|
38
|
-
* regardless of the base path setting (e.g. `/eventcatalog/docs/...` → `/docs/...`).
|
|
39
|
-
*/
|
|
40
|
-
const stripBasePath = (pathname: string): string => {
|
|
41
|
-
const base = __EC_BASE__.replace(/\/$/, ''); // e.g. '/eventcatalog'
|
|
42
|
-
if (base && pathname.startsWith(base)) {
|
|
43
|
-
return pathname.slice(base.length) || '/';
|
|
44
|
-
}
|
|
45
|
-
return pathname;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
42
|
// ============================================
|
|
49
43
|
// Component
|
|
50
44
|
// ============================================
|
|
51
45
|
|
|
52
|
-
type
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
46
|
+
type NestedSideBarProps = {
|
|
47
|
+
/**
|
|
48
|
+
* This page's level, built on the server. It renders the sidebar into the HTML and keeps
|
|
49
|
+
* the island showing it until the full navigation data (sidebar-data.json) is loaded.
|
|
50
|
+
*/
|
|
51
|
+
snapshot?: SidebarSnapshot;
|
|
57
52
|
};
|
|
58
53
|
|
|
59
|
-
export default function NestedSideBar() {
|
|
60
|
-
const
|
|
54
|
+
export default function NestedSideBar({ snapshot }: NestedSideBarProps) {
|
|
55
|
+
const storeData = useStore(sidebarStore);
|
|
61
56
|
const favorites = useStore(favoritesStore);
|
|
57
|
+
// Drill-down, restoring state and favorites need the whole catalog; until it loads the
|
|
58
|
+
// snapshot only shows this page's level
|
|
59
|
+
const hasFullData = storeData !== null;
|
|
60
|
+
const data = storeData ?? snapshot?.data ?? null;
|
|
62
61
|
|
|
63
62
|
// Guard against undefined data (e.g., during hydration)
|
|
64
63
|
// Use useMemo to ensure stable references for roots and nodes
|
|
65
64
|
const roots = useMemo(() => data?.roots ?? [], [data?.roots]);
|
|
66
65
|
const nodes = useMemo(() => data?.nodes ?? {}, [data?.nodes]);
|
|
67
66
|
|
|
68
|
-
const [navigationStack, setNavigationStack] = useState<NavigationLevel[]>(
|
|
69
|
-
{ key: null, entries: [], title: 'Documentation' }
|
|
70
|
-
|
|
67
|
+
const [navigationStack, setNavigationStack] = useState<NavigationLevel[]>(
|
|
68
|
+
() => snapshot?.stack ?? [{ key: null, entries: [], title: 'Documentation' }]
|
|
69
|
+
);
|
|
71
70
|
const [animationKey, setAnimationKey] = useState(0);
|
|
72
71
|
const [slideDirection, setSlideDirection] = useState<'forward' | 'backward' | null>(null);
|
|
73
72
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
74
|
-
const [currentPath, setCurrentPath] = useState<string>('');
|
|
73
|
+
const [currentPath, setCurrentPath] = useState<string>(snapshot?.url ?? '');
|
|
75
74
|
const [sectionCollapsePreferences, setSectionCollapsePreferences] = useState(loadCollapsedSections);
|
|
76
75
|
const [showPathPreview, setShowPathPreview] = useState(false);
|
|
77
76
|
const [showFullPath, setShowFullPath] = useState(false);
|
|
@@ -79,33 +78,7 @@ export default function NestedSideBar() {
|
|
|
79
78
|
|
|
80
79
|
// Build a lookup map for faster URL navigation
|
|
81
80
|
// Map format: "type:id" -> "nodeKey"
|
|
82
|
-
const nodeLookup = useMemo(() =>
|
|
83
|
-
const lookup = new Map<string, string>();
|
|
84
|
-
|
|
85
|
-
Object.entries(nodes).forEach(([key, value]) => {
|
|
86
|
-
// Skip keys that are references (string values pointing to other keys)
|
|
87
|
-
// These are unversioned aliases like "domain:E-Commerce" -> "domain:E-Commerce:1.0.0"
|
|
88
|
-
if (typeof value === 'string') return;
|
|
89
|
-
|
|
90
|
-
// Key formats:
|
|
91
|
-
// - "type:id:version" (e.g., "service:OrdersService:0.0.3")
|
|
92
|
-
// - "type:id" (e.g., "user:john", "team:backend") - non-versioned resources
|
|
93
|
-
// - "list:name" (e.g., "list:domains") - skip these
|
|
94
|
-
const parts = key.split(':');
|
|
95
|
-
|
|
96
|
-
// Skip list items
|
|
97
|
-
if (parts[0] === 'list') return;
|
|
98
|
-
|
|
99
|
-
if (parts.length >= 2) {
|
|
100
|
-
// Store as "type:id"
|
|
101
|
-
const type = parts[0];
|
|
102
|
-
const id = parts[1];
|
|
103
|
-
lookup.set(`${type}:${id}`, key);
|
|
104
|
-
}
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
return lookup;
|
|
108
|
-
}, [nodes]);
|
|
81
|
+
const nodeLookup = useMemo(() => buildNodeLookup(nodes), [nodes]);
|
|
109
82
|
|
|
110
83
|
/**
|
|
111
84
|
* Toggle section collapse state
|
|
@@ -126,7 +99,7 @@ export default function NestedSideBar() {
|
|
|
126
99
|
setNavigationStack((prevStack) => {
|
|
127
100
|
// Only update if the current stack has no entries (initial state)
|
|
128
101
|
if (prevStack.length === 1 && prevStack[0].entries.length === 0) {
|
|
129
|
-
return [
|
|
102
|
+
return [rootLevel(roots)];
|
|
130
103
|
}
|
|
131
104
|
return prevStack;
|
|
132
105
|
});
|
|
@@ -146,21 +119,7 @@ export default function NestedSideBar() {
|
|
|
146
119
|
* Resolve a child reference to a NavNode
|
|
147
120
|
* Handles both direct keys and string references (unversioned aliases pointing to versioned keys)
|
|
148
121
|
*/
|
|
149
|
-
const resolveRef = useCallback(
|
|
150
|
-
(ref: ChildRef): NavNode | null => {
|
|
151
|
-
if (typeof ref === 'string') {
|
|
152
|
-
const node = nodes[ref];
|
|
153
|
-
if (!node) return null;
|
|
154
|
-
// If node is a string, it's a reference to another key (e.g., unversioned alias)
|
|
155
|
-
if (typeof node === 'string') {
|
|
156
|
-
return (nodes[node] as NavNode) ?? null;
|
|
157
|
-
}
|
|
158
|
-
return node;
|
|
159
|
-
}
|
|
160
|
-
return ref;
|
|
161
|
-
},
|
|
162
|
-
[nodes]
|
|
163
|
-
);
|
|
122
|
+
const resolveRef = useCallback((ref: ChildRef): NavNode | null => resolveNavRef(nodes, ref), [nodes]);
|
|
164
123
|
|
|
165
124
|
/**
|
|
166
125
|
* Check if a node is visible (default: true)
|
|
@@ -175,7 +134,7 @@ export default function NestedSideBar() {
|
|
|
175
134
|
*/
|
|
176
135
|
const buildStackFromPath = useCallback(
|
|
177
136
|
(path: string[]): NavigationLevel[] => {
|
|
178
|
-
const stack: NavigationLevel[] = [
|
|
137
|
+
const stack: NavigationLevel[] = [rootLevel(roots)];
|
|
179
138
|
|
|
180
139
|
for (const key of path) {
|
|
181
140
|
const node = resolveRef(key);
|
|
@@ -281,7 +240,7 @@ export default function NestedSideBar() {
|
|
|
281
240
|
if (foundNode && foundNode.pages && foundNode.pages.length > 0) {
|
|
282
241
|
// Fallback: Flattened navigation
|
|
283
242
|
return [
|
|
284
|
-
|
|
243
|
+
rootLevel(roots),
|
|
285
244
|
{ key: foundNodeKey, entries: foundNode.pages, title: foundNode.title, badge: foundNode.badge },
|
|
286
245
|
];
|
|
287
246
|
}
|
|
@@ -296,7 +255,7 @@ export default function NestedSideBar() {
|
|
|
296
255
|
setSlideDirection('backward');
|
|
297
256
|
setAnimationKey((prev) => prev + 1);
|
|
298
257
|
}
|
|
299
|
-
return [
|
|
258
|
+
return [rootLevel(roots)];
|
|
300
259
|
});
|
|
301
260
|
return true;
|
|
302
261
|
}
|
|
@@ -309,14 +268,14 @@ export default function NestedSideBar() {
|
|
|
309
268
|
* Restore state from localStorage on mount, or navigate to URL
|
|
310
269
|
*/
|
|
311
270
|
useEffect(() => {
|
|
312
|
-
if (!
|
|
271
|
+
if (!hasFullData || roots.length === 0 || isInitialized) return;
|
|
313
272
|
|
|
314
273
|
const currentUrl = window.location.pathname;
|
|
315
274
|
const normalizedCurrentUrl = stripBasePath(currentUrl);
|
|
316
275
|
|
|
317
276
|
// Force root navigation on homepage
|
|
318
277
|
if (normalizedCurrentUrl === '/' || normalizedCurrentUrl === '') {
|
|
319
|
-
setNavigationStack([
|
|
278
|
+
setNavigationStack([rootLevel(roots)]);
|
|
320
279
|
setIsInitialized(true);
|
|
321
280
|
return;
|
|
322
281
|
}
|
|
@@ -347,7 +306,7 @@ export default function NestedSideBar() {
|
|
|
347
306
|
const targetNode = resolveRef(targetKey);
|
|
348
307
|
if (targetNode && targetNode.pages && targetNode.pages.length > 0) {
|
|
349
308
|
finalStack = [
|
|
350
|
-
|
|
309
|
+
rootLevel(roots),
|
|
351
310
|
{ key: targetKey, entries: targetNode.pages, title: targetNode.title, badge: targetNode.badge },
|
|
352
311
|
];
|
|
353
312
|
}
|
|
@@ -355,13 +314,13 @@ export default function NestedSideBar() {
|
|
|
355
314
|
|
|
356
315
|
// 3. Fallback to root
|
|
357
316
|
if (!finalStack) {
|
|
358
|
-
setNavigationStack([
|
|
317
|
+
setNavigationStack([rootLevel(roots)]);
|
|
359
318
|
} else {
|
|
360
319
|
setNavigationStack(finalStack);
|
|
361
320
|
}
|
|
362
321
|
|
|
363
322
|
setIsInitialized(true);
|
|
364
|
-
}, [
|
|
323
|
+
}, [hasFullData, roots, isInitialized, buildStackFromPath, findNodeKeyForUrl, tryConnectStack, resolveRef]);
|
|
365
324
|
|
|
366
325
|
/**
|
|
367
326
|
* Save state whenever navigation changes
|
|
@@ -575,6 +534,7 @@ export default function NestedSideBar() {
|
|
|
575
534
|
* Handle drilling down into an item with children
|
|
576
535
|
*/
|
|
577
536
|
const handleDrillDown = (node: NavNode, nodeKey: string | null) => {
|
|
537
|
+
if (!hasFullData) return;
|
|
578
538
|
if (node.pages && node.pages.length > 0) {
|
|
579
539
|
setSlideDirection('forward');
|
|
580
540
|
setAnimationKey((prev) => prev + 1);
|
|
@@ -590,7 +550,7 @@ export default function NestedSideBar() {
|
|
|
590
550
|
* Navigate back one level
|
|
591
551
|
*/
|
|
592
552
|
const navigateBack = () => {
|
|
593
|
-
if (navigationStack.length > 1) {
|
|
553
|
+
if (hasFullData && navigationStack.length > 1) {
|
|
594
554
|
setSlideDirection('backward');
|
|
595
555
|
setAnimationKey((prev) => prev + 1);
|
|
596
556
|
setNavigationStack(navigationStack.slice(0, -1));
|
|
@@ -604,7 +564,7 @@ export default function NestedSideBar() {
|
|
|
604
564
|
* Navigate to a specific level in the stack
|
|
605
565
|
*/
|
|
606
566
|
const navigateToLevel = (levelIndex: number) => {
|
|
607
|
-
if (levelIndex < navigationStack.length - 1) {
|
|
567
|
+
if (hasFullData && levelIndex < navigationStack.length - 1) {
|
|
608
568
|
setSlideDirection('backward');
|
|
609
569
|
setAnimationKey((prev) => prev + 1);
|
|
610
570
|
setNavigationStack(navigationStack.slice(0, levelIndex + 1));
|
|
@@ -634,6 +594,7 @@ export default function NestedSideBar() {
|
|
|
634
594
|
* Navigate to a favorited item
|
|
635
595
|
*/
|
|
636
596
|
const navigateToFavorite = (favorite: FavoriteItem) => {
|
|
597
|
+
if (!hasFullData) return;
|
|
637
598
|
// If it has an href and no children, just navigate to the URL
|
|
638
599
|
const node = resolveRef(favorite.nodeKey);
|
|
639
600
|
if (favorite.href && (!node?.pages || node.pages.length === 0)) {
|
|
@@ -107,6 +107,8 @@ export const saveCollapsedSections = (preferences: SectionCollapsePreferences):
|
|
|
107
107
|
};
|
|
108
108
|
|
|
109
109
|
export const loadCollapsedSections = (): SectionCollapsePreferences => {
|
|
110
|
+
// Rendered on the server for the first paint, where there is nothing saved yet
|
|
111
|
+
if (typeof localStorage === 'undefined') return createSectionCollapsePreferences();
|
|
110
112
|
try {
|
|
111
113
|
const stored = localStorage.getItem(SECTION_PREFERENCES_KEY);
|
|
112
114
|
if (stored) {
|
|
@@ -1,9 +1,130 @@
|
|
|
1
1
|
// Shared utilities for NestedSideBar components
|
|
2
2
|
|
|
3
|
-
import type { NavNode } from '@stores/sidebar-store/state';
|
|
3
|
+
import type { ChildRef, NavigationData, NavNode } from '@stores/sidebar-store/state';
|
|
4
4
|
|
|
5
5
|
export const SIDEBAR_GROUP_COLLAPSE_THRESHOLD = 5;
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Strip the configured base path from a pathname so URL pattern matching works
|
|
9
|
+
* regardless of the base path setting (e.g. `/eventcatalog/docs/...` → `/docs/...`).
|
|
10
|
+
*/
|
|
11
|
+
export const stripBasePath = (pathname: string): string => {
|
|
12
|
+
const base = __EC_BASE__.replace(/\/$/, ''); // e.g. '/eventcatalog'
|
|
13
|
+
if (base && pathname.startsWith(base)) {
|
|
14
|
+
return pathname.slice(base.length) || '/';
|
|
15
|
+
}
|
|
16
|
+
return pathname;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type NavigationLevel = {
|
|
20
|
+
key: string | null; // The key of the node that was drilled into (null for root)
|
|
21
|
+
entries: ChildRef[];
|
|
22
|
+
title: string;
|
|
23
|
+
badge?: string; // Category badge (e.g., "Domain", "Service")
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const rootLevel = (roots: ChildRef[]): NavigationLevel => ({ key: null, entries: roots, title: 'Documentation' });
|
|
27
|
+
|
|
28
|
+
/** Resolve a child reference to a node, following unversioned aliases (string → key). */
|
|
29
|
+
export const resolveNavRef = (nodes: NavigationData['nodes'], ref: ChildRef): NavNode | null => {
|
|
30
|
+
if (typeof ref !== 'string') return ref;
|
|
31
|
+
const node = nodes[ref];
|
|
32
|
+
if (!node) return null;
|
|
33
|
+
if (typeof node === 'string') return (nodes[node] as NavNode) ?? null;
|
|
34
|
+
return node;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Map of "type:id" → versioned node key, used to match URLs to nodes. */
|
|
38
|
+
export const buildNodeLookup = (nodes: NavigationData['nodes']): Map<string, string> => {
|
|
39
|
+
const lookup = new Map<string, string>();
|
|
40
|
+
|
|
41
|
+
Object.entries(nodes).forEach(([key, value]) => {
|
|
42
|
+
// Skip keys that are references (string values pointing to other keys)
|
|
43
|
+
// These are unversioned aliases like "domain:E-Commerce" -> "domain:E-Commerce:1.0.0"
|
|
44
|
+
if (typeof value === 'string') return;
|
|
45
|
+
|
|
46
|
+
// Key formats:
|
|
47
|
+
// - "type:id:version" (e.g., "service:OrdersService:0.0.3")
|
|
48
|
+
// - "type:id" (e.g., "user:john", "team:backend") - non-versioned resources
|
|
49
|
+
// - "list:name" (e.g., "list:domains") - skip these
|
|
50
|
+
const parts = key.split(':');
|
|
51
|
+
|
|
52
|
+
// Skip list items
|
|
53
|
+
if (parts[0] === 'list') return;
|
|
54
|
+
|
|
55
|
+
if (parts.length >= 2) {
|
|
56
|
+
// Store as "type:id"
|
|
57
|
+
lookup.set(`${parts[0]}:${parts[1]}`, key);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return lookup;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* What the sidebar shows for a URL before it knows the reader's saved drill-down path:
|
|
66
|
+
* the root level on the homepage or for pages without children, otherwise the matching
|
|
67
|
+
* resource's own level (the same "flattened" stack the client falls back to).
|
|
68
|
+
*/
|
|
69
|
+
export const getStackForUrl = (
|
|
70
|
+
url: string,
|
|
71
|
+
data: NavigationData,
|
|
72
|
+
nodeLookup: ReadonlyMap<string, string> = buildNodeLookup(data.nodes)
|
|
73
|
+
): NavigationLevel[] => {
|
|
74
|
+
const root = rootLevel(data.roots);
|
|
75
|
+
if (url === '/' || url === '') return [root];
|
|
76
|
+
|
|
77
|
+
const targetKey = findNodeKeyByUrl(url, data.nodes, nodeLookup);
|
|
78
|
+
const target = targetKey ? resolveNavRef(data.nodes, targetKey) : null;
|
|
79
|
+
if (!targetKey || !target?.pages?.length) return [root];
|
|
80
|
+
|
|
81
|
+
return [root, { key: targetKey, entries: target.pages, title: target.title, badge: target.badge }];
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Everything the sidebar needs to render one page's level, without the whole catalog: the
|
|
86
|
+
* stack for the URL and only the nodes that level shows (its items, and the contents of its
|
|
87
|
+
* groups). Rendered on the server so the sidebar appears with the HTML, and handed to the
|
|
88
|
+
* island so it keeps showing it until the full navigation data loads.
|
|
89
|
+
*/
|
|
90
|
+
export type SidebarSnapshot = {
|
|
91
|
+
url: string;
|
|
92
|
+
stack: NavigationLevel[];
|
|
93
|
+
data: NavigationData;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const nodeLookupCache = new WeakMap<NavigationData, Map<string, string>>();
|
|
97
|
+
|
|
98
|
+
export const buildSidebarSnapshot = (data: NavigationData, url: string): SidebarSnapshot => {
|
|
99
|
+
let nodeLookup = nodeLookupCache.get(data);
|
|
100
|
+
if (!nodeLookup) {
|
|
101
|
+
nodeLookup = buildNodeLookup(data.nodes);
|
|
102
|
+
nodeLookupCache.set(data, nodeLookup);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const stack = getStackForUrl(url, data, nodeLookup);
|
|
106
|
+
const level = stack[stack.length - 1];
|
|
107
|
+
const nodes: NavigationData['nodes'] = {};
|
|
108
|
+
|
|
109
|
+
const include = (ref: ChildRef) => {
|
|
110
|
+
if (typeof ref === 'string') {
|
|
111
|
+
if (ref in nodes) return;
|
|
112
|
+
const value = data.nodes[ref];
|
|
113
|
+
if (value === undefined) return;
|
|
114
|
+
nodes[ref] = value;
|
|
115
|
+
if (typeof value === 'string' && data.nodes[value] !== undefined) nodes[value] = data.nodes[value];
|
|
116
|
+
}
|
|
117
|
+
const node = resolveNavRef(data.nodes, ref);
|
|
118
|
+
// Groups render their children inline; items with children only need their own node
|
|
119
|
+
if (node?.type === 'group') node.pages?.forEach(include);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (level.key) include(level.key);
|
|
123
|
+
level.entries.forEach(include);
|
|
124
|
+
|
|
125
|
+
return { url, stack, data: { roots: data.roots, nodes } };
|
|
126
|
+
};
|
|
127
|
+
|
|
7
128
|
/**
|
|
8
129
|
* The user's overrides of each group's default collapse state, keyed by group id.
|
|
9
130
|
* A group is in at most one set; absence means "use the group's default".
|
|
@@ -3,10 +3,18 @@ import type { HTMLAttributes } from 'astro/types';
|
|
|
3
3
|
|
|
4
4
|
interface Props extends Omit<HTMLAttributes<'div'>, 'children'> {}
|
|
5
5
|
import NestedSideBar from './NestedSideBar';
|
|
6
|
+
import { buildSidebarSnapshot, stripBasePath } from './NestedSideBar/utils';
|
|
7
|
+
import { getNestedSideBarData } from '@stores/sidebar-store/state';
|
|
6
8
|
import { ClientRouter } from 'astro:transitions';
|
|
9
|
+
|
|
10
|
+
// Render this page's sidebar level into the HTML so it shows before any JavaScript runs;
|
|
11
|
+
// the full navigation still loads from /api/sidebar-data.json (see SearchDataLoader)
|
|
12
|
+
const snapshot = buildSidebarSnapshot(await getNestedSideBarData(), stripBasePath(Astro.url.pathname));
|
|
7
13
|
---
|
|
8
14
|
|
|
9
15
|
<div {...Astro.props}>
|
|
10
16
|
<ClientRouter />
|
|
11
|
-
<NestedSideBar client:only="react"
|
|
17
|
+
<NestedSideBar client:only="react" snapshot={snapshot}>
|
|
18
|
+
<NestedSideBar slot="fallback" snapshot={snapshot} />
|
|
19
|
+
</NestedSideBar>
|
|
12
20
|
</div>
|
package/package.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"license": "BUSL-1.1",
|
|
9
9
|
"type": "module",
|
|
10
|
-
"version": "5.0.0-beta.
|
|
10
|
+
"version": "5.0.0-beta.14",
|
|
11
11
|
"publishConfig": {
|
|
12
12
|
"access": "public"
|
|
13
13
|
},
|
|
@@ -140,9 +140,9 @@
|
|
|
140
140
|
"y-protocols": "^1.0.7",
|
|
141
141
|
"yjs": "^13.6.33",
|
|
142
142
|
"zod": "^4.3.6",
|
|
143
|
+
"@eventcatalog/sdk": "2.30.0-beta.1",
|
|
143
144
|
"@eventcatalog/linter": "1.2.0-beta.2",
|
|
144
|
-
"@eventcatalog/visualiser": "^5.0.0-beta.6"
|
|
145
|
-
"@eventcatalog/sdk": "2.30.0-beta.1"
|
|
145
|
+
"@eventcatalog/visualiser": "^5.0.0-beta.6"
|
|
146
146
|
},
|
|
147
147
|
"devDependencies": {
|
|
148
148
|
"@astrojs/check": "^0.9.10",
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getFederationDiagnostics,
|
|
3
|
-
resolveFederationRules
|
|
4
|
-
} from "./chunk-JJPB6EOZ.js";
|
|
5
1
|
import {
|
|
6
2
|
withFederationOutputTransaction
|
|
7
3
|
} from "./chunk-WRNH5C3U.js";
|
|
@@ -14,6 +10,10 @@ import {
|
|
|
14
10
|
import {
|
|
15
11
|
createFederationContentCache
|
|
16
12
|
} from "./chunk-R7Z5ALYI.js";
|
|
13
|
+
import {
|
|
14
|
+
getFederationDiagnostics,
|
|
15
|
+
resolveFederationRules
|
|
16
|
+
} from "./chunk-JJPB6EOZ.js";
|
|
17
17
|
import {
|
|
18
18
|
getEventCatalogConfigFile
|
|
19
19
|
} from "./chunk-RFIXKATC.js";
|