@eventcatalog/core 3.35.1 → 3.36.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) 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-4SNN54V4.js → chunk-6D65JSOA.js} +1 -1
  6. package/dist/{chunk-B7C4DHFE.js → chunk-C7JCOHTI.js} +1 -1
  7. package/dist/chunk-D6IBLY3O.js +320 -0
  8. package/dist/{chunk-R4DR3YAH.js → chunk-HDENGAZL.js} +1 -1
  9. package/dist/{chunk-JEQZWJWP.js → chunk-UJ7DX4SA.js} +3 -3
  10. package/dist/{chunk-3KXCGYET.js → chunk-ULZYHF3V.js} +5 -0
  11. package/dist/{chunk-VJ357XOI.js → chunk-V22QY5Q3.js} +1 -1
  12. package/dist/constants.cjs +1 -1
  13. package/dist/constants.js +1 -1
  14. package/dist/docs/api/02-config.md +22 -0
  15. package/dist/docs/api/_category_.json +1 -1
  16. package/dist/docs/contributing/_category_.json +1 -1
  17. package/dist/docs/development/_category_.json +1 -1
  18. package/dist/docs/development/ask-your-architecture/02-eventcatalog-assistant/_category_.json +1 -1
  19. package/dist/docs/development/ask-your-architecture/03-mcp-server/_category_.json +1 -1
  20. package/dist/docs/development/ask-your-architecture/04-skills/_category_.json +1 -1
  21. package/dist/docs/development/authentication/providers/_category_.json +1 -1
  22. package/dist/docs/development/bring-your-own-documentation/custom-pages/_category_.json +1 -1
  23. package/dist/docs/development/customization/01-customize-landing-page.md +1 -1
  24. package/dist/docs/development/customization/03-search.md +79 -0
  25. package/dist/docs/development/customization/custom-components/_category_.json +1 -1
  26. package/dist/docs/development/customization/customize-sidebars/_category_.json +1 -1
  27. package/dist/docs/development/customization/customize-visualizer/_category_.json +1 -1
  28. package/dist/docs/development/design/_category_.json +1 -1
  29. package/dist/docs/development/guides/changelogs/_category_.json +1 -1
  30. package/dist/docs/development/guides/channels/_category_.json +1 -1
  31. package/dist/docs/development/guides/channels/ownership-and-components/_category_.json +1 -1
  32. package/dist/docs/development/guides/channels/versioning-and-lifecycle/_category_.json +1 -1
  33. package/dist/docs/development/guides/data/_category_.json +1 -1
  34. package/dist/docs/development/guides/data/ownership-and-components/_category_.json +1 -1
  35. package/dist/docs/development/guides/data-products/_category_.json +1 -1
  36. package/dist/docs/development/guides/domains/02-creating-domains/_category_.json +1 -1
  37. package/dist/docs/development/guides/domains/03-ownership-and-language/_category_.json +1 -1
  38. package/dist/docs/development/guides/domains/05-entities/_category_.json +1 -1
  39. package/dist/docs/development/guides/domains/_category_.json +1 -1
  40. package/dist/docs/development/guides/flows/_category_.json +1 -1
  41. package/dist/docs/development/guides/messages/_category_.json +1 -1
  42. package/dist/docs/development/guides/messages/commands/_category_.json +1 -1
  43. package/dist/docs/development/guides/messages/common/_category_.json +1 -1
  44. package/dist/docs/development/guides/messages/events/_category_.json +1 -1
  45. package/dist/docs/development/guides/messages/queries/_category_.json +1 -1
  46. package/dist/docs/development/guides/owners/_category_.json +1 -1
  47. package/dist/docs/development/guides/owners/teams/_category_.json +1 -1
  48. package/dist/docs/development/guides/owners/users/_category_.json +1 -1
  49. package/dist/docs/development/guides/schemas/_category_.json +1 -1
  50. package/dist/docs/development/guides/services/_category_.json +1 -1
  51. package/dist/docs/development/guides/services/adding-to-services/_category_.json +1 -1
  52. package/dist/docs/development/guides/services/ownership-and-components/_category_.json +1 -1
  53. package/dist/docs/development/guides/services/versioning-and-lifecycle/_category_.json +1 -1
  54. package/dist/docs/plugins/_category_.json +1 -1
  55. package/dist/docs/plugins/amazon-apigateway/_category_.json +1 -1
  56. package/dist/docs/plugins/asyncapi/_category_.json +1 -1
  57. package/dist/docs/plugins/aws-glue-registry/_category_.json +1 -1
  58. package/dist/docs/plugins/backstage/_category_.json +1 -1
  59. package/dist/docs/plugins/confluent-schema-registry/_category_.json +1 -1
  60. package/dist/docs/plugins/eventbridge/_category_.json +1 -1
  61. package/dist/docs/plugins/eventcatalog-federation/_category_.json +1 -1
  62. package/dist/docs/plugins/github/_category_.json +1 -1
  63. package/dist/docs/plugins/graphql/_category_.json +1 -1
  64. package/dist/docs/plugins/hookdeck/_category_.json +1 -1
  65. package/dist/docs/plugins/openapi/_category_.json +1 -1
  66. package/dist/eventcatalog.cjs +434 -35
  67. package/dist/eventcatalog.config.d.cts +8 -0
  68. package/dist/eventcatalog.config.d.ts +8 -0
  69. package/dist/eventcatalog.js +87 -10
  70. package/dist/features.cjs +6 -0
  71. package/dist/features.d.cts +2 -1
  72. package/dist/features.d.ts +2 -1
  73. package/dist/features.js +3 -1
  74. package/dist/generate.cjs +1 -1
  75. package/dist/generate.js +3 -3
  76. package/dist/search-indexer.cjs +356 -0
  77. package/dist/search-indexer.d.cts +30 -0
  78. package/dist/search-indexer.d.ts +30 -0
  79. package/dist/search-indexer.js +10 -0
  80. package/dist/utils/cli-logger.cjs +1 -1
  81. package/dist/utils/cli-logger.js +2 -2
  82. package/eventcatalog/astro.config.mjs +28 -32
  83. package/eventcatalog/src/components/Search/SearchModal.tsx +248 -148
  84. package/eventcatalog/src/components/Search/search-utils.spec.ts +138 -1
  85. package/eventcatalog/src/components/Search/search-utils.ts +271 -0
  86. package/eventcatalog/src/env.d.ts +1 -0
  87. package/package.json +3 -2
