@eventcatalog/core 5.7.5 → 5.8.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 (39) hide show
  1. package/dist/analytics/analytics.cjs +1 -1
  2. package/dist/analytics/analytics.js +2 -2
  3. package/dist/analytics/log-build.cjs +1 -1
  4. package/dist/analytics/log-build.js +3 -3
  5. package/dist/{chunk-NNR5RP47.js → chunk-DT63JMNP.js} +1 -1
  6. package/dist/{chunk-SAB3WVWU.js → chunk-E636DOFG.js} +1 -1
  7. package/dist/{chunk-3I4MO4G6.js → chunk-WLXXZTLD.js} +1 -1
  8. package/dist/{chunk-SR6NZ4QL.js → chunk-YU6FEGOK.js} +1 -1
  9. package/dist/{chunk-Q6ZCTZM3.js → chunk-Z6UUCTIB.js} +1 -1
  10. package/dist/constants.cjs +1 -1
  11. package/dist/constants.js +1 -1
  12. package/dist/docs/development/components/components/15-nodegraph.md +2 -2
  13. package/dist/docs/development/guides/resources/agents/03-adding-tools.md +6 -0
  14. package/dist/eventcatalog.cjs +1 -1
  15. package/dist/eventcatalog.js +5 -5
  16. package/dist/generate.cjs +1 -1
  17. package/dist/generate.js +3 -3
  18. package/dist/utils/cli-logger.cjs +1 -1
  19. package/dist/utils/cli-logger.js +2 -2
  20. package/eventcatalog/src/components/SpecificationDownloadButton.astro +24 -0
  21. package/eventcatalog/src/features/mcp/apps/generated/canvas.html +70 -70
  22. package/eventcatalog/src/features/mcp/apps/generated/viewer.html +69 -69
  23. package/eventcatalog/src/pages/api/schemas/services/[id]/[version]/[specification]/index.ts +25 -27
  24. package/eventcatalog/src/pages/discover/[type]/index.astro +9 -2
  25. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename]/[file].ts +7 -0
  26. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +19 -7
  27. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/generic/[filename]/[file].ts +7 -0
  28. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/generic/[filename].astro +21 -1
  29. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename]/[file].ts +7 -0
  30. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/graphql/[filename].astro +19 -1
  31. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename]/[file].ts +7 -0
  32. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/[filename].astro +4 -0
  33. package/eventcatalog/src/pages/docs/services/[id]/asyncapi/[filename]/[file].ts +7 -0
  34. package/eventcatalog/src/pages/docs/services/[id]/graphql/[filename]/[file].ts +7 -0
  35. package/eventcatalog/src/pages/docs/services/[id]/spec/[filename]/[file].ts +7 -0
  36. package/eventcatalog/src/pages/docs/services/_latest-raw-specification-route.ts +37 -0
  37. package/eventcatalog/src/stores/sidebar-store/builders/data-product.ts +7 -3
  38. package/eventcatalog/src/utils/raw-specification.ts +126 -0
  39. package/package.json +4 -4
@@ -1,34 +1,32 @@
1
1
  import type { APIRoute } from 'astro';
2
- import { getCollection } from 'astro:content';
3
- import path from 'node:path';
4
- import fs from 'node:fs';
2
+ import { getCollection, type CollectionEntry } from 'astro:content';
5
3
  import { getSpecificationsForService } from '@utils/collections/services';
4
+ import { getSpecificationMediaType } from '@utils/collections/util';
6
5
  import { resourceFileExists, readResourceFile } from '@utils/resource-files';
7
6
 
