@eventcatalog/core 4.10.8 → 4.10.9

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 (39) 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-LB5EMDAY.js → chunk-3EEFSHHW.js} +1 -1
  6. package/dist/{chunk-CDWE6WWB.js → chunk-5WB6CXZH.js} +1 -1
  7. package/dist/{chunk-W5C4K7F2.js → chunk-BYPWHDX6.js} +1 -1
  8. package/dist/{chunk-U52FCZQ6.js → chunk-EG62NTWS.js} +7 -7
  9. package/dist/{chunk-W6IMDOJJ.js → chunk-ONRJA5DX.js} +1 -1
  10. package/dist/{chunk-DRK5QSQC.js → chunk-WD5UW4A4.js} +1 -1
  11. package/dist/constants.cjs +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/eventcatalog.cjs +1 -1
  14. package/dist/eventcatalog.js +13 -13
  15. package/dist/federation/federate.js +5 -5
  16. package/dist/federation/source-provider.js +2 -2
  17. package/dist/generate.cjs +1 -1
  18. package/dist/generate.js +3 -3
  19. package/dist/utils/cli-logger.cjs +1 -1
  20. package/dist/utils/cli-logger.js +2 -2
  21. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraph.astro +6 -2
  22. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.spec.ts +52 -0
  23. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/field-lineage/_index.data.ts +20 -11
  24. package/eventcatalog/src/stores/sidebar-store/state.ts +3 -28
  25. package/eventcatalog/src/utils/collections/field-usage.ts +48 -0
  26. package/eventcatalog/src/utils/node-graphs/channel-node-graph.ts +2 -2
  27. package/eventcatalog/src/utils/node-graphs/compact-visualiser-graph.ts +246 -0
  28. package/eventcatalog/src/utils/node-graphs/container-node-graph.ts +2 -2
  29. package/eventcatalog/src/utils/node-graphs/data-products-node-graph.ts +2 -2
  30. package/eventcatalog/src/utils/node-graphs/domains-canvas.ts +2 -2
  31. package/eventcatalog/src/utils/node-graphs/domains-node-graph.ts +2 -2
  32. package/eventcatalog/src/utils/node-graphs/flows-node-graph.ts +2 -2
  33. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +2 -2
  34. package/eventcatalog/src/utils/node-graphs/services-node-graph.ts +2 -2
  35. package/eventcatalog/src/utils/node-graphs/system-context-node-graph.ts +2 -2
  36. package/eventcatalog/src/utils/node-graphs/systems-node-graph.ts +8 -3
  37. package/eventcatalog/src/utils/node-graphs/utils/utils.ts +30 -2
  38. package/package.json +3 -3
  39. package/dist/{chunk-6K7XZAYI.js → chunk-A2RZR3U4.js} +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.10.8";
39
+ var version = "4.10.9";
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-CDWE6WWB.js";
4
- import "../chunk-LB5EMDAY.js";
3
+ } from "../chunk-5WB6CXZH.js";
4
+ import "../chunk-3EEFSHHW.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.10.8";
143
+ var version = "4.10.9";
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-W6IMDOJJ.js";
4
- import "../chunk-CDWE6WWB.js";
3
+ } from "../chunk-ONRJA5DX.js";
4
+ import "../chunk-5WB6CXZH.js";
5
5
  import "../chunk-ZAZHAVKB.js";
6
- import "../chunk-LB5EMDAY.js";
6
+ import "../chunk-3EEFSHHW.js";
7
7
  import "../chunk-6QENHZZP.js";
8
8
  export {
9
9
  log_build_default as default
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "4.10.8";
2
+ var version = "4.10.9";
3
3
 
4
4
  // src/constants.ts
5
5
  var VERSION = version;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-LB5EMDAY.js";
3
+ } from "./chunk-3EEFSHHW.js";
4
4
 
5
5
  // src/analytics/analytics.js
6
6
  import os from "os";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  VERSION
3
- } from "./chunk-LB5EMDAY.js";
3
+ } from "./chunk-3EEFSHHW.js";
4
4
 
5
5
  // src/utils/cli-logger.ts
6
6
  import pc from "picocolors";
@@ -1,12 +1,6 @@
1
- import {
2
- withFederationOutputTransaction
3
- } from "./chunk-WRNH5C3U.js";
4
- import {
5
- composePublicAssets
6
- } from "./chunk-5EPNFDT5.js";
7
1
  import {
8
2
  createFederationSourceProvider
9
- } from "./chunk-6K7XZAYI.js";
3
+ } from "./chunk-A2RZR3U4.js";
10
4
  import {
11
5
  createFederationContentCache
12
6
  } from "./chunk-R7Z5ALYI.js";
