@eventcatalog/core 3.13.0 → 3.14.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 (50) 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/catalog-to-astro-content-directory.cjs +1 -1
  6. package/dist/{chunk-53JVQT34.js → chunk-2OZIUQR5.js} +1 -1
  7. package/dist/{chunk-33AANWUG.js → chunk-BWF7CUGM.js} +1 -1
  8. package/dist/{chunk-I3U7AKYR.js → chunk-KZCWZ5PZ.js} +1 -1
  9. package/dist/{chunk-BX3YBDRJ.js → chunk-Q23QNBZG.js} +1 -1
  10. package/dist/{chunk-6OL426GP.js → chunk-SJ5OPJEM.js} +1 -1
  11. package/dist/constants.cjs +1 -1
  12. package/dist/constants.js +1 -1
  13. package/dist/eventcatalog.cjs +66 -4
  14. package/dist/eventcatalog.config.d.cts +2 -1
  15. package/dist/eventcatalog.config.d.ts +2 -1
  16. package/dist/eventcatalog.js +70 -7
  17. package/dist/generate.cjs +1 -1
  18. package/dist/generate.js +3 -3
  19. package/dist/utils/cli-logger.cjs +1 -1
  20. package/dist/utils/cli-logger.js +2 -2
  21. package/eventcatalog/integrations/eventcatalog-features.ts +9 -0
  22. package/eventcatalog/src/components/Header.astro +5 -5
  23. package/eventcatalog/src/components/MDX/NodeGraph/AstroNodeGraph.tsx +14 -11
  24. package/eventcatalog/src/components/MDX/NodeGraph/NodeGraphPortal.tsx +1 -1
  25. package/eventcatalog/src/components/MDX/RemoteFile.astro +5 -16
  26. package/eventcatalog/src/components/Search/Search.astro +1 -1
  27. package/eventcatalog/src/components/Search/SearchDataLoader.astro +25 -13
  28. package/eventcatalog/src/components/Search/SearchModal.tsx +101 -39
  29. package/eventcatalog/src/components/ThemeToggle.tsx +11 -3
  30. package/eventcatalog/src/content.config.ts +12 -11
  31. package/eventcatalog/src/enterprise/api/catalog.ts +22 -0
  32. package/eventcatalog/src/pages/api/search-index.json.ts +46 -0
  33. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/asyncapi/[filename].astro +51 -5
  34. package/eventcatalog/src/pages/docs/[type]/[id]/[version]/spec/_OpenAPI.tsx +26 -21
  35. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/_index.data.ts +4 -4
  36. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/data/index.astro +7 -2
  37. package/eventcatalog/src/pages/visualiser/[type]/[id]/[version]/index.astro +7 -2
  38. package/eventcatalog/src/pages/visualiser/[type]/[id]/index.astro +5 -0
  39. package/eventcatalog/src/pages/visualiser/context-map/index.astro +2 -2
  40. package/eventcatalog/src/pages/visualiser/designs/[id]/_index.data.ts +4 -4
  41. package/eventcatalog/src/pages/visualiser/designs/[id]/index.astro +6 -2
  42. package/eventcatalog/src/pages/visualiser/domain-integrations/index.astro +2 -2
  43. package/eventcatalog/src/pages/visualiser/domains/[id]/[version]/entity-map/index.astro +7 -2
  44. package/eventcatalog/src/utils/collections/util.ts +2 -0
  45. package/eventcatalog/src/utils/eventcatalog-config/catalog.ts +3 -18
  46. package/eventcatalog/src/utils/feature.ts +2 -0
  47. package/eventcatalog/src/utils/remote-file.ts +15 -0
  48. package/eventcatalog/src/utils/remote-spec.ts +45 -0
  49. package/package.json +5 -5
  50. package/eventcatalog/src/pages/api/catalog.ts +0 -34
@@ -24,8 +24,6 @@ import {
24
24
  } from '@heroicons/react/24/outline';
25
25
  import { StarIcon as StarIconSolid, CircleStackIcon } from '@heroicons/react/24/solid';
26
26
  import { useStore } from '@nanostores/react';
27
- import { sidebarStore } from '../../stores/sidebar-store';
28
- import type { NavNode } from '../../stores/sidebar-store/state';
29
27
  import { favoritesStore, toggleFavorite as toggleFavoriteAction } from '../../stores/favorites-store';
30
28
  import { buildUrl } from '@utils/url-builder';
31
29
 