7
+ // The Schema API: kept for the API consumers that already use it. It finds a specification by its type, so it
8
+ // serves the first of each type. The file of every specification is also served next to its page:
9
+ // /docs/{type}/{id}/{version}/{asyncapi|spec|graphql|generic}/{filename}/{file}
10
+ const getFirstSpecificationOfEachType = (service: CollectionEntry<'services'>) =>
11
+ getSpecificationsForService(service).filter(
12
+ (specification, index, specifications) => specifications.findIndex((spec) => spec.type === specification.type) === index
13
+ );
14
+
8
15
  export async function getStaticPaths() {
9
16
  const services = await getCollection('services');
10
- const servicesWithSpecifications = services.filter((service) => getSpecificationsForService(service).length > 0);
11
- return servicesWithSpecifications.reduce<
12
- { params: { collection: string; id: string; version: string; specification: string }; props: { schema: string } }[]
13
- >(
14
- (acc, service) => {
15
- const specifications = getSpecificationsForService(service);
16
- return [
17
- ...acc,
18
- ...specifications.map((specification) => ({
19
- params: {
20
- collection: service.collection,
21
- id: service.data.id,
22
- version: service.data.version,
23
- specification: specification.type,
24
- },
25
- props: {
26
- schema: fs.readFileSync(path.join(path.dirname(service.filePath ?? ''), specification.path ?? ''), 'utf8'),
27
- },
28
- })),
29
- ];
30
- },
31
- [] as { params: { collection: string; id: string; version: string; specification: string }; props: { schema: string } }[]
17
+ return services.flatMap((service) =>
18
+ getFirstSpecificationOfEachType(service).map((specification) => ({
19
+ params: {
20
+ collection: service.collection,
21
+ id: service.data.id,
22
+ version: service.data.version,
23
+ specification: specification.type,
24
+ },
25
+ props: {
26
+ schema: readResourceFile(service, specification.path),
27
+ contentType: getSpecificationMediaType(specification),
28
+ },
29
+ }))
32
30
  );
33
31
  }
34
32
 
@@ -36,7 +34,7 @@ export const GET: APIRoute = async ({ props, params }) => {
36
34
  // In static mode, props are pre-computed by getStaticPaths
37
35
  if (props.schema) {
38
36
  return new Response(props.schema, {
39
- headers: { 'Content-Type': 'text/plain' },
37
+ headers: { 'Content-Type': props.contentType },
40
38
  });
41
39
  }
42
40
 
@@ -80,6 +78,6 @@ export const GET: APIRoute = async ({ props, params }) => {
80
78
  }
81
79
 
82
80
  return new Response(schema, {
83
- headers: { 'Content-Type': 'text/plain' },
81
+ headers: { 'Content-Type': getSpecificationMediaType(spec) },
84
82
  });
85
83
  };
@@ -282,6 +282,13 @@ function resolvePointer(pointer: { id: string; version?: string }) {
282
282
  };
283
283
  }
284
284
 
285
+ // Resolve a list of pointers, listing each resource once (a data product can point at the
286
+ // same resource more than once, e.g. one output per data contract)
287
+ function resolvePointers(pointers: { id: string; version?: string }[] = []) {
288
+ const resolved = pointers.map(resolvePointer).filter((item) => item !== null);
289
+ return [...new Map(resolved.map((item) => [`${item.collection}:${item.data.id}:${item.data.version}`, item])).values()];
290
+ }
291
+
285
292
  // Build a Set of subdomain IDs (domains that are nested within other domains)
286
293
  const allSubdomainIds = new Set(
287
294
  domains.flatMap((d: any) => (d.data?.domains || []).map((sd: any) => sd.data?.id || sd.id)).filter(Boolean)
@@ -368,8 +375,8 @@ const tableData = enrichedData.map((d: any) => ({
368
375
  flows: d.data?.flows?.map(mapToItem) ?? [],
369
376
  servicesThatWriteToContainer: d.data?.servicesThatWriteToContainer?.map(mapToItem) ?? [],
370
377
  servicesThatReadFromContainer: d.data?.servicesThatReadFromContainer?.map(mapToItem) ?? [],
371
- inputs: d.data?.inputs?.map(resolvePointer).filter(Boolean) ?? [],
372
- outputs: d.data?.outputs?.map(resolvePointer).filter(Boolean) ?? [],
378
+ inputs: resolvePointers(d.data?.inputs),
379
+ outputs: resolvePointers(d.data?.outputs),
373
380
  },
374
381
  }));
375
382
 
@@ -0,0 +1,7 @@
1
+ import { isSSR } from '@utils/feature';
2
+ import { createRawSpecificationRoute } from '@utils/raw-specification';
3
+ import { Page } from '../_[filename].data';
4
+
5
+ // The specification's file, as written, next to its page
6
+ export const prerender = !isSSR();
7
+ export const { getStaticPaths, GET } = createRawSpecificationRoute('asyncapi', Page);
@@ -12,6 +12,8 @@ import js from '@asyncapi/react-component/browser/standalone/without-parser.js?u
12
12
  import { AsyncApiComponentWP, type ConfigInterface } from '@asyncapi/react-component';
