@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rimelight/cms",
3
- "version": "0.0.1",
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": "workspace:*",
46
- "@rimelight/ui": "workspace:*",
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": "workspace:*",
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 === "ParagraphBlock" ? (
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, "&amp;")
175
+ .replace(/</g, "&lt;")
176
+ .replace(/>/g, "&gt;")
177
+ .replace(/"/g, "&quot;")
178
+ .replace(/'/g, "&#039;");
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>
@@ -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
+ })
@@ -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
@@ -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
+ }