@@ -17,6 +11,12 @@ import {
17
11
  import {
18
12
  isFederationEnabled
19
13
  } from "./chunk-FV56YFM4.js";
14
+ import {
15
+ withFederationOutputTransaction
16
+ } from "./chunk-WRNH5C3U.js";
17
+ import {
18
+ composePublicAssets
19
+ } from "./chunk-5EPNFDT5.js";
20
20
  import {
21
21
  getEventCatalogConfigFile
22
22
  } from "./chunk-6QENHZZP.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  raiseEvent
3
- } from "./chunk-CDWE6WWB.js";
3
+ } from "./chunk-5WB6CXZH.js";
4
4
  import {
5
5
  countResources,
6
6
  hashCatalogContent,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  logger
3
- } from "./chunk-W5C4K7F2.js";
3
+ } from "./chunk-BYPWHDX6.js";
4
4
  import {
5
5
  cleanup,
6
6
  getEventCatalogConfigFile
@@ -25,7 +25,7 @@ __export(constants_exports, {
25
25
  module.exports = __toCommonJS(constants_exports);
26
26
 
27
27
  // package.json
28
- var version = "4.10.8";
28
+ var version = "4.10.9";
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-LB5EMDAY.js";
3
+ } from "./chunk-3EEFSHHW.js";
4
4
  export {
5
5
  VERSION
6
6
  };
@@ -144,7 +144,7 @@ var verifyRequiredFieldsAreInCatalogConfigFile = async (projectDirectory) => {
144
144
  var import_picocolors = __toESM(require("picocolors"), 1);
145
145
 
146
146
  // package.json
147
- var version = "4.10.8";
147
+ var version = "4.10.9";
148
148
 
149
149
  // src/constants.ts
150
150
  var VERSION = version;
@@ -1,21 +1,18 @@
1
+ import {
2
+ log_build_default
3
+ } from "./chunk-ONRJA5DX.js";
4
+ import "./chunk-5WB6CXZH.js";
5
+ import "./chunk-ZAZHAVKB.js";
1
6
  import {
2
7
  runMigrations
3
8
  } from "./chunk-XUAF2H54.js";
4
9
  import "./chunk-CA4U2JP7.js";
5
- import {
6
- log_build_default
7
- } from "./chunk-W6IMDOJJ.js";
8
- import "./chunk-CDWE6WWB.js";
9
- import "./chunk-ZAZHAVKB.js";
10
10
  import {
11
11
  FederationConflictError,
12
12
  FederationDiagnosticError,
13
13
  federateCatalog
14
- } from "./chunk-U52FCZQ6.js";
15
- import "./chunk-WRNH5C3U.js";
16
- import "./chunk-5EPNFDT5.js";
17
- import "./chunk-6K7XZAYI.js";
18
- import "./chunk-352FGP6W.js";
14
+ } from "./chunk-EG62NTWS.js";
15
+ import "./chunk-A2RZR3U4.js";
19
16
  import "./chunk-R7Z5ALYI.js";
20
17
  import {
21
18
  getFederationDiagnosticCounts,
@@ -23,6 +20,9 @@ import {
23
20
  } from "./chunk-JJPB6EOZ.js";
24
21
  import "./chunk-FV56YFM4.js";
25
22
  import "./chunk-SDZQJTJW.js";
23
+ import "./chunk-352FGP6W.js";
24
+ import "./chunk-WRNH5C3U.js";
25
+ import "./chunk-5EPNFDT5.js";
26
26
  import {
27
27
  getProjectOutDir,
28
28
  isAuthEnabled,
@@ -31,10 +31,10 @@ import {
31
31
  } from "./chunk-B7HCX5HM.js";
32
32
  import {
33
33
  generate
34
- } from "./chunk-DRK5QSQC.js";
34
+ } from "./chunk-WD5UW4A4.js";
35
35
  import {
36
36
  logger
37
- } from "./chunk-W5C4K7F2.js";
37
+ } from "./chunk-BYPWHDX6.js";
38
38
  import {
39
39
  resolve_catalog_dependencies_default
40
40
  } from "./chunk-LHR4G2UO.js";
@@ -54,7 +54,7 @@ import {
54
54
  import "./chunk-W5JQON7Z.js";
55
55
  import {
56
56
  VERSION
57
- } from "./chunk-LB5EMDAY.js";
57
+ } from "./chunk-3EEFSHHW.js";
58
58
  import {
59
59
  linkCoreNodeModules,
60
60
  resolveInstalledCoreNodeModules
@@ -2,15 +2,15 @@ import {
2
2
  FederationConflictError,
3
3
  FederationDiagnosticError,
4
4
  federateCatalog
5
- } from "../chunk-U52FCZQ6.js";
6
- import "../chunk-WRNH5C3U.js";
7
- import "../chunk-5EPNFDT5.js";
8
- import "../chunk-6K7XZAYI.js";
9
- import "../chunk-352FGP6W.js";
5
+ } from "../chunk-EG62NTWS.js";
6
+ import "../chunk-A2RZR3U4.js";
10
7
  import "../chunk-R7Z5ALYI.js";
11
8
  import "../chunk-JJPB6EOZ.js";
12
9
  import "../chunk-FV56YFM4.js";
13
10
  import "../chunk-SDZQJTJW.js";
11
+ import "../chunk-352FGP6W.js";
12
+ import "../chunk-WRNH5C3U.js";
13
+ import "../chunk-5EPNFDT5.js";
14
14
  import "../chunk-6QENHZZP.js";
15
15
  export {
16
16
  FederationConflictError,
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createFederationSourceProvider
3
- } from "../chunk-6K7XZAYI.js";
4
- import "../chunk-352FGP6W.js";
3
+ } from "../chunk-A2RZR3U4.js";
5
4
  import "../chunk-SDZQJTJW.js";
5
+ import "../chunk-352FGP6W.js";
6
6
  export {
7
7
  createFederationSourceProvider
8
8
  };
package/dist/generate.cjs CHANGED
@@ -108,7 +108,7 @@ var getEventCatalogConfigFile = async (projectDirectory) => {
108
108
  var import_picocolors = __toESM(require("picocolors"), 1);
109
109
 
110
110
  // package.json
111
- var version = "4.10.8";
111
+ var version = "4.10.9";
112
112
 
113
113
  // src/constants.ts
114
114
  var VERSION = version;
package/dist/generate.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  generate
3
- } from "./chunk-DRK5QSQC.js";
4
- import "./chunk-W5C4K7F2.js";
5
- import "./chunk-LB5EMDAY.js";
3
+ } from "./chunk-WD5UW4A4.js";
4
+ import "./chunk-BYPWHDX6.js";
5
+ import "./chunk-3EEFSHHW.js";
6
6
  import "./chunk-6QENHZZP.js";
