@eventcatalog/core 4.10.15 → 4.11.1

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 (165) 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/astro-output.cjs +60 -1
  6. package/dist/astro-output.js +1 -1
  7. package/dist/{chunk-U52FCZQ6.js → chunk-2YJRF4ZX.js} +1 -1
  8. package/dist/chunk-634NFV5U.js +75 -0
  9. package/dist/{chunk-I6HUOX4R.js → chunk-AXDNSGAN.js} +1 -1
  10. package/dist/chunk-DHH5U5PF.js +53 -0
  11. package/dist/{chunk-A4JRWRCV.js → chunk-DSCIWCX2.js} +1 -1
  12. package/dist/chunk-FWXEV4LF.js +19 -0
  13. package/dist/{chunk-Q2ATWE7N.js → chunk-GZVDXCYF.js} +1 -1
  14. package/dist/{chunk-7LWOCX4R.js → chunk-RYNPEIHQ.js} +1 -1
  15. package/dist/{chunk-HBVTU6ZP.js → chunk-YYVW4DCO.js} +1 -1
  16. package/dist/constants.cjs +1 -1
  17. package/dist/constants.js +1 -1
  18. package/dist/copy-core.cjs +88 -0
  19. package/dist/copy-core.d.cts +4 -0
  20. package/dist/copy-core.d.ts +4 -0
  21. package/dist/copy-core.js +8 -0
  22. package/dist/eventcatalog.cjs +234 -115
  23. package/dist/eventcatalog.js +36 -30
  24. package/dist/federation/federate.js +3 -3
  25. package/dist/federation/source-provider.js +2 -2
  26. package/dist/generate.cjs +1 -1
  27. package/dist/generate.js +3 -3
  28. package/dist/update-check.cjs +44 -0
  29. package/dist/update-check.d.cts +18 -0
  30. package/dist/update-check.d.ts +18 -0
  31. package/dist/update-check.js +8 -0
  32. package/dist/utils/cli-logger.cjs +1 -1
  33. package/dist/utils/cli-logger.js +2 -2
  34. package/eventcatalog/astro.config.mjs +8 -1
  35. package/eventcatalog/public/icons/languages/android.svg +1 -0
  36. package/eventcatalog/public/icons/languages/angular.svg +1 -0
  37. package/eventcatalog/public/icons/languages/apple.svg +1 -0
  38. package/eventcatalog/public/icons/languages/arduino.svg +1 -0
  39. package/eventcatalog/public/icons/languages/bash.svg +1 -0
  40. package/eventcatalog/public/icons/languages/bun.svg +1 -0
  41. package/eventcatalog/public/icons/languages/c.svg +1 -0
  42. package/eventcatalog/public/icons/languages/clojure.svg +1 -0
  43. package/eventcatalog/public/icons/languages/cpp.svg +1 -0
  44. package/eventcatalog/public/icons/languages/crystal.svg +1 -0
  45. package/eventcatalog/public/icons/languages/csharp.svg +1 -0
  46. package/eventcatalog/public/icons/languages/css.svg +1 -0
  47. package/eventcatalog/public/icons/languages/curl.svg +1 -0
  48. package/eventcatalog/public/icons/languages/dart.svg +1 -0
  49. package/eventcatalog/public/icons/languages/deno.svg +1 -0
  50. package/eventcatalog/public/icons/languages/docker.svg +1 -0
  51. package/eventcatalog/public/icons/languages/elm.svg +1 -0
  52. package/eventcatalog/public/icons/languages/erlang.svg +1 -0
  53. package/eventcatalog/public/icons/languages/flutter.svg +1 -0
  54. package/eventcatalog/public/icons/languages/fortran.svg +1 -0
  55. package/eventcatalog/public/icons/languages/fsharp.svg +1 -0
  56. package/eventcatalog/public/icons/languages/git.svg +1 -0
  57. package/eventcatalog/public/icons/languages/gradle.svg +1 -0
  58. package/eventcatalog/public/icons/languages/graphql.svg +1 -0
  59. package/eventcatalog/public/icons/languages/grpc.svg +1 -0
  60. package/eventcatalog/public/icons/languages/haskell.svg +1 -0
  61. package/eventcatalog/public/icons/languages/hasura.svg +1 -0
  62. package/eventcatalog/public/icons/languages/html.svg +1 -0
  63. package/eventcatalog/public/icons/languages/httpie.svg +1 -0
  64. package/eventcatalog/public/icons/languages/javascript.svg +1 -0
  65. package/eventcatalog/public/icons/languages/json-schema.svg +1 -0
  66. package/eventcatalog/public/icons/languages/json.svg +1 -0
  67. package/eventcatalog/public/icons/languages/julia.svg +1 -0
  68. package/eventcatalog/public/icons/languages/jupyter.svg +1 -0
  69. package/eventcatalog/public/icons/languages/kafka.svg +1 -0
  70. package/eventcatalog/public/icons/languages/kotlin.svg +1 -0
  71. package/eventcatalog/public/icons/languages/lua.svg +1 -0
  72. package/eventcatalog/public/icons/languages/markdown.svg +1 -0
  73. package/eventcatalog/public/icons/languages/maven.svg +1 -0
  74. package/eventcatalog/public/icons/languages/mongodb.svg +1 -0
  75. package/eventcatalog/public/icons/languages/mysql.svg +1 -0
  76. package/eventcatalog/public/icons/languages/nginx.svg +1 -0
  77. package/eventcatalog/public/icons/languages/nim.svg +1 -0
  78. package/eventcatalog/public/icons/languages/ocaml.svg +1 -0
  79. package/eventcatalog/public/icons/languages/perl.svg +1 -0
  80. package/eventcatalog/public/icons/languages/php.svg +1 -0
  81. package/eventcatalog/public/icons/languages/postgresql.svg +1 -0
  82. package/eventcatalog/public/icons/languages/prisma.svg +1 -0
  83. package/eventcatalog/public/icons/languages/purescript.svg +1 -0
  84. package/eventcatalog/public/icons/languages/python.svg +1 -0
  85. package/eventcatalog/public/icons/languages/r.svg +1 -0
  86. package/eventcatalog/public/icons/languages/react.svg +1 -0
  87. package/eventcatalog/public/icons/languages/reasonml.svg +1 -0
  88. package/eventcatalog/public/icons/languages/redis.svg +1 -0
  89. package/eventcatalog/public/icons/languages/ruby.svg +1 -0
  90. package/eventcatalog/public/icons/languages/rust.svg +1 -0
  91. package/eventcatalog/public/icons/languages/sass.svg +1 -0
  92. package/eventcatalog/public/icons/languages/scala.svg +1 -0
  93. package/eventcatalog/public/icons/languages/solidity.svg +1 -0
  94. package/eventcatalog/public/icons/languages/spring.svg +1 -0
  95. package/eventcatalog/public/icons/languages/sqlite.svg +1 -0
  96. package/eventcatalog/public/icons/languages/svelte.svg +1 -0
  97. package/eventcatalog/public/icons/languages/swagger.svg +1 -0
  98. package/eventcatalog/public/icons/languages/swift.svg +1 -0
  99. package/eventcatalog/public/icons/languages/tailwind.svg +1 -0
  100. package/eventcatalog/public/icons/languages/terraform.svg +1 -0
  101. package/eventcatalog/public/icons/languages/toml.svg +1 -0
  102. package/eventcatalog/public/icons/languages/typescript.svg +1 -0
  103. package/eventcatalog/public/icons/languages/unity.svg +1 -0
  104. package/eventcatalog/public/icons/languages/vim.svg +1 -0
  105. package/eventcatalog/public/icons/languages/vue.svg +1 -0
  106. package/eventcatalog/public/icons/languages/webassembly.svg +1 -0
  107. package/eventcatalog/public/icons/languages/yaml.svg +1 -0
  108. package/eventcatalog/public/icons/languages/zig.svg +1 -0
  109. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.astro +12 -0
  110. package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +176 -0
  111. package/eventcatalog/src/components/MDX/CodeGroup/MarkdownCodeGroup.tsx +22 -0
  112. package/eventcatalog/src/components/MDX/CodeGroup/code-group-icons.ts +345 -0
  113. package/eventcatalog/src/components/MDX/CodeGroup/code-group.css +22 -0
  114. package/eventcatalog/src/components/MDX/CodeGroup/parse-panels.ts +11 -0
  115. package/eventcatalog/src/components/MDX/Columns/Column.astro +10 -0
  116. package/eventcatalog/src/components/MDX/Columns/Columns.astro +14 -0
  117. package/eventcatalog/src/components/MDX/Columns/MarkdownColumns.tsx +37 -0
  118. package/eventcatalog/src/components/MDX/Columns/layout.ts +16 -0
  119. package/eventcatalog/src/components/MDX/components.tsx +6 -0
  120. package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +1 -1
  121. package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +151 -114
  122. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +510 -248
  123. package/eventcatalog/src/components/SchemaExplorer/SchemaFlows.tsx +76 -0
  124. package/eventcatalog/src/components/SchemaExplorer/SchemaGraph.tsx +47 -0
  125. package/eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx +11 -0
  126. package/eventcatalog/src/components/SchemaExplorer/SchemaUsage.tsx +17 -0
  127. package/eventcatalog/src/components/SchemaExplorer/types.ts +48 -3
  128. package/eventcatalog/src/components/SchemaExplorer/utils.ts +28 -1
  129. package/eventcatalog/src/content.config.ts +28 -0
  130. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +38 -7
  131. package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +144 -37
  132. package/eventcatalog/src/pages/schemas/explorer/index.astro +5 -0
  133. package/eventcatalog/src/plugins/empty-collection-warning.d.ts +21 -0
  134. package/eventcatalog/src/plugins/empty-collection-warning.mjs +107 -0
  135. package/eventcatalog/src/plugins/quiet-empty-collections-logger.mjs +11 -0
  136. package/eventcatalog/src/remark-plugins/code-group.ts +39 -0
  137. package/eventcatalog/src/remark-plugins/markdown-code-group.ts +59 -0
  138. package/eventcatalog/src/remark-plugins/markdown-columns.ts +66 -0
  139. package/eventcatalog/src/utils/collections/channels.ts +12 -4
  140. package/eventcatalog/src/utils/collections/examples.ts +133 -41
  141. package/eventcatalog/src/utils/collections/flows.ts +43 -0
  142. package/eventcatalog/src/utils/collections/messages.ts +2 -2
  143. package/eventcatalog/src/utils/collections/schemas.ts +15 -0
  144. package/eventcatalog/src/utils/node-graphs/message-node-graph.ts +17 -15
  145. package/eventcatalog/src/utils/schema-explorer.ts +73 -7
  146. package/eventcatalog/src/utils/schema-usage-graph.ts +92 -0
  147. package/package.json +5 -4
  148. package/dist/chunk-RDYYZQYU.js +0 -18
  149. package/eventcatalog/src/components/EnvironmentDropdown.test.ts +0 -64
  150. package/eventcatalog/src/components/Grids/message-link.spec.ts +0 -59
  151. package/eventcatalog/src/components/MDX/CustomProperties/custom-properties.spec.ts +0 -38
  152. package/eventcatalog/src/components/MDX/SchemaViewer/schema-viewer-utils.spec.ts +0 -116
  153. package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.test.tsx +0 -114
  154. package/eventcatalog/src/components/SchemaExplorer/ProtobufSchemaViewer.test.tsx +0 -91
  155. package/eventcatalog/src/components/SchemaExplorer/SchemaViewerHover.test.tsx +0 -33
  156. package/eventcatalog/src/components/SchemaExplorer/protobuf-validation.test.ts +0 -84
  157. package/eventcatalog/src/components/Search/search-utils.spec.ts +0 -173
  158. package/eventcatalog/src/components/SideNav/NestedSideBar/utils.spec.ts +0 -105
  159. package/eventcatalog/src/components/navigation-progress.spec.ts +0 -68
  160. package/eventcatalog/src/enterprise/directory/user-team-directory.spec.ts +0 -527
  161. package/eventcatalog/src/enterprise/fields/field-extractor.test.ts +0 -302
  162. package/eventcatalog/src/enterprise/fields/fields-db.test.ts +0 -185
  163. package/eventcatalog/src/stores/eventcatalog-store.spec.ts +0 -60
  164. package/eventcatalog/src/utils/collections/glob-loader.spec.ts +0 -87
  165. package/dist/{chunk-6K7XZAYI.js → chunk-A2RZR3U4.js} +3 -3