@@ -1,5 +1,14 @@
1
1
  import { describe, expect, it, beforeEach } from 'vitest';
2
- import { getUrlForSearchItem } from './search-utils';
2
+ import {
3
+ applyActiveFilter,
4
+ getIndexedResultRank,
5
+ getSearchFilters,
6
+ getUrlForSearchItem,
7
+ hasMeaningfulIndexedMatch,
8
+ highlightQuery,
9
+ mapPagefindResultsToSearchItems,
10
+ normalizeResultUrl,
11
+ } from './search-utils';
3
12
 
4
13
  declare global {
5
14
  interface Window {
@@ -34,3 +43,131 @@ describe('getUrlForSearchItem', () => {
34
43
  expect(getUrlForSearchItem({}, 'unknown:Customer360:1.0.0')).toBeNull();
35
44
  });
36
45
  });
46
+
47
+ describe('indexed search result helpers', () => {
48
+ it('filters weak single-character highlighted matches for longer queries', () => {
49
+ expect(
50
+ hasMeaningfulIndexedMatch({
51
+ query: 'random',
52
+ title: 'Orders Service',
53
+ content: 'Order Metadata in api service server logs',
54
+ excerpt: 'Order Metadata in api db L -- <mark>R</mark> server',
55
+ })
56
+ ).toBe(false);
57
+ });
58
+
59
+ it('keeps short but meaningful marked terms', () => {
60
+ expect(
61
+ hasMeaningfulIndexedMatch({
62
+ query: 'db',
63
+ title: 'Orders Service',
64
+ content: 'Order Metadata in api service server logs',
65
+ excerpt: 'Order Metadata in api <mark>db</mark>',
66
+ })
67
+ ).toBe(true);
68
+ });
69
+
70
+ it('keeps exact title or content matches even without marked excerpts', () => {
71
+ expect(
72
+ hasMeaningfulIndexedMatch({
73
+ query: 'payments',
74
+ title: 'Payments Database',
75
+ content: '',
76
+ excerpt: '',
77
+ })
78
+ ).toBe(true);
79
+
80
+ expect(
81
+ hasMeaningfulIndexedMatch({
82
+ query: 'idempotency',
83
+ title: 'Payments Database',
84
+ content: 'Always include an idempotency key in payment creation.',
85
+ excerpt: '',
86
+ })
87
+ ).toBe(true);
88
+ });
89
+
90
+ it('normalizes Pagefind URLs to root-relative paths', () => {
91
+ expect(normalizeResultUrl('docs/containers/payments-db/0.0.1')).toBe('/docs/containers/payments-db/0.0.1');
92
+ expect(normalizeResultUrl('/docs/containers/payments-db/0.0.1')).toBe('/docs/containers/payments-db/0.0.1');
93
+ expect(normalizeResultUrl('https://example.com/docs')).toBe('https://example.com/docs');
94
+ });
95
+
96
+ it('highlights title matches while escaping unsafe title text', () => {
97
+ expect(highlightQuery('Payment <Service>', 'payment')).toBe('<mark>Payment</mark> &lt;Service&gt;');
98
+ });
99
+
100
+ it('ranks title matches above id/url matches and content matches', () => {
101
+ const query = 'payment';
102
+ const contentRank = getIndexedResultRank({
103
+ query,
104
+ title: 'Orders Service',
105
+ url: '/docs/services/OrdersService/1.0.0',
106
+ content: 'Handles payment retries',
107
+ });
108
+ const urlRank = getIndexedResultRank({
109
+ query,
110
+ title: 'Orders Service',
111
+ url: '/docs/services/PaymentService/1.0.0',
112
+ content: 'Handles orders',
113
+ });
114
+ const titleRank = getIndexedResultRank({
115
+ query,
116
+ title: 'Payment Service',
117
+ url: '/docs/services/PaymentService/1.0.0',
118
+ content: 'Handles orders',
119
+ });
120
+
121
+ expect(titleRank).toBeGreaterThan(urlRank);
122
+ expect(urlRank).toBeGreaterThan(contentRank);
123
+ });
124
+
125
+ it('applies grouped search filters', () => {
126
+ const items = [{ type: 'Event' }, { type: 'Command' }, { type: 'Team' }, { type: 'User' }, { type: 'Service' }];
127
+
128
+ expect(applyActiveFilter(items, 'Message')).toEqual([{ type: 'Event' }, { type: 'Command' }]);
129
+ expect(applyActiveFilter(items, 'Team')).toEqual([{ type: 'Team' }, { type: 'User' }]);
130
+ expect(applyActiveFilter(items, 'Service')).toEqual([{ type: 'Service' }]);
131
+ });
132
+
133
+ it('builds filter metadata without encoding behavior in labels', () => {
134
+ expect(getSearchFilters({ items: [], query: 'missing' })).toEqual([{ id: 'all', name: 'All (0)', count: 0 }]);
135
+ expect(getSearchFilters({ items: [{ type: 'Event' }, { type: 'Service' }], query: 'order' })).toEqual([
136
+ { id: 'all', name: 'All (2)', count: 2 },
137
+ { id: 'Service', name: 'Services (1)', count: 1 },
138
+ { id: 'Message', name: 'Messages (1)', count: 1 },
139
+ ]);
140
+ });
141
+
142
+ it('maps Pagefind results into ranked search items', async () => {
143
+ const items = await mapPagefindResultsToSearchItems({
144
+ query: 'payment',
145
+ limit: 10,
146
+ results: [
147
+ {
148
+ id: 'content',
149
+ score: 10,
150
+ data: async () => ({
151
+ url: 'docs/services/OrdersService/1.0.0',
152
+ content: 'Handles payment retries',
153
+ excerpt: 'Handles <mark>payment</mark> retries',
154
+ meta: { title: 'Orders Service', type: 'Service', id: 'OrdersService' },
155
+ }),
156
+ },
157
+ {
158
+ id: 'title',
159
+ score: 1,
160
+ data: async () => ({
161
+ url: 'docs/services/PaymentService/1.0.0',
162
+ content: 'Handles orders',
163
+ excerpt: '<mark>Payment</mark> service',
164
+ meta: { title: 'Payment Service', type: 'Service', id: 'PaymentService' },
165
+ }),
166
+ },
167
+ ],
168
+ });
169
+
170
+ expect(items.map((item) => item.id)).toEqual(['title', 'content']);
171
+ expect(items[0].url).toBe('/docs/services/PaymentService/1.0.0');
172
+ });
173
+ });
@@ -1,5 +1,49 @@
1
1
  import { buildUrl } from '@utils/url-builder';