7
7
  export {
8
8
  generate
@@ -36,7 +36,7 @@ module.exports = __toCommonJS(cli_logger_exports);
36
36
  var import_picocolors = __toESM(require("picocolors"), 1);
37
37
 
38
38
  // package.json
39
- var version = "4.10.8";
39
+ var version = "4.10.9";
40
40
 
41
41
  // src/constants.ts
42
42
  var VERSION = version;
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  logger
3
- } from "../chunk-W5C4K7F2.js";
4
- import "../chunk-LB5EMDAY.js";
3
+ } from "../chunk-BYPWHDX6.js";
4
+ import "../chunk-3EEFSHHW.js";
5
5
  export {
6
6
  logger
7
7
  };
@@ -31,6 +31,7 @@ import { getNodesAndEdges as getNodesAndEdgesForChannel } from '@utils/node-grap
31
31
  import config from '@config';
32
32
  import { isEventCatalogChatEnabled, isDevMode } from '@utils/feature';
33
33
  import { loadSavedLayout, applyLayoutToNodes, buildResourceKey } from '@utils/node-graphs/layout-persistence';
34
+ import { compactVisualiserGraph } from '@utils/node-graphs/compact-visualiser-graph';
34
35
 
35
36
  const isChatEnabled = isEventCatalogChatEnabled();
36
37
 
@@ -198,13 +199,16 @@ if (collection === 'services-containers') {
198
199
  const resourceKey = buildResourceKey(collection, id, version);
199
200
  const savedLayout = await loadSavedLayout(resourceKey);
200
201
  const nodesWithLayout = applyLayoutToNodes(nodes as any[], savedLayout);
202
+ // Drop hydrated collection entries / markdown bodies before Astro serialises
203
+ // the graph into the page. Visualiser components only need a small field set.
204
+ const { nodes: clientNodes, edges: clientEdges } = compactVisualiserGraph(nodesWithLayout as any[], edges as any[]);
201
205
  ---
202
206
 
203
207
  <div>
204
208
  <AstroNodeGraph
205
209
  id={id}
206
- nodes={nodesWithLayout}
207
- edges={edges}
210
+ nodes={clientNodes}
211
+ edges={clientEdges}
208
212
  title={title}
209
213
  hrefLabel={href?.label}
210
214
  href={href?.url}
@@ -0,0 +1,52 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ const getMessageIdsWithFieldUsage = vi.fn();
4
+ const pageDataLoader = {
5
+ events: vi.fn(),
6
+ commands: vi.fn(),
7
+ queries: vi.fn(),
8
+ };
9
+
10
+ vi.mock('@utils/feature', () => ({
11
+ isSSR: () => false,
12
+ }));
13
+
14
+ vi.mock('@utils/collections/field-usage', () => ({
15
+ getMessageIdsWithFieldUsage: () => getMessageIdsWithFieldUsage(),
16
+ }));
17
+
18
+ vi.mock('@utils/page-loaders/page-data-loader', () => ({
19
+ pageDataLoader,
20
+ }));
21
+
22
+ import { Page } from './_index.data';
23
+
24
+ const event = (id: string) => ({
25
+ collection: 'events',
26
+ data: { id, version: '1.0.0', name: id },
27
+ });
28
+
29
+ describe('field-lineage static paths', () => {
30
+ beforeEach(() => {
31
+ getMessageIdsWithFieldUsage.mockReset();
32
+ pageDataLoader.events.mockReset();
33
+ pageDataLoader.commands.mockReset();
34
+ pageDataLoader.queries.mockReset();
35
+ });
36
+
37
+ it('when we build a catalog, field usage pages are only created for messages that declare fields', async () => {
38
+ getMessageIdsWithFieldUsage.mockResolvedValue(new Set(['OrderCreated']));
39
+ pageDataLoader.events.mockResolvedValue([event('OrderCreated'), event('OrderCancelled')]);
40
+ pageDataLoader.commands.mockResolvedValue([]);
41
+ pageDataLoader.queries.mockResolvedValue([]);
42
+
43
+ const paths = await Page.getStaticPaths();
44
+
45
+ expect(paths).toEqual([
46
+ {
47
+ params: { type: 'events', id: 'OrderCreated', version: '1.0.0' },
48
+ props: { type: 'events', ...event('OrderCreated') },
49
+ },
50
+ ]);
51
+ });
52
+ });
@@ -1,6 +1,7 @@
1
1
  import { isSSR } from '@utils/feature';
2
2
  import { HybridPage } from '@utils/page-loaders/hybrid-page';
3
3
  import type { PageTypes } from '@types';
4
+ import { getMessageIdsWithFieldUsage } from '@utils/collections/field-usage';
4
5
 
5
6
  const MESSAGE_TYPES: PageTypes[] = ['events', 'commands', 'queries'];
6
7
 
@@ -15,21 +16,24 @@ export class Page extends HybridPage {
15
16
  }
16
17
 
17
18
  const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
19
+ const messagesWithFieldUsage = await getMessageIdsWithFieldUsage();
18
20
 
19
21
  const allItems = await Promise.all(MESSAGE_TYPES.map((type) => pageDataLoader[type]()));
20
22
 
21
23
  return allItems.flatMap((items, index) =>
22
- items.map((item) => ({
23
- params: {
24
- type: MESSAGE_TYPES[index],
25
- id: item.data.id,
26
- version: item.data.version,
27
- },
28
- props: {
29
- type: MESSAGE_TYPES[index],
30
- ...item,
31
- },
32
- }))
24
+ items
25
+ .filter((item) => messagesWithFieldUsage.has(item.data.id))
26
+ .map((item) => ({
27
+ params: {
28
+ type: MESSAGE_TYPES[index],
29
+ id: item.data.id,
30
+ version: item.data.version,
31
+ },
32
+ props: {
33
+ type: MESSAGE_TYPES[index],
34
+ ...item,
35
+ },
36
+ }))
33
37
  );
34
38
  }