@@ -1,6 +1,8 @@
1
1
  ---
2
2
  import type { PageTypes } from '@types';
3
- import { getCollection } from 'astro:content';
3
+ import { getCollection, render } from 'astro:content';
4
+ import components from '@components/MDX/components';
5
+ import { getExampleEntriesForMessage, getExampleEntryDetails } from '@utils/collections/examples';
4
6
  import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
5
7
  import { Page } from './_index.data';
6
8
  import SchemaPageViewer from '@components/SchemaExplorer/SchemaPageViewer';
@@ -9,8 +11,24 @@ import { sortVersioned } from '@utils/collections/util';
9
11
  import { isEventCatalogScaleEnabled } from '@utils/feature';
10
12
  import { resourceFileExists, readResourceFile } from '@utils/resource-files';
11
13
  import path from 'node:path';
12
- import type { SchemaItem } from '@components/SchemaExplorer/types';
14
+ import type { FlowUsage, SchemaItem } from '@components/SchemaExplorer/types';
13
15
  import { getExamplesForResource } from '@utils/collections/examples';
16
+ import { getSchemaRelationshipReference } from '@components/SchemaExplorer/utils';
17
+ import { getSchemaExtensionForFormat } from '@utils/collections/schemas';
18
+ import { getFlowsForMessages } from '@utils/collections/flows';
19
+ import { attachFlowGraphs, getMessageUsageGraph } from '@utils/schema-usage-graph';
20
+ // Ships the visualiser CSS in the page head so it survives ClientRouter navigations (see NodeGraph.astro).
21
+ import '@eventcatalog/visualiser/styles-core.css';
22
+
23
+ /** Reduce hydrated resource entries to the details the schema page renders. */
24
+ const toSchemaRelationships = (references: any[] | undefined) =>
25
+ (references ?? []).map((reference) => getSchemaRelationshipReference(reference));
26
+ const toMessageChannels = (channels: any[] | undefined) =>
27
+ (channels ?? []).map((channel) => ({
28
+ id: channel.data.id,
29
+ version: channel.data.version,
30
+ ...(channel.data.name ? { name: channel.data.name } : {}),
31
+ }));
14
32
 
