@eventcatalog/core 4.10.14 → 4.11.0
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/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/{chunk-ONDFEA5R.js → chunk-DWC5GRFX.js} +1 -1
- package/dist/{chunk-LITOQH56.js → chunk-H3324DNT.js} +1 -1
- package/dist/{chunk-V2SKOQYF.js → chunk-SW7SJZIX.js} +1 -1
- package/dist/{chunk-HERAHLM2.js → chunk-W4LDIMUI.js} +1 -1
- package/dist/{chunk-LUW6RDA7.js → chunk-XJOKK5ZO.js} +1 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/docs/api/02-config.md +6 -6
- package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/02-configuration.md +18 -11
- package/dist/docs/development/components/components/13-mermaid-file-loader.md +12 -0
- package/dist/docs/development/components/diagram-syntax/06-mermaid.md +45 -6
- package/dist/docs/development/customization/05-resource-sidebar.md +1 -1
- package/dist/eventcatalog.cjs +1 -1
- package/dist/eventcatalog.js +5 -5
- 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 +2 -1
- package/eventcatalog/public/icons/languages/android.svg +1 -0
- package/eventcatalog/public/icons/languages/angular.svg +1 -0
- package/eventcatalog/public/icons/languages/apple.svg +1 -0
- package/eventcatalog/public/icons/languages/arduino.svg +1 -0
- package/eventcatalog/public/icons/languages/bash.svg +1 -0
- package/eventcatalog/public/icons/languages/bun.svg +1 -0
- package/eventcatalog/public/icons/languages/c.svg +1 -0
- package/eventcatalog/public/icons/languages/clojure.svg +1 -0
- package/eventcatalog/public/icons/languages/cpp.svg +1 -0
- package/eventcatalog/public/icons/languages/crystal.svg +1 -0
- package/eventcatalog/public/icons/languages/csharp.svg +1 -0
- package/eventcatalog/public/icons/languages/css.svg +1 -0
- package/eventcatalog/public/icons/languages/curl.svg +1 -0
- package/eventcatalog/public/icons/languages/dart.svg +1 -0
- package/eventcatalog/public/icons/languages/deno.svg +1 -0
- package/eventcatalog/public/icons/languages/docker.svg +1 -0
- package/eventcatalog/public/icons/languages/elm.svg +1 -0
- package/eventcatalog/public/icons/languages/erlang.svg +1 -0
- package/eventcatalog/public/icons/languages/flutter.svg +1 -0
- package/eventcatalog/public/icons/languages/fortran.svg +1 -0
- package/eventcatalog/public/icons/languages/fsharp.svg +1 -0
- package/eventcatalog/public/icons/languages/git.svg +1 -0
- package/eventcatalog/public/icons/languages/gradle.svg +1 -0
- package/eventcatalog/public/icons/languages/graphql.svg +1 -0
- package/eventcatalog/public/icons/languages/grpc.svg +1 -0
- package/eventcatalog/public/icons/languages/haskell.svg +1 -0
- package/eventcatalog/public/icons/languages/hasura.svg +1 -0
- package/eventcatalog/public/icons/languages/html.svg +1 -0
- package/eventcatalog/public/icons/languages/httpie.svg +1 -0
- package/eventcatalog/public/icons/languages/javascript.svg +1 -0
- package/eventcatalog/public/icons/languages/json-schema.svg +1 -0
- package/eventcatalog/public/icons/languages/json.svg +1 -0
- package/eventcatalog/public/icons/languages/julia.svg +1 -0
- package/eventcatalog/public/icons/languages/jupyter.svg +1 -0
- package/eventcatalog/public/icons/languages/kafka.svg +1 -0
- package/eventcatalog/public/icons/languages/kotlin.svg +1 -0
- package/eventcatalog/public/icons/languages/lua.svg +1 -0
- package/eventcatalog/public/icons/languages/markdown.svg +1 -0
- package/eventcatalog/public/icons/languages/maven.svg +1 -0
- package/eventcatalog/public/icons/languages/mongodb.svg +1 -0
- package/eventcatalog/public/icons/languages/mysql.svg +1 -0
- package/eventcatalog/public/icons/languages/nginx.svg +1 -0
- package/eventcatalog/public/icons/languages/nim.svg +1 -0
- package/eventcatalog/public/icons/languages/ocaml.svg +1 -0
- package/eventcatalog/public/icons/languages/perl.svg +1 -0
- package/eventcatalog/public/icons/languages/php.svg +1 -0
- package/eventcatalog/public/icons/languages/postgresql.svg +1 -0
- package/eventcatalog/public/icons/languages/prisma.svg +1 -0
- package/eventcatalog/public/icons/languages/purescript.svg +1 -0
- package/eventcatalog/public/icons/languages/python.svg +1 -0
- package/eventcatalog/public/icons/languages/r.svg +1 -0
- package/eventcatalog/public/icons/languages/react.svg +1 -0
- package/eventcatalog/public/icons/languages/reasonml.svg +1 -0
- package/eventcatalog/public/icons/languages/redis.svg +1 -0
- package/eventcatalog/public/icons/languages/ruby.svg +1 -0
- package/eventcatalog/public/icons/languages/rust.svg +1 -0
- package/eventcatalog/public/icons/languages/sass.svg +1 -0
- package/eventcatalog/public/icons/languages/scala.svg +1 -0
- package/eventcatalog/public/icons/languages/solidity.svg +1 -0
- package/eventcatalog/public/icons/languages/spring.svg +1 -0
- package/eventcatalog/public/icons/languages/sqlite.svg +1 -0
- package/eventcatalog/public/icons/languages/svelte.svg +1 -0
- package/eventcatalog/public/icons/languages/swagger.svg +1 -0
- package/eventcatalog/public/icons/languages/swift.svg +1 -0
- package/eventcatalog/public/icons/languages/tailwind.svg +1 -0
- package/eventcatalog/public/icons/languages/terraform.svg +1 -0
- package/eventcatalog/public/icons/languages/toml.svg +1 -0
- package/eventcatalog/public/icons/languages/typescript.svg +1 -0
- package/eventcatalog/public/icons/languages/unity.svg +1 -0
- package/eventcatalog/public/icons/languages/vim.svg +1 -0
- package/eventcatalog/public/icons/languages/vue.svg +1 -0
- package/eventcatalog/public/icons/languages/webassembly.svg +1 -0
- package/eventcatalog/public/icons/languages/yaml.svg +1 -0
- package/eventcatalog/public/icons/languages/zig.svg +1 -0
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.astro +12 -0
- package/eventcatalog/src/components/MDX/CodeGroup/CodeGroup.tsx +176 -0
- package/eventcatalog/src/components/MDX/CodeGroup/MarkdownCodeGroup.tsx +22 -0
- package/eventcatalog/src/components/MDX/CodeGroup/code-group-icons.ts +345 -0
- package/eventcatalog/src/components/MDX/CodeGroup/code-group.css +22 -0
- package/eventcatalog/src/components/MDX/CodeGroup/code-group.spec.tsx +89 -0
- package/eventcatalog/src/components/MDX/CodeGroup/parse-panels.ts +11 -0
- package/eventcatalog/src/components/MDX/Columns/Column.astro +10 -0
- package/eventcatalog/src/components/MDX/Columns/Columns.astro +14 -0
- package/eventcatalog/src/components/MDX/Columns/MarkdownColumns.tsx +37 -0
- package/eventcatalog/src/components/MDX/Columns/columns.spec.tsx +66 -0
- package/eventcatalog/src/components/MDX/Columns/layout.ts +16 -0
- package/eventcatalog/src/components/MDX/components.tsx +6 -0
- package/eventcatalog/src/components/SchemaExplorer/ApiContentViewer.tsx +1 -1
- package/eventcatalog/src/components/SchemaExplorer/ExamplesViewer.tsx +151 -114
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.test.tsx +114 -0
- package/eventcatalog/src/components/SchemaExplorer/JSONSchemaViewer.tsx +28 -9
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +510 -248
- package/eventcatalog/src/components/SchemaExplorer/SchemaFlows.tsx +76 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaGraph.tsx +47 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaPageViewer.tsx +11 -0
- package/eventcatalog/src/components/SchemaExplorer/SchemaUsage.tsx +17 -0
- package/eventcatalog/src/components/SchemaExplorer/types.ts +48 -3
- package/eventcatalog/src/components/SchemaExplorer/utils.ts +28 -1
- package/eventcatalog/src/content.config.ts +28 -0
- package/eventcatalog/src/pages/docs/[type]/[id]/[version]/examples/[...filename].astro +38 -7
- package/eventcatalog/src/pages/schemas/[type]/[id]/[version]/index.astro +144 -37
- package/eventcatalog/src/pages/schemas/explorer/index.astro +5 -0
- package/eventcatalog/src/remark-plugins/code-group.ts +39 -0
- package/eventcatalog/src/remark-plugins/markdown-code-group.ts +59 -0
- package/eventcatalog/src/remark-plugins/markdown-columns.ts +66 -0
- package/eventcatalog/src/utils/collections/examples.ts +133 -41
- package/eventcatalog/src/utils/collections/flows.ts +43 -0
- package/eventcatalog/src/utils/collections/glob-loader.ts +2 -2
- package/eventcatalog/src/utils/collections/messages.ts +2 -2
- package/eventcatalog/src/utils/collections/schemas.ts +15 -0
- package/eventcatalog/src/utils/schema-explorer.ts +73 -7
- package/eventcatalog/src/utils/schema-usage-graph.ts +92 -0
- package/package.json +8 -8
|
@@ -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 =
|
|
93
|
-
.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
{
|
|
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)]
|
|
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={
|
|
152
|
-
showProducersConsumers={
|
|
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,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
|
+
}
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
3
4
|
import yaml from 'js-yaml';
|
|
4
5
|
import { getResourceBasePath } from '@utils/resource-files';
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* A usage example for a message: any text file inside the message's `examples/` folder.
|
|
9
|
+
*/
|
|
6
10
|
export interface MessageExample {
|
|
11
|
+
/** File path relative to the `examples/` folder, for example `basic-order.json`. */
|
|
7
12
|
fileName: string;
|
|
13
|
+
/** Display title: frontmatter `title`, else the first level-one heading, else the file name. */
|
|
8
14
|
title: string;
|
|
15
|
+
/** File extension without the leading dot. */
|
|
9
16
|
extension: string;
|
|
10
|
-
|
|
17
|
+
/** Markdown is rendered as prose; other formats and unsupported MDX are rendered as source. */
|
|
18
|
+
renderMode: 'markdown' | 'code';
|
|
19
|
+
/** Optional short description from frontmatter `summary`. */
|
|
11
20
|
summary?: string;
|
|
21
|
+
/** Optional usage instructions from the legacy examples config. */
|
|
12
22
|
usage?: string;
|
|
23
|
+
/** File contents. Markdown frontmatter is removed. */
|
|
24
|
+
content: string;
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
interface ExampleConfig {
|
|
@@ -18,68 +30,148 @@ interface ExampleConfig {
|
|
|
18
30
|
usage?: string;
|
|
19
31
|
}
|
|
20
32
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const jsonPath = path.join(examplesDir, 'examples.config.json');
|
|
25
|
-
|
|
26
|
-
let configPath: string | null = null;
|
|
27
|
-
if (fs.existsSync(yamlPath)) configPath = yamlPath;
|
|
28
|
-
else if (fs.existsSync(ymlPath)) configPath = ymlPath;
|
|
29
|
-
else if (fs.existsSync(jsonPath)) configPath = jsonPath;
|
|
33
|
+
const CONFIG_FILES = new Set(['examples.config.yaml', 'examples.config.yml', 'examples.config.json']);
|
|
34
|
+
const MARKDOWN_EXTENSIONS = new Set(['.md', '.mdx']);
|
|
35
|
+
const CLIENT_RENDERED_MDX_COMPONENTS = new Set(['Column', 'Columns', 'CodeGroup']);
|
|
30
36
|
|
|
37
|
+
const loadConfig = (examplesDir: string): Record<string, ExampleConfig> => {
|
|
38
|
+
const configPath = ['examples.config.yaml', 'examples.config.yml', 'examples.config.json']
|
|
39
|
+
.map((fileName) => path.join(examplesDir, fileName))
|
|
40
|
+
.find(fs.existsSync);
|
|
31
41
|
if (!configPath) return {};
|
|
32
42
|
|
|
33
43
|
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
return (yaml.load(raw) as Record<string, ExampleConfig>) || {};
|
|
38
|
-
}
|
|
44
|
+
return configPath.endsWith('.json') ? JSON.parse(raw) || {} : (yaml.load(raw) as Record<string, ExampleConfig>) || {};
|
|
45
|
+
};
|
|
39
46
|
|
|
40
|
-
const
|
|
47
|
+
const humanizeFileName = (fileName: string) =>
|
|
48
|
+
path
|
|
49
|
+
.parse(fileName)
|
|
50
|
+
.name.replace(/[-_]+/g, ' ')
|
|
51
|
+
.replace(/\s+/g, ' ')
|
|
52
|
+
.trim()
|
|
53
|
+
.replace(/^\w/, (character) => character.toUpperCase());
|
|
41
54
|
|
|
42
|
-
|
|
43
|
-
const results:
|
|
55
|
+
const collectExampleFiles = (dir: string, baseDir: string): string[] => {
|
|
56
|
+
const results: string[] = [];
|
|
44
57
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
45
58
|
|
|
46
59
|
for (const entry of entries) {
|
|
47
60
|
const fullPath = path.join(dir, entry.name);
|
|
48
61
|
if (entry.isDirectory()) {
|
|
49
|
-
results.push(...
|
|
50
|
-
} else if (entry.isFile() && !CONFIG_FILES.
|
|
51
|
-
results.push(
|
|
52
|
-
fileName: path.relative(baseDir, fullPath),
|
|
53
|
-
title: path.parse(entry.name).name,
|
|
54
|
-
extension: path.parse(entry.name).ext.slice(1),
|
|
55
|
-
content: fs.readFileSync(fullPath, 'utf-8'),
|
|
56
|
-
});
|
|
62
|
+
results.push(...collectExampleFiles(fullPath, baseDir));
|
|
63
|
+
} else if (entry.isFile() && !CONFIG_FILES.has(entry.name)) {
|
|
64
|
+
results.push(path.relative(baseDir, fullPath));
|
|
57
65
|
}
|
|
58
66
|
}
|
|
59
67
|
|
|
60
68
|
return results;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const hasUnsupportedMdxComponent = (content: string) =>
|
|
72
|
+
[...content.matchAll(/<([A-Z][A-Za-z0-9.]*)\b/g)].some((match) => !CLIENT_RENDERED_MDX_COMPONENTS.has(match[1]));
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Splits an example file into its title and Markdown body. The title comes from frontmatter,
|
|
76
|
+
* else the leading level-one heading, else the file name. The body is left as written.
|
|
77
|
+
*/
|
|
78
|
+
export const parseExampleFile = (fileName: string, raw: string): MessageExample => {
|
|
79
|
+
const { data, content } = matter(raw);
|
|
80
|
+
const body = content.replace(/^\s*\n/, '');
|
|
81
|
+
let title = typeof data.title === 'string' && data.title.trim() ? data.title.trim() : undefined;
|
|
82
|
+
const extension = path.extname(fileName).slice(1).toLowerCase();
|
|
83
|
+
|
|
84
|
+
const headingMatch = body.match(/^#[ \t]+(.+)(?:\r?\n|$)/);
|
|
85
|
+
if (headingMatch) title = title ?? headingMatch[1].trim();
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
fileName: fileName.replace(/\\/g, '/'),
|
|
89
|
+
title: title ?? humanizeFileName(fileName),
|
|
90
|
+
extension,
|
|
91
|
+
renderMode: extension === 'mdx' && hasUnsupportedMdxComponent(body) ? 'code' : 'markdown',
|
|
92
|
+
...(typeof data.summary === 'string' && data.summary.trim() ? { summary: data.summary.trim() } : {}),
|
|
93
|
+
content: body.trim(),
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const parseRawExampleFile = (fileName: string, raw: string, config: ExampleConfig = {}): MessageExample => ({
|
|
98
|
+
fileName: fileName.replace(/\\/g, '/'),
|
|
99
|
+
title: config.name?.trim() || humanizeFileName(fileName),
|
|
100
|
+
extension: path.extname(fileName).slice(1).toLowerCase(),
|
|
101
|
+
renderMode: 'code',
|
|
102
|
+
...(config.summary?.trim() ? { summary: config.summary.trim() } : {}),
|
|
103
|
+
...(config.usage?.trim() ? { usage: config.usage.trim() } : {}),
|
|
104
|
+
content: raw,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/** The parts of an `examples` collection entry the schema pages need. */
|
|
108
|
+
export interface ExampleEntryLike {
|
|
109
|
+
id: string;
|
|
110
|
+
filePath?: string;
|
|
111
|
+
body?: string;
|
|
112
|
+
data: { title?: string; summary?: string };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const normalize = (value: string) => path.resolve(value).replace(/\\/g, '/');
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Selects the example entries that belong to a message: the files under the `examples/`
|
|
119
|
+
* folder beside the message file, sorted by path.
|
|
120
|
+
*/
|
|
121
|
+
export function getExampleEntriesForMessage<T extends ExampleEntryLike>(entries: T[], messageFilePath?: string): T[] {
|
|
122
|
+
if (!messageFilePath) return [];
|
|
123
|
+
const examplesDir = `${normalize(path.dirname(messageFilePath))}/examples/`;
|
|
124
|
+
|
|
125
|
+
return entries
|
|
126
|
+
.filter((entry) => entry.filePath && normalize(entry.filePath).startsWith(examplesDir))
|
|
127
|
+
.sort((a, b) => (a.filePath as string).localeCompare(b.filePath as string));
|
|
61
128
|
}
|
|
62
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Display details for a collection entry, mirroring `parseExampleFile`: frontmatter title,
|
|
132
|
+
* else the first level-one heading, else the file name.
|
|
133
|
+
*/
|
|
134
|
+
export function getExampleEntryDetails(entry: ExampleEntryLike, examplesDir?: string) {
|
|
135
|
+
const fileName =
|
|
136
|
+
entry.filePath && examplesDir
|
|
137
|
+
? path.relative(examplesDir, entry.filePath).replace(/\\/g, '/')
|
|
138
|
+
: path.basename(entry.filePath ?? entry.id);
|
|
139
|
+
const parsed = parseExampleFile(fileName, entry.body ?? '');
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
fileName,
|
|
143
|
+
title: entry.data.title?.trim() || parsed.title,
|
|
144
|
+
summary: entry.data.summary?.trim() || parsed.summary,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Reads usage examples stored in the `examples/` folder next to a message.
|
|
150
|
+
*/
|
|
63
151
|
export function getExamplesForResource(resource: { filePath?: string }): MessageExample[] {
|
|
64
152
|
const basePath = getResourceBasePath(resource);
|
|
65
153
|
if (!basePath) return [];
|
|
66
154
|
|
|
67
155
|
const examplesDir = path.join(basePath, 'examples');
|
|
68
156
|
if (!fs.existsSync(examplesDir)) return [];
|
|
69
|
-
|
|
70
157
|
const config = loadConfig(examplesDir);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
.map((
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
158
|
+
|
|
159
|
+
return collectExampleFiles(examplesDir, examplesDir)
|
|
160
|
+
.sort((a, b) => a.localeCompare(b))
|
|
161
|
+
.map((fileName) => {
|
|
162
|
+
const raw = fs.readFileSync(path.join(examplesDir, fileName), 'utf-8');
|
|
163
|
+
const normalizedFileName = fileName.replace(/\\/g, '/');
|
|
164
|
+
const extension = path.extname(fileName).toLowerCase();
|
|
165
|
+
if (MARKDOWN_EXTENSIONS.has(extension)) {
|
|
166
|
+
const example = parseExampleFile(fileName, raw);
|
|
167
|
+
const metadata = config[normalizedFileName];
|
|
168
|
+
return {
|
|
169
|
+
...example,
|
|
170
|
+
...(metadata?.name?.trim() ? { title: metadata.name.trim() } : {}),
|
|
171
|
+
...(metadata?.summary?.trim() ? { summary: metadata.summary.trim() } : {}),
|
|
172
|
+
...(metadata?.usage?.trim() ? { usage: metadata.usage.trim() } : {}),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return parseRawExampleFile(fileName, raw, config[normalizedFileName]);
|
|
176
|
+
});
|
|
85
177
|
}
|