35
39
 
@@ -40,6 +44,11 @@ export class Page extends HybridPage {
40
44
  return null;
41
45
  }
42
46
 
47
+ const messagesWithFieldUsage = await getMessageIdsWithFieldUsage();
48
+ if (!messagesWithFieldUsage.has(id)) {
49
+ return null;
50
+ }
51
+
43
52
  const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
44
53
 
45
54
  const items = await pageDataLoader[type as PageTypes]();
@@ -42,6 +42,7 @@ import {
42
42
  withArchitectureDecisionsSection,
43
43
  } from './builders/shared';
44
44
  import { isArchitectureGraphEnabled, isChangelogEnabled, isResourceDocsEnabled } from '@utils/feature';
45
+ import { collectMessageIdsWithFieldUsage } from '@utils/collections/field-usage';
45
46
 
46
47
  export type { NavigationData, NavNode, ChildRef };
47
48
 
@@ -580,34 +581,8 @@ export const getNestedSideBarData = async (): Promise<NavigationData> => {
580
581
  {} as Record<string, NavNode | string>
581
582
  );
582
583
 
583
- // Build a set of message IDs that have field usage declared by any resource that can receive messages.
584
- // We use raw collections because the hydrated resources replace
585
- // sends/receives pointers with resolved message entries, which strips the fields property.
586
- const messagesWithFieldUsage = new Set<string>();
587
- for (const agent of rawAgents) {
588
- for (const pointer of agent.data.sends || []) {
589
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
590
- }
591
- for (const pointer of agent.data.receives || []) {
592
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
593
- }
594
- }
595
- for (const service of rawServices) {
596
- for (const pointer of service.data.sends || []) {
597
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
598
- }
599
- for (const pointer of service.data.receives || []) {
600
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
601
- }
602
- }
603
- for (const domain of rawDomains) {
604
- for (const pointer of domain.data.sends || []) {
605
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
606
- }
607
- for (const pointer of domain.data.receives || []) {
608
- if (pointer.fields?.length) messagesWithFieldUsage.add(pointer.id);
609
- }
610
- }
584
+ // Raw collections keep send/receive pointers (and their `fields`) which hydration strips.
585
+ const messagesWithFieldUsage = collectMessageIdsWithFieldUsage(rawAgents, rawServices, rawDomains);
611
586
 
