@eventcatalog/core 5.5.2 → 5.6.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.
Files changed (55) 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 +5 -5
  5. package/dist/{chunk-CNN747RO.js → chunk-7TTNTHHN.js} +10 -10
  6. package/dist/{chunk-7ZEKF2LA.js → chunk-DEO4QEGO.js} +1 -1
  7. package/dist/{chunk-WG6ZK6L3.js → chunk-ETS4G6RR.js} +1 -1
  8. package/dist/{chunk-5HSYALWK.js → chunk-T3U27UXC.js} +1 -1
  9. package/dist/{chunk-MC4F3VVM.js → chunk-XBRKEQ66.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/docs/development/customization/05-resource-sidebar.md +1 -1
  13. package/dist/docs/development/guides/domains/09-reference.md +5 -1
  14. package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/04-add-generic-specifications.md +139 -0
  15. package/dist/docs/development/guides/resources/schemas/03-add-specifications-to-services/_category_.json +1 -1
  16. package/dist/docs/development/guides/resources/services/08-reference.md +6 -1
  17. package/dist/eventcatalog.cjs +1 -1
  18. package/dist/eventcatalog.js +16 -16
  19. package/dist/federation/federate.js +3 -3
  20. package/dist/generate.cjs +1 -1
  21. package/dist/generate.js +3 -3
  22. package/dist/utils/cli-logger.cjs +1 -1
  23. package/dist/utils/cli-logger.js +2 -2
  24. package/eventcatalog/public/icons/specification.svg +1 -0
  25. package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +38 -20
  26. package/eventcatalog/src/components/Grids/specification-utils.ts +7 -1
  27. package/eventcatalog/src/components/MDX/ResourceRef/ResourceRef.astro +1 -1
  28. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeBlock.tsx +58 -0
  29. package/eventcatalog/src/components/SchemaExplorer/SchemaCodeModal.tsx +2 -1
  30. package/eventcatalog/src/components/SchemaExplorer/SchemaContentViewer.tsx +38 -76
  31. package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +15 -11
  32. package/eventcatalog/src/components/SchemaExplorer/SchemaExplorer.tsx +171 -142
  33. package/eventcatalog/src/components/SchemaExplorer/SchemaListItem.tsx +5 -8
  34. package/eventcatalog/src/components/SchemaExplorer/types.ts +2 -0
  35. package/eventcatalog/src/components/SchemaExplorer/utils.ts +70 -1
  36. package/eventcatalog/src/content.config.ts +3 -1
  37. package/eventcatalog/src/features/ai/chat-api.ts +6 -1
  38. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +1 -1
  39. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +1 -1
  40. package/eventcatalog/src/features/tools/catalog-tools.ts +8 -3
  41. package/eventcatalog/src/pages/.well-known/api-catalog.ts +1 -11
  42. package/eventcatalog/src/pages/api-catalog/specifications/[collection]/[id]/[version]/[specification].ts +1 -11
  43. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/generic/[filename].astro +137 -0
  44. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/generic/_SpecificationSource.tsx +44 -0
  45. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/generic/_[filename].data.ts +75 -0
  46. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/index.astro +14 -1
  47. package/eventcatalog/src/stores/sidebar-store/builders/domain.ts +10 -1
  48. package/eventcatalog/src/stores/sidebar-store/builders/service.ts +8 -1
  49. package/eventcatalog/src/stores/sidebar-store/custom-sidebar.ts +3 -1
  50. package/eventcatalog/src/utils/collections/util.ts +38 -1
  51. package/eventcatalog/src/utils/icon.ts +8 -0
  52. package/eventcatalog/src/utils/node-graphs/utils/utils.ts +5 -2
  53. package/eventcatalog/src/utils/schema-explorer.ts +7 -2
  54. package/package.json +6 -6
  55. package/dist/{chunk-QJ7KKN2D.js → chunk-NCEZDQSW.js} +7 -7
@@ -5,7 +5,12 @@
5
5
  import { getCollection } from 'astro:content';
6
6
  import { z } from 'zod';
7
7
  import { getSchemaFormatFromURL } from '@utils/collections/schemas';
8
- import { getItemsFromCollectionByIdAndSemverOrLatest, processSpecifications, versionMatches } from '@utils/collections/util';
8
+ import {
9
+ getItemsFromCollectionByIdAndSemverOrLatest,
10
+ getSpecificationCodeLanguage,
11
+ processSpecifications,
12
+ versionMatches,
13
+ } from '@utils/collections/util';
9
14
  import { getUbiquitousLanguageWithSubdomains } from '@utils/collections/domains';
10
15
  import { getAbsoluteFilePathForAstroFile } from '@utils/files';
11
16
  import fs from 'node:fs';
@@ -464,7 +469,7 @@ export async function getSchemaForResource(params: { resourceId: string; resourc
464
469
  ...processSpecifications(resource.data.specifications).map((spec) => ({
465
470
  name: spec.name,
466
471
  path: spec.path,
467
- format: spec.type,
472
+ format: spec.type === 'generic' ? getSpecificationCodeLanguage(spec.path) : spec.type,
468
473
  })),
469
474
  ];
470
475
 
@@ -1332,7 +1337,7 @@ export const toolDescriptions = {
1332
1337
  getMessagesProducedOrConsumedByResource:
1333
1338
  'Use this tool to get the messages (events, commands, queries) a service, agent, domain or system sends and receives. Each message is resolved to its catalog id, version, name and summary; messages that are referenced but not documented in the catalog are marked notFound. For a domain or system, this includes what the services, agents, systems and subdomains inside it send and receive, and each message lists `by`: the resources that send or receive it (e.g. services/order-service). Omit the version to use the latest version of the resource.',
1334
1339
  getSchemaForResource:
1335
- 'Use this tool to get the schema or specifications (openapi or asyncapi or graphql) for a resource by its id and version',
1340
+ 'Use this tool to get the schema or specifications (openapi, asyncapi, graphql or generic specifications such as XML, JSON or YAML files) for a resource by its id and version',
1336
1341
  findResourcesByOwner:
1337
1342
  'Use this tool to find all resources (agents, services, events, commands, etc.) owned by a specific team or user',
1338
1343
  getProducersOfMessage:
@@ -2,7 +2,7 @@ import type { APIRoute } from 'astro';
2
2
  import yaml from 'js-yaml';
3
3
  import { getServices, getSpecificationsForService } from '@utils/collections/services';
4
4
  import { getDomains, getSpecificationsForDomain } from '@utils/collections/domains';
5
- import type { ProcessedSpecification } from '@utils/collections/util';
5
+ import { getSpecificationMediaType, type ProcessedSpecification } from '@utils/collections/util';
6
6
  import { buildUrl } from '@utils/url-builder';
7
7
  import { readResourceFile } from '@utils/resource-files';
8
8
  import { isEventCatalogMCPEnabled } from '@utils/feature';
@@ -26,16 +26,6 @@ type ApiCatalogResource = Awaited<ReturnType<typeof getServices>>[number] | Awai
26
26
 
27
27
  const absoluteUrl = (request: Request, pathOrUrl: string) => new URL(pathOrUrl, request.url).toString();
28
28
 
29
- const getSpecificationMediaType = (specification: ProcessedSpecification) => {
30
- const extension = specification.filename.split('.').pop()?.toLowerCase();
31
-
32
- if (specification.type === 'graphql') return 'application/graphql';
33
- if (extension === 'json') return 'application/json';
34
- if (extension === 'yaml' || extension === 'yml') return 'application/yaml';
35
-
36
- return 'text/plain';
37
- };
38
-
39
29
  const getSpecificationIdentifier = (specification: ProcessedSpecification) => {
40
30
  return `${specification.type}-${Buffer.from(specification.path).toString('base64url')}`;
41
31
  };
@@ -2,7 +2,7 @@ import type { APIRoute } from 'astro';
2
2
  import { getCollection } from 'astro:content';
3
3
  import { getSpecificationsForDomain } from '@utils/collections/domains';
4
4
  import { getSpecificationsForService } from '@utils/collections/services';
5
- import type { ProcessedSpecification } from '@utils/collections/util';
5
+ import { getSpecificationMediaType, type ProcessedSpecification } from '@utils/collections/util';
6
6
  import { readResourceFile } from '@utils/resource-files';
7
7
 
8
8
  type SupportedCollection = 'domains' | 'services';
@@ -11,16 +11,6 @@ const isSupportedCollection = (collection: string | undefined): collection is Su
11
11
  return collection === 'domains' || collection === 'services';
12
12
  };
13
13
 
14
- const getSpecificationMediaType = (specification: ProcessedSpecification) => {
15
- const extension = specification.filename.split('.').pop()?.toLowerCase();
16
-
17
- if (specification.type === 'graphql') return 'application/graphql';
18
- if (extension === 'json') return 'application/json';
19
- if (extension === 'yaml' || extension === 'yml') return 'application/yaml';
20
-
21
- return 'text/plain';
22
- };
23
-
24
14
  const getSpecificationIdentifier = (specification: ProcessedSpecification) => {
25
15
  return `${specification.type}-${Buffer.from(specification.path).toString('base64url')}`;
26
16
  };
@@ -0,0 +1,137 @@
1
+ ---
2
+ import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
3
+ import Footer from '@layouts/Footer.astro';
4
+ import { Page } from './_[filename].data';
5
+ import SpecificationSource from './_SpecificationSource';
6
+ import { buildUrl } from '@utils/url-builder';
7
+ import { getGenericSpecificationIcon, resolveIconUrl } from '@utils/icon';
8
+ import { readResourceFile, resourceFileExists } from '@utils/resource-files';
9
+ import { resolveHeaders } from '@utils/remote-spec';
10
+ import Admonition from '@components/MDX/Admonition';
11
+ import NewVersionBanner from '@components/NewVersionBanner.astro';
12
+ import Badge from '@components/Badge.astro';
13
+ import { getResourceTypeBadge } from '@utils/collections/resource-type-badge';
14
+ import { getResourceNoun, getSpecificationCodeLanguage } from '@utils/collections/util';
15
+
16
+ export const prerender = Page.prerender;
17
+ export const getStaticPaths = Page.getStaticPaths;
18
+
19
+ // Get data
20
+ const pageData = await Page.getData(Astro);
21
+ if (!pageData) return Astro.rewrite('/404');
22
+ const {
23
+ collection,
24
+ data,
25
+ filename,
26
+ filenameWithoutExtension,
27
+ path: relativeSpecPath,
28
+ specificationName,
29
+ icon,
30
+ headers = {},
31
+ } = pageData;
32
+
33
+ // Generic specifications are shown as source, without parsing them
34
+ const isRemote = /^https?:\/\//i.test(relativeSpecPath);
35
+ let fileExists = isRemote || resourceFileExists(pageData, relativeSpecPath);
36
+ let content = '';
37
+
38
+ if (isRemote) {
39
+ // A remote file that can't be fetched is shown as not found, like a missing local file, so it doesn't fail the build
40
+ const response = await fetch(relativeSpecPath, { headers: resolveHeaders(headers) }).catch(() => null);
41
+
42
+ if (response?.ok) {
43
+ content = await response.text();
44
+ } else {
45
+ fileExists = false;
46
+ console.warn(`Failed to fetch specification ${relativeSpecPath}: ${response ? response.status : 'network error'}`);
47
+ }
48
+ } else if (fileExists) {
49
+ content = readResourceFile(pageData, relativeSpecPath) ?? '';
50
+ }
51
+
52
+ const iconURL = resolveIconUrl(getGenericSpecificationIcon(icon));
53
+
54
+ // Create comprehensive page title
55
+ const pageTitle = `${collection} | ${data.name} | ${specificationName}`.replace(/^\w/, (c) => c.toUpperCase());
56
+
57
+ const badges = [
58
+ getResourceTypeBadge(collection),
59
+ {
60
+ textColor: 'gray',
61
+ content: specificationName,
62
+ iconURL,
63
+ id: 'generic-specification-badge',
64
+ },
65
+ ];
66
+
67
+ // Index only the latest version
68
+ const pagefindAttributes =
69
+ data.version === data.latestVersion
70
+ ? {
71
+ 'data-pagefind-body': '',
72
+ 'data-pagefind-meta': `title:${pageTitle}`,
73
+ }
74
+ : {};
75
+ ---
76
+
77
+ <VerticalSideBarLayout title={pageTitle} description={`${specificationName} for ${data.name}`}>
78
+ <main class="flex docs-layout min-h-full bg-[rgb(var(--ec-page-bg))]" {...pagefindAttributes}>
79
+ <div class="flex docs-layout w-full">
80
+ <div class="w-full min-w-0 lg:mr-2 xl:pr-12 py-8 bg-[rgb(var(--ec-page-bg))]">
81
+ <!-- Title and subtitle sized like the resource's overview page, kept compact so the source starts high up -->
82
+ <div class="border-b border-[rgb(var(--ec-page-border))] pb-4">
83
+ <h1 class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{data.name}</h1>
84
+ <p class="text-base pt-2 text-[rgb(var(--ec-page-text-muted))] font-light">{specificationName}</p>
85
+ <div class="flex flex-wrap gap-1.5 pt-3">
86
+ {badges.map((badge: any) => <Badge badge={badge} />)}
87
+ </div>
88
+ </div>
89
+
90
+ <div data-pagefind-ignore>
91
+ {
92
+ data.version !== data.latestVersion && (
93
+ <NewVersionBanner
94
+ subject={getResourceNoun(collection)}
95
+ name={data.name}
96
+ latestOf={specificationName}
97
+ latestVersion={data.latestVersion}
98
+ href={buildUrl(`/docs/${collection}/${data.id}/${data.latestVersion}/generic/${filenameWithoutExtension}`)}
99
+ />
100
+ )
101
+ }
102
+ </div>
103
+
104
+ {
105
+ !fileExists && (
106
+ <Admonition type="warning" title="Specification not found">
107
+ <p>The specification file could not be found at {relativeSpecPath}.</p>
108
+ </Admonition>
109
+ )
110
+ }
111
+
112
+ {
113
+ fileExists && (
114
+ <div class="mt-4 not-prose">
115
+ <SpecificationSource
116
+ client:load
117
+ name={`${data.name} - ${specificationName}`}
118
+ version={data.version}
119
+ code={content}
120
+ language={getSpecificationCodeLanguage(filename)}
121
+ />
122
+ </div>
123
+ )
124
+ }
125
+
126
+ <Footer />
127
+ </div>
128
+ </div>
129
+ </main>
130
+
131
+ <style is:global>
132
+ .docs-layout .prose {
133
+ max-width: none;
134
+ overflow: auto;
135
+ }
136
+ </style>
137
+ </VerticalSideBarLayout>
@@ -0,0 +1,44 @@
1
+ import { useState } from 'react';
2
+ import SchemaCodeBlock from '@components/SchemaExplorer/SchemaCodeBlock';
3
+ import SchemaCodeModal from '@components/SchemaExplorer/SchemaCodeModal';
4
+ import { copyToClipboard } from '@components/SchemaExplorer/utils';
5
+
6
+ interface Props {
7
+ /** The specification's name, shown in fullscreen */
8
+ name: string;
9
+ version: string;
10
+ code: string;
11
+ /** The syntax to highlight: json, yaml, xml or text */
12
+ language: string;
13
+ }
14
+
15
+ /** A generic specification's source, in the same code box as the Schema Explorer */
16
+ export default function SpecificationSource({ name, version, code, language }: Props) {
17
+ const [isCopied, setIsCopied] = useState(false);
18
+ const [isFullscreen, setIsFullscreen] = useState(false);
19
+
20
+ const onCopy = async () => {
21
+ if (!(await copyToClipboard(code))) return;
22
+ setIsCopied(true);
23
+ setTimeout(() => setIsCopied(false), 2000);
24
+ };
25
+
26
+ return (
27
+ <>
28
+ <SchemaCodeBlock
29
+ code={code}
30
+ extension={language}
31
+ onCopy={onCopy}
32
+ isCopied={isCopied}
33
+ onOpenFullscreen={() => setIsFullscreen(true)}
34
+ />
35
+ <SchemaCodeModal
36
+ isOpen={isFullscreen}
37
+ onOpenChange={setIsFullscreen}
38
+ message={{ data: { name, version }, schemaExtension: language, schemaContent: code }}
39
+ onCopy={onCopy}
40
+ isCopied={isCopied}
41
+ />
42
+ </>
43
+ );
44
+ }
@@ -0,0 +1,75 @@
1
+ // pages/docs/[type]/[id]/[version]/generic/[filename].astro
2
+ import { isSSR } from '@utils/feature';
3
+ import { HybridPage } from '@utils/page-loaders/hybrid-page';
4
+ import type { CollectionEntry } from 'astro:content';
5
+ import type { CollectionTypes, PageTypes } from '@types';
6
+ import type { ProcessedSpecification } from '@utils/collections/util';
7
+
8
+ const toPageProps = (type: PageTypes, spec: ProcessedSpecification, item: CollectionEntry<CollectionTypes>) => ({
9
+ type,
10
+ filenameWithoutExtension: spec.filenameWithoutExtension || spec.type,
11
+ filename: spec.filename || spec.type,
12
+ path: spec.path,
13
+ specificationName: spec.name,
14
+ icon: spec.icon,
15
+ headers: spec.headers,
16
+ ...item,
17
+ });
18
+
19
+ export class Page extends HybridPage {
20
+ static get prerender(): boolean {
21
+ return !isSSR();
22
+ }
23
+
24
+ static async getStaticPaths(): Promise<Array<{ params: any; props: any }>> {
25
+ if (isSSR()) {
26
+ return [];
27
+ }
28
+
29
+ const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
30
+ const { getSpecificationsForService } = await import('@utils/collections/services');
31
+
32
+ const itemTypes: PageTypes[] = ['events', 'commands', 'queries', 'services', 'domains'];
33
+ const allItems = await Promise.all(itemTypes.map((type) => pageDataLoader[type]()));
34
+
35
+ return allItems.flatMap((items, index) =>
36
+ items.flatMap((item) =>
37
+ getSpecificationsForService(item)
38
+ .filter((spec) => spec.type === 'generic')
39
+ .map((spec) => ({
40
+ params: {
41
+ type: itemTypes[index],
42
+ id: item.data.id,
43
+ version: item.data.version,
44
+ filename: spec.filenameWithoutExtension || spec.type,
45
+ },
46
+ props: toPageProps(itemTypes[index], spec, item),
47
+ }))
48
+ )
49
+ );
50
+ }
51
+
52
+ protected static async fetchData(params: any) {
53
+ const { type, id, version, filename } = params;
54
+
55
+ if (!type || !id || !version || !filename) {
56
+ return null;
57
+ }
58
+
59
+ const { pageDataLoader } = await import('@utils/page-loaders/page-data-loader');
60
+ const { getSpecificationsForService } = await import('@utils/collections/services');
61
+
62
+ const items = await pageDataLoader[type as PageTypes]();
63
+ const item = items.find((i) => i.data.id === id && i.data.version === version);
64
+
65
+ if (!item) {
66
+ return null;
67
+ }
68
+
69
+ const spec = getSpecificationsForService(item).find(
70
+ (s) => s.type === 'generic' && (s.filenameWithoutExtension || s.type) === filename
71
+ );
72
+
73
+ return spec ? toPageProps(type, spec, item) : null;
74
+ }
75
+ }
@@ -65,7 +65,7 @@ import {
65
65
  import { getSchemasFromResource } from '@utils/collections/schemas';
66
66
  import { getIcon } from '@utils/badges';
67
67
  import { buildUrl, buildEditUrlForResource } from '@utils/url-builder';
68
- import { isIconPath, resolveIconUrl } from '@utils/icon';
68
+ import { getGenericSpecificationIcon, isIconPath, resolveIconUrl } from '@utils/icon';
69
69
  import {
70
70
  isEventCatalogChatVisible,
71
71
  isMarkdownDownloadEnabled,
@@ -249,6 +249,7 @@ const getSpecificationBadges = () => {
249
249
  const asyncapiSpecs = specifications.filter((spec) => spec.type === 'asyncapi');
250
250
  const openapiSpecs = specifications.filter((spec) => spec.type === 'openapi');
251
251
  const graphQLSpecs = specifications.filter((spec) => spec.type === 'graphql');
252
+ const genericSpecs = specifications.filter((spec) => spec.type === 'generic');
252
253
 
253
254
  if (openapiSpecs.length > 0) {
254
255
  for (const spec of openapiSpecs) {
@@ -290,6 +291,18 @@ const getSpecificationBadges = () => {
290
291
  }
291
292
  }
292
293
 
294
+ for (const spec of genericSpecs) {
295
+ badges.push({
296
+ content: spec.name,
297
+ iconURL: resolveIconUrl(getGenericSpecificationIcon(spec.icon)),
298
+ class: 'text-[rgb(var(--ec-page-text))] hover:underline',
299
+ id: 'generic-specification-badge',
300
+ url: buildUrl(
301
+ `/docs/${props.collection}/${props.data.id}/${props.data.version}/generic/${spec.filenameWithoutExtension}`
302
+ ),
303
+ });
304
+ }
305
+
293
306
  return badges;
294
307
  };
295
308
 
@@ -16,7 +16,7 @@ import {
16
16
  import { isVisualiserEnabled, isChangelogEnabled } from '@utils/feature';
17
17
  import { pluralizeMessageType } from '@utils/collections/messages';
18
18
  import { getSpecificationsForDomain, hasUbiquitousLanguageTermsWithSubdomainsInCollection } from '@utils/collections/domains';
19
- import { customIconFieldsForResource } from '@utils/icon';
19
+ import { customIconFieldsForResource, getGenericSpecificationIcon } from '@utils/icon';
20
20
  import { resolveSidebarPages, toCustomSidebarContext, type SidebarSpec } from '../custom-sidebar';
21
21
 
22
22
  // Sort resolved collection entries A-Z by their display name (falling back to id).
@@ -143,6 +143,7 @@ export const buildDomainSections = (
143
143
  const openAPISpecifications = specifications.filter((specification) => specification.type === 'openapi');
144
144
  const asyncAPISpecifications = specifications.filter((specification) => specification.type === 'asyncapi');
145
145
  const graphQLSpecifications = specifications.filter((specification) => specification.type === 'graphql');
146
+ const genericSpecifications = specifications.filter((specification) => specification.type === 'generic');
146
147
  const renderSpecifications = hasSpecifications && shouldRenderSideBarSection(domain, 'specifications');
147
148
 
148
149
  // Resource subsections. These are built once and shared between the "Resources"
@@ -284,6 +285,14 @@ export const buildDomainSections = (
284
285
  `/docs/domains/${domain.data.id}/${domain.data.version}/graphql/${specification.filenameWithoutExtension}`
285
286
  ),
286
287
  })),
288
+ ...genericSpecifications.map((specification) => ({
289
+ type: 'item',
290
+ title: specification.name,
291
+ leftIcon: getGenericSpecificationIcon(specification.icon),
292
+ href: buildUrl(
293
+ `/docs/domains/${domain.data.id}/${domain.data.version}/generic/${specification.filenameWithoutExtension}`
294
+ ),
295
+ })),
287
296
  ] as ChildRef[],
288
297
  }
289
298
  : null,
@@ -17,7 +17,7 @@ import {
17
17
  } from './shared';
18
18
  import { isVisualiserEnabled, isChangelogEnabled } from '@utils/feature';
19
19
  import { pluralizeMessageType } from '@utils/collections/messages';
20
- import { iconFieldsForResource } from '@utils/icon';
20
+ import { getGenericSpecificationIcon, iconFieldsForResource } from '@utils/icon';
21
21
  import { resolveSidebarPages, toCustomSidebarContext, type SidebarSpec } from '../custom-sidebar';
22
22
 
23
23
  const uniqueRefs = (refs: string[]) => [...new Set(refs)];
@@ -86,6 +86,7 @@ export const buildServiceSections = (
86
86
  const openAPISpecifications = specifications.filter((specification) => specification.type === 'openapi');
87
87
  const asyncAPISpecifications = specifications.filter((specification) => specification.type === 'asyncapi');
88
88
  const graphQLSpecifications = specifications.filter((specification) => specification.type === 'graphql');
89
+ const genericSpecifications = specifications.filter((specification) => specification.type === 'generic');
89
90
 
90
91
  const dataStoresInService = uniqueBy([...(service.data.writesTo || []), ...(service.data.readsFrom || [])], 'id');
91
92
 
@@ -189,6 +190,12 @@ export const buildServiceSections = (
189
190
  leftIcon: '/icons/graphql-black.svg',
190
191
  href: buildUrl(`${docsBasePath}/graphql/${specification.filenameWithoutExtension}`),
191
192
  })),
193
+ ...genericSpecifications.map((specification) => ({
194
+ type: 'item',
195
+ title: `${specification.name}`,
196
+ leftIcon: getGenericSpecificationIcon(specification.icon),
197
+ href: buildUrl(`${docsBasePath}/generic/${specification.filenameWithoutExtension}`),
198
+ })),
192
199
  ] as ChildRef[],