13
13
  import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
14
14
  import CopyAsMarkdown from '@components/CopyAsMarkdown';
15
+ import SpecificationDownloadButton from '@components/SpecificationDownloadButton.astro';
16
+ import { getRawSpecificationUrl } from '@utils/raw-specification';
15
17
  import { isEventCatalogChatVisible } from '@utils/feature';
16
18
  import Config from '@utils/eventcatalog-config/catalog';
17
19
  import { Page } from './_[filename].data';
@@ -23,7 +25,7 @@ export const getStaticPaths = Page.getStaticPaths;
23
25
  // Get data
24
26
  const pageData = await Page.getData(Astro);
25
27
  if (!pageData) return Astro.rewrite('/404');
26
- const { collection, data, filePath, filename, path: relativeSpecPath, headers = {} } = pageData;
28
+ const { collection, data, filePath, filename, filenameWithoutExtension, path: relativeSpecPath, headers = {} } = pageData;
27
29
 
28
30
  const fileName = filename || 'asyncapi.yaml';
29
31
  const pathToSpec = getAbsoluteFilePathForAstroFile(filePath, fileName);
@@ -149,6 +151,15 @@ const renderedComponent = renderToString(component);
149
151
  // Capitalize the first letter of a string
150
152
  const pageTitle = `${collection} | ${data.name} | AsyncApi Spec`.replace(/^\w/, (c) => c.toUpperCase());
151
153
 
154
+ const downloadUrl = getRawSpecificationUrl({
155
+ collection,
156
+ id: data.id,
157
+ version: data.version,
158
+ type: 'asyncapi',
159
+ filename,
160
+ filenameWithoutExtension,
161
+ });
162
+
152
163
  // Chat configuration
153
164
  const chatEnabled = isEventCatalogChatVisible();
154
165
  const chatQuery = `Tell me about the AsyncAPI specification for "${data.name}" (version ${data.version})`;
@@ -175,9 +186,10 @@ const pagefindAttributes =
175
186
  // https://github.com/CloudCannon/pagefind/issues/437
176
187
  }
177
188
  <h2 class="hidden">{pageTitle}</h2>