2
2
 
3
+ export interface SearchNode {
4
+ key: string;
5
+ title: string;
6
+ badge?: string;
7
+ summary?: string;
8
+ href?: string;
9
+ icon?: string;
10
+ leftIcon?: string;
11
+ }
12
+
13
+ export interface SearchItem {
14
+ id: string;
15
+ name: string;
16
+ url: string;
17
+ type: string;
18
+ key?: string;
19
+ rawNode: {
20
+ title?: string;
21
+ badge?: string;
22
+ summary?: string;
23
+ icon?: string;
24
+ leftIcon?: string;
25
+ matchedExcerpt?: string;
26
+ };
27
+ isFavorite?: boolean;
28
+ }
29
+
30
+ export interface SearchFilter {
31
+ id: string;
32
+ name: string;
33
+ count: number;
34
+ }
35
+
36
+ interface PagefindResult {
37
+ id: string;
38
+ score?: number;
39
+ data: () => Promise<{
40
+ url: string;
41
+ excerpt?: string;
42
+ content?: string;
43
+ meta?: Record<string, any>;
44
+ }>;
45
+ }
46
+
3
47
  const docsPathByType: Record<string, string> = {
4
48
  channel: 'channels',
5
49
  command: 'commands',
@@ -32,3 +76,230 @@ export const getUrlForSearchItem = (node: { href?: string }, key: string) => {
32
76
 
33
77
  return buildUrl(`/docs/${docsPath}/${id}/${version}`);
34
78
  };
79
+
80
+ export const stripHtml = (value: string) =>
81
+ value
82
+ .replace(/<[^>]+>/g, '')
83
+ .replace(/\s+/g, ' ')
84
+ .trim();
85
+
86
+ const escapeHtml = (value: string) =>
87
+ value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
88
+
89
+ const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
90
+
91
+ const getSearchTerms = (query: string) => [...new Set(query.trim().toLowerCase().split(/\s+/).filter(Boolean))];
92
+
93
+ const getMarkedTerms = (value: string) => {
94
+ return [...value.matchAll(/<mark>(.*?)<\/mark>/gi)].map((match) => stripHtml(match[1]).toLowerCase());
95
+ };
96
+
97
+ export const highlightQuery = (value: string, query: string) => {
98
+ const terms = getSearchTerms(query).sort((a, b) => b.length - a.length);
99
+
100
+ if (terms.length === 0) {
101
+ return escapeHtml(value);
102
+ }
103
+
104
+ const termLookup = new Set(terms);
105
+ const pattern = new RegExp(`(${terms.map(escapeRegExp).join('|')})`, 'gi');
106
+
107
+ return value
108
+ .split(pattern)
109
+ .map((part) => (termLookup.has(part.toLowerCase()) ? `<mark>${escapeHtml(part)}</mark>` : escapeHtml(part)))
110
+ .join('');
111
+ };
112
+
113
+ export const normalizeResultUrl = (url: string) => {
114
+ if (/^(https?:)?\/\//.test(url) || url.startsWith('/')) {
115
+ return url;
116
+ }
117
+
118
+ return `/${url}`;
119
+ };
120
+
121
+ export const hasMeaningfulIndexedMatch = ({
122
+ query,
123
+ title,
124
+ content,
125
+ excerpt,
126
+ }: {
127
+ query: string;
128
+ title: string;
129
+ content?: string;
130
+ excerpt: string;
131
+ }) => {
132
+ const terms = getSearchTerms(query);
133
+ if (terms.length === 0) {
134
+ return false;
135
+ }
136
+
137
+ const searchableText = `${title} ${content || ''}`.toLowerCase();
138
+ if (terms.some((term) => searchableText.includes(term))) {
139
+ return true;
140
+ }
141
+
142
+ const minimumMarkedLength = Math.min(3, Math.max(...terms.map((term) => term.length)));
143
+ return getMarkedTerms(excerpt).some((term) => term.length >= minimumMarkedLength);
144
+ };
145
+
146
+ export const getIndexedResultRank = ({
147
+ query,
148
+ title,
149
+ id,
150
+ url,
151
+ content,
152
+ }: {
153
+ query: string;
154
+ title: string;
155
+ id?: string;
156
+ url: string;
157
+ content?: string;
158
+ }) => {
159
+ const terms = getSearchTerms(query);
160
+ const titleText = title.toLowerCase();
161
+ const identityText = `${id || ''} ${url}`.toLowerCase();
162
+ const contentText = (content || '').toLowerCase();
163
+
164
+ if (terms.some((term) => titleText.includes(term))) return 3;
165
+ if (terms.some((term) => identityText.includes(term))) return 2;
166
+ if (terms.some((term) => contentText.includes(term))) return 1;
167
+ return 0;
168
+ };
169
+
170
+ export const applyActiveFilter = <T extends { type: string }>(items: T[], activeFilter: string) => {
171
+ if (activeFilter === 'all') {
172
+ return items;
173
+ }
174
+
175
+ if (activeFilter === 'Message') {
176
+ return items.filter((item) => ['Event', 'Command', 'Query'].includes(item.type));
177
+ }
178
+
179
+ if (activeFilter === 'Team') {
180
+ return items.filter((item) => ['Team', 'User'].includes(item.type));
181
+ }
182
+
183
+ return items.filter((item) => item.type === activeFilter);
184
+ };
185
+
186
+ export const getSearchFilters = ({ items, query }: { items: Array<{ type: string }>; query: string }): SearchFilter[] => {
187
+ if (!items.length && query !== '') {
188
+ return [{ id: 'all', name: 'All (0)', count: 0 }];
189
+ }
190
+
191
+ const counts: Record<string, number> = {
192
+ all: items.length,
193
+ Domain: 0,
194
+ Service: 0,
195
+ Message: 0,
196
+ Team: 0,
197
+ Container: 0,
198
+ Entity: 0,
199
+ Design: 0,
200
+ Channel: 0,
201
+ Flow: 0,
202
+ 'Data Product': 0,
203
+ 'Custom Doc': 0,
204
+ 'Resource Doc': 0,
205
+ Changelog: 0,
206
+ };
207
+
208
+ items.forEach((item) => {
209
+ if (counts[item.type] !== undefined) {
210
+ counts[item.type]++;
211
+ }
212
+
213
+ if (['Event', 'Command', 'Query'].includes(item.type)) {
214
+ counts.Message++;
215
+ }
216
+
217
+ if (['Team', 'User'].includes(item.type)) {
218
+ counts.Team++;
219
+ }
220
+ });
221
+
222
+ const filters: SearchFilter[] = [{ id: 'all', name: `All (${counts.all})`, count: counts.all }];
223
+ const addFilter = (id: string, name: string) => {
224
+ if (counts[id] > 0) {
225
+ filters.push({ id, name: `${name} (${counts[id]})`, count: counts[id] });
226
+ }
227
+ };
228
+
229
+ addFilter('Domain', 'Domains');
230
+ addFilter('Service', 'Services');
231
+ addFilter('Message', 'Messages');
232
+ addFilter('Container', 'Data Stores');
233
+ addFilter('Entity', 'Entities');
234
+ addFilter('Channel', 'Channels');
235
+ addFilter('Flow', 'Flows');
236
+ addFilter('Data Product', 'Data Products');
237
+ addFilter('Custom Doc', 'Custom Docs');
238
+ addFilter('Resource Doc', 'Resource Docs');
239
+ addFilter('Changelog', 'Changelogs');
240
+ addFilter('Design', 'Designs');
241
+ addFilter('Team', 'Teams & Users');
242
+
243
+ return filters;
244
+ };
245
+
246
+ export const mapPagefindResultsToSearchItems = async ({
247
+ results,
248
+ query,
249
+ limit,
250
+ }: {
251
+ results: PagefindResult[];
252
+ query: string;
253
+ limit: number;
254
+ }): Promise<SearchItem[]> => {
255
+ const mappedResults = await Promise.all(
256
+ results.slice(0, limit).map(async (result, resultIndex) => {
257
+ const data = await result.data();
258
+ const meta = data.meta || {};
259
+ const type = meta.type || 'Page';
260
+ const title = meta.title || data.url || 'Untitled';
261
+ const summary = meta.summary || stripHtml(data.excerpt || '');
262
+ const url = normalizeResultUrl(data.url);
263
+
264
+ if (
265
+ !hasMeaningfulIndexedMatch({
266
+ query,
267
+ title,
268
+ content: data.content,
269
+ excerpt: data.excerpt || '',
270
+ })
271
+ ) {
272
+ return null;
273
+ }
274
+
275
+ return {
276
+ item: {
277
+ id: result.id,
278
+ name: title,
279
+ url,
280
+ type,
281
+ rawNode: {
282
+ title,
283
+ badge: type,
284
+ summary,
285
+ matchedExcerpt: data.excerpt || summary,
286
+ },
287
+ } satisfies SearchItem,
288
+ resultIndex,
289
+ rank: getIndexedResultRank({
290
+ query,
291
+ title,
292
+ id: meta.id,
293
+ url,
294
+ content: data.content,
295
+ }),
296
+ score: typeof result.score === 'number' ? result.score : 0,
297
+ };
298
+ })
299
+ );
300
+
301
+ return mappedResults
302
+ .filter((result): result is NonNullable<typeof result> => result !== null)
303
+ .sort((a, b) => b.rank - a.rank || b.score - a.score || a.resultIndex - b.resultIndex)
304
+ .map((result) => result.item);
305
+ };
@@ -3,6 +3,7 @@
3
3
 
4
4
  declare const __EC_TRAILING_SLASH__: boolean;
5
5
  declare const __EC_BASE__: string;
6
+ declare const __EC_SEARCH_TYPE__: 'resource' | 'indexed';
6
7
 
7
8
  interface EventCatalogConfig {
8
9
  mermaid?: {
package/package.json CHANGED
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "license": "SEE LICENSE IN LICENSE",
9
9
  "type": "module",
10
- "version": "3.35.1",
10
+ "version": "3.36.1",
11
11
  "publishConfig": {
12
12
  "access": "public"
13
13
  },
@@ -80,6 +80,7 @@
80
80
  "mdast-util-find-and-replace": "^3.0.2",
81
81
  "mermaid": "^11.12.3",
82
82
  "nanostores": "^1.1.0",
83
+ "pagefind": "^1.5.2",
83
84
  "pako": "^2.1.0",
84
85
  "picocolors": "^1.1.1",
85
86
  "react": "^18.3.1",
@@ -105,8 +106,8 @@
105
106
  "update-notifier": "^7.3.1",
106
107
  "uuid": "^10.0.0",
107
108
  "zod": "^4.3.6",
108
- "@eventcatalog/linter": "1.0.22",
109
109
  "@eventcatalog/sdk": "2.21.0",
110
+ "@eventcatalog/linter": "1.0.22",
110
111
  "@eventcatalog/visualiser": "^3.20.0"
111
112
  },
112
113
  "devDependencies": {