193
200
  }
194
201
  : null,
@@ -1,6 +1,7 @@
1
1
  import path from 'node:path';
2
2
  import type { CollectionEntry } from 'astro:content';
3
3
  import { buildUrl } from '@utils/url-builder';
4
+ import { isIconPath } from '@utils/icon';
4
5
  import { processSpecifications } from '@utils/collections/util';
5
6
  import type { ResourceCollection, ResourceDocEntry } from '@utils/collections/resource-docs';
6
7
  import type { NavNode, ChildRef } from './builders/shared';
@@ -119,6 +120,7 @@ const SPEC_ROUTES: Record<string, { segment: string; icon: string }> = {
119
120
  openapi: { segment: 'spec', icon: '/icons/openapi-black.svg' },
120
121
  asyncapi: { segment: 'asyncapi', icon: '/icons/asyncapi-black.svg' },
121
122
  graphql: { segment: 'graphql', icon: '/icons/graphql-black.svg' },
123
+ generic: { segment: 'generic', icon: '/icons/specification.svg' },
122
124
  };
123
125
 
124
126
  const MESSAGE_COLLECTIONS: Record<string, keyof CustomSidebarContext> = {
@@ -335,7 +337,7 @@ const resolveSpecPage = (
335
337
  return {
336
338
  type: 'item',
337
339
  title: specification.name,
338
- leftIcon: route.icon,
340
+ leftIcon: specification.type === 'generic' && isIconPath(specification.icon) ? specification.icon : route.icon,
339
341
  href: buildUrl(
340
342
  `/docs/${owner.collection}/${owner.id}/${owner.version}/${route.segment}/${specification.filenameWithoutExtension}`
341
343
  ),
@@ -18,12 +18,13 @@ export const getFolderNameFromFilePath = (filePath: string): string => {
18
18
 
19
19
  // --- SPECIFICATION HELPERS ---
20
20
 
21
- export type SpecificationType = 'asyncapi' | 'openapi' | 'graphql';
21
+ export type SpecificationType = 'asyncapi' | 'openapi' | 'graphql' | 'generic';
22
22
 
23
23
  export interface SpecificationInput {
24
24
  type: SpecificationType;
25
25
  path: string;
26
26
  name?: string;
27
+ icon?: string;
27
28
  headers?: Record<string, string>;
28
29
  }
29
30
 
@@ -33,6 +34,7 @@ export interface ProcessedSpecification {
33
34
  name: string;
34
35
  filename: string;
35
36
  filenameWithoutExtension: string;
37
+ icon?: string;
36
38
  headers?: Record<string, string>;
37
39
  }
38
40
 
@@ -49,6 +51,41 @@ export const getDefaultSpecificationName = (type: string): string => {
49
51
  }
50
52
  };
51
53
 
54
+ export type SpecificationCodeLanguage = 'json' | 'yaml' | 'xml' | 'text';
55
+
56
+ const CODE_LANGUAGE_BY_EXTENSION: Record<string, SpecificationCodeLanguage> = {
57
+ json: 'json',
58
+ yaml: 'yaml',
59
+ yml: 'yaml',
60
+ xml: 'xml',
61
+ xsd: 'xml',
62
+ wsdl: 'xml',
63
+ xsl: 'xml',
64
+ xslt: 'xml',
65
+ };
66
+
67
+ const MEDIA_TYPE_BY_CODE_LANGUAGE: Record<SpecificationCodeLanguage, string> = {
68
+ json: 'application/json',
69
+ yaml: 'application/yaml',
70
+ xml: 'application/xml',
71
+ text: 'text/plain',
72
+ };
73
+
74
+ /** The syntax of a specification file, from its extension (query strings on remote URLs are ignored) */
75
+ export const getSpecificationCodeLanguage = (filename: string): SpecificationCodeLanguage => {
76
+ const extension =
77
+ getBasename(filename.split(/[?#]/, 1)[0])
78
+ .match(/\.([^.]+)$/)?.[1]
79
+ .toLowerCase() ?? '';
80
+ return CODE_LANGUAGE_BY_EXTENSION[extension] ?? 'text';
81
+ };
82
+
83
+ /** The media type a specification file is served with */
84
+ export const getSpecificationMediaType = (specification: Pick<ProcessedSpecification, 'type' | 'filename'>): string =>
85
+ specification.type === 'graphql'
86
+ ? 'application/graphql'
87
+ : MEDIA_TYPE_BY_CODE_LANGUAGE[getSpecificationCodeLanguage(specification.filename)];
88
+
52
89
  interface LegacySpecificationFormat {
53
90
  asyncapiPath?: string;
54
91
  openapiPath?: string;
@@ -28,3 +28,11 @@ export function customIconFieldsForResource(data: { styles?: { icon?: string } }
28
28
  const styleIcon = data?.styles?.icon;
29
29
  return isIconPath(styleIcon) ? { leftIcon: styleIcon } : {};
30
30
  }
31
+
32
+ /**
33
+ * The icon for a generic specification: its custom `icon` when one is set, otherwise the default
34
+ * specification icon. Unlike the black logos of the other specification types, it reads in both themes.
35
+ */
36
+ export function getGenericSpecificationIcon(icon: string | undefined): string {
37
+ return isIconPath(icon) ? icon : '/icons/specification.svg';
38
+ }
@@ -173,7 +173,7 @@ const getSpecMenuItems = (
173
173
  ): { label: string; href: string; separator?: boolean }[] => {
174
174
  const items: { label: string; href: string; separator?: boolean }[] = [];
175
175
 
176
- const addSpec = (type: string, filePath: string, isFirst: boolean) => {
176
+ const addSpec = (type: string, filePath: string, isFirst: boolean, name?: string) => {
177
177
  const filename = filePath.split('/').pop() || filePath;
178
178
  const filenameNoExt = filename.replace(/\.[^/.]+$/, '');
179
179
  const typeLower = type.toLowerCase();
@@ -189,6 +189,9 @@ const getSpecMenuItems = (
189
189
  } else if (typeLower === 'graphql') {
190
190
  label = 'View GraphQL spec';
191
191
  urlSegment = 'graphql';
192
+ } else if (typeLower === 'generic') {
193
+ label = name ? `View ${name}` : label;
194
+ urlSegment = 'generic';
192
195
  }
193
196
 
194
197
  items.push({
@@ -200,7 +203,7 @@ const getSpecMenuItems = (
200
203
 
201
204
  if (Array.isArray(specs)) {
202
205
  specs.forEach((spec: any, i: number) => {
203
- if (spec?.type && spec?.path) addSpec(spec.type, spec.path, i === 0);
206
+ if (spec?.type && spec?.path) addSpec(spec.type, spec.path, i === 0, spec.name);
204
207
  });
205
208
  } else if (specs && typeof specs === 'object') {
206
209
  const legacy = specs as Record<string, string>;
@@ -11,6 +11,7 @@ import { getFlowsForMessages } from '@utils/collections/flows';
11
11
  import { getOwner } from '@utils/collections/owners';
12
12
  import { buildUrl } from '@utils/url-builder';
13
13
  import { getSchemaExtensionForFormat } from '@utils/collections/schemas';
14
+ import { getSpecificationCodeLanguage } from '@utils/collections/util';
14
15
  import { resourceFileExists, readResourceFile } from '@utils/resource-files';
15
16
  import { getExamplesForResource } from '@utils/collections/examples';
16
17
  import { attachFlowGraphs, getMessageUsageGraph } from '@utils/schema-usage-graph';
@@ -172,7 +173,8 @@ async function buildRegistry() {
172
173
  return null;
173
174
  }
174
175
 
175
- const schemaExtension = spec.type;
176
+ // Generic specifications are shown as source, so their extension is the syntax to highlight
177
+ const schemaExtension = spec.type === 'generic' ? getSpecificationCodeLanguage(spec.path) : spec.type;
176
178
  const enrichedOwners = await enrichOwners(service.data.owners || []);
177
179
 
178
180
  return {
@@ -189,6 +191,7 @@ async function buildRegistry() {
189
191
  schemaExtension,
190
192
  specType: spec.type,
191
193
  specName: spec.name,
194
+ specIcon: spec.icon,
192
195
  specFilenameWithoutExtension: spec.filenameWithoutExtension,
193
196
  };
194
197
  })
@@ -222,7 +225,8 @@ async function buildRegistry() {
222
225
  return null;
223
226
  }
224
227
 
225
- const schemaExtension = spec.type;
228
+ // Generic specifications are shown as source, so their extension is the syntax to highlight
229
+ const schemaExtension = spec.type === 'generic' ? getSpecificationCodeLanguage(spec.path) : spec.type;
226
230
  const enrichedOwners = await enrichOwners(domain.data.owners || []);
227
231
 
228
232
  return {
@@ -239,6 +243,7 @@ async function buildRegistry() {
239
243
  schemaExtension,
240
244
  specType: spec.type,
241
245
  specName: spec.name,
246
+ specIcon: spec.icon,
242
247
  specFilenameWithoutExtension: spec.filenameWithoutExtension,
243
248
  };
244
249
  })
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "BUSL-1.1",
9
9
  "type": "module",
10
- "version": "5.5.2",
10
+ "version": "5.6.0",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -141,11 +141,11 @@
141
141
  "y-protocols": "^1.0.7",
142
142
  "yjs": "^13.6.33",
143
143
  "zod": "^4.6.6",
144
- "@eventcatalog/exporters": "0.1.2",
145
- "@eventcatalog/fitness": "0.1.0",
146
- "@eventcatalog/linter": "1.2.4",
147
- "@eventcatalog/sdk": "2.31.2",
148
- "@eventcatalog/visualiser": "^5.1.1"
144
+ "@eventcatalog/fitness": "0.1.1",
145
+ "@eventcatalog/linter": "1.3.0",
146
+ "@eventcatalog/visualiser": "^5.1.2",
147
+ "@eventcatalog/sdk": "2.32.0",
148
+ "@eventcatalog/exporters": "0.1.3"
149
149
  },
150
150
  "devDependencies": {
151
151
  "@astrojs/check": "^0.9.10",
@@ -1,3 +1,10 @@
1
+ import {
2
+ createFederationContentCache
3
+ } from "./chunk-R7Z5ALYI.js";
4
+ import {
5
+ getFederationDiagnostics,
6
+ resolveFederationRules
7
+ } from "./chunk-JJPB6EOZ.js";
1
8
  import {
2
9
  withFederationOutputTransaction
3
10
  } from "./chunk-WRNH5C3U.js";
@@ -7,13 +14,6 @@ import {
7
14
  import {
8
15
  createFederationSourceProvider
9
16
  } from "./chunk-A2RZR3U4.js";
10
- import {
11
- createFederationContentCache
12
- } from "./chunk-R7Z5ALYI.js";
13
- import {
14
- getFederationDiagnostics,
15
- resolveFederationRules
16
- } from "./chunk-JJPB6EOZ.js";
17
17
  import {
18
18
  getEventCatalogConfigFile
19
19
  } from "./chunk-4G7FSRPN.js";