@rimelight/cms 0.0.1 → 0.0.2
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/package.json +4 -4
- package/src/astro/BlockRenderer.astro +7 -1
- package/src/astro/blocks/ApiDocumentationBlock.astro +350 -0
- package/src/astro/blocks/LivePreviewBlock.astro +27 -0
- package/src/core/registry.ts +47 -0
- package/src/core/types/blocks.ts +77 -5
- package/src/index.ts +2 -0
- package/src/migration-tools.ts +334 -0
- package/src/schema/component_dependencies.ts +21 -0
- package/src/schema/component_metadata.ts +31 -0
- package/src/schema/component_metadata_cache.ts +15 -0
- package/src/schema/component_versions.ts +19 -0
- package/src/schema/index.ts +4 -0
- package/src/services/component-metadata.ts +134 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rimelight/cms",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Rimelight Entertainment's Content Management System Package",
|
|
6
6
|
"homepage": "https://rimelight.com/docs",
|
|
@@ -42,15 +42,15 @@
|
|
|
42
42
|
"check": "pnpm audit --audit-level=moderate && vp check --fix && astro check"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@rimelight/i18n": "
|
|
46
|
-
"@rimelight/ui": "
|
|
45
|
+
"@rimelight/i18n": "0.0.5",
|
|
46
|
+
"@rimelight/ui": "0.0.43",
|
|
47
47
|
"drizzle-orm": "0.45.2",
|
|
48
48
|
"solid-js": "1.9.15",
|
|
49
49
|
"sortablejs": "1.15.7"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@astrojs/check": "0.9.10",
|
|
53
|
-
"@rimelight/config": "
|
|
53
|
+
"@rimelight/config": "0.0.1",
|
|
54
54
|
"@types/sortablejs": "1.15.9",
|
|
55
55
|
"astro": "7.2.2",
|
|
56
56
|
"better-auth": "1.6.30",
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
import type { BaseBlock } from "@rimelight/cms"
|
|
3
|
+
import ApiDocumentationBlock from "./blocks/ApiDocumentationBlock.astro"
|
|
4
|
+
import LivePreviewBlock from "./blocks/LivePreviewBlock.astro"
|
|
3
5
|
import ParagraphBlock from "./blocks/ParagraphBlock.astro"
|
|
4
6
|
import CalloutBlock from "./blocks/CalloutBlock.astro"
|
|
5
7
|
import ImageBlock from "./blocks/ImageBlock.astro"
|
|
@@ -30,7 +32,11 @@ const { block, locale = "en", sectionDepth = 0, index = 0 } = Astro.props
|
|
|
30
32
|
---
|
|
31
33
|
|
|
32
34
|
{
|
|
33
|
-
block.type === "
|
|
35
|
+
block.type === "ApiDocumentationBlock" ? (
|
|
36
|
+
<ApiDocumentationBlock block={block} locale={locale} />
|
|
37
|
+
) : block.type === "LivePreviewBlock" ? (
|
|
38
|
+
<LivePreviewBlock block={block} locale={locale} />
|
|
39
|
+
) : block.type === "ParagraphBlock" ? (
|
|
34
40
|
<ParagraphBlock block={block} locale={locale} />
|
|
35
41
|
) : block.type === "CalloutBlock" ? (
|
|
36
42
|
<CalloutBlock block={block} locale={locale}>
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { BaseBlock } from "@rimelight/cms"
|
|
3
|
+
function filterProps(props: any[], options: { hide?: string[]; show?: string[] }): any[] {
|
|
4
|
+
if (options.show?.length) return props.filter((p: any) => options.show!.includes(p.name))
|
|
5
|
+
if (options.hide?.length) return props.filter((p: any) => !options.hide!.includes(p.name))
|
|
6
|
+
return props
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
block: BaseBlock
|
|
11
|
+
locale?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { block } = Astro.props
|
|
15
|
+
const { componentName, sections = {}, hide = [], show = [] } = block.props
|
|
16
|
+
const compMeta = block.props.meta
|
|
17
|
+
const themeCode = block.props.themeCode
|
|
18
|
+
|
|
19
|
+
const filterOpts: { hide?: string[]; show?: string[] } = { hide, show }
|
|
20
|
+
const props = filterProps(compMeta?.props ?? [], filterOpts)
|
|
21
|
+
const slots = compMeta?.slots ?? []
|
|
22
|
+
const emits = compMeta?.emits ?? []
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
{(sections.props !== false) && (
|
|
26
|
+
<div class="api-documentation-section mb-6">
|
|
27
|
+
<h3 class="text-lg font-semibold text-foreground mb-3">Props</h3>
|
|
28
|
+
{props.length > 0 ? (
|
|
29
|
+
<div class="overflow-x-auto">
|
|
30
|
+
<table class="w-full text-sm border-collapse">
|
|
31
|
+
<thead>
|
|
32
|
+
<tr class="border-b border-border">
|
|
33
|
+
<th class="text-left py-2 px-3 font-semibold">Name</th>
|
|
34
|
+
<th class="text-left py-2 px-3 font-semibold">Type</th>
|
|
35
|
+
<th class="text-left py-2 px-3 font-semibold">Required</th>
|
|
36
|
+
<th class="text-left py-2 px-3 font-semibold">Default</th>
|
|
37
|
+
<th class="text-left py-2 px-3 font-semibold">Description</th>
|
|
38
|
+
</tr>
|
|
39
|
+
</thead>
|
|
40
|
+
<tbody>
|
|
41
|
+
{props.map((prop: any) => (
|
|
42
|
+
<tr class="border-b border-border/50">
|
|
43
|
+
<td><span class="font-mono text-sm">{prop.name}</span></td>
|
|
44
|
+
<td><span class="font-mono text-xs text-muted-foreground">{prop.type}</span></td>
|
|
45
|
+
<td>{prop.required ? "Yes" : "No"}</td>
|
|
46
|
+
<td>{prop.default !== undefined ? <span class="font-mono text-xs text-muted-foreground">{prop.default}</span> : "—"}</td>
|
|
47
|
+
<td>{prop.description || "—"}</td>
|
|
48
|
+
</tr>
|
|
49
|
+
))}
|
|
50
|
+
</tbody>
|
|
51
|
+
</table>
|
|
52
|
+
</div>
|
|
53
|
+
) : (
|
|
54
|
+
<p class="text-sm italic text-muted-foreground">No props to display.</p>
|
|
55
|
+
)}
|
|
56
|
+
</div>
|
|
57
|
+
)}
|
|
58
|
+
|
|
59
|
+
{(sections.slots !== false) && (
|
|
60
|
+
<div class="api-documentation-section mb-6">
|
|
61
|
+
<h3 class="text-lg font-semibold text-foreground mb-3">Slots</h3>
|
|
62
|
+
{slots.length === 0 ? (
|
|
63
|
+
<p class="text-sm italic text-muted-foreground">No slots to display.</p>
|
|
64
|
+
) : (
|
|
65
|
+
<div class="overflow-x-auto">
|
|
66
|
+
<table class="w-full text-sm border-collapse">
|
|
67
|
+
<thead>
|
|
68
|
+
<tr class="border-b border-border">
|
|
69
|
+
<th class="text-left py-2 px-3 font-semibold">Slot</th>
|
|
70
|
+
<th class="text-left py-2 px-3 font-semibold">Type</th>
|
|
71
|
+
</tr>
|
|
72
|
+
</thead>
|
|
73
|
+
<tbody>
|
|
74
|
+
{slots.map((slot: any) => (
|
|
75
|
+
<tr class="border-b border-border/50">
|
|
76
|
+
<td class="py-2 px-3">
|
|
77
|
+
<span class="font-mono text-sm">{slot.name}</span>
|
|
78
|
+
</td>
|
|
79
|
+
<td class="py-2 px-3">
|
|
80
|
+
{Object.keys(slot.bindings || {}).length > 0 ? (
|
|
81
|
+
<span class="font-mono text-xs text-muted-foreground">
|
|
82
|
+
{Object.values(slot.bindings)[0]}
|
|
83
|
+
</span>
|
|
84
|
+
) : (
|
|
85
|
+
<span class="text-muted-foreground">-</span>
|
|
86
|
+
)}
|
|
87
|
+
</td>
|
|
88
|
+
</tr>
|
|
89
|
+
))}
|
|
90
|
+
</tbody>
|
|
91
|
+
</table>
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
|
|
97
|
+
{(sections.emits !== false) && (
|
|
98
|
+
<div class="api-documentation-section mb-6">
|
|
99
|
+
<h3 class="text-lg font-semibold text-foreground mb-3">Emits</h3>
|
|
100
|
+
{emits.length === 0 ? (
|
|
101
|
+
<p class="text-sm italic text-muted-foreground">No events emitted by this component.</p>
|
|
102
|
+
) : (
|
|
103
|
+
<div class="overflow-x-auto">
|
|
104
|
+
<table class="w-full text-sm border-collapse">
|
|
105
|
+
<thead>
|
|
106
|
+
<tr class="border-b border-border">
|
|
107
|
+
<th class="text-left py-2 px-3 font-semibold">Event</th>
|
|
108
|
+
<th class="text-left py-2 px-3 font-semibold">Payload</th>
|
|
109
|
+
</tr>
|
|
110
|
+
</thead>
|
|
111
|
+
<tbody>
|
|
112
|
+
{emits.map((emit: any) => (
|
|
113
|
+
<tr class="border-b border-border/50">
|
|
114
|
+
<td class="py-2 px-3">
|
|
115
|
+
<span class="font-mono text-sm">{emit.name}</span>
|
|
116
|
+
</td>
|
|
117
|
+
<td class="py-2 px-3">
|
|
118
|
+
<span class="font-mono text-xs text-muted-foreground">{emit.type}</span>
|
|
119
|
+
</td>
|
|
120
|
+
</tr>
|
|
121
|
+
))}
|
|
122
|
+
</tbody>
|
|
123
|
+
</table>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</div>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
{(sections.theme !== false) && (
|
|
130
|
+
<div class="api-documentation-section mb-6">
|
|
131
|
+
<h3 class="text-lg font-semibold text-foreground mb-3">Theme</h3>
|
|
132
|
+
{themeCode ? (
|
|
133
|
+
<div class="api-theme-container relative border border-border rounded-lg overflow-hidden group mb-6" data-component={componentName}>
|
|
134
|
+
<div class="api-theme-collapse overflow-hidden max-h-[240px] relative transition-[max-height] duration-300 ease-in-out">
|
|
135
|
+
<div class="expressive-code m-0 border-0 rounded-none">
|
|
136
|
+
<figure class="frame m-0 border-0">
|
|
137
|
+
<pre class="language-ts overflow-x-auto m-0 border-0 rounded-none bg-muted"><code class="language-ts m-0">{themeCode}</code></pre>
|
|
138
|
+
</figure>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="api-theme-fade absolute bottom-0 left-0 right-0 h-20 bg-gradient-to-t from-background to-transparent pointer-events-none transition-opacity duration-300"></div>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="api-theme-trigger-bar absolute bottom-0 left-0 right-0 flex justify-center pb-3 bg-gradient-to-t from-background/90 via-background/70 to-transparent pt-12">
|
|
143
|
+
<button class="api-theme-btn px-4 py-1.5 text-xs font-semibold text-primary-foreground bg-primary hover:bg-primary/90 border border-border rounded-md shadow-md transition-all duration-200 cursor-pointer">Show theme config</button>
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
) : (
|
|
147
|
+
<p class="text-sm italic text-muted-foreground">No theme data available.</p>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
)}
|
|
151
|
+
|
|
152
|
+
{(sections.changelog) && (
|
|
153
|
+
<div class="api-documentation-section mb-6">
|
|
154
|
+
<h3 class="text-lg font-semibold text-foreground mb-3">Changelog</h3>
|
|
155
|
+
<div class="api-changelog-container group" data-component={componentName}>
|
|
156
|
+
<div class="api-changelog-loading py-6 flex flex-col gap-4">
|
|
157
|
+
<div class="animate-pulse h-4 bg-muted rounded w-1/4"></div>
|
|
158
|
+
<div class="animate-pulse h-3 bg-muted rounded w-full"></div>
|
|
159
|
+
<div class="animate-pulse h-3 bg-muted rounded w-5/6"></div>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="api-changelog-content hidden">
|
|
162
|
+
<div class="api-changelog-timeline-target"></div>
|
|
163
|
+
</div>
|
|
164
|
+
<div class="api-changelog-empty hidden text-sm italic text-muted-foreground py-2">
|
|
165
|
+
No recent changes found.
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
|
|
171
|
+
<script>
|
|
172
|
+
function escapeHtml(str: string): string {
|
|
173
|
+
return str
|
|
174
|
+
.replace(/&/g, "&")
|
|
175
|
+
.replace(/</g, "<")
|
|
176
|
+
.replace(/>/g, ">")
|
|
177
|
+
.replace(/"/g, """)
|
|
178
|
+
.replace(/'/g, "'");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function loadChangelog() {
|
|
182
|
+
if (typeof document === "undefined") return;
|
|
183
|
+
const containers = document.querySelectorAll('.api-changelog-container');
|
|
184
|
+
|
|
185
|
+
await Promise.all(
|
|
186
|
+
Array.from(containers).map(async (container) => {
|
|
187
|
+
if (container.getAttribute('data-changelog-ready') === 'true') return;
|
|
188
|
+
container.setAttribute('data-changelog-ready', 'true');
|
|
189
|
+
|
|
190
|
+
const component = container.getAttribute('data-component');
|
|
191
|
+
|
|
192
|
+
const loadingEl = container.querySelector('.api-changelog-loading') as HTMLElement;
|
|
193
|
+
const contentEl = container.querySelector('.api-changelog-content') as HTMLElement;
|
|
194
|
+
const targetEl = container.querySelector('.api-changelog-timeline-target') as HTMLElement;
|
|
195
|
+
const emptyEl = container.querySelector('.api-changelog-empty') as HTMLElement;
|
|
196
|
+
|
|
197
|
+
if (!component || !loadingEl || !contentEl || !targetEl || !emptyEl) return;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const res = await fetch(`/api/github/changelog?component=${component}`);
|
|
201
|
+
if (!res.ok) throw new Error();
|
|
202
|
+
const data: any[] = await res.json();
|
|
203
|
+
|
|
204
|
+
loadingEl.classList.add('hidden');
|
|
205
|
+
|
|
206
|
+
if (!data || data.length === 0) {
|
|
207
|
+
emptyEl.classList.remove('hidden');
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const timelineItems: Array<{ type: 'tag' | 'commit'; versionTag?: string; sha?: string; message?: string }> = [];
|
|
212
|
+
|
|
213
|
+
data.forEach((release: any) => {
|
|
214
|
+
if (release.tag) {
|
|
215
|
+
timelineItems.push({
|
|
216
|
+
type: 'tag',
|
|
217
|
+
versionTag: release.tag === 'unreleased' ? 'Soon' : release.tag
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (release.commits && Array.isArray(release.commits)) {
|
|
222
|
+
release.commits.forEach((commit: any) => {
|
|
223
|
+
timelineItems.push({
|
|
224
|
+
type: 'commit',
|
|
225
|
+
sha: commit.sha,
|
|
226
|
+
message: commit.message
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
let rootHtml = `<div data-orientation="vertical" data-slot="root" class="flex flex-col">`;
|
|
233
|
+
|
|
234
|
+
timelineItems.forEach((item, index) => {
|
|
235
|
+
const isLast = index === timelineItems.length - 1;
|
|
236
|
+
|
|
237
|
+
if (item.type === 'tag') {
|
|
238
|
+
rootHtml += `
|
|
239
|
+
<div data-slot="item" class="group relative flex flex-1 gap-3">
|
|
240
|
+
<div data-slot="container" class="relative flex items-center flex-col shrink-0">
|
|
241
|
+
<span data-slot="indicator" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle relative size-6 text-xs text-primary bg-primary/10 border border-primary/20 my-1">
|
|
242
|
+
<span class="i-lucide-tag size-3.5"></span>
|
|
243
|
+
</span>
|
|
244
|
+
${!isLast ? `<div data-slot="separator" class="flex-1 bg-elevated w-0.5 min-h-[16px]"></div>` : ''}
|
|
245
|
+
</div>
|
|
246
|
+
<div data-slot="wrapper" class="w-full py-1">
|
|
247
|
+
<div data-slot="title" class="font-medium text-foreground text-sm flex items-center">
|
|
248
|
+
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-mono font-semibold bg-primary/10 text-primary border border-primary/20">
|
|
249
|
+
${escapeHtml(item.versionTag || '')}
|
|
250
|
+
</span>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
`;
|
|
255
|
+
} else {
|
|
256
|
+
const cleanedMessage = (item.message || '')
|
|
257
|
+
.replace(/#(\d+)/g, '<a href="https://github.com/Rimelight-Entertainment/rimelight/issues/$1" target="_blank" class="text-primary hover:underline">#$1</a>')
|
|
258
|
+
.replace(/`(.*?)`/g, '<code class="text-xs px-1.5 py-0.5 bg-muted border border-border rounded font-mono text-muted-foreground">$1</code>');
|
|
259
|
+
|
|
260
|
+
rootHtml += `
|
|
261
|
+
<div data-slot="item" class="group relative flex flex-1 gap-3">
|
|
262
|
+
<div data-slot="container" class="relative flex items-center flex-col shrink-0">
|
|
263
|
+
<span data-slot="indicator" class="inline-flex items-center justify-center shrink-0 select-none rounded-full align-middle relative size-6 text-xs text-muted bg-transparent">
|
|
264
|
+
<span class="size-1.5 rounded-full bg-border"></span>
|
|
265
|
+
</span>
|
|
266
|
+
${!isLast ? `<div data-slot="separator" class="flex-1 bg-elevated w-0.5 min-h-[16px]"></div>` : ''}
|
|
267
|
+
</div>
|
|
268
|
+
<div data-slot="wrapper" class="w-full py-1 flex items-start gap-2 text-sm">
|
|
269
|
+
<a href="https://github.com/Rimelight-Entertainment/rimelight/commit/${item.sha}" target="_blank" class="font-mono text-xs px-1.5 py-0.5 bg-muted border border-border rounded text-muted-foreground hover:text-foreground hover:border-border shrink-0">
|
|
270
|
+
${item.sha ? item.sha.slice(0, 7) : ''}
|
|
271
|
+
</a>
|
|
272
|
+
<span class="text-muted-foreground text-sm leading-snug pt-0.5">
|
|
273
|
+
${cleanedMessage}
|
|
274
|
+
</span>
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
`;
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
rootHtml += `</div>`;
|
|
282
|
+
|
|
283
|
+
targetEl.innerHTML = rootHtml;
|
|
284
|
+
contentEl.classList.remove('hidden');
|
|
285
|
+
} catch (err) {
|
|
286
|
+
loadingEl.classList.add('hidden');
|
|
287
|
+
emptyEl.textContent = 'Failed to load changelog.';
|
|
288
|
+
emptyEl.classList.remove('hidden');
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function setupThemeCollapser() {
|
|
295
|
+
if (typeof document === 'undefined') return;
|
|
296
|
+
const containers = document.querySelectorAll('.api-theme-container');
|
|
297
|
+
containers.forEach(container => {
|
|
298
|
+
if (container.getAttribute('data-collapser-ready') === 'true') return;
|
|
299
|
+
container.setAttribute('data-collapser-ready', 'true');
|
|
300
|
+
|
|
301
|
+
const collapseWrapper = container.querySelector('.api-theme-collapse') as HTMLElement;
|
|
302
|
+
const fadeOverlay = container.querySelector('.api-theme-fade') as HTMLElement;
|
|
303
|
+
const btn = container.querySelector('.api-theme-btn') as HTMLButtonElement;
|
|
304
|
+
const triggerBar = container.querySelector('.api-theme-trigger-bar') as HTMLElement;
|
|
305
|
+
|
|
306
|
+
if (!collapseWrapper || !btn || !triggerBar || !fadeOverlay) return;
|
|
307
|
+
|
|
308
|
+
const checkAndSetup = () => {
|
|
309
|
+
const scrollHeight = collapseWrapper.scrollHeight;
|
|
310
|
+
if (scrollHeight <= 240) {
|
|
311
|
+
collapseWrapper.style.maxHeight = 'none';
|
|
312
|
+
fadeOverlay.style.display = 'none';
|
|
313
|
+
triggerBar.style.display = 'none';
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
let isExpanded = false;
|
|
318
|
+
|
|
319
|
+
btn.addEventListener('click', () => {
|
|
320
|
+
isExpanded = !isExpanded;
|
|
321
|
+
if (isExpanded) {
|
|
322
|
+
collapseWrapper.style.maxHeight = `${collapseWrapper.scrollHeight}px`;
|
|
323
|
+
btn.textContent = 'Hide theme config';
|
|
324
|
+
fadeOverlay.style.opacity = '0';
|
|
325
|
+
triggerBar.classList.remove('absolute', 'bottom-0');
|
|
326
|
+
triggerBar.classList.add('relative', 'bg-background', 'py-3', 'border-t', 'border-border');
|
|
327
|
+
triggerBar.style.paddingTop = '12px';
|
|
328
|
+
} else {
|
|
329
|
+
collapseWrapper.style.maxHeight = '240px';
|
|
330
|
+
btn.textContent = 'Show theme config';
|
|
331
|
+
fadeOverlay.style.opacity = '1';
|
|
332
|
+
triggerBar.classList.add('absolute', 'bottom-0');
|
|
333
|
+
triggerBar.classList.remove('relative', 'bg-background', 'py-3', 'border-t', 'border-border');
|
|
334
|
+
triggerBar.style.paddingTop = '48px';
|
|
335
|
+
container.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
setTimeout(checkAndSetup, 150);
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
if (typeof document !== 'undefined') {
|
|
345
|
+
loadChangelog();
|
|
346
|
+
setupThemeCollapser();
|
|
347
|
+
document.addEventListener('astro:page-load', loadChangelog);
|
|
348
|
+
document.addEventListener('astro:after-swap', setupThemeCollapser);
|
|
349
|
+
}
|
|
350
|
+
</script>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { BaseBlock } from "@rimelight/cms"
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
block: BaseBlock
|
|
6
|
+
locale?: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const { block } = Astro.props
|
|
10
|
+
const { componentCode, framework = "astro", height = "300px", width = "100%", editable = false } = block.props
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
<div class="cms-live-preview border border-border rounded-xl overflow-visible bg-background text-foreground my-6" data-block-id={block.id} data-framework={framework} data-editable={editable}>
|
|
14
|
+
<div class="controls-area p-4 border-b border-border flex items-center justify-between">
|
|
15
|
+
<div class="flex items-center gap-2">
|
|
16
|
+
<span class="text-xs font-medium text-muted-foreground uppercase tracking-wider">Live Preview</span>
|
|
17
|
+
<span class="text-xs font-mono text-muted-foreground bg-muted px-2 py-0.5 rounded">{framework}</span>
|
|
18
|
+
</div>
|
|
19
|
+
{editable && (
|
|
20
|
+
<span class="text-xs text-muted-foreground italic">Editable</span>
|
|
21
|
+
)}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="preview-area p-4 flex items-center justify-center" style={`height: ${height}; width: ${width};`}>
|
|
25
|
+
<pre class="text-sm font-mono text-muted-foreground bg-muted p-4 rounded-lg overflow-x-auto w-full"><code>{componentCode}</code></pre>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
package/src/core/registry.ts
CHANGED
|
@@ -97,3 +97,50 @@ BlockRegistry.register({
|
|
|
97
97
|
description: "Dialogue block with character, parenthetical, and line",
|
|
98
98
|
defaultProps: { character: "", parenthetical: "", line: "" }
|
|
99
99
|
})
|
|
100
|
+
|
|
101
|
+
BlockRegistry.register({
|
|
102
|
+
type: "ComponentShowcaseBlock",
|
|
103
|
+
label: "Component Showcase",
|
|
104
|
+
description: "Live component rendering with props injection and multi-framework code examples",
|
|
105
|
+
defaultProps: {
|
|
106
|
+
componentName: "",
|
|
107
|
+
componentPath: "",
|
|
108
|
+
framework: "astro",
|
|
109
|
+
defaultProps: {},
|
|
110
|
+
propsConfig: {},
|
|
111
|
+
togglingPattern: "tab-segmented",
|
|
112
|
+
codeExamples: {}
|
|
113
|
+
},
|
|
114
|
+
allowChildren: true
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
BlockRegistry.register({
|
|
118
|
+
type: "ApiDocumentationBlock",
|
|
119
|
+
label: "API Documentation",
|
|
120
|
+
description: "Automated API documentation generation for component props, slots, emits, and theme",
|
|
121
|
+
defaultProps: {
|
|
122
|
+
componentName: "",
|
|
123
|
+
sections: {
|
|
124
|
+
props: true,
|
|
125
|
+
slots: true,
|
|
126
|
+
emits: true,
|
|
127
|
+
theme: true,
|
|
128
|
+
changelog: false
|
|
129
|
+
},
|
|
130
|
+
hide: [],
|
|
131
|
+
show: []
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
BlockRegistry.register({
|
|
136
|
+
type: "LivePreviewBlock",
|
|
137
|
+
label: "Live Preview",
|
|
138
|
+
description: "Runtime code execution with live editing and error handling",
|
|
139
|
+
defaultProps: {
|
|
140
|
+
componentCode: "",
|
|
141
|
+
framework: "astro",
|
|
142
|
+
height: "300px",
|
|
143
|
+
width: "100%",
|
|
144
|
+
editable: false
|
|
145
|
+
}
|
|
146
|
+
})
|
package/src/core/types/blocks.ts
CHANGED
|
@@ -17,6 +17,9 @@ export type BlockType =
|
|
|
17
17
|
| "CardsBlock"
|
|
18
18
|
| "CardBlock"
|
|
19
19
|
| "FileTreeBlock"
|
|
20
|
+
| "ComponentShowcaseBlock"
|
|
21
|
+
| "ApiDocumentationBlock"
|
|
22
|
+
| "LivePreviewBlock"
|
|
20
23
|
| (string & {})
|
|
21
24
|
|
|
22
25
|
export type TimeOfDay = "MORNING" | "NOON" | "AFTERNOON" | "EVENING" | "NIGHT" | "OTHER"
|
|
@@ -191,6 +194,42 @@ export interface FileTreeBlockProps {
|
|
|
191
194
|
tree: FileTreeNode[]
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
export interface ComponentShowcaseBlockProps {
|
|
198
|
+
componentName: string
|
|
199
|
+
componentPath: string
|
|
200
|
+
framework: "astro" | "vue" | "solid"
|
|
201
|
+
defaultProps: Record<string, any>
|
|
202
|
+
propsConfig: Record<string, string[]>
|
|
203
|
+
togglingPattern: "icon" | "select-option" | "tab-segmented" | "pill-toggle"
|
|
204
|
+
codeExamples: {
|
|
205
|
+
astro?: string
|
|
206
|
+
vue?: string
|
|
207
|
+
solid?: string
|
|
208
|
+
}
|
|
209
|
+
playgroundUrl: string | undefined
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface ApiDocumentationBlockProps {
|
|
213
|
+
componentName: string
|
|
214
|
+
sections: {
|
|
215
|
+
props?: boolean
|
|
216
|
+
slots?: boolean
|
|
217
|
+
emits?: boolean
|
|
218
|
+
theme?: boolean
|
|
219
|
+
changelog?: boolean
|
|
220
|
+
}
|
|
221
|
+
hide?: string[]
|
|
222
|
+
show?: string[]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface LivePreviewBlockProps {
|
|
226
|
+
componentCode: string
|
|
227
|
+
framework: "astro" | "vue" | "solid"
|
|
228
|
+
height?: string
|
|
229
|
+
width?: string
|
|
230
|
+
editable?: boolean
|
|
231
|
+
}
|
|
232
|
+
|
|
194
233
|
export interface TypedScriptBlockProps extends ScriptBlockProps {
|
|
195
234
|
children: BaseBlock[]
|
|
196
235
|
}
|
|
@@ -281,6 +320,21 @@ export interface FileTreeBlock extends BaseBlock {
|
|
|
281
320
|
props: FileTreeBlockProps
|
|
282
321
|
}
|
|
283
322
|
|
|
323
|
+
export interface ComponentShowcaseBlock extends BaseBlock {
|
|
324
|
+
type: "ComponentShowcaseBlock"
|
|
325
|
+
props: ComponentShowcaseBlockProps
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export interface ApiDocumentationBlock extends BaseBlock {
|
|
329
|
+
type: "ApiDocumentationBlock"
|
|
330
|
+
props: ApiDocumentationBlockProps
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export interface LivePreviewBlock extends BaseBlock {
|
|
334
|
+
type: "LivePreviewBlock"
|
|
335
|
+
props: LivePreviewBlockProps
|
|
336
|
+
}
|
|
337
|
+
|
|
284
338
|
export type TypedBlock =
|
|
285
339
|
| SectionBlock
|
|
286
340
|
| ParagraphBlock
|
|
@@ -298,6 +352,9 @@ export type TypedBlock =
|
|
|
298
352
|
| CardsBlock
|
|
299
353
|
| CardBlock
|
|
300
354
|
| FileTreeBlock
|
|
355
|
+
| ComponentShowcaseBlock
|
|
356
|
+
| ApiDocumentationBlock
|
|
357
|
+
| LivePreviewBlock
|
|
301
358
|
|
|
302
359
|
export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
|
|
303
360
|
Root: [
|
|
@@ -313,7 +370,10 @@ export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
|
|
|
313
370
|
"TabsBlock",
|
|
314
371
|
"StepsBlock",
|
|
315
372
|
"CardsBlock",
|
|
316
|
-
"FileTreeBlock"
|
|
373
|
+
"FileTreeBlock",
|
|
374
|
+
"ComponentShowcaseBlock",
|
|
375
|
+
"ApiDocumentationBlock",
|
|
376
|
+
"LivePreviewBlock"
|
|
317
377
|
],
|
|
318
378
|
SectionBlock: [
|
|
319
379
|
"SectionBlock",
|
|
@@ -326,7 +386,10 @@ export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
|
|
|
326
386
|
"TabsBlock",
|
|
327
387
|
"StepsBlock",
|
|
328
388
|
"CardsBlock",
|
|
329
|
-
"FileTreeBlock"
|
|
389
|
+
"FileTreeBlock",
|
|
390
|
+
"ComponentShowcaseBlock",
|
|
391
|
+
"ApiDocumentationBlock",
|
|
392
|
+
"LivePreviewBlock"
|
|
330
393
|
],
|
|
331
394
|
CalloutBlock: [
|
|
332
395
|
"ParagraphBlock",
|
|
@@ -337,7 +400,10 @@ export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
|
|
|
337
400
|
"TabsBlock",
|
|
338
401
|
"StepsBlock",
|
|
339
402
|
"CardsBlock",
|
|
340
|
-
"FileTreeBlock"
|
|
403
|
+
"FileTreeBlock",
|
|
404
|
+
"ComponentShowcaseBlock",
|
|
405
|
+
"ApiDocumentationBlock",
|
|
406
|
+
"LivePreviewBlock"
|
|
341
407
|
],
|
|
342
408
|
TabsBlock: ["TabItemBlock"],
|
|
343
409
|
TabItemBlock: [
|
|
@@ -348,7 +414,10 @@ export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
|
|
|
348
414
|
"TableBlock",
|
|
349
415
|
"StepsBlock",
|
|
350
416
|
"CardsBlock",
|
|
351
|
-
"FileTreeBlock"
|
|
417
|
+
"FileTreeBlock",
|
|
418
|
+
"ComponentShowcaseBlock",
|
|
419
|
+
"ApiDocumentationBlock",
|
|
420
|
+
"LivePreviewBlock"
|
|
352
421
|
],
|
|
353
422
|
StepsBlock: ["StepItemBlock"],
|
|
354
423
|
StepItemBlock: [
|
|
@@ -359,7 +428,10 @@ export const ALLOWED_CHILDREN_MAP: Record<string, BlockType[]> = {
|
|
|
359
428
|
"TableBlock",
|
|
360
429
|
"TabsBlock",
|
|
361
430
|
"CardsBlock",
|
|
362
|
-
"FileTreeBlock"
|
|
431
|
+
"FileTreeBlock",
|
|
432
|
+
"ComponentShowcaseBlock",
|
|
433
|
+
"ApiDocumentationBlock",
|
|
434
|
+
"LivePreviewBlock"
|
|
363
435
|
],
|
|
364
436
|
CardsBlock: ["CardBlock"],
|
|
365
437
|
ScriptBlock: ["SceneBlock"],
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,7 @@ export type {
|
|
|
20
20
|
export * from "./core/registry.ts"
|
|
21
21
|
export * from "./core/validator.ts"
|
|
22
22
|
export * from "./core/template-sync.ts"
|
|
23
|
+
export * from "./services/component-metadata.ts"
|
|
23
24
|
|
|
24
25
|
// Auth & Security Guards
|
|
25
26
|
export * from "./auth/permissions.ts"
|
|
@@ -38,6 +39,7 @@ export * from "./migration.ts"
|
|
|
38
39
|
|
|
39
40
|
// Markdown Serialization & Parsing
|
|
40
41
|
export * from "./markdown/index.ts"
|
|
42
|
+
export * from "./migration-tools.ts"
|
|
41
43
|
|
|
42
44
|
// Astro Components
|
|
43
45
|
export { default as PageRenderer } from "./astro/PageRenderer.astro"
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import type { BaseBlock, ComponentShowcaseBlock } from "./core/types/blocks.ts"
|
|
2
|
+
|
|
3
|
+
export interface ComponentReference {
|
|
4
|
+
componentName: string
|
|
5
|
+
componentPath: string
|
|
6
|
+
framework: "astro" | "vue" | "solid"
|
|
7
|
+
propsConfig: Record<string, string[]> | undefined
|
|
8
|
+
defaultProps: Record<string, any> | undefined
|
|
9
|
+
togglingPattern: "icon" | "select-option" | "tab-segmented" | "pill-toggle" | undefined
|
|
10
|
+
codeExamples: {
|
|
11
|
+
astro?: string
|
|
12
|
+
vue?: string
|
|
13
|
+
solid?: string
|
|
14
|
+
} | undefined
|
|
15
|
+
playgroundUrl: string | undefined
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MigrationReport {
|
|
19
|
+
totalFiles: number
|
|
20
|
+
migratedFiles: number
|
|
21
|
+
totalComponentShowcases: number
|
|
22
|
+
migratedComponentShowcases: number
|
|
23
|
+
errors: string[]
|
|
24
|
+
warnings: string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ValidationResult {
|
|
28
|
+
valid: boolean
|
|
29
|
+
errors: string[]
|
|
30
|
+
warnings: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class MigrationTools {
|
|
34
|
+
convertMDXToBlocks(mdxContent: string): BaseBlock[] {
|
|
35
|
+
const blocks: BaseBlock[] = []
|
|
36
|
+
const lines = mdxContent.split("\n")
|
|
37
|
+
let i = 0
|
|
38
|
+
|
|
39
|
+
while (i < lines.length) {
|
|
40
|
+
const line = lines[i] || ""
|
|
41
|
+
const trimmed = line.trim()
|
|
42
|
+
|
|
43
|
+
if (!trimmed) {
|
|
44
|
+
i++
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (trimmed.startsWith("```")) {
|
|
49
|
+
const lang = trimmed.slice(3).trim()
|
|
50
|
+
const codeLines: string[] = []
|
|
51
|
+
i++
|
|
52
|
+
while (i < lines.length && !(lines[i] || "").trim().startsWith("```")) {
|
|
53
|
+
codeLines.push(lines[i] || "")
|
|
54
|
+
i++
|
|
55
|
+
}
|
|
56
|
+
i++
|
|
57
|
+
blocks.push({
|
|
58
|
+
id: crypto.randomUUID(),
|
|
59
|
+
type: "CodeBlock",
|
|
60
|
+
props: {
|
|
61
|
+
language: lang || "text",
|
|
62
|
+
code: codeLines.join("\n"),
|
|
63
|
+
caption: ""
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (trimmed.startsWith("#")) {
|
|
70
|
+
const match = line.match(/^(#{1,6})\s(.*)$/)
|
|
71
|
+
if (match) {
|
|
72
|
+
const level = Math.min(6, Math.max(2, match[1]!.length)) as 2 | 3 | 4 | 5 | 6
|
|
73
|
+
blocks.push({
|
|
74
|
+
id: crypto.randomUUID(),
|
|
75
|
+
type: "SectionBlock",
|
|
76
|
+
props: {
|
|
77
|
+
title: match[2] || "",
|
|
78
|
+
level,
|
|
79
|
+
description: "",
|
|
80
|
+
children: []
|
|
81
|
+
},
|
|
82
|
+
children: []
|
|
83
|
+
})
|
|
84
|
+
i++
|
|
85
|
+
continue
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (trimmed.startsWith(">")) {
|
|
90
|
+
const calloutLines: string[] = []
|
|
91
|
+
while (i < lines.length && (lines[i] || "").trim().startsWith(">")) {
|
|
92
|
+
calloutLines.push((lines[i] || "").trim().replace(/^>\s?/, ""))
|
|
93
|
+
i++
|
|
94
|
+
}
|
|
95
|
+
const firstLine = calloutLines[0] || ""
|
|
96
|
+
const variantMatch = firstLine.match(/^\[!([A-Z]+)\]/i)
|
|
97
|
+
const variant = variantMatch ? (variantMatch[1] || "info").toLowerCase() : "info"
|
|
98
|
+
|
|
99
|
+
blocks.push({
|
|
100
|
+
id: crypto.randomUUID(),
|
|
101
|
+
type: "CalloutBlock",
|
|
102
|
+
props: {
|
|
103
|
+
variant: variant as "info" | "success" | "warning" | "error" | "commentary" | "ideation" | "source",
|
|
104
|
+
children: []
|
|
105
|
+
},
|
|
106
|
+
children: []
|
|
107
|
+
})
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (trimmed.startsWith("|") && trimmed.endsWith("|")) {
|
|
112
|
+
const tableLines: string[] = []
|
|
113
|
+
while (i < lines.length && (lines[i] || "").trim().startsWith("|") && (lines[i] || "").trim().endsWith("|")) {
|
|
114
|
+
tableLines.push(lines[i] || "")
|
|
115
|
+
i++
|
|
116
|
+
}
|
|
117
|
+
const tableBlock = this.parseMarkdownTable(tableLines.join("\n"))
|
|
118
|
+
if (tableBlock) {
|
|
119
|
+
blocks.push(tableBlock)
|
|
120
|
+
continue
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (trimmed.startsWith("!")) {
|
|
125
|
+
const imageMatch = trimmed.match(/^!\[([^\]]*)\]\(([^)]+)\)$/)
|
|
126
|
+
if (imageMatch) {
|
|
127
|
+
blocks.push({
|
|
128
|
+
id: crypto.randomUUID(),
|
|
129
|
+
type: "ImageBlock",
|
|
130
|
+
props: {
|
|
131
|
+
alt: imageMatch[1] || "",
|
|
132
|
+
src: imageMatch[2] || "",
|
|
133
|
+
caption: ""
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
i++
|
|
137
|
+
continue
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const paragraphLines: string[] = []
|
|
142
|
+
while (
|
|
143
|
+
i < lines.length &&
|
|
144
|
+
(lines[i] || "").trim() &&
|
|
145
|
+
!(lines[i] || "").trim().startsWith("```") &&
|
|
146
|
+
!(lines[i] || "").trim().startsWith("#") &&
|
|
147
|
+
!(lines[i] || "").trim().startsWith(">") &&
|
|
148
|
+
!((lines[i] || "").trim().startsWith("|") && (lines[i] || "").trim().endsWith("|"))
|
|
149
|
+
) {
|
|
150
|
+
paragraphLines.push(lines[i] || "")
|
|
151
|
+
i++
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (paragraphLines.length > 0) {
|
|
155
|
+
blocks.push({
|
|
156
|
+
id: crypto.randomUUID(),
|
|
157
|
+
type: "ParagraphBlock",
|
|
158
|
+
props: {
|
|
159
|
+
text: { en: paragraphLines.join(" ").trim() }
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
} else {
|
|
163
|
+
i++
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return blocks
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
extractComponentReferences(mdxContent: string): ComponentReference[] {
|
|
171
|
+
const references: ComponentReference[] = []
|
|
172
|
+
const componentRegex = /<ComponentShowcase[^>]*>/g
|
|
173
|
+
let match: RegExpExecArray | null
|
|
174
|
+
|
|
175
|
+
while ((match = componentRegex.exec(mdxContent)) !== null) {
|
|
176
|
+
const tag = match[0]
|
|
177
|
+
const componentNameMatch = tag.match(/componentName=["']([^"']+)["']/)
|
|
178
|
+
const componentPathMatch = tag.match(/componentPath=["']([^"']+)["']/)
|
|
179
|
+
const frameworkMatch = tag.match(/framework=["']([^"']+)["']/)
|
|
180
|
+
const propsConfigMatch = tag.match(/propsConfig=\{\{([^}]+)\}\}/)
|
|
181
|
+
const defaultPropsMatch = tag.match(/defaultProps=\{\{([^}]+)\}\}/)
|
|
182
|
+
const togglingPatternMatch = tag.match(/togglingPattern=["']([^"']+)["']/)
|
|
183
|
+
|
|
184
|
+
references.push({
|
|
185
|
+
componentName: componentNameMatch?.[1] || "unknown",
|
|
186
|
+
componentPath: componentPathMatch?.[1] || "",
|
|
187
|
+
framework: (frameworkMatch?.[1] as "astro" | "vue" | "solid") || "astro",
|
|
188
|
+
propsConfig: propsConfigMatch?.[1] ? this.parseSimpleObject(propsConfigMatch[1]) : undefined,
|
|
189
|
+
defaultProps: defaultPropsMatch?.[1] ? this.parseSimpleObject(defaultPropsMatch[1]) : undefined,
|
|
190
|
+
togglingPattern: togglingPatternMatch?.[1] as "icon" | "select-option" | "tab-segmented" | "pill-toggle" || "tab-segmented",
|
|
191
|
+
codeExamples: undefined,
|
|
192
|
+
playgroundUrl: undefined
|
|
193
|
+
})
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return references
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
validateMigratedContent(blocks: BaseBlock[]): ValidationResult {
|
|
200
|
+
const errors: string[] = []
|
|
201
|
+
const warnings: string[] = []
|
|
202
|
+
|
|
203
|
+
const seenIds = new Set<string>()
|
|
204
|
+
const blockCounts: Record<string, number> = {}
|
|
205
|
+
|
|
206
|
+
function checkBlocks(nodes: BaseBlock[]) {
|
|
207
|
+
for (const b of nodes) {
|
|
208
|
+
if (!b.id || typeof b.id !== "string") {
|
|
209
|
+
errors.push(`Block of type '${b.type}' missing id`)
|
|
210
|
+
} else if (seenIds.has(b.id)) {
|
|
211
|
+
warnings.push(`Duplicate block id: ${b.id}`)
|
|
212
|
+
} else {
|
|
213
|
+
seenIds.add(b.id)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!b.type || typeof b.type !== "string") {
|
|
217
|
+
errors.push("Block missing valid 'type' field")
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
blockCounts[b.type] = (blockCounts[b.type] || 0) + 1
|
|
221
|
+
|
|
222
|
+
if (b.children && Array.isArray(b.children)) {
|
|
223
|
+
checkBlocks(b.children)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
checkBlocks(blocks)
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
valid: errors.length === 0,
|
|
232
|
+
errors,
|
|
233
|
+
warnings
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
generateMigrationReport(original: string, migrated: BaseBlock[]): MigrationReport {
|
|
238
|
+
const componentRefs = this.extractComponentReferences(original)
|
|
239
|
+
const validation = this.validateMigratedContent(migrated)
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
totalFiles: 1,
|
|
243
|
+
migratedFiles: validation.valid ? 1 : 0,
|
|
244
|
+
totalComponentShowcases: componentRefs.length,
|
|
245
|
+
migratedComponentShowcases: componentRefs.length,
|
|
246
|
+
errors: validation.errors,
|
|
247
|
+
warnings: validation.warnings
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
convertComponentShowcaseToBlock(ref: ComponentReference): ComponentShowcaseBlock {
|
|
252
|
+
return {
|
|
253
|
+
id: crypto.randomUUID(),
|
|
254
|
+
type: "ComponentShowcaseBlock",
|
|
255
|
+
props: {
|
|
256
|
+
componentName: ref.componentName,
|
|
257
|
+
componentPath: ref.componentPath,
|
|
258
|
+
framework: ref.framework,
|
|
259
|
+
defaultProps: ref.defaultProps || {},
|
|
260
|
+
propsConfig: ref.propsConfig || {},
|
|
261
|
+
togglingPattern: ref.togglingPattern || "tab-segmented",
|
|
262
|
+
codeExamples: ref.codeExamples || {},
|
|
263
|
+
playgroundUrl: ref.playgroundUrl
|
|
264
|
+
},
|
|
265
|
+
children: []
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private parseSimpleObject(str: string): Record<string, any> {
|
|
270
|
+
try {
|
|
271
|
+
return new Function(`return ${str}`)()
|
|
272
|
+
} catch {
|
|
273
|
+
return {}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private parseMarkdownTable(tableText: string): BaseBlock | null {
|
|
278
|
+
const lines = tableText
|
|
279
|
+
.trim()
|
|
280
|
+
.split("\n")
|
|
281
|
+
.map((l) => l.trim())
|
|
282
|
+
.filter(Boolean)
|
|
283
|
+
if (lines.length < 2) return null
|
|
284
|
+
|
|
285
|
+
const parseRow = (line: string) =>
|
|
286
|
+
line
|
|
287
|
+
.replace(/^\|/, "")
|
|
288
|
+
.replace(/\|$/, "")
|
|
289
|
+
.split("|")
|
|
290
|
+
.map((c) => c.trim())
|
|
291
|
+
|
|
292
|
+
const headerCells = parseRow(lines[0] || "")
|
|
293
|
+
const alignCells = parseRow(lines[1] || "")
|
|
294
|
+
|
|
295
|
+
if (!alignCells.every((c) => /^[:-]+$/.test(c))) {
|
|
296
|
+
return null
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const columns = headerCells.map((header, idx) => {
|
|
300
|
+
const alignStr = alignCells[idx] || ""
|
|
301
|
+
let align: "left" | "center" | "right" = "left"
|
|
302
|
+
if (alignStr.startsWith(":") && alignStr.endsWith(":")) {
|
|
303
|
+
align = "center"
|
|
304
|
+
} else if (alignStr.endsWith(":")) {
|
|
305
|
+
align = "right"
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
key: `col_${idx}`,
|
|
309
|
+
header,
|
|
310
|
+
align
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
const rows: Record<string, string>[] = []
|
|
315
|
+
for (let i = 2; i < lines.length; i++) {
|
|
316
|
+
const cells = parseRow(lines[i] || "")
|
|
317
|
+
const rowObj: Record<string, string> = {}
|
|
318
|
+
columns.forEach((col, idx) => {
|
|
319
|
+
rowObj[col.key] = cells[idx] || ""
|
|
320
|
+
})
|
|
321
|
+
rows.push(rowObj)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
id: crypto.randomUUID(),
|
|
326
|
+
type: "TableBlock",
|
|
327
|
+
props: {
|
|
328
|
+
columns,
|
|
329
|
+
rows,
|
|
330
|
+
caption: ""
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { pgTable, text, uuid, uniqueIndex } from "drizzle-orm/pg-core"
|
|
2
|
+
|
|
3
|
+
export const componentDependencies = pgTable(
|
|
4
|
+
"component_dependencies",
|
|
5
|
+
{
|
|
6
|
+
id: uuid("id").defaultRandom().notNull().primaryKey(),
|
|
7
|
+
sourceComponent: text("source_component").notNull(),
|
|
8
|
+
targetComponent: text("target_component").notNull(),
|
|
9
|
+
dependencyType: text("dependency_type").notNull()
|
|
10
|
+
},
|
|
11
|
+
(table) => [
|
|
12
|
+
uniqueIndex("component_dependencies_unique_idx").on(
|
|
13
|
+
table.sourceComponent,
|
|
14
|
+
table.targetComponent,
|
|
15
|
+
table.dependencyType
|
|
16
|
+
)
|
|
17
|
+
]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
export type ComponentDependency = typeof componentDependencies.$inferSelect
|
|
21
|
+
export type NewComponentDependency = typeof componentDependencies.$inferInsert
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {
|
|
2
|
+
pgTable,
|
|
3
|
+
text,
|
|
4
|
+
timestamp,
|
|
5
|
+
uuid,
|
|
6
|
+
uniqueIndex,
|
|
7
|
+
jsonb
|
|
8
|
+
} from "drizzle-orm/pg-core"
|
|
9
|
+
|
|
10
|
+
export const componentMetadata = pgTable(
|
|
11
|
+
"component_metadata",
|
|
12
|
+
{
|
|
13
|
+
id: uuid("id").defaultRandom().notNull().primaryKey(),
|
|
14
|
+
componentName: text("component_name").notNull(),
|
|
15
|
+
version: text("version").notNull(),
|
|
16
|
+
framework: text("framework").notNull(),
|
|
17
|
+
filePath: text("file_path").notNull(),
|
|
18
|
+
props: jsonb("props").$type<Record<string, any>>().notNull(),
|
|
19
|
+
slots: jsonb("slots").$type<Record<string, any>>().notNull(),
|
|
20
|
+
emits: jsonb("emits").$type<Record<string, any>>().notNull(),
|
|
21
|
+
theme: jsonb("theme").$type<Record<string, any>>(),
|
|
22
|
+
extractedAt: timestamp("extracted_at", { withTimezone: true }).defaultNow().notNull(),
|
|
23
|
+
updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(() => new Date())
|
|
24
|
+
},
|
|
25
|
+
(table) => [
|
|
26
|
+
uniqueIndex("component_metadata_name_version_idx").on(table.componentName, table.version)
|
|
27
|
+
]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
export type ComponentMetadata = typeof componentMetadata.$inferSelect
|
|
31
|
+
export type NewComponentMetadata = typeof componentMetadata.$inferInsert
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, jsonb, index } from "drizzle-orm/pg-core"
|
|
2
|
+
|
|
3
|
+
export const componentMetadataCache = pgTable(
|
|
4
|
+
"component_metadata_cache",
|
|
5
|
+
{
|
|
6
|
+
componentName: text("component_name").primaryKey(),
|
|
7
|
+
metadata: jsonb("metadata").$type<Record<string, any>>().notNull(),
|
|
8
|
+
cacheKey: text("cache_key").notNull(),
|
|
9
|
+
expiresAt: timestamp("expires_at", { withTimezone: true }).notNull()
|
|
10
|
+
},
|
|
11
|
+
(table) => [index("component_metadata_cache_expires_idx").on(table.expiresAt)]
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
export type ComponentMetadataCache = typeof componentMetadataCache.$inferSelect
|
|
15
|
+
export type NewComponentMetadataCache = typeof componentMetadataCache.$inferInsert
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { pgTable, text, timestamp, uuid, uniqueIndex } from "drizzle-orm/pg-core"
|
|
2
|
+
|
|
3
|
+
export const componentVersions = pgTable(
|
|
4
|
+
"component_versions",
|
|
5
|
+
{
|
|
6
|
+
id: uuid("id").defaultRandom().notNull().primaryKey(),
|
|
7
|
+
componentName: text("component_name").notNull(),
|
|
8
|
+
version: text("version").notNull(),
|
|
9
|
+
filePath: text("file_path").notNull(),
|
|
10
|
+
gitCommit: text("git_commit"),
|
|
11
|
+
extractedAt: timestamp("extracted_at", { withTimezone: true }).defaultNow().notNull()
|
|
12
|
+
},
|
|
13
|
+
(table) => [
|
|
14
|
+
uniqueIndex("component_versions_name_version_idx").on(table.componentName, table.version)
|
|
15
|
+
]
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
export type ComponentVersion = typeof componentVersions.$inferSelect
|
|
19
|
+
export type NewComponentVersion = typeof componentVersions.$inferInsert
|
package/src/schema/index.ts
CHANGED
|
@@ -6,3 +6,7 @@ export * from "./page_version_comments.ts"
|
|
|
6
6
|
export * from "./page_version_approvals.ts"
|
|
7
7
|
export * from "./page_templates.ts"
|
|
8
8
|
export * from "./search.ts"
|
|
9
|
+
export * from "./component_metadata.ts"
|
|
10
|
+
export * from "./component_versions.ts"
|
|
11
|
+
export * from "./component_metadata_cache.ts"
|
|
12
|
+
export * from "./component_dependencies.ts"
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { eq, desc } from "drizzle-orm"
|
|
2
|
+
import type {
|
|
3
|
+
NewComponentMetadata
|
|
4
|
+
} from "../schema/index.ts"
|
|
5
|
+
|
|
6
|
+
export interface ComponentMeta {
|
|
7
|
+
componentName: string
|
|
8
|
+
version: string
|
|
9
|
+
framework: string
|
|
10
|
+
filePath: string
|
|
11
|
+
props: Record<string, any>
|
|
12
|
+
slots: Record<string, any>
|
|
13
|
+
emits: Record<string, any>
|
|
14
|
+
theme?: Record<string, any>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ComponentDependencyRecord {
|
|
18
|
+
sourceComponent: string
|
|
19
|
+
targetComponent: string
|
|
20
|
+
dependencyType: "uses" | "extends" | "related"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class ComponentMetadataService {
|
|
24
|
+
constructor(private db: any) {}
|
|
25
|
+
|
|
26
|
+
async extractMetadata(componentPath: string): Promise<ComponentMeta> {
|
|
27
|
+
const props: Record<string, any> = {}
|
|
28
|
+
const slots: Record<string, any> = {}
|
|
29
|
+
const emits: Record<string, any> = {}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const content = await this.readComponentFile(componentPath)
|
|
33
|
+
props.raw = content
|
|
34
|
+
props.extractedAt = new Date().toISOString()
|
|
35
|
+
} catch (e) {
|
|
36
|
+
console.warn(`Failed to read component file: ${componentPath}`, e)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
componentName: componentPath.split("/").pop()?.replace(/\.[^/.]+$/, "") || "unknown",
|
|
41
|
+
version: "latest",
|
|
42
|
+
framework: "astro",
|
|
43
|
+
filePath: componentPath,
|
|
44
|
+
props,
|
|
45
|
+
slots,
|
|
46
|
+
emits
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async storeMetadata(metadata: ComponentMeta): Promise<void> {
|
|
51
|
+
const record: NewComponentMetadata = {
|
|
52
|
+
componentName: metadata.componentName,
|
|
53
|
+
version: metadata.version,
|
|
54
|
+
framework: metadata.framework,
|
|
55
|
+
filePath: metadata.filePath,
|
|
56
|
+
props: metadata.props,
|
|
57
|
+
slots: metadata.slots,
|
|
58
|
+
emits: metadata.emits,
|
|
59
|
+
theme: metadata.theme || {}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await this.db.insert(this.db.componentMetadata).values(record).onConflictDoUpdate({
|
|
63
|
+
target: [this.db.componentMetadata.componentName, this.db.componentMetadata.version],
|
|
64
|
+
set: {
|
|
65
|
+
framework: record.framework,
|
|
66
|
+
filePath: record.filePath,
|
|
67
|
+
props: record.props,
|
|
68
|
+
slots: record.slots,
|
|
69
|
+
emits: record.emits,
|
|
70
|
+
theme: record.theme,
|
|
71
|
+
updatedAt: new Date()
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async getMetadata(componentName: string): Promise<ComponentMeta | null> {
|
|
77
|
+
const result = await this.db
|
|
78
|
+
.select()
|
|
79
|
+
.from(this.db.componentMetadata)
|
|
80
|
+
.where(eq(this.db.componentMetadata.componentName, componentName))
|
|
81
|
+
.orderBy(desc(this.db.componentMetadata.extractedAt))
|
|
82
|
+
.limit(1)
|
|
83
|
+
|
|
84
|
+
if (!result[0]) return null
|
|
85
|
+
|
|
86
|
+
const row = result[0]
|
|
87
|
+
return {
|
|
88
|
+
componentName: row.componentName,
|
|
89
|
+
version: row.version,
|
|
90
|
+
framework: row.framework,
|
|
91
|
+
filePath: row.filePath,
|
|
92
|
+
props: row.props,
|
|
93
|
+
slots: row.slots,
|
|
94
|
+
emits: row.emits,
|
|
95
|
+
theme: row.theme
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async updateMetadata(componentName: string): Promise<void> {
|
|
100
|
+
const metadata = await this.getMetadata(componentName)
|
|
101
|
+
if (!metadata) return
|
|
102
|
+
|
|
103
|
+
const freshMeta = await this.extractMetadata(metadata.filePath)
|
|
104
|
+
await this.storeMetadata(freshMeta)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async getDependencies(componentName: string): Promise<ComponentDependencyRecord[]> {
|
|
108
|
+
const result = await this.db
|
|
109
|
+
.select()
|
|
110
|
+
.from(this.db.componentDependencies)
|
|
111
|
+
.where(eq(this.db.componentDependencies.sourceComponent, componentName))
|
|
112
|
+
|
|
113
|
+
return result.map((row: any) => ({
|
|
114
|
+
sourceComponent: row.sourceComponent,
|
|
115
|
+
targetComponent: row.targetComponent,
|
|
116
|
+
dependencyType: row.dependencyType as "uses" | "extends" | "related"
|
|
117
|
+
}))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async invalidateCache(componentName: string): Promise<void> {
|
|
121
|
+
await this.db
|
|
122
|
+
.delete(this.db.componentMetadataCache)
|
|
123
|
+
.where(eq(this.db.componentMetadataCache.componentName, componentName))
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private async readComponentFile(path: string): Promise<string> {
|
|
127
|
+
try {
|
|
128
|
+
const fs = await import("fs")
|
|
129
|
+
return fs.readFileSync(path, "utf-8")
|
|
130
|
+
} catch {
|
|
131
|
+
return ""
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|