178
- {
179
- chatEnabled && (
180
- <div class="absolute top-3 right-4 z-10">
189
+ <div class="absolute top-3 right-4 z-10 flex items-center gap-2" data-pagefind-ignore>
190
+ {(fileExists || isRemote) && <SpecificationDownloadButton href={downloadUrl} filename={filename} />}
191
+ {
192
+ chatEnabled && (
181
193
  <CopyAsMarkdown
182
194
  client:only="react"
183
195
  schemas={[]}
@@ -189,9 +201,9 @@ const pagefindAttributes =
189
201
  preferChatAsDefault={true}
190
202
  chatButtonText="Ask about this specification"
191
203
  />
192
- </div>
193
- )
194
- }
204
+ )
205
+ }
206
+ </div>
195
207
  {
196
208
  failedToParse ? (
197
209
  <div class="not-prose mx-auto my-12 max-w-3xl rounded-lg border border-red-300 bg-[rgb(var(--ec-card-bg))] p-6">
@@ -0,0 +1,7 @@
1
+ import { isSSR } from '@utils/feature';
2
+ import { createRawSpecificationRoute } from '@utils/raw-specification';
3
+ import { Page } from '../_[filename].data';
4
+
5
+ // The specification's file, as written, next to its page
6
+ export const prerender = !isSSR();
7
+ export const { getStaticPaths, GET } = createRawSpecificationRoute('generic', Page);
@@ -10,6 +10,8 @@ import { resolveHeaders } from '@utils/remote-spec';
10
10
  import Admonition from '@components/MDX/Admonition';
11
11
  import NewVersionBanner from '@components/NewVersionBanner.astro';
12
12
  import Badge from '@components/Badge.astro';
13
+ import SpecificationDownloadButton from '@components/SpecificationDownloadButton.astro';
14
+ import { getRawSpecificationUrl } from '@utils/raw-specification';
13
15
  import { getResourceTypeBadge } from '@utils/collections/resource-type-badge';
14
16
  import { getResourceNoun, getSpecificationCodeLanguage } from '@utils/collections/util';
15
17
 
@@ -49,6 +51,15 @@ if (isRemote) {
49
51
  content = readResourceFile(pageData, relativeSpecPath) ?? '';
50
52
  }
51
53
 
54
+ const downloadUrl = getRawSpecificationUrl({
55
+ collection,
56
+ id: data.id,
57
+ version: data.version,
58
+ type: 'generic',
59
+ filename,
60
+ filenameWithoutExtension,
61
+ });
62
+
52
63
  const iconURL = resolveIconUrl(getGenericSpecificationIcon(icon));
53
64
 
54
65
  // Create comprehensive page title
@@ -80,7 +91,16 @@ const pagefindAttributes =
80
91
  <div class="w-full min-w-0 lg:mr-2 xl:pr-12 py-8 bg-[rgb(var(--ec-page-bg))]">
81
92
  <!-- Title and subtitle sized like the resource's overview page, kept compact so the source starts high up -->
82
93
  <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>
94
+ <div class="flex items-start justify-between gap-4">
95
+ <h1 class="text-2xl md:text-4xl font-bold text-[rgb(var(--ec-page-text))]">{data.name}</h1>
96
+ {
97
+ fileExists && (
98
+ <div data-pagefind-ignore>
99
+ <SpecificationDownloadButton href={downloadUrl} filename={filename} />
100
+ </div>
101
+ )
102
+ }
103
+ </div>
84
104
  <p class="text-base pt-2 text-[rgb(var(--ec-page-text-muted))] font-light">{specificationName}</p>
85
105
  <div class="flex flex-wrap gap-1.5 pt-3">
86
106
  {badges.map((badge: any) => <Badge badge={badge} />)}
@@ -0,0 +1,7 @@
1
+ import { isSSR } from '@utils/feature';
2
+ import { createRawSpecificationRoute } from '@utils/raw-specification';
3
+ import { Page } from '../_[filename].data';
4
+
5
+ // The specification's file, as written, next to its page
6
+ export const prerender = !isSSR();
7
+ export const { getStaticPaths, GET } = createRawSpecificationRoute('graphql', Page);
@@ -10,6 +10,8 @@ import { buildUrl, buildEditUrlForResource } from '@utils/url-builder';
10
10
  import Admonition from '@components/MDX/Admonition';
11
11
  import NewVersionBanner from '@components/NewVersionBanner.astro';
12
12
  import Badge from '@components/Badge.astro';
13
+ import SpecificationDownloadButton from '@components/SpecificationDownloadButton.astro';
14
+ import { getRawSpecificationUrl } from '@utils/raw-specification';
13
15
  import { getResourceTypeBadge } from '@utils/collections/resource-type-badge';
14
16
  import { getResourceNoun } from '@utils/collections/util';
15
17
 
@@ -19,7 +21,7 @@ export const getStaticPaths = Page.getStaticPaths;
19
21
  // Get data
20
22
  const pageData = await Page.getData(Astro);
21
23
  if (!pageData) return Astro.rewrite('/404');
22
- const { collection, data, filePath, filename, path: relativeSpecPath } = pageData;
24
+ const { collection, data, filePath, filename, filenameWithoutExtension, path: relativeSpecPath } = pageData;
23
25
 
24
26
  const fileName = filename || 'schema.graphql';
25
27
  const pathToSpec = getAbsoluteFilePathForAstroFile(filePath, fileName);
@@ -36,6 +38,15 @@ if (isRemote) {
36
38
  content = await fetch(relativeSpecPath).then((res) => res.text());
37
39
  }
38
40
 
41
+ const downloadUrl = getRawSpecificationUrl({
42
+ collection,
43
+ id: data.id,
44
+ version: data.version,
45
+ type: 'graphql',
46
+ filename,
47
+ filenameWithoutExtension,
48
+ });
49
+
39
50
  // Create comprehensive page title
40
51
  const pageTitle = `${collection} | ${data.name} | GraphQL Schema`.replace(/^\w/, (c) => c.toUpperCase());
41
52
 
@@ -79,6 +90,13 @@ const pagefindAttributes =
79
90
  </h1>
80
91
  <h2 class="text-lg text-[rgb(var(--ec-page-text-muted))] font-medium mb-1">GraphQL Schema</h2>
81
92
  </div>
93
+ {
94
+ (fileExists || isRemote) && (
95
+ <div data-pagefind-ignore>
96
+ <SpecificationDownloadButton href={downloadUrl} filename={filename} />
97
+ </div>
98
+ )
99
+ }
82
100
  </div>
83
101
 
84
102
  {
@@ -0,0 +1,7 @@
1
+ import { isSSR } from '@utils/feature';
2
+ import { createRawSpecificationRoute } from '@utils/raw-specification';
3
+ import { Page } from '../_[filename].data';
4
+
5
+ // The specification's file, as written, next to its page
6
+ export const prerender = !isSSR();
7
+ export const { getStaticPaths, GET } = createRawSpecificationRoute('openapi', Page);
@@ -76,6 +76,10 @@ if (isRemote) {
76
76
  // https://github.com/CloudCannon/pagefind/issues/437
77
77
  }
78
78
  <h2 class="hidden">{pageTitle}</h2>
79
+ {
80
+ // No download button here, unlike the other specification pages: Scalar shows its own
81
+ // "Download OpenAPI Document" link. The file is also served at ./{filename}/{file}.
82
+ }
79
83
  {chatEnabled && (
80
84
  <div class="absolute top-3 right-14 z-10">
81
85
  <CopyAsMarkdown
@@ -0,0 +1,7 @@
1
+ import { isSSR } from '@utils/feature';
2
+ import { Page } from '../../../../[type]/[id]/[version]/asyncapi/_[filename].data';
3
+ import { createLatestServiceRawSpecificationRoute } from '../../../_latest-raw-specification-route';
4
+
5
+ // The file of the service's latest specification, at a URL that doesn't change with each release
6
+ export const prerender = !isSSR();
7
+ export const { getStaticPaths, GET } = createLatestServiceRawSpecificationRoute('asyncapi', Page);
@@ -0,0 +1,7 @@
1
+ import { isSSR } from '@utils/feature';
2
+ import { Page } from '../../../../[type]/[id]/[version]/graphql/_[filename].data';
3
+ import { createLatestServiceRawSpecificationRoute } from '../../../_latest-raw-specification-route';
4
+
5
+ // The file of the service's latest specification, at a URL that doesn't change with each release
6
+ export const prerender = !isSSR();
7
+ export const { getStaticPaths, GET } = createLatestServiceRawSpecificationRoute('graphql', Page);
@@ -0,0 +1,7 @@
1
+ import { isSSR } from '@utils/feature';
2
+ import { Page } from '../../../../[type]/[id]/[version]/spec/_[filename].data';
3
+ import { createLatestServiceRawSpecificationRoute } from '../../../_latest-raw-specification-route';
4
+
5
+ // The file of the service's latest specification, at a URL that doesn't change with each release
6
+ export const prerender = !isSSR();
7
+ export const { getStaticPaths, GET } = createLatestServiceRawSpecificationRoute('openapi', Page);
@@ -0,0 +1,37 @@
1
+ import type { APIContext, APIRoute } from 'astro';
2
+ import type { SpecificationType } from '@utils/collections/util';
3
+ import {
4
+ getRawSpecificationResponse,
5
+ isRawSpecificationFile,
6
+ notFound,
7
+ toRawSpecificationPaths,
8
+ type SpecificationPage,
9
+ type SpecificationPageData,
10
+ type StaticPath,
11
+ } from '@utils/raw-specification';
12
+ import { getLatestServicePaths, getLatestServiceVersion } from './_latest-version-route';
13
+
14
+ /**
15
+ * The route that serves the file of a service's latest version, for links that don't change with each
16
+ * release: /docs/services/{id}/{asyncapi|spec|graphql}/{filename}/{file}
17
+ */
18
+ export const createLatestServiceRawSpecificationRoute = (type: SpecificationType, Page: SpecificationPage) => {
19
+ const getStaticPaths = async () =>
20
+ (await getLatestServicePaths(toRawSpecificationPaths(await Page.getStaticPaths()))) as StaticPath[];
21
+
22
+ const GET: APIRoute = async ({ params, props }: APIContext) => {
23
+ const version = props.redirectVersion ?? (params.id ? await getLatestServiceVersion(params.id) : undefined);
24
+ if (!version) return notFound('Specification not found');
25
+
26
+ const pageData: SpecificationPageData | undefined = await Page.getData({
27
+ params: { type: 'services', id: params.id, version, filename: params.filename },
28
+ props,
29
+ });
30
+
31
+ if (!pageData || !isRawSpecificationFile(pageData, params.file)) return notFound('Specification not found');
32
+
33
+ return getRawSpecificationResponse(type, pageData);
34
+ };
35
+
36
+ return { getStaticPaths, GET };
37
+ };
@@ -119,9 +119,13 @@ export const buildDataProductSections = (
119
119
  context.resourceDocCategories
120
120
  );
121
121
 
122
- // Resolve inputs and outputs to their proper sidebar references
123
- const resolvedInputs = inputs.map((input) => resolvePointerToRef(input, context)).filter(Boolean) as string[];
124
- const resolvedOutputs = outputs.map((output) => resolvePointerToRef(output, context)).filter(Boolean) as string[];
122
+ // Resolve inputs and outputs to their proper sidebar references. The same resource can be
123
+ // listed more than once (e.g. one output per data contract), so show each resource only once.
124
+ const resolveRefs = (pointers: { id: string; version?: string }[]) => [
125
+ ...new Set(pointers.map((pointer) => resolvePointerToRef(pointer, context)).filter((ref): ref is string => !!ref)),
126
+ ];
127
+ const resolvedInputs = resolveRefs(inputs);
128
+ const resolvedOutputs = resolveRefs(outputs);
125
129
 
126
130
  // Extract data contracts from outputs that have a contract field
127
131
  const dataContracts = outputs
@@ -0,0 +1,126 @@
1
+ import type { APIContext, APIRoute } from 'astro';
2
+ import type { CollectionEntry } from 'astro:content';
3
+ import type { CollectionTypes } from '@types';
4
+ import { getSpecificationMediaType, processSpecifications, type SpecificationType } from '@utils/collections/util';
5
+ import { readResourceFile, resourceFileExists } from '@utils/resource-files';
6
+ import { resolveHeaders } from '@utils/remote-spec';
7
+ import { buildUrl } from '@utils/url-builder';
8
+ import Config from '@utils/eventcatalog-config/catalog';
9
+
10
+ /** The folder each kind of specification page lives in, under /docs/{type}/{id}/{version}/ */
11
+ const PAGE_SEGMENT_BY_SPECIFICATION_TYPE: Record<SpecificationType, string> = {
12
+ asyncapi: 'asyncapi',
13
+ openapi: 'spec',
14
+ graphql: 'graphql',
15
+ generic: 'generic',
16
+ };
17
+
18
+ export type StaticPath = { params: Record<string, string | undefined>; props: Record<string, any> };
19
+
20
+ /** The data a specification page loads: the resource, plus the specification's path and file names */
21
+ export type SpecificationPageData = CollectionEntry<CollectionTypes> & {
22
+ path: string;
23
+ filename: string;
24
+ filenameWithoutExtension: string;
25
+ };
26
+
27
+ /** A specification page's data loader (the `Page` class in each `_[filename].data.ts`) */
28
+ export type SpecificationPage = {
29
+ getStaticPaths(): Promise<StaticPath[]>;
30
+ getData(astro: { params: Record<string, string | undefined>; props: Record<string, any> }): Promise<any>;
31
+ };
32
+
33
+ /**
34
+ * The name a specification's file has in its URL: a remote specification's file name without the URL's query
35
+ * or fragment (orders.yaml?token=abc is orders.yaml), which would otherwise end the URL's path
36
+ */
37
+ export const getRawSpecificationFileName = (filename: string) => filename.split(/[?#]/, 1)[0];
38
+
39
+ /** Whether a raw specification URL's `file` names this specification's file */
40
+ export const isRawSpecificationFile = (pageData: SpecificationPageData, file: string | undefined) =>
41
+ getRawSpecificationFileName(pageData.filename) === file;
42
+
43
+ /**
44
+ * The URL of a specification's file, as written (JSON, YAML, GraphQL...): its page's URL followed by the
45
+ * file's name, e.g. /docs/services/Orders/1.0.0/asyncapi/orders/orders.yaml
46
+ */
47
+ export const getRawSpecificationUrl = ({
48
+ collection,
49
+ id,
50
+ version,
51
+ type,
52
+ filename,
53
+ filenameWithoutExtension,
54
+ }: {
55
+ collection: string;
56
+ id: string;
57
+ version: string;
58
+ type: SpecificationType;
59
+ filename: string;
60
+ filenameWithoutExtension: string;
61
+ }) =>
62
+ buildUrl(
63
+ `/docs/${collection}/${id}/${version}/${PAGE_SEGMENT_BY_SPECIFICATION_TYPE[type]}/${filenameWithoutExtension}/${getRawSpecificationFileName(filename)}`,
64
+ true
65
+ );
66
+
67
+ const isRemote = (path: string) => /^https?:\/\//i.test(path);
68
+
69
+ export const notFound = (error: string) =>
70
+ new Response(JSON.stringify({ error }), { status: 404, headers: { 'Content-Type': 'application/json' } });
71
+
72
+ /** The specification's file, as written, with the media type of its format */
73
+ export const getRawSpecificationResponse = async (
74
+ type: SpecificationType,
75
+ pageData: SpecificationPageData
76
+ ): Promise<Response> => {
77
+ const specification = processSpecifications((pageData.data as any).specifications).find(
78
+ (spec) => spec.type === type && spec.path === pageData.path
79
+ );
80
+
81
+ if (!specification) return notFound('Specification not found');
82
+
83
+ const headers = { 'Content-Type': getSpecificationMediaType(specification) };
84
+
85
+ if (isRemote(specification.path)) {
86
+ const allowAnyEnvInHeaders = type === 'asyncapi' && (Config?.asyncAPI?.allowAnyEnvInSpecHeaders ?? false);
87
+ const response = await fetch(specification.path, {
88
+ headers: resolveHeaders(specification.headers ?? {}, { allowAnyEnvInHeaders }),
89
+ }).catch(() => null);
90
+
91
+ if (!response?.ok) return notFound('Specification file could not be fetched');
92
+ return new Response(await response.text(), { headers });
93
+ }
94
+
95
+ const content = readResourceFile(pageData, specification.path);
96
+ if (content === null) return notFound('Specification file could not be read');
97
+
98
+ return new Response(content, { headers });
99
+ };
100
+
101
+ /**
102
+ * Each specification page's path, with the specification's file name added as `file`. A local file that
103
+ * doesn't exist gets no path: a static build would otherwise save the 404's JSON under the file's name.
104
+ */
105
+ export const toRawSpecificationPaths = (paths: StaticPath[]): StaticPath[] =>
106
+ paths
107
+ .filter(({ props }) => isRemote(props.path) || resourceFileExists(props, props.path))
108
+ .map(({ params, props }) => ({ params: { ...params, file: getRawSpecificationFileName(props.filename) }, props }));
109
+
110
+ /**
111
+ * The route that serves a specification's file next to its page:
112
+ * /docs/{type}/{id}/{version}/{asyncapi|spec|graphql|generic}/{filename}/{file}
113
+ */
114
+ export const createRawSpecificationRoute = (type: SpecificationType, Page: SpecificationPage) => {
115
+ const getStaticPaths = async () => toRawSpecificationPaths(await Page.getStaticPaths());
116
+
117
+ const GET: APIRoute = async ({ params, props }: APIContext) => {
118
+ const pageData: SpecificationPageData | undefined = await Page.getData({ params, props });
119
+
120
+ if (!pageData || !isRawSpecificationFile(pageData, params.file)) return notFound('Specification not found');
121
+
122
+ return getRawSpecificationResponse(type, pageData);
123
+ };
124
+
125
+ return { getStaticPaths, GET };
126
+ };
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "BUSL-1.1",
9
9
  "type": "module",
10
- "version": "5.7.5",
10
+ "version": "5.8.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/fitness": "0.1.1",
145
144
  "@eventcatalog/exporters": "0.1.4",
146
- "@eventcatalog/linter": "1.3.1",
145
+ "@eventcatalog/fitness": "0.1.1",
147
146
  "@eventcatalog/sdk": "2.32.0",
148
- "@eventcatalog/visualiser": "^5.2.3"
147
+ "@eventcatalog/visualiser": "^5.2.4",
148
+ "@eventcatalog/linter": "1.3.1"
149
149
  },
150
150
  "devDependencies": {
151
151
  "@astrojs/check": "^0.9.10",