@eventcatalog/core 4.0.6 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/analytics/analytics.cjs +1 -1
- package/dist/analytics/analytics.js +2 -2
- package/dist/analytics/log-build.cjs +1 -1
- package/dist/analytics/log-build.js +3 -3
- package/dist/catalog-to-astro-content-directory.cjs +32 -1
- package/dist/catalog-to-astro-content-directory.js +2 -2
- package/dist/{chunk-7443A7LZ.js → chunk-2PPDVKNK.js} +1 -1
- package/dist/{chunk-ZYWUN3T3.js → chunk-2T6ESD3L.js} +1 -1
- package/dist/{chunk-O3Y2G6CY.js → chunk-CAJUIMDJ.js} +1 -1
- package/dist/{chunk-Z7BGWB5J.js → chunk-OQ36RUHT.js} +1 -1
- package/dist/{chunk-FHVBLKWN.js → chunk-PL4YZUAA.js} +1 -1
- package/dist/{chunk-ZXAF4JO7.js → chunk-RXZKZGDC.js} +1 -1
- package/dist/{chunk-WQNKHIDM.js → chunk-W3SAPOZU.js} +13 -1
- package/dist/{chunk-VC6VUJIX.js → chunk-W5JQON7Z.js} +20 -1
- package/dist/constants.cjs +1 -1
- package/dist/constants.js +1 -1
- package/dist/eventcatalog.cjs +33 -2
- package/dist/eventcatalog.config.d.cts +8 -0
- package/dist/eventcatalog.config.d.ts +8 -0
- package/dist/eventcatalog.js +8 -8
- package/dist/generate.cjs +1 -1
- package/dist/generate.js +3 -3
- package/dist/map-catalog-to-astro.cjs +20 -1
- package/dist/map-catalog-to-astro.js +1 -1
- package/dist/utils/cli-logger.cjs +1 -1
- package/dist/utils/cli-logger.js +2 -2
- package/dist/watcher.cjs +20 -1
- package/dist/watcher.js +2 -2
- package/eventcatalog/src/components/ChatPanel/ChatPanel.tsx +6 -2
- package/eventcatalog/src/components/MDX/RemoteFile.astro +5 -0
- package/eventcatalog/src/components/SchemaExplorer/DiffViewer.tsx +7 -9
- package/eventcatalog/src/components/SchemaExplorer/SchemaDetailsPanel.tsx +127 -41
- package/eventcatalog/src/components/SchemaExplorer/VersionHistoryModal.tsx +2 -2
- package/eventcatalog/src/components/SchemaExplorer/types.ts +4 -4
- package/eventcatalog/src/enterprise/custom-pages/routes.ts +142 -0
- package/eventcatalog/src/enterprise/feature.ts +3 -0
- package/eventcatalog/src/enterprise/integrations/eventcatalog-features.ts +84 -1
- package/eventcatalog/src/pages/index.astro +1 -1
- package/eventcatalog/src/toolkit/layouts/Layout.astro +25 -0
- package/eventcatalog/src/toolkit/utils/index.ts +26 -0
- package/eventcatalog/src/utils/feature.ts +1 -0
- package/eventcatalog/src/utils/json-schema-refs.ts +158 -0
- package/eventcatalog/tsconfig.json +5 -1
- package/package.json +3 -3
|
@@ -4,6 +4,7 @@ import JSONSchemaViewer from '@components/SchemaExplorer/JSONSchemaViewer';
|
|
|
4
4
|
import { Code } from 'astro-expressive-code/components';
|
|
5
5
|
import { isPrivateRemoteSchemaEnabled } from '@utils/feature';
|
|
6
6
|
import { resolveTemplateVariables } from '@utils/remote-file';
|
|
7
|
+
import { resolveSchemaRefs } from '@utils/json-schema-refs';
|
|
7
8
|
|
|
8
9
|
const {
|
|
9
10
|
url,
|
|
@@ -89,6 +90,10 @@ try {
|
|
|
89
90
|
if (isValidJSON(content)) {
|
|
90
91
|
contentType = 'json';
|
|
91
92
|
processedData = JSON.parse(content);
|
|
93
|
+
processedData = await resolveSchemaRefs(processedData, {
|
|
94
|
+
baseUrl: String(resolvedUrl),
|
|
95
|
+
headers: resolvedHeaders as HeadersInit,
|
|
96
|
+
});
|
|
92
97
|
|
|
93
98
|
// Check if it's a JSON Schema
|
|
94
99
|
isSchema = isJSONSchema(processedData);
|
|
@@ -122,10 +122,10 @@ export default function DiffViewer({ diffs, onOpenFullscreen, apiAccessEnabled =
|
|
|
122
122
|
{isDark && <style dangerouslySetInnerHTML={{ __html: DIFF_DARK_STYLES }} />}
|
|
123
123
|
<div className="mb-5 flex items-start justify-between">
|
|
124
124
|
<div className="flex-1">
|
|
125
|
-
<h3 className="text-base font-semibold text-[rgb(var(--ec-page-text))] mb-1">Version
|
|
125
|
+
<h3 className="text-base font-semibold text-[rgb(var(--ec-page-text))] mb-1">Version Comparison</h3>
|
|
126
126
|
<p className="text-sm text-[rgb(var(--ec-page-text-muted))]">
|
|
127
127
|
{apiAccessEnabled
|
|
128
|
-
? `${diffs.length}
|
|
128
|
+
? `${diffs.length} selected comparison${diffs.length !== 1 ? 's' : ''}`
|
|
129
129
|
: 'Compare schema versions side-by-side'}
|
|
130
130
|
</p>
|
|
131
131
|
</div>
|
|
@@ -142,21 +142,19 @@ export default function DiffViewer({ diffs, onOpenFullscreen, apiAccessEnabled =
|
|
|
142
142
|
</div>
|
|
143
143
|
{apiAccessEnabled ? (
|
|
144
144
|
<div className={`space-y-6 ${isDark ? 'diff-dark-mode' : ''}`}>
|
|
145
|
-
{diffs.map((diff
|
|
145
|
+
{diffs.map((diff) => (
|
|
146
146
|
<div
|
|
147
|
-
key={`${diff.
|
|
147
|
+
key={`${diff.fromVersion}-${diff.toVersion}`}
|
|
148
148
|
className="border border-[rgb(var(--ec-page-border))] rounded-lg overflow-hidden"
|
|
149
149
|
>
|
|
150
150
|
<div className="bg-[rgb(var(--ec-content-hover))] border-b border-[rgb(var(--ec-page-border))] px-4 py-2.5">
|
|
151
151
|
<div className="flex items-center justify-between">
|
|
152
152
|
<div className="text-sm">
|
|
153
|
-
<span className="font-semibold text-[rgb(var(--ec-page-text))]">v{diff.
|
|
153
|
+
<span className="font-semibold text-[rgb(var(--ec-page-text))]">v{diff.fromVersion}</span>
|
|
154
154
|
<span className="text-[rgb(var(--ec-page-text-muted))] mx-2">→</span>
|
|
155
|
-
<span className="font-semibold text-[rgb(var(--ec-page-text))]">v{diff.
|
|
155
|
+
<span className="font-semibold text-[rgb(var(--ec-page-text))]">v{diff.toVersion}</span>
|
|
156
156
|
</div>
|
|
157
|
-
<span className="text-xs text-[rgb(var(--ec-page-text-muted))]">
|
|
158
|
-
{index === 0 ? 'Latest change' : `${index + 1} version${index + 1 !== 1 ? 's' : ''} ago`}
|
|
159
|
-
</span>
|
|
157
|
+
<span className="text-xs text-[rgb(var(--ec-page-text-muted))]">Selected comparison</span>
|
|
160
158
|
</div>
|
|
161
159
|
</div>
|
|
162
160
|
<div className="relative">
|
|
@@ -64,43 +64,70 @@ export default function SchemaDetailsPanel({
|
|
|
64
64
|
const consumers = message.data.consumers || [];
|
|
65
65
|
const filename = message.data.schemaPath?.split('/').pop() || `${message.data.id}.${ext || 'json'}`;
|
|
66
66
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return
|
|
103
|
-
|
|
67
|
+
const uniqueAvailableVersions = useMemo(
|
|
68
|
+
() =>
|
|
69
|
+
availableVersions.filter(
|
|
70
|
+
(version, index, versions) => versions.findIndex((item) => item.data.version === version.data.version) === index
|
|
71
|
+
),
|
|
72
|
+
[availableVersions]
|
|
73
|
+
);
|
|
74
|
+
const defaultToVersion = uniqueAvailableVersions[0]?.data.version || '';
|
|
75
|
+
const defaultFromVersion = uniqueAvailableVersions[1]?.data.version || defaultToVersion;
|
|
76
|
+
const [diffFromVersion, setDiffFromVersion] = useState(defaultFromVersion);
|
|
77
|
+
const [diffToVersion, setDiffToVersion] = useState(defaultToVersion);
|
|
78
|
+
const schemaResourceKey = [
|
|
79
|
+
message.collection,
|
|
80
|
+
message.data.id,
|
|
81
|
+
message.specType || '',
|
|
82
|
+
message.specFilenameWithoutExtension || message.specName || '',
|
|
83
|
+
].join(':');
|
|
84
|
+
|
|
85
|
+
useEffect(() => {
|
|
86
|
+
setDiffFromVersion(defaultFromVersion);
|
|
87
|
+
setDiffToVersion(defaultToVersion);
|
|
88
|
+
setIsDiffModalOpen(false);
|
|
89
|
+
}, [schemaResourceKey, defaultFromVersion, defaultToVersion]);
|
|
90
|
+
|
|
91
|
+
const diffFromItem = useMemo(
|
|
92
|
+
() => uniqueAvailableVersions.find((version) => version.data.version === diffFromVersion),
|
|
93
|
+
[diffFromVersion, uniqueAvailableVersions]
|
|
94
|
+
);
|
|
95
|
+
const diffToItem = useMemo(
|
|
96
|
+
() => uniqueAvailableVersions.find((version) => version.data.version === diffToVersion),
|
|
97
|
+
[diffToVersion, uniqueAvailableVersions]
|
|
98
|
+
);
|
|
99
|
+
const hasDiffFromContent = !!diffFromItem?.schemaContent?.trim();
|
|
100
|
+
const hasDiffToContent = !!diffToItem?.schemaContent?.trim();
|
|
101
|
+
const selectedDiff: VersionDiff | null = useMemo(() => {
|
|
102
|
+
if (!diffFromItem || !diffToItem) return null;
|
|
103
|
+
if (diffFromItem.data.version === diffToItem.data.version) return null;
|
|
104
|
+
if (!diffFromItem.schemaContent?.trim() || !diffToItem.schemaContent?.trim()) return null;
|
|
105
|
+
|
|
106
|
+
const diff = Diff.createTwoFilesPatch(
|
|
107
|
+
`v${diffFromItem.data.version}`,
|
|
108
|
+
`v${diffToItem.data.version}`,
|
|
109
|
+
diffFromItem.schemaContent,
|
|
110
|
+
diffToItem.schemaContent,
|
|
111
|
+
'',
|
|
112
|
+
'',
|
|
113
|
+
{ context: 3 }
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const diffHtml = html(diff, {
|
|
117
|
+
drawFileList: false,
|
|
118
|
+
matching: 'lines',
|
|
119
|
+
outputFormat: 'side-by-side',
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
fromVersion: diffFromItem.data.version,
|
|
124
|
+
toVersion: diffToItem.data.version,
|
|
125
|
+
diffHtml,
|
|
126
|
+
fromContent: diffFromItem.schemaContent,
|
|
127
|
+
toContent: diffToItem.schemaContent,
|
|
128
|
+
};
|
|
129
|
+
}, [diffFromItem, diffToItem]);
|
|
130
|
+
const selectedDiffs = selectedDiff ? [selectedDiff] : [];
|
|
104
131
|
|
|
105
132
|
// Check if this is a JSON schema
|
|
106
133
|
const parsedSchema = useMemo(() => {
|
|
@@ -212,7 +239,7 @@ export default function SchemaDetailsPanel({
|
|
|
212
239
|
if (examples.length > 0) {
|
|
213
240
|
tabs.push({ id: 'examples', label: 'Usage Examples', icon: <BookOpenIcon className="h-3.5 w-3.5" /> });
|
|
214
241
|
}
|
|
215
|
-
if (
|
|
242
|
+
if (hasMultipleVersions) {
|
|
216
243
|
tabs.push({ id: 'diff', label: 'Changes', icon: <ClockIcon className="h-3.5 w-3.5" /> });
|
|
217
244
|
}
|
|
218
245
|
tabs.push({ id: 'api', label: 'API', icon: <GlobeAltIcon className="h-3.5 w-3.5" /> });
|
|
@@ -341,8 +368,67 @@ export default function SchemaDetailsPanel({
|
|
|
341
368
|
copiedId={copiedId}
|
|
342
369
|
apiAccessEnabled={apiAccessEnabled}
|
|
343
370
|
/>
|
|
344
|
-
) : activeTab === 'diff' &&
|
|
345
|
-
<
|
|
371
|
+
) : activeTab === 'diff' && hasMultipleVersions ? (
|
|
372
|
+
<div className="flex h-full min-h-0 flex-col overflow-hidden">
|
|
373
|
+
<div className="mb-4 flex flex-shrink-0 flex-col gap-3 rounded-lg border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-content-hover)/0.45)] p-3 sm:flex-row sm:items-end sm:justify-between">
|
|
374
|
+
<div className="grid flex-1 grid-cols-1 gap-3 sm:grid-cols-2">
|
|
375
|
+
<label className="flex flex-col gap-1.5">
|
|
376
|
+
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-[rgb(var(--ec-page-text-muted))]">
|
|
377
|
+
From
|
|
378
|
+
</span>
|
|
379
|
+
<select
|
|
380
|
+
value={diffFromVersion}
|
|
381
|
+
onChange={(event) => setDiffFromVersion(event.target.value)}
|
|
382
|
+
className="h-9 rounded-md border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-sm font-mono tabular-nums text-[rgb(var(--ec-page-text))] outline-hidden transition-colors focus:border-[rgb(var(--ec-accent))] focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
|
|
383
|
+
>
|
|
384
|
+
{uniqueAvailableVersions.map((version) => (
|
|
385
|
+
<option key={`from-${version.data.version}`} value={version.data.version}>
|
|
386
|
+
v{version.data.version}
|
|
387
|
+
</option>
|
|
388
|
+
))}
|
|
389
|
+
</select>
|
|
390
|
+
</label>
|
|
391
|
+
<label className="flex flex-col gap-1.5">
|
|
392
|
+
<span className="text-[11px] font-medium uppercase tracking-[0.14em] text-[rgb(var(--ec-page-text-muted))]">
|
|
393
|
+
To
|
|
394
|
+
</span>
|
|
395
|
+
<select
|
|
396
|
+
value={diffToVersion}
|
|
397
|
+
onChange={(event) => setDiffToVersion(event.target.value)}
|
|
398
|
+
className="h-9 rounded-md border border-[rgb(var(--ec-dropdown-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-sm font-mono tabular-nums text-[rgb(var(--ec-page-text))] outline-hidden transition-colors focus:border-[rgb(var(--ec-accent))] focus:ring-1 focus:ring-[rgb(var(--ec-accent)/0.3)]"
|
|
399
|
+
>
|
|
400
|
+
{uniqueAvailableVersions.map((version) => (
|
|
401
|
+
<option key={`to-${version.data.version}`} value={version.data.version}>
|
|
402
|
+
v{version.data.version}
|
|
403
|
+
</option>
|
|
404
|
+
))}
|
|
405
|
+
</select>
|
|
406
|
+
</label>
|
|
407
|
+
</div>
|
|
408
|
+
<button
|
|
409
|
+
type="button"
|
|
410
|
+
onClick={() => setIsDiffModalOpen(true)}
|
|
411
|
+
disabled={!selectedDiff}
|
|
412
|
+
className="inline-flex h-9 items-center justify-center gap-1.5 rounded-md border border-[rgb(var(--ec-page-border))] bg-[rgb(var(--ec-dropdown-bg))] px-3 text-xs font-medium text-[rgb(var(--ec-page-text-muted))] transition-colors hover:bg-[rgb(var(--ec-content-hover))] hover:text-[rgb(var(--ec-page-text))] disabled:cursor-not-allowed disabled:opacity-50"
|
|
413
|
+
>
|
|
414
|
+
<ArrowTopRightOnSquareIcon className="h-3.5 w-3.5" />
|
|
415
|
+
Expand
|
|
416
|
+
</button>
|
|
417
|
+
</div>
|
|
418
|
+
<div className="min-h-0 flex-1 overflow-hidden">
|
|
419
|
+
{diffFromVersion === diffToVersion ? (
|
|
420
|
+
<div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
|
|
421
|
+
<p className="text-sm">Select two different versions</p>
|
|
422
|
+
</div>
|
|
423
|
+
) : !hasDiffFromContent || !hasDiffToContent ? (
|
|
424
|
+
<div className="flex h-full items-center justify-center text-[rgb(var(--ec-page-text-muted))]">
|
|
425
|
+
<p className="text-sm">No schema content available</p>
|
|
426
|
+
</div>
|
|
427
|
+
) : (
|
|
428
|
+
<DiffViewer diffs={selectedDiffs} apiAccessEnabled={apiAccessEnabled} />
|
|
429
|
+
)}
|
|
430
|
+
</div>
|
|
431
|
+
</div>
|
|
346
432
|
) : (
|
|
347
433
|
<SchemaContentViewer
|
|
348
434
|
message={message}
|
|
@@ -507,7 +593,7 @@ export default function SchemaDetailsPanel({
|
|
|
507
593
|
<VersionHistoryModal
|
|
508
594
|
isOpen={isDiffModalOpen}
|
|
509
595
|
onOpenChange={setIsDiffModalOpen}
|
|
510
|
-
diffs={
|
|
596
|
+
diffs={selectedDiffs}
|
|
511
597
|
messageName={message.data.name}
|
|
512
598
|
apiAccessEnabled={apiAccessEnabled}
|
|
513
599
|
/>
|
|
@@ -28,7 +28,7 @@ export default function VersionHistoryModal({
|
|
|
28
28
|
<div className="flex items-center gap-3">
|
|
29
29
|
<ArrowsPointingOutIcon className="h-6 w-6 text-[rgb(var(--ec-icon-color))]" />
|
|
30
30
|
<div>
|
|
31
|
-
<Dialog.Title className="text-xl font-semibold text-[rgb(var(--ec-page-text))]">Version
|
|
31
|
+
<Dialog.Title className="text-xl font-semibold text-[rgb(var(--ec-page-text))]">Version Comparison</Dialog.Title>
|
|
32
32
|
<Dialog.Description className="text-sm text-[rgb(var(--ec-page-text-muted))] mt-1">
|
|
33
33
|
{messageName}
|
|
34
34
|
</Dialog.Description>
|
|
@@ -51,7 +51,7 @@ export default function VersionHistoryModal({
|
|
|
51
51
|
<DiffViewer diffs={diffs} apiAccessEnabled={apiAccessEnabled} />
|
|
52
52
|
) : (
|
|
53
53
|
<div className="flex items-center justify-center h-full">
|
|
54
|
-
<p className="text-[rgb(var(--ec-page-text-muted))] text-center">No version
|
|
54
|
+
<p className="text-[rgb(var(--ec-page-text-muted))] text-center">No version comparison available</p>
|
|
55
55
|
</div>
|
|
56
56
|
)}
|
|
57
57
|
</div>
|
|
@@ -45,9 +45,9 @@ export interface SchemaItem {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export interface VersionDiff {
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
fromVersion: string;
|
|
49
|
+
toVersion: string;
|
|
50
50
|
diffHtml: string;
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
fromContent: string;
|
|
52
|
+
toContent: string;
|
|
53
53
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Licensed under the EventCatalog Commercial License.
|
|
3
|
+
* See /packages/core/eventcatalog/src/enterprise/LICENSE
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
export interface CustomPageRoute {
|
|
10
|
+
/** Route pattern including the configured prefix, e.g. /custom/reports/[id] */
|
|
11
|
+
pattern: string;
|
|
12
|
+
/** File path relative to the custom pages directory */
|
|
13
|
+
file: string;
|
|
14
|
+
type: 'page' | 'endpoint';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_CUSTOM_PAGES_PREFIX = 'custom';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Tracks the routable files during dev so route injection re-runs when files
|
|
21
|
+
* are added or removed. Never a route itself.
|
|
22
|
+
*/
|
|
23
|
+
export const CUSTOM_PAGES_MANIFEST_FILENAME = '.routes-manifest.json';
|
|
24
|
+
|
|
25
|
+
const CUSTOM_PAGE_EXTENSIONS = /\.(astro|ts|js|mjs|json)$/i;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Top-level route segments owned by EventCatalog. The custom pages prefix
|
|
29
|
+
* cannot start with any of these — everything else is collision-free because
|
|
30
|
+
* all user routes live under the prefix.
|
|
31
|
+
*/
|
|
32
|
+
const RESERVED_PREFIXES = [
|
|
33
|
+
'api',
|
|
34
|
+
'api-catalog',
|
|
35
|
+
'architecture',
|
|
36
|
+
'auth',
|
|
37
|
+
'diagrams',
|
|
38
|
+
'directory',
|
|
39
|
+
'discover',
|
|
40
|
+
'docs',
|
|
41
|
+
'icepanel',
|
|
42
|
+
'miro',
|
|
43
|
+
'rss',
|
|
44
|
+
'schemas',
|
|
45
|
+
'settings',
|
|
46
|
+
'studio',
|
|
47
|
+
'unauthorized',
|
|
48
|
+
'visualiser',
|
|
49
|
+
'.well-known',
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Normalizes and validates the configured prefix (e.g. 'custom', 'internal/tools').
|
|
54
|
+
* Throws when the prefix is empty, contains unsafe characters or shadows a core route.
|
|
55
|
+
*/
|
|
56
|
+
export const resolveCustomPagesPrefix = (prefix: string | undefined): string => {
|
|
57
|
+
const normalized = (prefix ?? DEFAULT_CUSTOM_PAGES_PREFIX).replace(/^\/+|\/+$/g, '');
|
|
58
|
+
|
|
59
|
+
if (normalized.length === 0) {
|
|
60
|
+
throw new Error('[EventCatalog] pages.prefix cannot be empty.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!/^[a-zA-Z0-9-_]+(\/[a-zA-Z0-9-_]+)*$/.test(normalized)) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
`[EventCatalog] pages.prefix "${prefix}" is invalid. Use URL-safe characters (letters, numbers, - and _), optionally separated by "/".`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const firstSegment = normalized.split('/')[0].toLowerCase();
|
|
70
|
+
if (RESERVED_PREFIXES.includes(firstSegment)) {
|
|
71
|
+
throw new Error(`[EventCatalog] pages.prefix "${prefix}" is reserved by EventCatalog. Choose a different prefix.`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return normalized;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Derives an Astro route pattern from a file path relative to the custom pages
|
|
79
|
+
* directory. Dynamic segments ([id], [...slug]) pass through untouched.
|
|
80
|
+
*/
|
|
81
|
+
export const deriveRoutePattern = (relativeFilePath: string, prefix: string): string => {
|
|
82
|
+
const withoutExtension = relativeFilePath.replace(CUSTOM_PAGE_EXTENSIONS, '');
|
|
83
|
+
const segments = withoutExtension.split(/[\\/]/).filter(Boolean);
|
|
84
|
+
|
|
85
|
+
if (segments[segments.length - 1] === 'index') {
|
|
86
|
+
segments.pop();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return `/${[prefix, ...segments].join('/')}`;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const walkDirectory = (directory: string, relativeTo: string): string[] => {
|
|
93
|
+
const files: string[] = [];
|
|
94
|
+
|
|
95
|
+
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
96
|
+
const fullPath = path.join(directory, entry.name);
|
|
97
|
+
if (entry.isDirectory()) {
|
|
98
|
+
files.push(...walkDirectory(fullPath, relativeTo));
|
|
99
|
+
} else if (entry.isFile()) {
|
|
100
|
+
files.push(path.relative(relativeTo, fullPath));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return files;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Lists the routable code files in the custom pages directory (sorted, relative paths).
|
|
109
|
+
* Underscore-prefixed files and directories follow the Astro convention: they are
|
|
110
|
+
* never routable (partials, helpers, colocated components).
|
|
111
|
+
*/
|
|
112
|
+
export const listCustomPageFiles = (customPagesDir: string): string[] => {
|
|
113
|
+
if (!fs.existsSync(customPagesDir)) return [];
|
|
114
|
+
|
|
115
|
+
return walkDirectory(customPagesDir, customPagesDir)
|
|
116
|
+
.filter((file) => CUSTOM_PAGE_EXTENSIONS.test(file))
|
|
117
|
+
.filter((file) => file !== CUSTOM_PAGES_MANIFEST_FILENAME)
|
|
118
|
+
.filter((file) => !file.split(path.sep).some((segment) => segment.startsWith('_')))
|
|
119
|
+
.sort();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Reads the custom pages directory and returns the routes to inject.
|
|
124
|
+
*/
|
|
125
|
+
export const getCustomPageRoutes = (customPagesDir: string, prefix: string): CustomPageRoute[] => {
|
|
126
|
+
const routes: CustomPageRoute[] = [];
|
|
127
|
+
|
|
128
|
+
for (const file of listCustomPageFiles(customPagesDir)) {
|
|
129
|
+
// pages/homepage.astro is the custom landing page, rendered by src/pages/index.astro
|
|
130
|
+
if (file === 'homepage.astro') continue;
|
|
131
|
+
|
|
132
|
+
routes.push({
|
|
133
|
+
pattern: deriveRoutePattern(file, prefix),
|
|
134
|
+
file,
|
|
135
|
+
type: file.toLowerCase().endsWith('.astro') ? 'page' : 'endpoint',
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return routes;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export const isApiRoute = (route: CustomPageRoute): boolean => route.file.split(path.sep)[0] === 'api';
|
|
@@ -53,6 +53,9 @@ export const isEventCatalogUpgradeEnabled = () => !isEventCatalogStarterEnabled(
|
|
|
53
53
|
// Custom landing pages are always enabled now.
|
|
54
54
|
export const isCustomLandingPageEnabled = () => true;
|
|
55
55
|
|
|
56
|
+
// User-defined pages and API routes (pages/*.astro, pages/api/*.ts)
|
|
57
|
+
export const isCustomPagesEnabled = () => isEventCatalogScaleEnabled();
|
|
58
|
+
|
|
56
59
|
export const isAuthEnabled = () => {
|
|
57
60
|
const isAuthEnabledInCatalog = config?.auth?.enabled ?? false;
|
|
58
61
|
const directory = process.env.PROJECT_DIR || process.cwd();
|
|
@@ -4,23 +4,80 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { AstroIntegration } from 'astro';
|
|
7
|
+
import fs from 'node:fs';
|
|
7
8
|
import path from 'path';
|
|
8
9
|
import config from '../../../eventcatalog.config.js';
|
|
9
10
|
import {
|
|
10
11
|
isEventCatalogChatEnabled,
|
|
11
12
|
isAuthEnabled,
|
|
12
13
|
isEventCatalogScaleEnabled,
|
|
13
|
-
isEventCatalogStarterEnabled,
|
|
14
14
|
isEventCatalogMCPEnabled,
|
|
15
15
|
isEventCatalogMCPAuthEnabled,
|
|
16
16
|
isFullCatalogAPIEnabled,
|
|
17
17
|
isDevMode,
|
|
18
18
|
isIntegrationsEnabled,
|
|
19
19
|
isCustomDocsEnabled,
|
|
20
|
+
isCustomPagesEnabled,
|
|
20
21
|
isSSR,
|
|
21
22
|
} from '../../utils/feature';
|
|
23
|
+
import {
|
|
24
|
+
CUSTOM_PAGES_MANIFEST_FILENAME,
|
|
25
|
+
getCustomPageRoutes,
|
|
26
|
+
isApiRoute,
|
|
27
|
+
listCustomPageFiles,
|
|
28
|
+
resolveCustomPagesPrefix,
|
|
29
|
+
} from '../custom-pages/routes';
|
|
22
30
|
|
|
23
31
|
const catalogDirectory = process.env.CATALOG_DIR || process.cwd();
|
|
32
|
+
const customPagesDirectory = path.join(catalogDirectory, 'src/custom-pages');
|
|
33
|
+
|
|
34
|
+
// Routes are injected at astro:config:setup, so adding or removing a custom page
|
|
35
|
+
// file needs a full Astro restart before its route exists. This manifest is
|
|
36
|
+
// registered via addWatchFile — rewriting it with a changed file list triggers
|
|
37
|
+
// that restart (a Vite-level server.restart() does NOT re-run route injection).
|
|
38
|
+
const customPagesManifest = path.join(customPagesDirectory, CUSTOM_PAGES_MANIFEST_FILENAME);
|
|
39
|
+
|
|
40
|
+
const writeCustomPagesManifest = () => {
|
|
41
|
+
const content = JSON.stringify(listCustomPageFiles(customPagesDirectory));
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (fs.existsSync(customPagesManifest) && fs.readFileSync(customPagesManifest, 'utf-8') === content) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fs.mkdirSync(customPagesDirectory, { recursive: true });
|
|
49
|
+
fs.writeFileSync(customPagesManifest, content);
|
|
50
|
+
} catch (error: any) {
|
|
51
|
+
console.warn(`[EventCatalog] Could not update the custom pages manifest: ${error.message}`);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const configureCustomPages = (params: {
|
|
56
|
+
command: 'dev' | 'build' | 'preview' | 'sync';
|
|
57
|
+
injectRoute: (route: { pattern: string; entrypoint: string }) => void;
|
|
58
|
+
}) => {
|
|
59
|
+
const prefix = resolveCustomPagesPrefix(config.pages?.prefix);
|
|
60
|
+
const routes = getCustomPageRoutes(customPagesDirectory, prefix);
|
|
61
|
+
|
|
62
|
+
const apiRoutes = routes.filter(isApiRoute);
|
|
63
|
+
if (apiRoutes.length > 0 && !isSSR()) {
|
|
64
|
+
const message = `Custom API routes (${apiRoutes.map((route) => `pages/${route.file}`).join(', ')}) require EventCatalog to run in server mode. Set output: 'server' in your eventcatalog.config.js or remove the pages/api directory.`;
|
|
65
|
+
|
|
66
|
+
// The dev server serves endpoints dynamically regardless of output mode,
|
|
67
|
+
// so only fail the build — but warn early during development.
|
|
68
|
+
if (params.command === 'build') {
|
|
69
|
+
throw new Error(`[EventCatalog] ${message}`);
|
|
70
|
+
}
|
|
71
|
+
console.warn(`[EventCatalog] ${message}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const route of routes) {
|
|
75
|
+
params.injectRoute({
|
|
76
|
+
pattern: route.pattern,
|
|
77
|
+
entrypoint: path.join(customPagesDirectory, route.file),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
};
|
|
24
81
|
|
|
25
82
|
const configureAuthentication = (params: {
|
|
26
83
|
injectRoute: (route: { pattern: string; entrypoint: string }) => void;
|
|
@@ -148,6 +205,18 @@ export default function eventCatalogIntegration(): AstroIntegration {
|
|
|
148
205
|
});
|
|
149
206
|
}
|
|
150
207
|
|
|
208
|
+
// User-defined pages and API routes (Scale plan)
|
|
209
|
+
if (isCustomPagesEnabled()) {
|
|
210
|
+
configureCustomPages(params);
|
|
211
|
+
} else if (listCustomPageFiles(customPagesDirectory).length > 0) {
|
|
212
|
+
console.warn('[EventCatalog] Custom pages require the Scale plan. The routes for your pages will not be served.');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Keep routes in sync during dev — a manifest change restarts the dev
|
|
216
|
+
// server so injectRoute runs again with the new file list
|
|
217
|
+
writeCustomPagesManifest();
|
|
218
|
+
params.addWatchFile(customPagesManifest);
|
|
219
|
+
|
|
151
220
|
// Warn if integrations are configured without Scale plan
|
|
152
221
|
if (config.integrations && !isIntegrationsEnabled()) {
|
|
153
222
|
console.warn('[EventCatalog] Integrations require the Scale plan. Analytics integrations will not be loaded.');
|
|
@@ -165,6 +234,20 @@ export default function eventCatalogIntegration(): AstroIntegration {
|
|
|
165
234
|
});
|
|
166
235
|
}
|
|
167
236
|
},
|
|
237
|
+
'astro:server:setup': ({ server }) => {
|
|
238
|
+
// When custom page files appear or disappear, refresh the manifest.
|
|
239
|
+
// A content change restarts the dev server (via addWatchFile above),
|
|
240
|
+
// which re-runs route injection. Unchanged content writes nothing,
|
|
241
|
+
// so events from the watcher's initial scan are no-ops.
|
|
242
|
+
const onCustomPageChange = (file: string) => {
|
|
243
|
+
if (!path.resolve(file).startsWith(customPagesDirectory + path.sep)) return;
|
|
244
|
+
if (!/\.(astro|ts|js|mjs)$/i.test(file)) return;
|
|
245
|
+
writeCustomPagesManifest();
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
server.watcher.on('add', onCustomPageChange);
|
|
249
|
+
server.watcher.on('unlink', onCustomPageChange);
|
|
250
|
+
},
|
|
168
251
|
},
|
|
169
252
|
};
|
|
170
253
|
}
|
|
@@ -21,7 +21,7 @@ const props = Astro.props;
|
|
|
21
21
|
const pathToUserDefinedLandingPage = path.join(process.env.PROJECT_DIR || '', 'pages/homepage.astro');
|
|
22
22
|
|
|
23
23
|
if (existsSync(pathToUserDefinedLandingPage) && isCustomLandingPageEnabled()) {
|
|
24
|
-
const customPages = import.meta.glob('
|
|
24
|
+
const customPages = import.meta.glob('/src/custom-pages/homepage.astro', { eager: true });
|
|
25
25
|
// @ts-ignore
|
|
26
26
|
CustomContent = Object.values(customPages)[0]?.default || null;
|
|
27
27
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* Public layout shell for user-defined custom pages, imported by catalog
|
|
4
|
+
* authors as `@catalog/layouts/Layout.astro`.
|
|
5
|
+
*
|
|
6
|
+
* The props here are a documented, stable interface — internal layouts can
|
|
7
|
+
* change freely, this contract cannot.
|
|
8
|
+
*/
|
|
9
|
+
interface Props {
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
/** Render the resource sidebar (domains, services, messages). Defaults to true. */
|
|
13
|
+
sidebar?: boolean;
|
|
14
|
+
/** Show the EventCatalog header (search, navigation). Defaults to true. */
|
|
15
|
+
showHeader?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
import VerticalSideBarLayout from '@layouts/VerticalSideBarLayout.astro';
|
|
19
|
+
|
|
20
|
+
const { title, description, sidebar = true, showHeader = true } = Astro.props;
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<VerticalSideBarLayout title={title} description={description} showHeader={showHeader} showNestedSideBar={sidebar}>
|
|
24
|
+
<slot />
|
|
25
|
+
</VerticalSideBarLayout>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public utilities for user-defined custom pages, imported by catalog
|
|
3
|
+
* authors as `@catalog/utils`.
|
|
4
|
+
*
|
|
5
|
+
* These re-exports are a documented, stable interface — internal utils can
|
|
6
|
+
* change freely, this contract cannot. Getters return hydrated, cached
|
|
7
|
+
* collection entries (pass { getAllVersions: false } for latest versions only).
|
|
8
|
+
*/
|
|
9
|
+
export { getDomains } from '@utils/collections/domains';
|
|
10
|
+
export { getServices } from '@utils/collections/services';
|
|
11
|
+
export { getSystems } from '@utils/collections/systems';
|
|
12
|
+
export { getEvents } from '@utils/collections/events';
|
|
13
|
+
export { getCommands } from '@utils/collections/commands';
|
|
14
|
+
export { getQueries } from '@utils/collections/queries';
|
|
15
|
+
export { getFlows } from '@utils/collections/flows';
|
|
16
|
+
export { getChannels } from '@utils/collections/channels';
|
|
17
|
+
export { getEntities } from '@utils/collections/entities';
|
|
18
|
+
export { getAgents } from '@utils/collections/agents';
|
|
19
|
+
export { getContainers } from '@utils/collections/containers';
|
|
20
|
+
export { getDataProducts } from '@utils/collections/data-products';
|
|
21
|
+
export { getAdrs } from '@utils/collections/adrs';
|
|
22
|
+
export { getTeams } from '@utils/collections/teams';
|
|
23
|
+
export { getUsers } from '@utils/collections/users';
|
|
24
|
+
|
|
25
|
+
// Resolve a specific version (semver) or the latest version of items in a collection
|
|
26
|
+
export { getItemsFromCollectionByIdAndSemverOrLatest } from '@utils/collections/util';
|