15
33
  export const prerender = Page.prerender;
16
34
  export const getStaticPaths = Page.getStaticPaths;
@@ -26,6 +44,14 @@ const contractPath = Astro.url.searchParams.get('contract') || (props as any).co
26
44
 
27
45
  let currentMessage: SchemaItem | null = null;
28
46
  let sortedVersions: SchemaItem[] = [];
47
+ // Examples rendered through the MDX pipeline so they can use EventCatalog's components.
48
+ let renderedExamples: {
49
+ fileName: string;
50
+ title: string;
51
+ summary?: string;
52
+ Content: Awaited<ReturnType<typeof render>>['Content'];
53
+ }[] = [];
54
+ let exampleComponents: ReturnType<typeof components> | undefined;
29
55
  let pageTitle = `${type} | ${data.name}`.replace(/^\w/, (c) => c.toUpperCase());
30
56
 
31
57
  if (isDataProduct && contractPath) {
@@ -79,6 +105,7 @@ if (isDataProduct && contractPath) {
79
105
  // Handle regular messages (events, commands, queries)
80
106
  const [allItems, schemaEntries] = await Promise.all([pageDataLoader[type](), getCollection('schemas')]);
81
107
  const versions = allItems.filter((item) => item.data.id === data.id);
108
+ const flowsByMessage = await getFlowsForMessages(versions);
82
109
  const schemasForMessage = schemaEntries.filter(
83
110
  (schema) => schema.data.message.collectionName === type && schema.data.message.id === data.id
84
111
  );
@@ -89,44 +116,81 @@ if (isDataProduct && contractPath) {
89
116
  };
90
117
 
91
118
  // Transform to SchemaItems
92
- const availableVersions = versions
93
- .map((message) => {
94
- try {
95
- const schema = getSchemaForVersion(message.data.version);
96
- if (!schema) return null;
119
+ const availableVersions = (
120
+ await Promise.all(
121
+ versions.map(async (message) => {
122
+ try {
123
+ const schema = getSchemaForVersion(message.data.version);
124
+ const messageFlows: FlowUsage[] =
125
+ flowsByMessage.get(`${message.collection}:${message.data.id}:${message.data.version}`) ?? [];
126
+ if (!schema) return null;
97
127
 
98
- const schemaPath = schema.data.file || schema.data.source.path || '';
99
- const schemaExtension = path.extname(schemaPath).slice(1);
128
+ const schemaPath = schema.data.file || schema.data.source.path || '';
129
+ const schemaExtension = path.extname(schemaPath).slice(1) || getSchemaExtensionForFormat(schema.data.format);
100
130
 
101
- return {
102
- collection: message.collection,
103
- data: {
104
- id: message.data.id,
105
- name: schema.data.message.name || message.data.name,
106
- version: message.data.version,
107
- summary: schema.data.message.summary || message.data.summary,
108
- schemaPath,
109
- // @ts-ignore
110
- producers: message.data.producers || [],
111
- // @ts-ignore
112
- consumers: message.data.consumers || [],
113
- },
114
- schemaContent: schema.data.content || '',
115
- schemaExtension,
116
- examples: getExamplesForResource(message),
117
- } as SchemaItem;
118
- } catch (error) {
119
- console.error(`Error reading schema for ${message.data.id}:`, error);
120
- return null;
121
- }
122
- })
123
- .filter((v): v is SchemaItem => v !== null);
131
+ return {
132
+ collection: message.collection,
133
+ data: {
134
+ id: message.data.id,
135
+ name: schema.data.message.name || message.data.name,
136
+ version: message.data.version,
137
+ summary: schema.data.message.summary || message.data.summary,
138
+ schemaPath,
139
+ // @ts-ignore
140
+ producers: toSchemaRelationships(message.data.producers),
141
+ // @ts-ignore
142
+ consumers: toSchemaRelationships(message.data.consumers),
143
+ flows: await (message.data.version === data.version
144
+ ? attachFlowGraphs(messageFlows, { id: message.data.id, version: message.data.version })
145
+ : messageFlows),
146
+ // @ts-ignore
147
+ channels: toMessageChannels(message.data.messageChannels),
148
+ },
149
+ schemaContent: schema.data.content || '',
150
+ schemaExtension,
151
+ ...(schema.data.name ? { schemaName: schema.data.name } : {}),
152
+ ...(schema.data.ref ? { schemaRef: schema.data.ref, source: schema.data.source } : {}),
153
+ examples: getExamplesForResource(message),
154
+ // Only the version being viewed needs the usage graph; other versions load their own page.
155
+ ...(message.data.version === data.version
156
+ ? {
157
+ graph: await getMessageUsageGraph({
158
+ collection: message.collection,
159
+ id: message.data.id,
160
+ version: message.data.version,
161
+ }),
162
+ }
163
+ : {}),
164
+ } as SchemaItem;
165
+ } catch (error) {
166
+ console.error(`Error reading schema for ${message.data.id}:`, error);
167
+ return null;
168
+ }
169
+ })
170
+ )
171
+ ).filter((v): v is SchemaItem => v !== null);
124
172
 
125
173
  sortedVersions = sortVersioned(availableVersions, (item) => item.data.version);
126
174
  currentMessage = sortedVersions.find((v) => v.data.version === data.version) ?? null;
175
+
176
+ const messageEntry = versions.find((item) => item.data.version === data.version);
177
+ if (messageEntry?.filePath) {
178
+ const exampleEntries = getExampleEntriesForMessage(await getCollection('examples'), messageEntry.filePath);
179
+ const examplesDir = path.join(path.dirname(messageEntry.filePath), 'examples');
180
+ renderedExamples = await Promise.all(
181
+ exampleEntries.map(async (entry) => ({
182
+ ...getExampleEntryDetails(entry, examplesDir),
183
+ Content: (await render(entry)).Content,
184
+ }))
185
+ );
186
+ exampleComponents = components(messageEntry);
187
+ }
127
188
  }
128
189
 
129
190
  const apiAccessEnabled = isEventCatalogScaleEnabled();
191
+ // Render the requested tab on the server so the page does not flash the default tab before hydration.
192
+ // Static builds cannot see the query string, so the panel also restores the tab on the client.
193
+ const initialTab = Astro.url.searchParams.get('tab') ?? undefined;
130
194
  ---
131
195
 
132
196
  <VerticalSideBarLayout title={pageTitle}>
@@ -134,11 +198,16 @@ const apiAccessEnabled = isEventCatalogScaleEnabled();
134
198
  :root[data-embedded='true'] #schema-detail-page {
135
199
  height: 100dvh !important;
136
200
  }