@@ -99,11 +97,48 @@ const getUrlForItem = (node: any, key: string) => {
99
97
  return buildUrl(`/docs/${pluralType}/${id}/${version}`);
100
98
  };
101
99
 
100
+ interface SearchNode {
101
+ key: string;
102
+ title: string;
103
+ badge?: string;
104
+ summary?: string;
105
+ href?: string;
106
+ }
107
+
108
+ interface SearchNodeCompact {
109
+ k: string;
110
+ t: string;
111
+ b?: string;
112
+ s?: string;
113
+ h?: string;
114
+ }
115
+
116
+ interface SearchIndexPayload {
117
+ i?: SearchNodeCompact[];
118
+ items?: SearchNode[];
119
+ }
120
+
121
+ const normalizeSearchIndexPayload = (payload: SearchIndexPayload): SearchNode[] => {
122
+ if (payload.i) {
123
+ return payload.i.map((item) => ({
124
+ key: item.k,
125
+ title: item.t,
126
+ badge: item.b,
127
+ summary: item.s,
128
+ href: item.h,
129
+ }));
130
+ }
131
+
132
+ return payload.items || [];
133
+ };
134
+
102
135
  export default function SearchModal() {
103
136
  const [query, setQuery] = useState('');
104
137
  const [open, setOpen] = useState(false);
105
138
  const [activeFilter, setActiveFilter] = useState('all');
106
- const data = useStore(sidebarStore);
139
+ const [searchNodes, setSearchNodes] = useState<SearchNode[]>([]);
140
+ const [isLoadingSearchIndex, setIsLoadingSearchIndex] = useState(false);
141
+ const [searchIndexLoadError, setSearchIndexLoadError] = useState<string | null>(null);
107
142
  const favorites = useStore(favoritesStore);
108
143
  const inputRef = useRef<HTMLInputElement>(null);
109
144
 
@@ -122,6 +157,34 @@ export default function SearchModal() {
122
157
  return () => window.removeEventListener('searchModalToggle', handleModalToggle as EventListener);
123
158
  }, []);
124
159
 