612
587
  const flowRefsByMessage = buildFlowReferencesByMessage({
613
588
  flows,
@@ -0,0 +1,48 @@
1
+ import { getCollection } from 'astro:content';
2
+
3
+ type PointerWithFields = { id?: string; fields?: string[] };
4
+
5
+ type ResourceWithMessagePointers = {
6
+ data: {
7
+ sends?: PointerWithFields[];
8
+ receives?: PointerWithFields[];
9
+ };
10
+ };
11
+
12
+ const addFieldUsageFromPointers = (messageIds: Set<string>, pointers: PointerWithFields[] | undefined) => {
13
+ for (const pointer of pointers || []) {
14
+ if (pointer.id && pointer.fields?.length) {
15
+ messageIds.add(pointer.id);
16
+ }
17
+ }
18
+ };
19
+
20
+ /**
21
+ * Message ids that have field-level lineage declared on a send/receive pointer.
22
+ * Uses raw (unhydrated) resources because hydration replaces those pointers and drops `fields`.
23
+ * Matches the sidebar "Field Usage" link gate.
24
+ */
25
+ export const collectMessageIdsWithFieldUsage = (
26
+ agents: ResourceWithMessagePointers[],
27
+ services: ResourceWithMessagePointers[],
28
+ domains: ResourceWithMessagePointers[]
29
+ ): Set<string> => {
30
+ const messageIds = new Set<string>();
31
+
32
+ for (const resource of [...agents, ...services, ...domains]) {
33
+ addFieldUsageFromPointers(messageIds, resource.data.sends);
34
+ addFieldUsageFromPointers(messageIds, resource.data.receives);
35
+ }
36
+
37
+ return messageIds;
38
+ };
39
+
40
+ export const getMessageIdsWithFieldUsage = async (): Promise<Set<string>> => {
41
+ const [agents, services, domains] = await Promise.all([
42
+ getCollection('agents'),
43
+ getCollection('services'),
44
+ getCollection('domains'),
45
+ ]);
46
+
47
+ return collectMessageIdsWithFieldUsage(agents, services, domains);
48
+ };
@@ -1,5 +1,4 @@
1
1
  import { getCollection, type CollectionEntry } from 'astro:content';
2
- import dagre from 'dagre';
3
2
  import {
4
3
  buildContextMenuForAgent,
5
4
  buildContextMenuForMessage,
@@ -17,6 +16,7 @@ import {
17
16
  getOperationFields,
18
17
  DEFAULT_NODE_WIDTH,
19
18
  DEFAULT_NODE_HEIGHT,
19
+ layoutDagreGraph,
20
20
  } from './utils/utils';
21
21
  import { createVersionedMap, findInMap } from '@utils/collections/util';
22
22
  import { getChannelChain, getChannels } from '@utils/collections/channels';
@@ -457,7 +457,7 @@ export const getNodesAndEdges = async ({ id, version, defaultFlow, mode = 'simpl
457
457
  });
458
458
 
459
459
  if (layout) {
460
- dagre.layout(flow);
460
+ layoutDagreGraph(flow);
461
461
  }
462
462
 
463
463
  return {
@@ -0,0 +1,246 @@
1
+ import type { Edge, Node } from '@xyflow/react';
2
+
3
+ /**
4
+ * Fields the visualiser nodes actually read from a resource. Everything else
5
+ * (markdown bodies, file paths, hydrated producers/consumers, full send/receive
6
+ * collections) is dropped before Astro serialises the graph into the page.
7
+ */
8
+ const RESOURCE_DATA_KEYS = [
9
+ 'id',
10
+ 'name',
11
+ 'version',
12
+ 'summary',
13
+ 'owners',
14
+ 'deprecated',
15
+ 'draft',
16
+ 'notes',
17
+ 'styles',
18
+ 'badges',
19
+ 'latestVersion',
20
+ 'method',
21
+ 'path',
22
+ 'statusCodes',
23
+ 'specifications',
24
+ 'externalSystem',
25
+ 'repository',
26
+ 'model',
27
+ 'tools',
28
+ 'parameters',
29
+ 'protocols',
30
+ 'address',
31
+ 'deliveryGuarantee',
32
+ 'type',
33
+ 'container_type',
34
+ 'schemas',
35
+ 'properties',
36
+ 'aggregateRoot',
37
+ 'sidebar',
38
+ ] as const;
39
+
40
+ const RESOURCE_OBJECT_KEYS = new Set([
41
+ 'message',
42
+ 'service',
43
+ 'agent',
44
+ 'domain',
45
+ 'channel',
46
+ 'dataProduct',
47
+ 'container',
48
+ 'entity',
49
+ 'flow',
50
+ 'data',
51
+ ]);
52
+
53
+ const DROP_EDGE_RESOURCE_KEYS = new Set(['publisherService', 'consumerService']);
54
+
55
+ const isPlainObject = (value: unknown): value is Record<string, unknown> =>
56
+ typeof value === 'object' && value !== null && !Array.isArray(value);
57
+
58
+ const isCollectionEntry = (value: Record<string, unknown>): boolean =>
59
+ isPlainObject(value.data) &&
60
+ ('collection' in value || 'filePath' in value || 'digest' in value || 'body' in value || 'rendered' in value);
61
+
62
+ const compactDomainService = (service: unknown) => {
63
+ if (!isPlainObject(service)) return service;
64
+
65
+ const data = isPlainObject(service.data) ? service.data : service;
66
+ const id = data.id ?? service.id;
67
+ const name = data.name ?? service.name;
68
+ const version = data.version ?? service.version;
69
+
70
+ return {
71
+ data: {
72
+ ...(id !== undefined ? { id } : {}),
73
+ ...(name !== undefined ? { name } : {}),
74
+ ...(version !== undefined ? { version } : {}),
75
+ },
76
+ };
77
+ };
78
+
79
+ const compactOwners = (owners: unknown): unknown => {
80
+ if (!Array.isArray(owners)) return owners;
81
+ return owners.map((owner) => {
82
+ if (typeof owner === 'string') return owner;
83
+ if (!isPlainObject(owner)) return owner;
84
+ const data = isPlainObject(owner.data) ? owner.data : owner;
85
+ const id = data.id ?? owner.id;
86
+ const name = data.name ?? owner.name;
87
+ if (id === undefined && name === undefined) return owner;
88
+ return {
89
+ ...(id !== undefined ? { id } : {}),
90
+ ...(name !== undefined ? { name } : {}),
91
+ };
92
+ });
93
+ };
94
+
95
+ export const compactVisualiserResource = (value: unknown): unknown => {
96
+ if (value == null || typeof value !== 'object') return value;
97
+ if (Array.isArray(value)) return value.map(compactVisualiserResource);
98
+
99
+ const obj = value as Record<string, unknown>;
100
+
101
+ if (isCollectionEntry(obj)) {
102
+ const data = compactVisualiserResourceData(obj.data);
103
+ const resourceId = isPlainObject(data) && data.id !== undefined ? data.id : obj.id;
104
+ return {
105
+ ...(resourceId !== undefined ? { id: resourceId } : {}),
106
+ ...(obj.collection !== undefined ? { collection: obj.collection } : {}),
107
+ data,
108
+ // EventNode / ServiceNode / ChannelNode read name/version/summary from the
109
+ // top level. Collection-entry shaped payloads only have those on `.data`.
110
+ ...(isPlainObject(data) ? data : {}),
111
+ };
112
+ }
113
+
114
+ return compactVisualiserResourceData(obj);
115
+ };
116
+
117
+ const compactVisualiserResourceData = (value: unknown): unknown => {
118
+ if (!isPlainObject(value)) return value;
119
+
120
+ const out: Record<string, unknown> = {};
121
+ for (const key of RESOURCE_DATA_KEYS) {
122
+ if (value[key] === undefined) continue;
123
+ if (key === 'owners') {
124
+ out[key] = compactOwners(value[key]);
125
+ continue;
126
+ }
127
+ if (key === 'tools' && Array.isArray(value[key])) {
128
+ out[key] = value[key];
129
+ continue;
130
+ }
131
+ out[key] = value[key];
132
+ }
133
+
134
+ // Domain nodes render `domain.data.services[].data.{id,name,version}`.
135
+ if (Array.isArray(value.services)) {
136
+ out.services = value.services.map(compactDomainService);
137
+ }
138
+
139
+ return out;
140
+ };
141
+
142
+ const compactEndpoint = (endpoint: unknown): unknown => {
143
+ if (!isPlainObject(endpoint)) return endpoint;
144
+
145
+ const data = isPlainObject(endpoint.data) ? endpoint.data : undefined;
146
+ const collection = endpoint.collection ?? data?.collection;
147
+ const id = endpoint.id ?? (data?.id !== undefined && data?.version !== undefined ? `${data.id}-${data.version}` : data?.id);
148
+
149
+ return {
150
+ ...(id !== undefined ? { id } : {}),
151
+ ...(collection !== undefined ? { collection } : {}),
152
+ ...(data
153
+ ? {
154
+ data: {
155
+ ...(data.id !== undefined ? { id: data.id } : {}),
156
+ ...(data.version !== undefined ? { version: data.version } : {}),
157
+ ...(collection !== undefined ? { collection } : {}),
158
+ },
159
+ }
160
+ : {}),
161
+ };
162
+ };
163
+
164
+ const compactNodeData = (data: unknown): unknown => {
165
+ if (!isPlainObject(data)) return data;
166
+
167
+ const out: Record<string, unknown> = {};
168
+
169
+ for (const [key, value] of Object.entries(data)) {
170
+ if (value === undefined) continue;
171
+
172
+ if (RESOURCE_OBJECT_KEYS.has(key)) {
173
+ out[key] = compactVisualiserResource(value);
174
+ continue;
175
+ }
176
+
177
+ if (key === 'messages' && Array.isArray(value)) {
178
+ out[key] = value.map((entry) => {
179
+ if (!isPlainObject(entry)) return entry;
180
+ return {
181
+ ...entry,
182
+ ...(entry.message !== undefined ? { message: compactVisualiserResource(entry.message) } : {}),
183
+ ...(Array.isArray(entry.channels) ? { channels: entry.channels.map(compactVisualiserResource) } : {}),
184
+ };
185
+ });
186
+ continue;
187
+ }
188
+
189
+ out[key] = value;
190
+ }
191
+
192
+ return out;
193
+ };
194
+
195
+ const compactEdgeData = (data: unknown): unknown => {
196
+ if (!isPlainObject(data)) return data;
197
+
198
+ const out: Record<string, unknown> = {};
199
+
200
+ for (const [key, value] of Object.entries(data)) {
201
+ if (value === undefined || DROP_EDGE_RESOURCE_KEYS.has(key)) continue;
202
+
203
+ if (key === 'rootSourceAndTarget' && isPlainObject(value)) {
204
+ out[key] = {
205
+ source: compactEndpoint(value.source),
206
+ target: compactEndpoint(value.target),
207
+ };
208
+ continue;
209
+ }
210
+
211
+ if (RESOURCE_OBJECT_KEYS.has(key) || key === 'message') {
212
+ out[key] = compactVisualiserResource(value);
213
+ continue;
214
+ }
215
+
216
+ out[key] = value;
217
+ }
218
+
219
+ return out;
220
+ };
221
+
222
+ const compactNode = <T extends Node>(node: T): T => {
223
+ const { data, ...rest } = node;
224
+ return {
225
+ ...rest,
226
+ ...(data !== undefined ? { data: compactNodeData(data) } : {}),
227
+ } as T;
228
+ };
229
+
230
+ const compactEdge = <T extends Edge>(edge: T): T => {
231
+ const { data, ...rest } = edge;
232
+ return {
233
+ ...rest,
234
+ ...(data !== undefined ? { data: compactEdgeData(data) } : {}),
235
+ } as T;
236
+ };
237
+
238
+ /**
239
+ * Strip collection-entry weight from a React Flow graph before it is serialised
240
+ * into an Astro island. Node/edge structure and the fields visualiser components
241
+ * read are preserved.
242
+ */
243
+ export const compactVisualiserGraph = <N extends Node, E extends Edge>(nodes: N[], edges: E[]): { nodes: N[]; edges: E[] } => ({
244
+ nodes: nodes.map(compactNode),
245
+ edges: edges.map(compactEdge),
246
+ });
@@ -1,5 +1,4 @@
1
1
  import type { CollectionEntry } from 'astro:content';
2
- import dagre from 'dagre';
3
2
  import {
4
3
  calculatedNodes,
5
4
  createDagreGraph,
@@ -10,6 +9,7 @@ import {
10
9
  buildContextMenuForResource,
11
10
  DEFAULT_NODE_WIDTH,
12
11
  DEFAULT_NODE_HEIGHT,
12
+ layoutDagreGraph,
13
13
  } from './utils/utils';
14
14
  import { MarkerType } from '@xyflow/react';
15
15
  import { findMatchingNodes } from '@utils/collections/util';
@@ -299,7 +299,7 @@ export const getNodesAndEdges = async ({
299
299
 
300
300
  // Render the diagram in memory getting hte X and Y
301
301
  if (layout) {
302
- dagre.layout(flow);
302
+ layoutDagreGraph(flow);
303
303
  }
304
304
 
305
305
  return {
@@ -1,5 +1,4 @@
1
1
  import { getCollection, type CollectionEntry } from 'astro:content';
2
- import dagre from 'dagre';
3
2
  import {
4
3
  createDagreGraph,
5
4
  generateIdForNode,
@@ -10,6 +9,7 @@ import {
10
9
  buildContextMenuForResource,
11
10
  DEFAULT_NODE_WIDTH,
12
11
  DEFAULT_NODE_HEIGHT,
12
+ layoutDagreGraph,
13
13
  } from '@utils/node-graphs/utils/utils';
14
14
 
15
15
  import { findInMap, createVersionedMap, mergeMaps, collectionToResourceMap } from '@utils/collections/util';
@@ -220,7 +220,7 @@ export const getNodesAndEdges = async ({ id, defaultFlow, version, mode = 'simpl
220
220
 
221
221
  if (layout) {
222
222
  // Render the diagram in memory getting the X and Y
223
- dagre.layout(flow);
223
+ layoutDagreGraph(flow);
224
224
  }
225
225
 
226
226
  // Find any duplicated edges, and merge them into one edge
@@ -1,11 +1,11 @@
1
1
  import { getCollection, type CollectionEntry } from 'astro:content';
2
- import dagre from 'dagre';
3
2
  import {
4
3
  generateIdForNode,
5
4
  createDagreGraph,
6
5
  calculatedNodes,
7
6
  createEdge,
8
7
  getOperationFields,
8
+ layoutDagreGraph,
9
9
  } from '@utils/node-graphs/utils/utils';
10
10
  import { findInMap, createVersionedMap } from '@utils/collections/util';
11
11
  import type { Node, Edge } from '@xyflow/react';
@@ -247,7 +247,7 @@ export const getDomainsCanvasData = async (): Promise<DomainCanvasData> => {
247
247
  });
248
248
 
249
249
  // Calculate layout using dagre
250
- dagre.layout(dagreGraph);
250
+ layoutDagreGraph(dagreGraph);
251
251
 
252
252
  // Apply calculated positions to nodes
253
253
  const layoutedDomainNodes = calculatedNodes(dagreGraph, domainNodes);
@@ -1,5 +1,4 @@
1
1
  import { getCollection } from 'astro:content';
2
- import dagre from 'dagre';
3
2
  import {
4
3
  createDagreGraph,
5
4
  calculatedNodes,
@@ -7,6 +6,7 @@ import {
7
6
  getEdgeLabelForServiceAsTarget,
8
7
  generatedIdForEdge,
9
8
  createEdge,
9
+ layoutDagreGraph,
10
10
  } from '@utils/node-graphs/utils/utils';
11
11
  import { getNodesAndEdges as getServicesNodeAndEdges } from './services-node-graph';
12
12
  import { getNodesAndEdges as getAgentsNodeAndEdges } from './agents-node-graph';
@@ -176,7 +176,7 @@ export const getNodesAndEdges = async ({
176
176
  }
177
177
 
178
178
  if (layout) {
179
- dagre.layout(flow);
179
+ layoutDagreGraph(flow);
180
180
  }
181
181
 
182
182
  return {
@@ -1,5 +1,4 @@
1
1
  import { getCollection, type CollectionEntry } from 'astro:content';
2
- import dagre from 'dagre';
3
2
  import {
4
3
  createDagreGraph,
5
4
  calculatedNodes,
@@ -7,6 +6,7 @@ import {
7
6
  DEFAULT_NODE_HEIGHT,
8
7
  buildContextMenuForResource,
9
8
  buildContextMenuForService,
9
+ layoutDagreGraph,
10
10
  } from '@utils/node-graphs/utils/utils';
11
11
  import { MarkerType } from '@xyflow/react';
12
12
  import type { Node as NodeType } from '@xyflow/react';
@@ -321,7 +321,7 @@ export const getNodesAndEdges = async ({ id, defaultFlow, version, mode = 'simpl
321
321
  graph.setEdge(edge.source, edge.target);
322
322
  });
323
323
 
324
- dagre.layout(graph);
324
+ layoutDagreGraph(graph);
325
325
 
326
326
  return {
327
327
  nodes: calculatedNodes(graph, nodes),
@@ -1,7 +1,6 @@
1
1
  // import { getColor } from '@utils/colors';
2
2
  import { getEvents } from '@utils/collections/events';
3
3
  import { getCollection, type CollectionEntry } from 'astro:content';
4
- import dagre from 'dagre';
5
4
  import {
6
5
  calculatedNodes,
7
6
  createDagreGraph,
@@ -12,6 +11,7 @@ import {
12
11
  getEdgeLabelForMessageAsSource,
13
12
  getEdgeLabelForServiceAsTarget,
14
13
  versionMatches,
14
+ layoutDagreGraph,
15
15
  } from './utils/utils';
16
16
  import { MarkerType, type Node, type Edge } from '@xyflow/react';
17
17
  import {
@@ -691,7 +691,7 @@ const getNodesAndEdges = async ({
691
691
  });
692
692
 
693
693
  // Render the diagram in memory getting hte X and Y
694
- dagre.layout(flow);
694
+ layoutDagreGraph(flow);
695
695
 
696
696
  return {
697
697
  nodes: calculatedNodes(flow, nodes),
@@ -1,5 +1,4 @@
1
1
  import { getCollection, type CollectionEntry } from 'astro:content';
2
- import dagre from 'dagre';
3
2
  import {
4
3
  createDagreGraph,
5
4
  generateIdForNode,
@@ -14,6 +13,7 @@ import {
14
13
  DEFAULT_NODE_HEIGHT,
15
14
  partitionMessagesByGroup,
16
15
  getOperationFields,
16
+ layoutDagreGraph,
17
17
  } from '@utils/node-graphs/utils/utils';
18
18
 
19
19
  const sanitizeGroupId = (name: string) => name.replace(/[^a-zA-Z0-9]/g, '-').toLowerCase();
@@ -636,7 +636,7 @@ export const getNodesAndEdges = async ({
636
636
 
637
637
  if (layout) {
638
638
  // Render the diagram in memory getting the X and Y
639
- dagre.layout(flow);
639
+ layoutDagreGraph(flow);
640
640
  }
641
641
 
642
642
  // Find any duplicated edges, and merge them into one edge
@@ -1,6 +1,5 @@
1
1
  import { getCollection } from 'astro:content';
2
2
  import type { CollectionEntry } from 'astro:content';
3
- import dagre from 'dagre';
4
3
  import {
5
4
  createDagreGraph,
6
5
  calculatedNodes,
@@ -8,6 +7,7 @@ import {
8
7
  generateIdForNode,
9
8
  buildContextMenuForSystem,
10
9
  DEFAULT_NODE_HEIGHT,
10
+ layoutDagreGraph,
11
11
  } from '@utils/node-graphs/utils/utils';
12
12
  import { MarkerType } from '@xyflow/react';
13
13
  import { createVersionedMap, findInMap } from '@utils/collections/util';
@@ -302,7 +302,7 @@ const buildContextGraphFromSeeds = ({
302
302
  edges.forEach((edge) => flow.setEdge(edge.source, edge.target));
303
303
 
304
304
  if (layout) {
305
- dagre.layout(flow);
305
+ layoutDagreGraph(flow);
306
306
  }
307
307
 
308
308
  return {
@@ -1,6 +1,11 @@
1
1
  import { getCollection } from 'astro:content';
2
- import dagre from 'dagre';
3
- import { createDagreGraph, calculatedNodes, DEFAULT_NODE_WIDTH, DEFAULT_NODE_HEIGHT } from '@utils/node-graphs/utils/utils';
2
+ import {
3
+ createDagreGraph,
4
+ calculatedNodes,
5
+ DEFAULT_NODE_WIDTH,
6
+ DEFAULT_NODE_HEIGHT,
7
+ layoutDagreGraph,
8
+ } from '@utils/node-graphs/utils/utils';
4
9
  import { getNodesAndEdges as getServicesNodeAndEdges } from './services-node-graph';
5
10
  import { getNodesAndEdges as getContainerNodeAndEdges } from './container-node-graph';
6
11
  import merge from 'lodash.merge';
@@ -121,7 +126,7 @@ export const getNodesAndEdges = async ({
121
126
  }
122
127
 
123
128
  if (layout) {
124
- dagre.layout(flow);
129
+ layoutDagreGraph(flow);
125
130
  }
126
131
 
127
132
  let laidOutNodes = calculatedNodes(flow, Array.from(nodes.values()));
@@ -109,6 +109,34 @@ export const createDagreGraph = ({ ranksep = 180, nodesep = 50, ...rest }: any)
109
109
  return graph;
110
110
  };
111
111
 
112
+ /**
113
+ * network-simplex produces the nicest layered layout but is O(V·E) and hangs on
114
+ * large architecture maps. tight-tree keeps the same LR layering with far less
115
+ * work. Callers can still force a ranker via createDagreGraph({ ranker }).
116
+ */
117
+ export const LARGE_GRAPH_NODE_THRESHOLD = 80;
118
+ export const LARGE_GRAPH_EDGE_THRESHOLD = 200;
119
+ export const LARGE_GRAPH_RANKER = 'tight-tree';
120
+
121
+ export const selectDagreRanker = (nodeCount: number, edgeCount: number, explicitRanker?: string) => {
122
+ if (explicitRanker) return explicitRanker;
123
+ if (nodeCount >= LARGE_GRAPH_NODE_THRESHOLD || edgeCount >= LARGE_GRAPH_EDGE_THRESHOLD) {
124
+ return LARGE_GRAPH_RANKER;
125
+ }
126
+ return undefined;
127
+ };
128
+
129
+ export const layoutDagreGraph = (flow: dagre.graphlib.Graph) => {
130
+ const label = (flow.graph() || {}) as { ranker?: string };
131
+ const nodeCount = flow.nodeCount();
132
+ const edgeCount = flow.edgeCount();
133
+ const ranker = selectDagreRanker(nodeCount, edgeCount, label.ranker);
134
+ if (ranker && label.ranker !== ranker) {
135
+ flow.setGraph({ ...label, ranker });
136
+ }
137
+ dagre.layout(flow);
138
+ };
139
+
112
140
  export const createEdge = (edgeOptions: Edge) => {
113
141
  return {
114
142
  label: 'subscribed by',
@@ -430,8 +458,8 @@ export const getNodesAndEdgesFromDagre = ({
430
458
  flow.setEdge(edge.source, edge.target);
431
459
  });
432
460
 
433
- // Render the diagram in memory getting hte X and Y
434
- dagre.layout(flow);
461
+ // Render the diagram in memory getting the X and Y
462
+ layoutDagreGraph(flow);
435
463
 
436
464
  return {
437
465
  nodes: calculatedNodes(flow, nodes),
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "SEE LICENSE IN LICENSE",
9
9
  "type": "module",
10
- "version": "4.10.8",
10
+ "version": "4.10.9",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -119,8 +119,8 @@
119
119
  "uuid": "^11.1.1",
120
120
  "zod": "^4.3.6",
121
121
  "@eventcatalog/linter": "1.1.18",
122
- "@eventcatalog/sdk": "2.29.0",
123
- "@eventcatalog/visualiser": "^4.1.3"
122
+ "@eventcatalog/visualiser": "^4.1.4",
123
+ "@eventcatalog/sdk": "2.29.0"
124
124
  },
125
125
  "devDependencies": {
126
126
  "@astrojs/check": "^0.9.10",
@@ -1,9 +1,9 @@
1
- import {
2
- createGitHubSourceProvider
3
- } from "./chunk-352FGP6W.js";
4
1
  import {
5
2
  createFileSystemSourceProvider
6
3
  } from "./chunk-SDZQJTJW.js";
4
+ import {
5
+ createGitHubSourceProvider
6
+ } from "./chunk-352FGP6W.js";
7
7
 
8
8
  // src/federation/source-provider.ts
9
9
  var createFederationSourceProvider = (projectDirectory, providers = {}) => {