201
+ .react-flow__attribution {
202
+ display: none;
203
+ }
137
204
  </style>
138
- {/* The schema viewer is an app-style surface: pull it out of the content gutter so it sits flush against the sidebar. */}
205
+ {
206
+ /* The schema viewer is an app-style surface: pull it out of the content gutter so it sits flush against the sidebar. The page scrolls normally; the embedded layout below pins it to the viewport instead. */
207
+ }
139
208
  <div
140
209
  id="schema-detail-page"
141
- class="h-[calc(100vh-4rem)] overflow-hidden flex flex-col"
210
+ class="min-h-[calc(100vh-4rem)] flex flex-col"
142
211
  style="margin-left: calc(var(--ec-app-content-padding-left, 5rem) * -1); margin-right: calc(var(--ec-app-content-padding-right, 5rem) * -1);"
143
212
  >
144
213
  {
@@ -148,9 +217,47 @@ const apiAccessEnabled = isEventCatalogScaleEnabled();
148
217
  message={currentMessage}
149
218
  availableVersions={sortedVersions}
150
219
  apiAccessEnabled={apiAccessEnabled}
151
- showOwners={false}
152
- showProducersConsumers={false}
153
- />
220
+ showOwners={true}
221
+ showProducersConsumers={true}
222
+ initialTab={initialTab}
223
+ hasRenderedExamples={(currentMessage.examples?.length ?? 0) > 0}
224
+ >
225
+ {(currentMessage.examples?.length ?? 0) > 0 && (
226
+ <div slot="examples" class="divide-y divide-[rgb(var(--ec-page-border))]">
227
+ {currentMessage.examples?.map((example) => {
228
+ const rendered = renderedExamples.find((candidate) => candidate.fileName === example.fileName);
229
+ const Content = rendered?.Content;
230
+ return (
231
+ <section
232
+ id={`example-${example.fileName.replace(/[^a-zA-Z0-9]+/g, '-')}`}
233
+ class="w-full pr-12 py-8 first:pt-0 last:pb-0"
234
+ >
235
+ {example.summary && <p class="mb-4 text-sm text-[rgb(var(--ec-page-text-muted))]">{example.summary}</p>}
236
+ {Content ? (
237
+ <div class="prose prose-sm max-w-none prose-h1:mt-0 prose-h1:text-4xl prose-h1:font-bold prose-h1:tracking-tight prose-headings:text-[rgb(var(--ec-page-text))] prose-p:text-[rgb(var(--ec-page-text-muted))] prose-li:text-[rgb(var(--ec-page-text-muted))] prose-strong:text-[rgb(var(--ec-page-text))] prose-a:text-[rgb(var(--ec-accent))] prose-blockquote:border-[rgb(var(--ec-page-border))] prose-hr:border-[rgb(var(--ec-page-border))]">
238
+ <Content components={exampleComponents} />
239
+ </div>
240
+ ) : (
241
+ <pre class="overflow-auto rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-code-bg))] p-5 text-sm">
242
+ <code>{example.content}</code>
243
+ </pre>
244
+ )}
245
+ {example.usage && (
246
+ <div class="mt-5">
247
+ <h3 class="mb-2 text-xs font-semibold uppercase tracking-wider text-[rgb(var(--ec-page-text-muted))]">
248
+ Usage
249
+ </h3>
250
+ <pre class="overflow-auto rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-code-bg))] p-5 text-sm">
251
+ <code>{example.usage}</code>
252
+ </pre>
253
+ </div>
254
+ )}
255
+ </section>
256
+ );
257
+ })}
258
+ </div>
259
+ )}
260
+ </SchemaPageViewer>
154
261
  ) : (
155
262
  <div class="p-8 flex items-center justify-center h-full text-[rgb(var(--ec-page-text-muted))]">
156
263
  Schema not found or could not be loaded.
@@ -3,6 +3,8 @@ import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
3
3
  import SchemaExplorer from '@components/SchemaExplorer/SchemaExplorer';
4
4
  import { isEventCatalogScaleEnabled } from '@utils/feature';
5
5
  import { Page } from './_index.data';
6
+ // Ships the visualiser CSS in the page head for the Usage tab's node graph (see NodeGraph.astro).
7
+ import '@eventcatalog/visualiser/styles-core.css';
6
8
 
7
9
  export const prerender = Page.prerender;
8
10
  export const getStaticPaths = Page.getStaticPaths;
@@ -14,6 +16,9 @@ const apiAccessEnabled = isEventCatalogScaleEnabled();
14
16
 
15
17
  <VerticalSideBarLayout title="Schema Explorer - EventCatalog" showNestedSideBar={false}>
16
18
  <style is:global>
19
+ .react-flow__attribution {
20
+ display: none;
21
+ }
17
22
  #eventcatalog-header {
18
23
  left: calc(var(--ec-vertical-nav-width, 14rem) + var(--ec-schema-sidebar-width, 360px)) !important;
19
24
  }