160
+ useEffect(() => {
161
+ if (!open || searchNodes.length > 0 || isLoadingSearchIndex) {
162
+ return;
163
+ }
164
+
165
+ setIsLoadingSearchIndex(true);
166
+ setSearchIndexLoadError(null);
167
+
168
+ const apiUrl = buildUrl('/api/search-index.json', true);
169
+
170
+ fetch(apiUrl)
171
+ .then((response) => {
172
+ if (!response.ok) {
173
+ throw new Error(`Failed to fetch search index: ${response.status}`);
174
+ }
175
+ return response.json() as Promise<SearchIndexPayload>;
176
+ })
177
+ .then((payload) => {
178
+ setSearchNodes(normalizeSearchIndexPayload(payload));
179
+ })
180
+ .catch((error) => {
181
+ setSearchIndexLoadError(error instanceof Error ? error.message : 'Unable to load search index');
182
+ })
183
+ .finally(() => {
184
+ setIsLoadingSearchIndex(false);
185
+ });
186
+ }, [open, searchNodes.length, isLoadingSearchIndex]);
187
+
125
188
  const closeModal = () => {
126
189
  if ((window as any).searchModalState) {
127
190
  (window as any).searchModalState.close();
@@ -131,30 +194,22 @@ export default function SearchModal() {
131
194
  };
132
195
 
133
196
  const items = useMemo(() => {
134
- if (!data?.nodes) return [];
135
-
136
- // Extract all items from nodes
137
- const allItems = Object.entries(data.nodes)
138
- .map(([key, node]) => {
139
- // Skip reference entries (string values that point to other keys)
140
- if (typeof node === 'string') return null;
141
-
142
- const url = getUrlForItem(node, key);
197
+ return searchNodes
198
+ .map((node) => {
199
+ const url = getUrlForItem(node as any, node.key);
143
200
  if (!url) return null;
144
201
 
145
202
  return {
146
- id: url, // Use URL as unique ID
203
+ id: url,
147
204
  name: node.title,
148
- url: url,
205
+ url,
149
206
  type: node.badge || 'Page',
150
- key: key,
207
+ key: node.key,
151
208
  rawNode: node,
152
209
  };
153
210
  })
154
211
  .filter((item): item is NonNullable<typeof item> => item !== null);
155
-
156
- return allItems;
157
- }, [data]);
212
+ }, [searchNodes]);
158
213
 
159
214
  // Get searchable items (items that match the query but not filtered by type yet)
160
215
  const searchableItems = useMemo(() => {
@@ -236,17 +291,9 @@ export default function SearchModal() {
236
291
  });
237
292
  };
238
293
 
239
- // Helper to resolve a node, following references if needed
240
- const resolveNode = (key: string) => {
241
- if (!data?.nodes) return null;
242
- const node = data.nodes[key];
243
- if (!node) return null;
244
- // If it's a string reference, follow it
245
- if (typeof node === 'string') {
246
- return data.nodes[node] as NavNode | undefined;
247
- }
248
- return node;
249
- };
294
+ const searchNodeLookup = useMemo(() => {
295
+ return new Map(searchNodes.map((node) => [node.key, node]));
296
+ }, [searchNodes]);
250
297
 
251
298
  const filteredItems = useMemo(() => {
252
299
  if (query === '') {
@@ -255,18 +302,17 @@ export default function SearchModal() {
255
302
  return favorites
256
303
  .slice(0, 5)
257
304
  .map((fav) => {
258
- const node = resolveNode(fav.nodeKey);
259
- if (!node || typeof node === 'string') return null;
260
- const url = getUrlForItem(node, fav.nodeKey);
305
+ const node = searchNodeLookup.get(fav.nodeKey);
306
+ const url = node ? getUrlForItem(node as any, fav.nodeKey) : fav.href;
261
307
  if (!url) return null;
262
308
 
263
309
  return {
264
310
  id: url,
265
311
  name: fav.title,
266
- url: url,
267
- type: fav.badge || 'Page',
312
+ url,
313
+ type: fav.badge || node?.badge || 'Page',
268
314
  key: fav.nodeKey,
269
- rawNode: node,
315
+ rawNode: node || { title: fav.title, badge: fav.badge, summary: undefined },
270
316
  isFavorite: true,
271
317
  };
272
318
  })
@@ -290,7 +336,7 @@ export default function SearchModal() {
290
336
  }
291
337
 
292
338
  return result.slice(0, 50); // Limit results for performance
293
- }, [searchableItems, query, activeFilter, favorites, data]);
339
+ }, [searchableItems, query, activeFilter, favorites, searchNodeLookup]);
294
340
 
295
341
  return (
296
342
  <Transition.Root
@@ -368,7 +414,23 @@ export default function SearchModal() {
368
414
  ))}
369
415
  </div>
370
416
 
371
- {filteredItems.length > 0 && (
417
+ {isLoadingSearchIndex && (
418
+ <div className="py-10 px-6 text-center text-sm sm:px-14">
419
+ <MagnifyingGlassIcon className="mx-auto h-6 w-6 text-[rgb(var(--ec-icon-color))] animate-pulse" />
420
+ <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">Loading search index…</p>
421
+ <p className="mt-2 text-[rgb(var(--ec-page-text-muted))]">Preparing resources for search.</p>
422
+ </div>
423
+ )}
424
+
425
+ {searchIndexLoadError && !isLoadingSearchIndex && (
426
+ <div className="py-10 px-6 text-center text-sm sm:px-14">
427
+ <ExclamationCircleIcon className="mx-auto h-6 w-6 text-red-500" />
428
+ <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">Search unavailable</p>
429
+ <p className="mt-2 text-[rgb(var(--ec-page-text-muted))]">{searchIndexLoadError}</p>
430
+ </div>
431
+ )}
432
+
433
+ {!isLoadingSearchIndex && !searchIndexLoadError && filteredItems.length > 0 && (
372
434
  <>
373
435
  {query === '' && favorites.length > 0 && (
374
436
  <div className="px-6 pt-3 pb-2">
@@ -462,7 +524,7 @@ export default function SearchModal() {
462
524
  </>
463
525
  )}
464
526
 
465
- {query !== '' && filteredItems.length === 0 && (
527
+ {!isLoadingSearchIndex && !searchIndexLoadError && query !== '' && filteredItems.length === 0 && (
466
528
  <div className="py-14 px-6 text-center text-sm sm:px-14">
467
529
  <ExclamationCircleIcon
468
530
  type="outline"
@@ -476,7 +538,7 @@ export default function SearchModal() {
476
538
  </div>
477
539
  )}
478
540
 
479
- {query === '' && filteredItems.length === 0 && (
541
+ {!isLoadingSearchIndex && !searchIndexLoadError && query === '' && filteredItems.length === 0 && (
480
542
  <div className="py-14 px-6 text-center text-sm sm:px-14">
481
543
  <MagnifyingGlassIcon className="mx-auto h-6 w-6 text-[rgb(var(--ec-icon-color))]" />
482
544
  <p className="mt-4 font-semibold text-[rgb(var(--ec-page-text))]">Search for anything</p>
@@ -1,18 +1,26 @@
1
1
  import { useStore } from '@nanostores/react';
2
+ import { useEffect, useState } from 'react';
2
3
  import { Moon, Sun } from 'lucide-react';
3
4
  import { themeStore, toggleTheme } from '@stores/theme-store';
4
5
 
5
6
  export default function ThemeToggle() {
6
7
  const theme = useStore(themeStore);
8
+ const [mounted, setMounted] = useState(false);
9
+
10
+ useEffect(() => {
11
+ setMounted(true);
12
+ }, []);
13
+
14
+ const label = mounted ? `Switch to ${theme === 'light' ? 'dark' : 'light'} mode` : 'Toggle theme';
7
15
 
8
16
  return (
9
17
  <button
10
18
  onClick={toggleTheme}
11
19
  className="p-2 rounded-md text-[rgb(var(--ec-icon-color))] hover:text-[rgb(var(--ec-icon-hover))] hover:bg-[rgb(var(--ec-dropdown-hover))] transition-colors"
12
- aria-label={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
13
- title={`Switch to ${theme === 'light' ? 'dark' : 'light'} mode`}
20
+ aria-label={label}
21
+ title={label}
14
22
  >
15
- {theme === 'light' ? <Moon className="w-5 h-5" /> : <Sun className="w-5 h-5" />}
23
+ {!mounted ? <Moon className="w-5 h-5" /> : theme === 'light' ? <Moon className="w-5 h-5" /> : <Sun className="w-5 h-5" />}
16
24
  </button>
17
25
  );
18
26
  }
@@ -31,7 +31,7 @@ export const projectDirBase = (() => {
31
31
 
32
32
  const pages = defineCollection({
33
33
  loader: glob({
34
- pattern: ['**/pages/*.(md|mdx)'],
34
+ pattern: ['**/pages/*.(md|mdx)', '!**/dist/**'],
35
35
  base: projectDirBase,
36
36
  }),
37
37
  schema: z
@@ -90,7 +90,7 @@ const resourcePointer = z.object({
90
90
 
91
91
  const changelogs = defineCollection({
92
92
  loader: glob({
93
- pattern: ['**/changelog.(md|mdx)'],
93
+ pattern: ['**/changelog.(md|mdx)', '!**/dist/**'],
94
94
  base: projectDirBase,
95
95
  }),
96
96
  schema: z.object({
@@ -147,6 +147,7 @@ const baseSchema = z.object({
147
147
  type: z.enum(['openapi', 'asyncapi', 'graphql']),
148
148
  path: z.string(),
149
149
  name: z.string().optional(),
150
+ headers: z.record(z.string()).optional(),
150
151
  })
151
152
  ),
152
153
  ])
@@ -228,7 +229,7 @@ const flowStep = z
228
229
 
229
230
  const flows = defineCollection({
230
231
  loader: glob({
231
- pattern: ['**/flows/*/index.(md|mdx)', '**/flows/*/versioned/*/index.(md|mdx)'],
232
+ pattern: ['**/flows/*/index.(md|mdx)', '**/flows/*/versioned/*/index.(md|mdx)', '!**/dist/**'],
232
233
  base: projectDirBase,
233
234
  generateId: ({ data }) => {
234
235
  return `${data.id}-${data.version}`;
@@ -316,7 +317,7 @@ const messageDetailsPanelPropertySchema = z.object({
316
317
 
317
318
  const events = defineCollection({
318
319
  loader: glob({
319
- pattern: ['**/events/*/index.(md|mdx)', '**/events/*/versioned/*/index.(md|mdx)'],
320
+ pattern: ['**/events/*/index.(md|mdx)', '**/events/*/versioned/*/index.(md|mdx)', '!**/dist/**'],
320
321
  base: projectDirBase,
321
322
  generateId: ({ data, ...rest }) => {
322
323
  return `${data.id}-${data.version}`;
@@ -336,7 +337,7 @@ const events = defineCollection({
336
337
 
337
338
  const commands = defineCollection({
338
339
  loader: glob({
339
- pattern: ['**/commands/*/index.(md|mdx)', '**/commands/*/versioned/*/index.(md|mdx)'],
340
+ pattern: ['**/commands/*/index.(md|mdx)', '**/commands/*/versioned/*/index.(md|mdx)', '!**/dist/**'],
340
341
  base: projectDirBase,
341
342
  generateId: ({ data }) => {
342
343
  return `${data.id}-${data.version}`;
@@ -356,7 +357,7 @@ const commands = defineCollection({
356
357
 
357
358
  const queries = defineCollection({
358
359
  loader: glob({
359
- pattern: ['**/queries/*/index.(md|mdx)', '**/queries/*/versioned/*/index.(md|mdx)'],
360
+ pattern: ['**/queries/*/index.(md|mdx)', '**/queries/*/versioned/*/index.(md|mdx)', '!**/dist/**'],
360
361
  base: projectDirBase,
361
362
  generateId: ({ data }) => {
362
363
  return `${data.id}-${data.version}`;
@@ -388,7 +389,7 @@ const dataProductOutputPointer = z.object({
388
389
 
389
390
  const dataProducts = defineCollection({
390
391
  loader: glob({
391
- pattern: ['**/data-products/*/index.(md|mdx)', '**/data-products/*/versioned/*/index.(md|mdx)'],
392
+ pattern: ['**/data-products/*/index.(md|mdx)', '**/data-products/*/versioned/*/index.(md|mdx)', '!**/dist/**'],
392
393
  base: projectDirBase,
393
394
  generateId: ({ data }) => {
394
395
  return `${data.id}-${data.version}`;
@@ -465,7 +466,7 @@ const dataClassificationEnum = z.enum(['public', 'internal', 'confidential', 're
465
466
 
466
467
  const containers = defineCollection({
467
468
  loader: glob({
468
- pattern: ['**/containers/**/index.(md|mdx)', '**/containers/**/versioned/*/index.(md|mdx)'],
469
+ pattern: ['**/containers/**/index.(md|mdx)', '**/containers/**/versioned/*/index.(md|mdx)', '!**/dist/**'],
469
470
  base: projectDirBase,
470
471
  generateId: ({ data }) => {
471
472
  return `${data.id}-${data.version}`;
@@ -556,7 +557,7 @@ const domains = defineCollection({
556
557
 
557
558
  const channels = defineCollection({
558
559
  loader: glob({
559
- pattern: ['**/channels/**/index.(md|mdx)', '**/channels/**/versioned/*/index.(md|mdx)'],
560
+ pattern: ['**/channels/**/index.(md|mdx)', '**/channels/**/versioned/*/index.(md|mdx)', '!**/dist/**'],
560
561
  base: projectDirBase,
561
562
  generateId: ({ data }) => {
562
563
  return `${data.id}-${data.version}`;
@@ -623,7 +624,7 @@ const ubiquitousLanguages = defineCollection({
623
624
 
624
625
  const entities = defineCollection({
625
626
  loader: glob({
626
- pattern: ['**/entities/*/index.(md|mdx)', '**/entities/*/versioned/*/index.(md|mdx)'],
627
+ pattern: ['**/entities/*/index.(md|mdx)', '**/entities/*/versioned/*/index.(md|mdx)', '!**/dist/**'],
627
628
  base: projectDirBase,
628
629
  generateId: ({ data, ...rest }) => {
629
630
  return `${data.id}-${data.version}`;
@@ -740,7 +741,7 @@ const designs = defineCollection({
740
741
 
741
742
  const diagrams = defineCollection({
742
743
  loader: glob({
743
- pattern: ['**/diagrams/**/index.(md|mdx)', '**/diagrams/**/versioned/*/index.(md|mdx)'],
744
+ pattern: ['**/diagrams/**/index.(md|mdx)', '**/diagrams/**/versioned/*/index.(md|mdx)', '!**/dist/**'],
744
745
  base: projectDirBase,
745
746
  generateId: ({ data }) => `${data.id}-${data.version}`,
746
747
  }),
@@ -0,0 +1,22 @@
1
+ import type { APIRoute } from 'astro';
2
+ import utils from '@eventcatalog/sdk';
3
+ import { isSSR } from '@utils/feature';
4
+
5
+ /**
6
+ * Route that dumps the whole catalog as JSON (without markdown)
7
+ * Experimental API
8
+ *
9
+ * This route is injected only when `api.fullCatalogAPIEnabled` is true.
10
+ */
11
+ export const GET: APIRoute = async () => {
12
+ const { dumpCatalog } = utils(process.env.PROJECT_DIR || '');
13
+ const catalog = await dumpCatalog({ includeMarkdown: false });
14
+
15
+ return new Response(JSON.stringify(catalog), {
16
+ headers: {
17
+ 'Content-Type': 'application/json',
18
+ },
19
+ });
20
+ };
21
+
22
+ export const prerender = !isSSR();
@@ -0,0 +1,46 @@
1
+ import type { APIRoute } from 'astro';
2
+ import { getNestedSideBarData } from '@stores/sidebar-store/state';
3
+
4
+ const isDev = import.meta.env.DEV;
5
+
6
+ interface SearchIndexItemCompact {
7
+ k: string;
8
+ t: string;
9
+ b?: string;
10
+ s?: string;
11
+ h?: string;
12
+ }
13
+
14
+ const SKIPPED_PREFIXES = ['list:'];
15
+
16
+ export const GET: APIRoute = async () => {
17
+ const sidebarData = await getNestedSideBarData();
18
+
19
+ const items = Object.entries(sidebarData.nodes).reduce<SearchIndexItemCompact[]>((acc, [key, node]) => {
20
+ if (typeof node === 'string') return acc;
21
+ if (SKIPPED_PREFIXES.some((prefix) => key.startsWith(prefix))) return acc;
22
+ if (!node.title) return acc;
23
+
24
+ acc.push({
25
+ k: key,
26
+ t: node.title,
27
+ ...(node.badge ? { b: node.badge } : {}),
28
+ ...(node.summary ? { s: node.summary } : {}),
29
+ ...(node.href ? { h: node.href } : {}),
30
+ });
31
+
32
+ return acc;
33
+ }, []);
34
+
35
+ return new Response(JSON.stringify({ i: items }), {
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ 'Cache-Control': isDev
39
+ ? 'no-cache, no-store, must-revalidate'
40
+ : 'public, max-age=3600, s-maxage=3600, stale-while-revalidate=86400',
41
+ Vary: 'Accept-Encoding',
42
+ },
43
+ });
44
+ };
45
+
46
+ export const prerender = !isDev;
@@ -5,6 +5,8 @@ import { Parser } from '@asyncapi/parser';
5
5
  import { AvroSchemaParser } from '@asyncapi/avro-schema-parser';
6
6
  import fs from 'fs';
7
7
 
8
+ import { isSameOrigin, resolveHeaders } from '@utils/remote-spec';
9
+
8
10
  import '@asyncapi/react-component/styles/default.min.css';
9
11
  import js from '@asyncapi/react-component/browser/standalone/without-parser.js?url';
10
12
  import { AsyncApiComponentWP, type ConfigInterface } from '@asyncapi/react-component';
@@ -19,7 +21,7 @@ export const prerender = Page.prerender;
19
21
  export const getStaticPaths = Page.getStaticPaths;
20
22
 
21
23
  // Get data
22
- const { collection, data, filePath, filename, path: relativeSpecPath } = await Page.getData(Astro);
24
+ const { collection, data, filePath, filename, path: relativeSpecPath, headers = {} } = await Page.getData(Astro);
23
25
 
24
26
  const fileName = filename || 'asyncapi.yaml';
25
27
  const pathToSpec = getAbsoluteFilePathForAstroFile(filePath, fileName);
@@ -27,18 +29,62 @@ const fileExists = fs.existsSync(pathToSpec);
27
29
  const isRemote = relativeSpecPath.includes('https://');
28
30
  let content = '';
29
31
 
32
+ const allowAnyEnvInSpecHeaders = Config?.asyncAPI?.allowAnyEnvInSpecHeaders ?? false;
33
+ const resolvedHeaders = resolveHeaders(headers, { allowAnyEnvInHeaders: allowAnyEnvInSpecHeaders });
34
+
30
35
  if (fileExists && !isRemote) {
31
36
  content = fs.readFileSync(pathToSpec, 'utf8');
32
37
  }
33
38
 
34
39
  if (isRemote) {
35
- // Fetch the content from the remote path
36
- content = await fetch(relativeSpecPath).then((res) => res.text());
40
+ const response = await fetch(relativeSpecPath, { headers: resolvedHeaders });
41
+
42
+ if (!response.ok) {
43
+ throw new Error(`Failed to fetch AsyncAPI spec: ${response.status} ${response.statusText}`);
44
+ }
45
+
46
+ content = await response.text();
37
47
  }
38
48
 
39
- // AsyncAPI parser will parser schemas for users, they can turn this off.
49
+ // AsyncAPI parser will parse schemas for users, they can turn this off.
40
50
  const parseSchemas = Config?.asyncAPI?.renderParsedSchemas ?? true;
41
- const parsed = await new Parser({ schemaParsers: [AvroSchemaParser()] }).parse(content, { parseSchemas });
51
+ const parserOptions: NonNullable<Parameters<Parser['parse']>[1]> = {
52
+ parseSchemas,
53
+ source: isRemote ? relativeSpecPath : pathToSpec,
54
+ };
55
+
56
+ if (isRemote && Object.keys(resolvedHeaders).length > 0) {
57
+ // NOTE: AsyncAPI parser exposes resolver wiring via __unstable.
58
+ // Keep this isolated so parser upgrades can adjust in one place.
59
+ const resolverWithHeaders = {
60
+ canRead: true,
61
+ read: async (uri: { toString: () => string }) => {
62
+ const targetUrl = uri.toString();
63
+ const shouldForwardHeaders = isSameOrigin(relativeSpecPath, targetUrl);
64
+
65
+ const response = await fetch(targetUrl, {
66
+ headers: shouldForwardHeaders ? resolvedHeaders : undefined,
67
+ });
68
+
69
+ if (!response.ok) {
70
+ throw new Error(`Failed to fetch reference: ${response.status} ${response.statusText}`);
71
+ }
72
+
73
+ return response.text();
74
+ },
75
+ };
76
+
77
+ parserOptions.__unstable = {
78
+ resolver: {
79
+ resolvers: [
80
+ { schema: 'https', ...resolverWithHeaders },
81
+ { schema: 'http', ...resolverWithHeaders },
82
+ ],
83
+ },
84
+ };
85
+ }
86
+
87
+ const parsed = await new Parser({ schemaParsers: [AvroSchemaParser()] }).parse(content, parserOptions);
42
88
  const stringified = parsed.document?.json();
43
89
  const config: ConfigInterface = { show: { sidebar: true, errors: true } };
44
90
 
@@ -1,8 +1,11 @@
1
- import { useState, useEffect } from 'react';
2
- import { ApiReferenceReact } from '@scalar/api-reference-react';
1
+ import { useState, useEffect, lazy, Suspense } from 'react';
3
2
  import '@scalar/api-reference-react/style.css';
4
3
  import './_styles.css';
5
4
 
5
+ const ApiReferenceReact = lazy(() =>
6
+ import('@scalar/api-reference-react').then((module) => ({ default: module.ApiReferenceReact }))
7
+ );
8
+
6
9
  const OpenAPISpec = ({ spec }: { spec: string }) => {
7
10
  const [loaded, setLoaded] = useState(false);
8
11
  const [isDarkMode, setIsDarkMode] = useState(() => {
@@ -31,25 +34,27 @@ const OpenAPISpec = ({ spec }: { spec: string }) => {
31
34
  return (
32
35
  <div>
33
36
  {!loaded && <div>Loading...</div>}
34
- <ApiReferenceReact
35
- key={isDarkMode ? 'dark' : 'light'}
36
- configuration={{
37
- spec: {
38
- content: spec,
39
- },
40
- theme: 'fastify',
41
- hideClientButton: true,
42
- onLoaded: () => {
43
- setLoaded(true);
44
- },
45
- forceDarkModeState: isDarkMode ? 'dark' : 'light',
46
- darkMode: isDarkMode,
47
- defaultOpenAllTags: true,
48
- hideDarkModeToggle: true,
49
- searchHotKey: 'p',
50
- showSidebar: true,
51
- }}
52
- />
37
+ <Suspense fallback={<div>Loading OpenAPI reference...</div>}>
38
+ <ApiReferenceReact
39
+ key={isDarkMode ? 'dark' : 'light'}
40
+ configuration={{
41
+ spec: {
42
+ content: spec,
43
+ },
44
+ theme: 'fastify',
45
+ hideClientButton: true,
46
+ onLoaded: () => {
47
+ setLoaded(true);
48
+ },
49
+ forceDarkModeState: isDarkMode ? 'dark' : 'light',
50
+ darkMode: isDarkMode,
51
+ defaultOpenAllTags: true,
52
+ hideDarkModeToggle: true,
53
+ searchHotKey: 'p',
54
+ showSidebar: true,
55
+ }}
56
+ />
57
+ </Suspense>
53
58
  </div>
54
59
  );
55
60
  };
@@ -1,5 +1,5 @@
1
1
  import { HybridPage } from '@utils/page-loaders/hybrid-page';
2
- import { isAuthEnabled } from '@utils/feature';
2
+ import { isAuthEnabled, isVisualiserEnabled } from '@utils/feature';
3
3
  import { getServices, type Service } from '@utils/collections/services';
4
4
 
5
5
  const serviceHasData = (service: Service) => {
@@ -8,7 +8,7 @@ const serviceHasData = (service: Service) => {
8
8
 
9
9
  export class Page extends HybridPage {
10
10
  static async getStaticPaths(): Promise<Array<{ params: any; props: any }>> {
11
- if (isAuthEnabled()) {
11
+ if (isAuthEnabled() || !isVisualiserEnabled()) {
12
12
  return [];
13
13
  }
14
14
 
@@ -33,7 +33,7 @@ export class Page extends HybridPage {
33
33
  protected static async fetchData(params: any) {
34
34
  const { id, version } = params;
35
35
 
36
- if (!id || !version) {
36
+ if (!id || !version || !isVisualiserEnabled()) {
37
37
  return null;
38
38
  }
39
39
 
@@ -58,7 +58,7 @@ export class Page extends HybridPage {
58
58
  }
59
59
 
60
60
  static get clientAuthScript(): string {
61
- if (!isAuthEnabled()) {
61
+ if (!isAuthEnabled() || !isVisualiserEnabled()) {
62
62
  return '';
63
63
  }
64
64
 
@@ -19,8 +19,13 @@ const {
19
19
  ---
20
20
 
21
21
  <VisualiserLayout title={`Visualiser | ${props.data.name} (${props.collection})`} description={props.data.summary}>
22
- <div class="bg-gray-100/50 m-4">
23
- <div class="h-[calc(100vh-130px)] w-full relative border border-gray-200" id={`${id}-portal`} transition:animate="fade"></div>
22
+ <div class="m-4">
23
+ <div
24
+ class="h-[calc(100vh-130px)] w-full relative border border-[rgb(var(--ec-page-border))] rounded-md"
25
+ id={`${id}-portal`}
26
+ transition:animate="fade"
27
+ >
28
+ </div>
24
29
  <NodeGraph
25
30
  id={id}
26
31
  collection={`${collection}-containers`}
@@ -20,8 +20,13 @@ const {
20
20
  ---
21
21
 
22
22
  <VisualiserLayout title={`Visualiser | ${props.data.name} (${props.collection})`} description={props.data.summary}>
23
- <div class="bg-gray-100/50 m-4">
24
- <div class="h-[calc(100vh-130px)] w-full relative border border-gray-200" id={`${id}-portal`} transition:animate="fade"></div>
23
+ <div class="m-4">
24
+ <div
25
+ class="h-[calc(100vh-130px)] w-full relative border border-[rgb(var(--ec-page-border))] rounded-md"
26
+ id={`${id}-portal`}
27
+ transition:animate="fade"
28
+ >
29
+ </div>
25
30
  <NodeGraph
26
31
  id={id}
27
32
  collection={collection}
@@ -8,8 +8,13 @@ import { getDomains } from '@utils/collections/domains';
8
8
  import { getContainers } from '@utils/collections/containers';
9
9
  import type { CollectionEntry } from 'astro:content';
10
10
  import type { CollectionTypes } from '@types';
11
+ import { isVisualiserEnabled } from '@utils/feature';
11
12
 
12
13
  export async function getStaticPaths() {
14
+ if (!isVisualiserEnabled()) {
15
+ return [];
16
+ }
17
+
13
18
  const [events, commands, services, domains, containers] = await Promise.all([
14
19
  getEvents(),
15
20
  getCommands(),
@@ -5,9 +5,9 @@ import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
5
5
  ---
6
6
 
7
7
  <VerticalSideBarLayout title={`Visualiser | Bounded context map`}>
8
- <div class="bg-gray-100/50 m-4">
8
+ <div class="m-4">
9
9
  <div
10
- class="h-[calc(100vh-130px)] w-full relative border border-gray-200"
10
+ class="h-[calc(100vh-130px)] w-full relative border border-[rgb(var(--ec-page-border))] rounded-md"
11
11
  id={`domain-context-map-portal`}
12
12
  transition:animate="fade"
13
13
  >