@@ -0,0 +1,21 @@
1
+ export const KNOWN_EVENTCATALOG_COLLECTIONS: ReadonlySet<string>;
2
+
3
+ export function collectionNameFromEmptyWarning(message: string): string | null;
4
+
5
+ export function isIntentionalEmptyCollectionMessage(message: string): boolean;
6
+
7
+ export function isIntentionalEmptyCollectionLine(line: string): boolean;
8
+
9
+ export interface QuietLoggerEvent {
10
+ level?: string;
11
+ label?: string | null;
12
+ message?: string;
13
+ }
14
+
15
+ export interface QuietLoggerDestination {
16
+ write: (event: QuietLoggerEvent) => void;
17
+ flush?: () => void;
18
+ close?: () => void;
19
+ }
20
+
21
+ export function wrapLoggerDestination<T extends QuietLoggerDestination>(destination: T): QuietLoggerDestination;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Astro's getCollection() warns when a collection is missing from the content store:
3
+ * `The collection "<name>" does not exist or is empty. Please check your content config file for errors.`
4
+ *
5
+ * EventCatalog always defines every resource collection. A catalog that does not use a resource
6
+ * type leaves that collection empty, and Astro repeats the warning for every getCollection() call.
7
+ * That warning is noise. Schema failures, loader errors, and warnings for unknown collection
8
+ * names use different messages (or a collection name outside this set) and stay visible.
9
+ *
10
+ * Keep this set aligned with `export const collections` in content.config.ts.
11
+ */
12
+ export const KNOWN_EVENTCATALOG_COLLECTIONS = new Set([
13
+ 'events',
14
+ 'commands',
15
+ 'queries',
16
+ 'examples',
17
+ 'services',
18
+ 'agents',
19
+ 'adrs',
20
+ 'channels',
21
+ 'users',
22
+ 'teams',
23
+ 'domains',
24
+ 'systems',
25
+ 'flows',
26
+ 'pages',
27
+ 'changelogs',
28
+ 'containers',
29
+ 'data-products',
30
+ 'ubiquitousLanguages',
31
+ 'entities',
32
+ 'customPages',
33
+ 'resourceDocs',
34
+ 'resourceDocCategories',
35
+ 'designs',
36
+ 'diagrams',
37
+ 'schemas',
38
+ 'sidebars',
39
+ ]);
40
+
41
+ const EMPTY_COLLECTION_MESSAGE =
42
+ /^The collection ("(?:\\.|[^"\\])*") does not exist or is empty\. Please check your content config file for errors\.$/;
43
+
44
+ const ANSI_PATTERN = /\u001B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
45
+
46
+ /**
47
+ * Timestamp text, then Astro's content logger prefix: `[WARN] [content]`.
48
+ * A `[WARN]` line from any other label, such as `[build]`, is not this warning.
49
+ */
50
+ const ASTRO_CONTENT_WARN_PREFIX = /^[^[\]]*\[WARN\]\s+\[content\](?:\s+\[[^\]]+\])*\s*$/;
51
+
52
+ export function collectionNameFromEmptyWarning(message) {
53
+ const match = message.match(EMPTY_COLLECTION_MESSAGE);
54
+ if (!match) return null;
55
+
56
+ try {
57
+ const name = JSON.parse(match[1]);
58
+ return typeof name === 'string' ? name : null;
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ export function isIntentionalEmptyCollectionMessage(message) {
65
+ const name = collectionNameFromEmptyWarning(message);
66
+ return name !== null && KNOWN_EVENTCATALOG_COLLECTIONS.has(name);
67
+ }
68
+
69
+ /**
70
+ * Matches the warning as Astro prints it: a bare message, or the node/console logger line
71
+ * `<time> [WARN] [content] <message>`, including ANSI color on the prefix.
72
+ * The same sentence from another logger label, extra diagnostic text, and `[ERROR]` lines stay visible.
73
+ */
74
+ export function isIntentionalEmptyCollectionLine(line) {
75
+ const plain = line.replace(ANSI_PATTERN, '').replace(/\r/g, '').trim();
76
+ const marker = 'The collection "';
77
+ const start = plain.indexOf(marker);
78
+ if (start === -1) return false;
79
+
80
+ const message = plain.slice(start).trim();
81
+ if (!isIntentionalEmptyCollectionMessage(message)) return false;
82
+
83
+ const before = plain.slice(0, start).trim();
84
+ return before.length === 0 || ASTRO_CONTENT_WARN_PREFIX.test(before);
85
+ }
86
+
87
+ export function wrapLoggerDestination(destination) {
88
+ return {
89
+ write(event) {
90
+ if (
91
+ event?.level === 'warn' &&
92
+ event?.label === 'content' &&
93
+ typeof event.message === 'string' &&
94
+ isIntentionalEmptyCollectionMessage(event.message)
95
+ ) {
96
+ return;
97
+ }
98
+ destination.write(event);
99
+ },
100
+ flush() {
101
+ return destination.flush?.();
102
+ },
103
+ close() {
104
+ return destination.close?.();
105
+ },
106
+ };
107
+ }
@@ -0,0 +1,11 @@
1
+ import nodeLogger from 'astro/logger/node';
2
+ import { wrapLoggerDestination } from './empty-collection-warning.mjs';
3
+
4
+ /**
5
+ * Astro logger destination used by EventCatalog builds and the dev server.
6
+ * Drops the empty-collection content warning for collections EventCatalog defines.
7
+ * Every other log event is written with Astro's default node logger.
8
+ */
9
+ export default function quietEmptyCollectionsLogger(options) {
10
+ return wrapLoggerDestination(nodeLogger(options));
11
+ }
@@ -0,0 +1,39 @@
1
+ type Node = {
2
+ type: string;
3
+ name?: string;
4
+ lang?: string;
5
+ meta?: string | null;
6
+ children?: Node[];
7
+ attributes?: { type: string; name: string; value: string }[];
8
+ };
9
+
10
+ /** Keep fenced code in the normal highlighting pipeline, with a panel per fence. */
11
+ export function remarkCodeGroup() {
12
+ return (tree: Node) => {
13
+ const walk = (node: Node) => {
14
+ if (node.type === 'mdxJsxFlowElement' && node.name === 'CodeGroup') {
15
+ node.children = node.children?.map((child, index) => {
16
+ if (child.type !== 'code') return child;
17
+ const meta = child.meta?.trim() ?? '';
18
+ const explicit = meta.match(/(?:^|\s)title=(?:"([^"]*)"|'([^']*)')/);
19
+ const bare = meta.match(/^(?:"([^"]+)"|'([^']+)'|([^\s={}]+))(?=\s|$)/);
20
+ const label =
21
+ explicit?.[1] ?? explicit?.[2] ?? bare?.[1] ?? bare?.[2] ?? bare?.[3] ?? child.lang ?? `Code ${index + 1}`;
22
+ // Bare titles are Mintlify syntax; remove them before Expressive Code reads modifiers.
23
+ if (!explicit && bare) child.meta = meta.slice(bare[0].length).trim();
24
+ return {
25
+ type: 'mdxJsxFlowElement',
26
+ name: 'div',
27
+ attributes: [
28
+ { type: 'mdxJsxAttribute', name: 'data-code-panel', value: label },
29
+ ...(child.lang ? [{ type: 'mdxJsxAttribute', name: 'data-code-lang', value: child.lang }] : []),
30
+ ],
31
+ children: [child],
32
+ };
33
+ });
34
+ }
35
+ node.children?.forEach(walk);
36
+ };
37
+ walk(tree);
38
+ };
39
+ }
@@ -0,0 +1,59 @@
1
+ type Node = {
2
+ type: string;
3
+ value?: string;
4
+ lang?: string;
5
+ meta?: string | null;
6
+ children?: Node[];
7
+ data?: Record<string, unknown>;
8
+ };
9
+
10
+ /** Recognize only CodeGroup wrappers in plain Markdown; never enable arbitrary HTML or JSX. */
11
+ export function remarkMarkdownCodeGroup() {
12
+ return (tree: Node) => {
13
+ const walk = (parent: Node) => {
14
+ const children = parent.children;
15
+ if (!children) return;
16
+ for (let index = 0; index < children.length; index++) {
17
+ const opening = children[index].type === 'html' && children[index].value?.trim().match(/^<CodeGroup\b([^>]*)>$/);
18
+ if (!opening) {
19
+ walk(children[index]);
20
+ continue;
21
+ }
22
+ const end = children.findIndex(
23
+ (child, i) => i > index && child.type === 'html' && child.value?.trim() === '</CodeGroup>'
24
+ );
25
+ if (end === -1) continue;
26
+ const blocks = children.slice(index + 1, end);
27
+ if (!blocks.length || blocks.some((child) => child.type !== 'code')) continue;
28
+ const className = opening[1].match(/\bclassName=(?:"([^"]*)"|'([^']*)')/);
29
+ const dropdown = /(?:^|\s)dropdown(?:\s|$|=\{true\})/.test(opening[1]);
30
+ children.splice(index, end - index + 1, {
31
+ type: 'codeGroup',
32
+ data: {
33
+ hName: 'div',
34
+ hProperties: {
35
+ 'data-code-group': 'true',
36
+ 'data-dropdown': String(dropdown),
37
+ className: className?.[1] ?? className?.[2],
38
+ },
39
+ },
40
+ children: blocks.map((code, i) => {
41
+ const meta = code.meta?.trim() ?? '';
42
+ const title =
43
+ meta.match(/(?:^|\s)title=(?:"([^"]*)"|'([^']*)')/) ?? meta.match(/^(?:"([^"]+)"|'([^']+)'|([^\s={}]+))/);
44
+ const label = title?.[1] ?? title?.[2] ?? title?.[3] ?? code.lang ?? `Code ${i + 1}`;
45
+ return {
46
+ type: 'codeGroupPanel',
47
+ data: {
48
+ hName: 'div',
49
+ hProperties: { 'data-code-panel': label, ...(code.lang ? { 'data-code-lang': code.lang } : {}) },
50
+ },
51
+ children: [code],
52
+ };
53
+ }),
54
+ });
55
+ }
56
+ };
57
+ walk(tree);
58
+ };
59
+ }
@@ -0,0 +1,66 @@
1
+ type Node = {
2
+ type: string;
3
+ value?: string;
4
+ children?: Node[];
5
+ data?: Record<string, unknown>;
6
+ };
7
+
8
+ const tagPattern = /<\/?(?:Columns|Column)\b[^>]*>/g;
9
+ const attribute = (attrs: string, name: string) =>
10
+ attrs
11
+ .match(new RegExp(`\\b${name}=(?:"([^"]*)"|'([^']*)'|\\{([\\d.]+)\\})`))
12
+ ?.slice(1)
13
+ .find((value) => value !== undefined);
14
+
15
+ /** Support the layout tags in Markdown without evaluating JSX or allowing arbitrary HTML. */
16
+ export function remarkMarkdownColumns() {
17
+ return (tree: Node) => {
18
+ const walk = (parent: Node) => {
19
+ if (!parent.children) return;
20
+ // Markdown may put adjacent opening/closing tags in the same HTML node.
21
+ const children = parent.children.flatMap((node) => {
22
+ if (node.type !== 'html' || !node.value || node.value.replace(tagPattern, '').trim()) return [node];
23
+ return Array.from(node.value.matchAll(tagPattern), ([value]) => ({ type: 'html', value }));
24
+ });
25
+ parent.children = children;
26
+ for (let i = 0; i < children.length; i++) {
27
+ const opening = children[i].type === 'html' && children[i].value?.trim().match(/^<(Columns|Column)\b([^>]*)>$/);
28
+ if (!opening) {
29
+ walk(children[i]);
30
+ continue;
31
+ }
32
+ const name = opening[1];
33
+ let depth = 1;
34
+ let end = i + 1;
35
+ for (; end < children.length; end++) {
36
+ if (children[end].type !== 'html') continue;
37
+ const value = children[end].value?.trim();
38
+ if (value?.match(new RegExp(`^<${name}\\b[^>]*>$`))) depth++;
39
+ if (value === `</${name}>` && --depth === 0) break;
40
+ }
41
+ if (end === children.length) continue;
42
+ const attrs = opening[2];
43
+ const wrapper: Node = {
44
+ type: 'columnLayout',
45
+ data: {
46
+ hName: 'div',
47
+ hProperties: {
48
+ ...(name === 'Columns'
49
+ ? {
50
+ 'data-columns': 'true',
51
+ 'data-cols': attribute(attrs, 'cols') ?? '2',
52
+ 'data-ratio': attribute(attrs, 'ratio'),
53
+ }
54
+ : { 'data-column': 'true', 'data-sticky': String(/(?:^|\s)sticky(?:\s|$|=\{true\})/.test(attrs)) }),
55
+ className: attribute(attrs, 'className'),
56
+ },
57
+ },
58
+ children: children.slice(i + 1, end),
59
+ };
60
+ walk(wrapper);
61
+ children.splice(i, end - i + 1, wrapper);
62
+ }
63
+ };
64
+ walk(tree);
65
+ };
66
+ }
@@ -118,11 +118,15 @@ export const getChannels = async ({ getAllVersions = true }: Props = {}): Promis
118
118
 
119
119
  // Could be recursive, we need to keep going until we find a loop or until we reach the target channel
120
120
  export const isChannelsConnected = (
121
- sourceChannel: CollectionEntry<'channels'>,
122
- targetChannel: CollectionEntry<'channels'>,
121
+ sourceChannel: CollectionEntry<'channels'> | undefined,
122
+ targetChannel: CollectionEntry<'channels'> | undefined,
123
123
  channels: CollectionEntry<'channels'>[],
124
124
  visited: Set<string> = new Set()
125
125
  ) => {
126
+ if (!sourceChannel?.data || !targetChannel?.data) {
127
+ return false;
128
+ }
129
+
126
130
  // Create a unique key for this channel (id + version to handle multiple versions)
127
131
  const channelKey = `${sourceChannel.data.id}:${sourceChannel.data.version}`;
128
132
 
@@ -159,10 +163,14 @@ export const isChannelsConnected = (
159
163
 
160
164
  // Go from the source to the target channel and return the channel chain
161
165
  export const getChannelChain = (
162
- sourceChannel: CollectionEntry<'channels'>,
163
- targetChannel: CollectionEntry<'channels'>,
166
+ sourceChannel: CollectionEntry<'channels'> | undefined,
167
+ targetChannel: CollectionEntry<'channels'> | undefined,
164
168
  channels: CollectionEntry<'channels'>[]
165
169
  ): CollectionEntry<'channels'>[] => {
170
+ if (!sourceChannel?.data || !targetChannel?.data) {
171
+ return [];
172
+ }
173
+
166
174
  // Base case: we've reached the target channel
167
175
  if (sourceChannel.data.id === targetChannel.data.id && sourceChannel.data.version === targetChannel.data.version) {
168
176
  return [sourceChannel];