@nuasite/cms 0.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/README.md +237 -0
- package/dist/src/build-processor.d.ts +20 -0
- package/dist/src/build-processor.d.ts.map +1 -0
- package/dist/src/collection-scanner.d.ts +6 -0
- package/dist/src/collection-scanner.d.ts.map +1 -0
- package/dist/src/component-registry.d.ts +63 -0
- package/dist/src/component-registry.d.ts.map +1 -0
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/dev-middleware.d.ts +20 -0
- package/dist/src/dev-middleware.d.ts.map +1 -0
- package/dist/src/editor/ai.d.ts +60 -0
- package/dist/src/editor/ai.d.ts.map +1 -0
- package/dist/src/editor/api.d.ts +140 -0
- package/dist/src/editor/api.d.ts.map +1 -0
- package/dist/src/editor/color-utils.d.ts +106 -0
- package/dist/src/editor/color-utils.d.ts.map +1 -0
- package/dist/src/editor/components/ai-chat.d.ts +11 -0
- package/dist/src/editor/components/ai-chat.d.ts.map +1 -0
- package/dist/src/editor/components/ai-tooltip.d.ts +12 -0
- package/dist/src/editor/components/ai-tooltip.d.ts.map +1 -0
- package/dist/src/editor/components/attribute-editor.d.ts +5 -0
- package/dist/src/editor/components/attribute-editor.d.ts.map +1 -0
- package/dist/src/editor/components/block-editor.d.ts +12 -0
- package/dist/src/editor/components/block-editor.d.ts.map +1 -0
- package/dist/src/editor/components/collections-browser.d.ts +2 -0
- package/dist/src/editor/components/collections-browser.d.ts.map +1 -0
- package/dist/src/editor/components/color-toolbar.d.ts +12 -0
- package/dist/src/editor/components/color-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/confirm-dialog.d.ts +2 -0
- package/dist/src/editor/components/confirm-dialog.d.ts.map +1 -0
- package/dist/src/editor/components/create-page-modal.d.ts +2 -0
- package/dist/src/editor/components/create-page-modal.d.ts.map +1 -0
- package/dist/src/editor/components/editable-highlights.d.ts +9 -0
- package/dist/src/editor/components/editable-highlights.d.ts.map +1 -0
- package/dist/src/editor/components/error-boundary.d.ts +32 -0
- package/dist/src/editor/components/error-boundary.d.ts.map +1 -0
- package/dist/src/editor/components/fields.d.ts +75 -0
- package/dist/src/editor/components/fields.d.ts.map +1 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts +29 -0
- package/dist/src/editor/components/frontmatter-fields.d.ts.map +1 -0
- package/dist/src/editor/components/highlight-overlay.d.ts +64 -0
- package/dist/src/editor/components/highlight-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/image-overlay.d.ts +12 -0
- package/dist/src/editor/components/image-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts +6 -0
- package/dist/src/editor/components/markdown-editor-overlay.d.ts.map +1 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts +10 -0
- package/dist/src/editor/components/markdown-inline-editor.d.ts.map +1 -0
- package/dist/src/editor/components/media-library.d.ts +2 -0
- package/dist/src/editor/components/media-library.d.ts.map +1 -0
- package/dist/src/editor/components/outline.d.ts +21 -0
- package/dist/src/editor/components/outline.d.ts.map +1 -0
- package/dist/src/editor/components/redirect-countdown.d.ts +2 -0
- package/dist/src/editor/components/redirect-countdown.d.ts.map +1 -0
- package/dist/src/editor/components/seo-editor.d.ts +2 -0
- package/dist/src/editor/components/seo-editor.d.ts.map +1 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts +8 -0
- package/dist/src/editor/components/text-style-toolbar.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast-container.d.ts +7 -0
- package/dist/src/editor/components/toast/toast-container.d.ts.map +1 -0
- package/dist/src/editor/components/toast/toast.d.ts +7 -0
- package/dist/src/editor/components/toast/toast.d.ts.map +1 -0
- package/dist/src/editor/components/toast/types.d.ts +7 -0
- package/dist/src/editor/components/toast/types.d.ts.map +1 -0
- package/dist/src/editor/components/toolbar.d.ts +21 -0
- package/dist/src/editor/components/toolbar.d.ts.map +1 -0
- package/dist/src/editor/config.d.ts +4 -0
- package/dist/src/editor/config.d.ts.map +1 -0
- package/dist/src/editor/constants.d.ts +101 -0
- package/dist/src/editor/constants.d.ts.map +1 -0
- package/dist/src/editor/context.d.ts +14 -0
- package/dist/src/editor/context.d.ts.map +1 -0
- package/dist/src/editor/dom.d.ts +77 -0
- package/dist/src/editor/dom.d.ts.map +1 -0
- package/dist/src/editor/editor.d.ts +64 -0
- package/dist/src/editor/editor.d.ts.map +1 -0
- package/dist/src/editor/history.d.ts +20 -0
- package/dist/src/editor/history.d.ts.map +1 -0
- package/dist/src/editor/hooks/index.d.ts +14 -0
- package/dist/src/editor/hooks/index.d.ts.map +1 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts +22 -0
- package/dist/src/editor/hooks/useAIHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts +18 -0
- package/dist/src/editor/hooks/useBlockEditorHandlers.d.ts.map +1 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts +26 -0
- package/dist/src/editor/hooks/useElementDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts +12 -0
- package/dist/src/editor/hooks/useImageHoverDetection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts +23 -0
- package/dist/src/editor/hooks/useTextSelection.d.ts.map +1 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts +19 -0
- package/dist/src/editor/hooks/useTooltipState.d.ts.map +1 -0
- package/dist/src/editor/hooks/utils.d.ts +32 -0
- package/dist/src/editor/hooks/utils.d.ts.map +1 -0
- package/dist/src/editor/index.d.ts +12 -0
- package/dist/src/editor/index.d.ts.map +1 -0
- package/dist/src/editor/lib/cn.d.ts +3 -0
- package/dist/src/editor/lib/cn.d.ts.map +1 -0
- package/dist/src/editor/manifest.d.ts +19 -0
- package/dist/src/editor/manifest.d.ts.map +1 -0
- package/dist/src/editor/markdown-api.d.ts +36 -0
- package/dist/src/editor/markdown-api.d.ts.map +1 -0
- package/dist/src/editor/signals.d.ts +242 -0
- package/dist/src/editor/signals.d.ts.map +1 -0
- package/dist/src/editor/storage.d.ts +27 -0
- package/dist/src/editor/storage.d.ts.map +1 -0
- package/dist/src/editor/text-styling.d.ts +350 -0
- package/dist/src/editor/text-styling.d.ts.map +1 -0
- package/dist/src/editor/themes.d.ts +38 -0
- package/dist/src/editor/themes.d.ts.map +1 -0
- package/dist/src/editor/types.d.ts +454 -0
- package/dist/src/editor/types.d.ts.map +1 -0
- package/dist/src/error-collector.d.ts +56 -0
- package/dist/src/error-collector.d.ts.map +1 -0
- package/dist/src/handlers/component-ops.d.ts +34 -0
- package/dist/src/handlers/component-ops.d.ts.map +1 -0
- package/dist/src/handlers/markdown-ops.d.ts +41 -0
- package/dist/src/handlers/markdown-ops.d.ts.map +1 -0
- package/dist/src/handlers/request-utils.d.ts +20 -0
- package/dist/src/handlers/request-utils.d.ts.map +1 -0
- package/dist/src/handlers/source-writer.d.ts +51 -0
- package/dist/src/handlers/source-writer.d.ts.map +1 -0
- package/dist/src/html-processor.d.ts +63 -0
- package/dist/src/html-processor.d.ts.map +1 -0
- package/dist/src/index.d.ts +41 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/manifest-writer.d.ts +111 -0
- package/dist/src/manifest-writer.d.ts.map +1 -0
- package/dist/src/media/contember.d.ts +15 -0
- package/dist/src/media/contember.d.ts.map +1 -0
- package/dist/src/media/local.d.ts +9 -0
- package/dist/src/media/local.d.ts.map +1 -0
- package/dist/src/media/s3.d.ts +12 -0
- package/dist/src/media/s3.d.ts.map +1 -0
- package/dist/src/media/types.d.ts +40 -0
- package/dist/src/media/types.d.ts.map +1 -0
- package/dist/src/preview-generator.d.ts +19 -0
- package/dist/src/preview-generator.d.ts.map +1 -0
- package/dist/src/seo-processor.d.ts +23 -0
- package/dist/src/seo-processor.d.ts.map +1 -0
- package/dist/src/source-finder/ast-extractors.d.ts +35 -0
- package/dist/src/source-finder/ast-extractors.d.ts.map +1 -0
- package/dist/src/source-finder/ast-parser.d.ts +16 -0
- package/dist/src/source-finder/ast-parser.d.ts.map +1 -0
- package/dist/src/source-finder/cache.d.ts +18 -0
- package/dist/src/source-finder/cache.d.ts.map +1 -0
- package/dist/src/source-finder/collection-finder.d.ts +29 -0
- package/dist/src/source-finder/collection-finder.d.ts.map +1 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts +39 -0
- package/dist/src/source-finder/cross-file-tracker.d.ts.map +1 -0
- package/dist/src/source-finder/element-finder.d.ts +42 -0
- package/dist/src/source-finder/element-finder.d.ts.map +1 -0
- package/dist/src/source-finder/image-finder.d.ts +24 -0
- package/dist/src/source-finder/image-finder.d.ts.map +1 -0
- package/dist/src/source-finder/index.d.ts +9 -0
- package/dist/src/source-finder/index.d.ts.map +1 -0
- package/dist/src/source-finder/search-index.d.ts +27 -0
- package/dist/src/source-finder/search-index.d.ts.map +1 -0
- package/dist/src/source-finder/snippet-utils.d.ts +90 -0
- package/dist/src/source-finder/snippet-utils.d.ts.map +1 -0
- package/dist/src/source-finder/source-lookup.d.ts +16 -0
- package/dist/src/source-finder/source-lookup.d.ts.map +1 -0
- package/dist/src/source-finder/types.d.ts +167 -0
- package/dist/src/source-finder/types.d.ts.map +1 -0
- package/dist/src/source-finder/variable-extraction.d.ts +37 -0
- package/dist/src/source-finder/variable-extraction.d.ts.map +1 -0
- package/dist/src/tailwind-colors.d.ts +54 -0
- package/dist/src/tailwind-colors.d.ts.map +1 -0
- package/dist/src/tsconfig.tsbuildinfo +1 -0
- package/dist/src/types.d.ts +367 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/utils.d.ts +61 -0
- package/dist/src/utils.d.ts.map +1 -0
- package/dist/src/vite-plugin.d.ts +14 -0
- package/dist/src/vite-plugin.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -0
- package/package.json +80 -0
- package/src/build-processor.ts +784 -0
- package/src/collection-scanner.ts +304 -0
- package/src/component-registry.ts +393 -0
- package/src/config.ts +74 -0
- package/src/dev-middleware.ts +525 -0
- package/src/dist/src/tsconfig.tsbuildinfo +1 -0
- package/src/editor/ai.ts +185 -0
- package/src/editor/api.ts +513 -0
- package/src/editor/color-utils.ts +556 -0
- package/src/editor/components/ai-chat.tsx +632 -0
- package/src/editor/components/ai-tooltip.tsx +179 -0
- package/src/editor/components/attribute-editor.tsx +596 -0
- package/src/editor/components/block-editor.tsx +546 -0
- package/src/editor/components/collections-browser.tsx +248 -0
- package/src/editor/components/color-toolbar.tsx +314 -0
- package/src/editor/components/confirm-dialog.tsx +69 -0
- package/src/editor/components/create-page-modal.tsx +163 -0
- package/src/editor/components/editable-highlights.tsx +260 -0
- package/src/editor/components/error-boundary.tsx +87 -0
- package/src/editor/components/fields.tsx +387 -0
- package/src/editor/components/frontmatter-fields.tsx +469 -0
- package/src/editor/components/highlight-overlay.ts +229 -0
- package/src/editor/components/image-overlay.tsx +230 -0
- package/src/editor/components/markdown-editor-overlay.tsx +505 -0
- package/src/editor/components/markdown-inline-editor.tsx +780 -0
- package/src/editor/components/media-library.tsx +297 -0
- package/src/editor/components/outline.tsx +402 -0
- package/src/editor/components/redirect-countdown.tsx +45 -0
- package/src/editor/components/seo-editor.tsx +498 -0
- package/src/editor/components/text-style-toolbar.tsx +362 -0
- package/src/editor/components/toast/toast-container.tsx +15 -0
- package/src/editor/components/toast/toast.tsx +49 -0
- package/src/editor/components/toast/types.ts +7 -0
- package/src/editor/components/toolbar.tsx +366 -0
- package/src/editor/config.ts +12 -0
- package/src/editor/constants.ts +106 -0
- package/src/editor/context.tsx +38 -0
- package/src/editor/dom.ts +357 -0
- package/src/editor/editor.ts +1510 -0
- package/src/editor/env.d.ts +4 -0
- package/src/editor/history.ts +355 -0
- package/src/editor/hooks/index.ts +19 -0
- package/src/editor/hooks/useAIHandlers.ts +345 -0
- package/src/editor/hooks/useBlockEditorHandlers.ts +206 -0
- package/src/editor/hooks/useElementDetection.ts +284 -0
- package/src/editor/hooks/useImageHoverDetection.ts +102 -0
- package/src/editor/hooks/useTextSelection.ts +187 -0
- package/src/editor/hooks/useTooltipState.ts +126 -0
- package/src/editor/hooks/utils.ts +101 -0
- package/src/editor/index.tsx +481 -0
- package/src/editor/lib/cn.ts +4 -0
- package/src/editor/manifest.ts +25 -0
- package/src/editor/markdown-api.ts +209 -0
- package/src/editor/signals.ts +1351 -0
- package/src/editor/storage.ts +266 -0
- package/src/editor/styles.css +465 -0
- package/src/editor/text-styling.ts +773 -0
- package/src/editor/themes.ts +210 -0
- package/src/editor/types.ts +591 -0
- package/src/error-collector.ts +106 -0
- package/src/handlers/component-ops.ts +463 -0
- package/src/handlers/markdown-ops.ts +202 -0
- package/src/handlers/request-utils.ts +151 -0
- package/src/handlers/source-writer.ts +649 -0
- package/src/html-processor.ts +1108 -0
- package/src/index.ts +284 -0
- package/src/manifest-writer.ts +371 -0
- package/src/media/contember.ts +84 -0
- package/src/media/local.ts +114 -0
- package/src/media/s3.ts +133 -0
- package/src/media/types.ts +33 -0
- package/src/preview-generator.ts +293 -0
- package/src/seo-processor.ts +567 -0
- package/src/source-finder/ast-extractors.ts +185 -0
- package/src/source-finder/ast-parser.ts +150 -0
- package/src/source-finder/cache.ts +76 -0
- package/src/source-finder/collection-finder.ts +335 -0
- package/src/source-finder/cross-file-tracker.ts +741 -0
- package/src/source-finder/element-finder.ts +387 -0
- package/src/source-finder/image-finder.ts +283 -0
- package/src/source-finder/index.ts +37 -0
- package/src/source-finder/search-index.ts +525 -0
- package/src/source-finder/snippet-utils.ts +668 -0
- package/src/source-finder/source-lookup.ts +200 -0
- package/src/source-finder/types.ts +210 -0
- package/src/source-finder/variable-extraction.ts +406 -0
- package/src/tailwind-colors.ts +874 -0
- package/src/tsconfig.json +25 -0
- package/src/types.ts +406 -0
- package/src/utils.ts +186 -0
- package/src/vite-plugin.ts +42 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { MediaListResult, MediaStorageAdapter, MediaUploadResult } from './types'
|
|
2
|
+
|
|
3
|
+
export interface ContemberStorageOptions {
|
|
4
|
+
/** Base URL of the worker API, e.g. 'https://api.example.com' */
|
|
5
|
+
apiBaseUrl: string
|
|
6
|
+
/** Project slug used in the API path */
|
|
7
|
+
projectSlug: string
|
|
8
|
+
/** Session token for authentication (NUA_SITE_SESSION_TOKEN cookie value) */
|
|
9
|
+
sessionToken?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Media storage adapter that proxies to the Contember worker's CMS media endpoints.
|
|
14
|
+
* Uses the existing /cms/:projectSlug/media/* API backed by R2 storage + Contember database.
|
|
15
|
+
*/
|
|
16
|
+
export function createContemberStorageAdapter(options: ContemberStorageOptions): MediaStorageAdapter {
|
|
17
|
+
const { apiBaseUrl, projectSlug, sessionToken } = options
|
|
18
|
+
const base = `${apiBaseUrl.replace(/\/$/, '')}/cms/${projectSlug}/media`
|
|
19
|
+
|
|
20
|
+
function headers(): Record<string, string> {
|
|
21
|
+
const h: Record<string, string> = {}
|
|
22
|
+
if (sessionToken) {
|
|
23
|
+
h.Cookie = `NUA_SITE_SESSION_TOKEN=${sessionToken}`
|
|
24
|
+
}
|
|
25
|
+
return h
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
async list(opts) {
|
|
30
|
+
const params = new URLSearchParams()
|
|
31
|
+
if (opts?.limit) params.set('limit', String(opts.limit))
|
|
32
|
+
if (opts?.cursor) params.set('cursor', opts.cursor)
|
|
33
|
+
|
|
34
|
+
const url = `${base}/list${params.toString() ? `?${params}` : ''}`
|
|
35
|
+
const res = await fetch(url, {
|
|
36
|
+
method: 'GET',
|
|
37
|
+
headers: headers(),
|
|
38
|
+
credentials: 'include',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
if (!res.ok) {
|
|
42
|
+
throw new Error(`Failed to list media (${res.status}): ${await res.text()}`)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return (await res.json()) as MediaListResult
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
async upload(file, filename, contentType) {
|
|
49
|
+
// Build a FormData with the file buffer
|
|
50
|
+
const blob = new Blob([new Uint8Array(file)], { type: contentType })
|
|
51
|
+
const formData = new FormData()
|
|
52
|
+
formData.append('file', blob, filename)
|
|
53
|
+
|
|
54
|
+
const res = await fetch(`${base}/upload`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: headers(),
|
|
57
|
+
body: formData,
|
|
58
|
+
credentials: 'include',
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
if (!res.ok) {
|
|
62
|
+
const text = await res.text().catch(() => '')
|
|
63
|
+
return { success: false, error: `Upload failed (${res.status}): ${text}` }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return (await res.json()) as MediaUploadResult
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
async delete(id) {
|
|
70
|
+
const res = await fetch(`${base}/${encodeURIComponent(id)}`, {
|
|
71
|
+
method: 'DELETE',
|
|
72
|
+
headers: headers(),
|
|
73
|
+
credentials: 'include',
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (!res.ok) {
|
|
77
|
+
const text = await res.text().catch(() => '')
|
|
78
|
+
return { success: false, error: `Delete failed (${res.status}): ${text}` }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { success: true }
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import type { MediaListResult, MediaStorageAdapter, MediaUploadResult } from './types'
|
|
5
|
+
|
|
6
|
+
export interface LocalStorageOptions {
|
|
7
|
+
/** Directory to store media files (relative to project root or absolute). Default: 'public/uploads' */
|
|
8
|
+
dir?: string
|
|
9
|
+
/** URL prefix for serving files. Default: '/uploads' */
|
|
10
|
+
urlPrefix?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function createLocalStorageAdapter(options: LocalStorageOptions = {}): MediaStorageAdapter {
|
|
14
|
+
const dir = path.resolve(options.dir ?? 'public/uploads')
|
|
15
|
+
const urlPrefix = (options.urlPrefix ?? '/uploads').replace(/\/$/, '')
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
staticFiles: { urlPrefix, dir },
|
|
19
|
+
|
|
20
|
+
async list(opts) {
|
|
21
|
+
const limit = opts?.limit ?? 50
|
|
22
|
+
const offset = opts?.cursor ? parseInt(opts.cursor, 10) : 0
|
|
23
|
+
|
|
24
|
+
await fs.mkdir(dir, { recursive: true })
|
|
25
|
+
|
|
26
|
+
const entries = await fs.readdir(dir, { withFileTypes: true })
|
|
27
|
+
const files = entries.filter((e) => e.isFile() && !e.name.startsWith('.'))
|
|
28
|
+
|
|
29
|
+
// Get stats for sorting by mtime desc
|
|
30
|
+
const withStats = await Promise.all(
|
|
31
|
+
files.map(async (f) => {
|
|
32
|
+
const filePath = path.join(dir, f.name)
|
|
33
|
+
const stat = await fs.stat(filePath)
|
|
34
|
+
return { name: f.name, stat }
|
|
35
|
+
}),
|
|
36
|
+
)
|
|
37
|
+
withStats.sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs)
|
|
38
|
+
|
|
39
|
+
const slice = withStats.slice(offset, offset + limit)
|
|
40
|
+
const hasMore = offset + limit < withStats.length
|
|
41
|
+
|
|
42
|
+
const items = slice.map((f) => {
|
|
43
|
+
const ext = path.extname(f.name).toLowerCase()
|
|
44
|
+
const contentType = mimeFromExt(ext)
|
|
45
|
+
return {
|
|
46
|
+
id: f.name,
|
|
47
|
+
url: `${urlPrefix}/${f.name}`,
|
|
48
|
+
filename: f.name,
|
|
49
|
+
contentType,
|
|
50
|
+
uploadedAt: f.stat.mtime.toISOString(),
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
items,
|
|
56
|
+
hasMore,
|
|
57
|
+
cursor: hasMore ? String(offset + limit) : undefined,
|
|
58
|
+
} satisfies MediaListResult
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
async upload(file, filename, contentType) {
|
|
62
|
+
await fs.mkdir(dir, { recursive: true })
|
|
63
|
+
|
|
64
|
+
const ext = getFileExtension(filename)
|
|
65
|
+
const uuid = randomUUID()
|
|
66
|
+
const newFilename = `${uuid}${ext ? `.${ext}` : ''}`
|
|
67
|
+
const filePath = path.join(dir, newFilename)
|
|
68
|
+
|
|
69
|
+
await fs.writeFile(filePath, file)
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
url: `${urlPrefix}/${newFilename}`,
|
|
74
|
+
filename: newFilename,
|
|
75
|
+
id: newFilename,
|
|
76
|
+
} satisfies MediaUploadResult
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
async delete(id) {
|
|
80
|
+
const filePath = path.join(dir, path.basename(id))
|
|
81
|
+
try {
|
|
82
|
+
await fs.unlink(filePath)
|
|
83
|
+
return { success: true }
|
|
84
|
+
} catch (error) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
86
|
+
return { success: false, error: message }
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getFileExtension(filename: string): string {
|
|
93
|
+
const parts = filename.split('.')
|
|
94
|
+
const ext = parts.length > 1 ? (parts.pop()?.toLowerCase() ?? '') : ''
|
|
95
|
+
// Only allow alphanumeric extensions to prevent injection
|
|
96
|
+
return /^[a-z0-9]+$/.test(ext) ? ext : ''
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function mimeFromExt(ext: string): string {
|
|
100
|
+
const map: Record<string, string> = {
|
|
101
|
+
'.jpg': 'image/jpeg',
|
|
102
|
+
'.jpeg': 'image/jpeg',
|
|
103
|
+
'.png': 'image/png',
|
|
104
|
+
'.gif': 'image/gif',
|
|
105
|
+
'.webp': 'image/webp',
|
|
106
|
+
'.avif': 'image/avif',
|
|
107
|
+
'.svg': 'image/svg+xml',
|
|
108
|
+
'.ico': 'image/x-icon',
|
|
109
|
+
'.mp4': 'video/mp4',
|
|
110
|
+
'.webm': 'video/webm',
|
|
111
|
+
'.pdf': 'application/pdf',
|
|
112
|
+
}
|
|
113
|
+
return map[ext] ?? 'application/octet-stream'
|
|
114
|
+
}
|
package/src/media/s3.ts
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import type { MediaListResult, MediaStorageAdapter, MediaUploadResult } from './types'
|
|
4
|
+
|
|
5
|
+
export interface S3StorageOptions {
|
|
6
|
+
bucket: string
|
|
7
|
+
region: string
|
|
8
|
+
accessKeyId?: string
|
|
9
|
+
secretAccessKey?: string
|
|
10
|
+
endpoint?: string
|
|
11
|
+
cdnPrefix?: string
|
|
12
|
+
prefix?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Dynamic import helper to avoid TS2307 for optional peer dependency
|
|
16
|
+
const s3Module = '@aws-sdk/client-s3'
|
|
17
|
+
async function loadS3(): Promise<any> {
|
|
18
|
+
return import(/* @vite-ignore */ s3Module)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function createS3StorageAdapter(options: S3StorageOptions): MediaStorageAdapter {
|
|
22
|
+
const { bucket, region, accessKeyId, secretAccessKey, endpoint, cdnPrefix, prefix = 'uploads' } = options
|
|
23
|
+
|
|
24
|
+
let s3Client: any = null
|
|
25
|
+
|
|
26
|
+
async function getClient() {
|
|
27
|
+
if (s3Client) return s3Client
|
|
28
|
+
const { S3Client } = await loadS3()
|
|
29
|
+
s3Client = new S3Client({
|
|
30
|
+
region,
|
|
31
|
+
...(endpoint ? { endpoint } : {}),
|
|
32
|
+
...(accessKeyId && secretAccessKey
|
|
33
|
+
? { credentials: { accessKeyId, secretAccessKey } }
|
|
34
|
+
: {}),
|
|
35
|
+
})
|
|
36
|
+
return s3Client
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getUrl(key: string): string {
|
|
40
|
+
if (cdnPrefix) {
|
|
41
|
+
return `${cdnPrefix.replace(/\/$/, '')}/${key}`
|
|
42
|
+
}
|
|
43
|
+
if (endpoint) {
|
|
44
|
+
return `${endpoint.replace(/\/$/, '')}/${bucket}/${key}`
|
|
45
|
+
}
|
|
46
|
+
return `https://${bucket}.s3.${region}.amazonaws.com/${key}`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
async list(opts) {
|
|
51
|
+
const { ListObjectsV2Command } = await loadS3()
|
|
52
|
+
const client = await getClient()
|
|
53
|
+
|
|
54
|
+
const limit = opts?.limit ?? 50
|
|
55
|
+
const command = new ListObjectsV2Command({
|
|
56
|
+
Bucket: bucket,
|
|
57
|
+
Prefix: prefix,
|
|
58
|
+
MaxKeys: limit + 1,
|
|
59
|
+
...(opts?.cursor ? { ContinuationToken: opts.cursor } : {}),
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const result = await client.send(command)
|
|
63
|
+
const contents = result.Contents ?? []
|
|
64
|
+
const hasMore = contents.length > limit
|
|
65
|
+
const items = contents.slice(0, limit).map((obj: any) => {
|
|
66
|
+
const key = obj.Key as string
|
|
67
|
+
const filename = key.split('/').pop() ?? key
|
|
68
|
+
return {
|
|
69
|
+
id: key,
|
|
70
|
+
url: getUrl(key),
|
|
71
|
+
filename,
|
|
72
|
+
contentType: 'application/octet-stream',
|
|
73
|
+
uploadedAt: obj.LastModified?.toISOString(),
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
items,
|
|
79
|
+
hasMore,
|
|
80
|
+
cursor: hasMore ? result.NextContinuationToken : undefined,
|
|
81
|
+
} satisfies MediaListResult
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
async upload(file, filename, contentType) {
|
|
85
|
+
const { PutObjectCommand } = await loadS3()
|
|
86
|
+
const client = await getClient()
|
|
87
|
+
|
|
88
|
+
const ext = getFileExtension(filename)
|
|
89
|
+
const uuid = randomUUID()
|
|
90
|
+
const newFilename = `${uuid}${ext ? `.${ext}` : ''}`
|
|
91
|
+
const key = prefix ? `${prefix}/${newFilename}` : newFilename
|
|
92
|
+
|
|
93
|
+
const command = new PutObjectCommand({
|
|
94
|
+
Bucket: bucket,
|
|
95
|
+
Key: key,
|
|
96
|
+
Body: file,
|
|
97
|
+
ContentType: contentType,
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
await client.send(command)
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
url: getUrl(key),
|
|
105
|
+
filename: newFilename,
|
|
106
|
+
id: key,
|
|
107
|
+
} satisfies MediaUploadResult
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
async delete(id) {
|
|
111
|
+
try {
|
|
112
|
+
const { DeleteObjectCommand } = await loadS3()
|
|
113
|
+
const client = await getClient()
|
|
114
|
+
|
|
115
|
+
const command = new DeleteObjectCommand({
|
|
116
|
+
Bucket: bucket,
|
|
117
|
+
Key: id,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
await client.send(command)
|
|
121
|
+
return { success: true }
|
|
122
|
+
} catch (error) {
|
|
123
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
124
|
+
return { success: false, error: message }
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getFileExtension(filename: string): string {
|
|
131
|
+
const parts = filename.split('.')
|
|
132
|
+
return parts.length > 1 ? (parts.pop()?.toLowerCase() ?? '') : ''
|
|
133
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export interface MediaItem {
|
|
2
|
+
id: string
|
|
3
|
+
url: string
|
|
4
|
+
filename: string
|
|
5
|
+
annotation?: string
|
|
6
|
+
contentType: string
|
|
7
|
+
width?: number
|
|
8
|
+
height?: number
|
|
9
|
+
uploadedAt?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface MediaListResult {
|
|
13
|
+
items: MediaItem[]
|
|
14
|
+
hasMore: boolean
|
|
15
|
+
cursor?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface MediaUploadResult {
|
|
19
|
+
success: boolean
|
|
20
|
+
url?: string
|
|
21
|
+
filename?: string
|
|
22
|
+
annotation?: string
|
|
23
|
+
id?: string
|
|
24
|
+
error?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface MediaStorageAdapter {
|
|
28
|
+
list(options?: { limit?: number; cursor?: string }): Promise<MediaListResult>
|
|
29
|
+
upload(file: Buffer, filename: string, contentType: string): Promise<MediaUploadResult>
|
|
30
|
+
delete(id: string): Promise<{ success: boolean; error?: string }>
|
|
31
|
+
/** Local filesystem info for direct file serving in dev (bypasses Vite's public dir cache) */
|
|
32
|
+
staticFiles?: { urlPrefix: string; dir: string }
|
|
33
|
+
}
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
import { parse } from 'node-html-parser'
|
|
2
|
+
import fs from 'node:fs/promises'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import type { ComponentDefinition, ComponentInstance, ManifestEntry } from './types'
|
|
5
|
+
import type { CollectionEntry, PageSeoData } from './types'
|
|
6
|
+
|
|
7
|
+
type PageData = {
|
|
8
|
+
entries: Record<string, ManifestEntry>
|
|
9
|
+
components: Record<string, ComponentInstance>
|
|
10
|
+
collection?: CollectionEntry
|
|
11
|
+
seo?: PageSeoData
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if an image element's src or srcset contains the given URL.
|
|
16
|
+
* Handles CDN-transformed URLs by comparing path suffixes.
|
|
17
|
+
*/
|
|
18
|
+
function imageMatchesSrc(el: ReturnType<typeof parse>, srcValue: string): boolean {
|
|
19
|
+
// Check src attribute
|
|
20
|
+
const src = el.getAttribute('src')
|
|
21
|
+
if (src === srcValue) return true
|
|
22
|
+
|
|
23
|
+
// Check if src or srcset URLs contain the original path
|
|
24
|
+
// (handles CDN transformations like /cdn-cgi/image/.../original-path)
|
|
25
|
+
let srcPath: string
|
|
26
|
+
try {
|
|
27
|
+
srcPath = new URL(srcValue).pathname
|
|
28
|
+
} catch {
|
|
29
|
+
srcPath = srcValue.split('?')[0] ?? srcValue
|
|
30
|
+
}
|
|
31
|
+
if (srcPath.length <= 5) return false
|
|
32
|
+
|
|
33
|
+
if (src) {
|
|
34
|
+
let elPath: string
|
|
35
|
+
try {
|
|
36
|
+
elPath = new URL(src).pathname
|
|
37
|
+
} catch {
|
|
38
|
+
elPath = src.split('?')[0] ?? src
|
|
39
|
+
}
|
|
40
|
+
if (elPath.endsWith(srcPath) || srcPath.endsWith(elPath)) return true
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check srcset URLs
|
|
44
|
+
const srcset = el.getAttribute('srcset')
|
|
45
|
+
if (srcset) {
|
|
46
|
+
const urls = srcset.split(',').map(entry => entry.trim().split(/\s+/)[0]).filter(Boolean)
|
|
47
|
+
for (const url of urls) {
|
|
48
|
+
if (!url) continue
|
|
49
|
+
let urlPath: string
|
|
50
|
+
try {
|
|
51
|
+
urlPath = new URL(url).pathname
|
|
52
|
+
} catch {
|
|
53
|
+
urlPath = url.split('?')[0] ?? url
|
|
54
|
+
}
|
|
55
|
+
if (urlPath.endsWith(srcPath) || srcPath.endsWith(urlPath)) return true
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return false
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Annotate elements in the component HTML with `data-cms-preview-prop` attributes.
|
|
64
|
+
* For each string prop, find the first leaf element whose trimmed text content
|
|
65
|
+
* matches the prop's rendered value, and tag it.
|
|
66
|
+
* Also annotates <img> elements whose src/srcset matches image prop values.
|
|
67
|
+
*/
|
|
68
|
+
function annotatePreviewProps(
|
|
69
|
+
componentHtml: ReturnType<typeof parse>,
|
|
70
|
+
props: Record<string, any>,
|
|
71
|
+
propDefs: ComponentDefinition['props'],
|
|
72
|
+
): void {
|
|
73
|
+
const annotated = new Set<string>()
|
|
74
|
+
|
|
75
|
+
for (const def of propDefs) {
|
|
76
|
+
// Only annotate string-type props
|
|
77
|
+
if (def.type !== 'string') continue
|
|
78
|
+
const value = props[def.name]
|
|
79
|
+
if (typeof value !== 'string' || !value.trim()) continue
|
|
80
|
+
|
|
81
|
+
const trimmedValue = value.trim()
|
|
82
|
+
|
|
83
|
+
// First, check <img> elements for image props (src/srcset matching)
|
|
84
|
+
if (!annotated.has(def.name)) {
|
|
85
|
+
const imgElements = componentHtml.querySelectorAll('img')
|
|
86
|
+
for (const img of imgElements) {
|
|
87
|
+
if (img.getAttribute('data-cms-preview-prop')) continue
|
|
88
|
+
if (imageMatchesSrc(img, trimmedValue)) {
|
|
89
|
+
img.setAttribute('data-cms-preview-prop', def.name)
|
|
90
|
+
img.setAttribute('data-cms-preview-type', 'image')
|
|
91
|
+
annotated.add(def.name)
|
|
92
|
+
break
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (annotated.has(def.name)) continue
|
|
98
|
+
|
|
99
|
+
// Find leaf text nodes whose content matches
|
|
100
|
+
const allElements = componentHtml.querySelectorAll('*')
|
|
101
|
+
for (const el of allElements) {
|
|
102
|
+
// Skip elements that already have an annotation
|
|
103
|
+
if (el.getAttribute('data-cms-preview-prop')) continue
|
|
104
|
+
|
|
105
|
+
// Check if this is a leaf element (no child elements, only text)
|
|
106
|
+
if (el.childNodes.length === 0) continue
|
|
107
|
+
const hasChildElements = el.childNodes.some(
|
|
108
|
+
(n) => n.nodeType === 1, // ELEMENT_NODE
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// For leaf elements or elements with only text children
|
|
112
|
+
if (!hasChildElements) {
|
|
113
|
+
const textContent = el.textContent.trim()
|
|
114
|
+
if (textContent === trimmedValue && !annotated.has(def.name)) {
|
|
115
|
+
el.setAttribute('data-cms-preview-prop', def.name)
|
|
116
|
+
annotated.add(def.name)
|
|
117
|
+
break
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Generate a standalone preview HTML page for a component.
|
|
126
|
+
*/
|
|
127
|
+
function generatePreviewHtml(
|
|
128
|
+
componentOuterHtml: string,
|
|
129
|
+
headStyles: string,
|
|
130
|
+
): string {
|
|
131
|
+
return `<!DOCTYPE html>
|
|
132
|
+
<html>
|
|
133
|
+
<head>
|
|
134
|
+
<meta charset="utf-8">
|
|
135
|
+
<meta name="robots" content="noindex, nofollow">
|
|
136
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
137
|
+
${headStyles}
|
|
138
|
+
<style>
|
|
139
|
+
body { margin: 0; padding: 0; }
|
|
140
|
+
.cms-preview-container { overflow: hidden; }
|
|
141
|
+
</style>
|
|
142
|
+
</head>
|
|
143
|
+
<body>
|
|
144
|
+
<div class="cms-preview-container">${componentOuterHtml}</div>
|
|
145
|
+
<script>
|
|
146
|
+
// Notify parent that preview is ready
|
|
147
|
+
if (window.parent !== window) {
|
|
148
|
+
window.parent.postMessage({ type: 'cms-preview-ready' }, window.location.origin);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Listen for prop updates from the CMS editor
|
|
152
|
+
window.addEventListener('message', function(event) {
|
|
153
|
+
// Only accept messages from same origin for security
|
|
154
|
+
if (event.origin !== window.location.origin) return;
|
|
155
|
+
if (!event.data || event.data.type !== 'cms-preview-update') return;
|
|
156
|
+
var props = event.data.props;
|
|
157
|
+
if (!props) return;
|
|
158
|
+
|
|
159
|
+
var elements = document.querySelectorAll('[data-cms-preview-prop]');
|
|
160
|
+
for (var i = 0; i < elements.length; i++) {
|
|
161
|
+
var el = elements[i];
|
|
162
|
+
var propName = el.getAttribute('data-cms-preview-prop');
|
|
163
|
+
if (propName && props[propName] !== undefined) {
|
|
164
|
+
if (el.getAttribute('data-cms-preview-type') === 'image') {
|
|
165
|
+
el.setAttribute('src', String(props[propName]));
|
|
166
|
+
el.removeAttribute('srcset');
|
|
167
|
+
} else {
|
|
168
|
+
el.textContent = String(props[propName]);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
</script>
|
|
174
|
+
</body>
|
|
175
|
+
</html>`
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extract <link rel="stylesheet"> and <style> tags from a page's <head>.
|
|
180
|
+
*/
|
|
181
|
+
function extractHeadStyles(root: ReturnType<typeof parse>): string {
|
|
182
|
+
const head = root.querySelector('head')
|
|
183
|
+
if (!head) return ''
|
|
184
|
+
|
|
185
|
+
const parts: string[] = []
|
|
186
|
+
|
|
187
|
+
// Extract <link rel="stylesheet"> tags
|
|
188
|
+
for (const link of head.querySelectorAll('link[rel="stylesheet"]')) {
|
|
189
|
+
parts.push(link.outerHTML)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Extract <style> tags
|
|
193
|
+
for (const style of head.querySelectorAll('style')) {
|
|
194
|
+
parts.push(style.outerHTML)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return parts.join('\n')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Generate standalone preview HTML files for each component that has
|
|
202
|
+
* at least one instance on a built page.
|
|
203
|
+
*
|
|
204
|
+
* Reads the built HTML, extracts the component DOM fragment, annotates
|
|
205
|
+
* text props for live preview updates, and writes a self-contained HTML
|
|
206
|
+
* page to `outDir/_cms-preview/<ComponentName>/index.html`.
|
|
207
|
+
*/
|
|
208
|
+
export async function generateComponentPreviews(
|
|
209
|
+
outDir: string,
|
|
210
|
+
pageManifests: Map<string, PageData>,
|
|
211
|
+
componentDefinitions: Record<string, ComponentDefinition>,
|
|
212
|
+
): Promise<void> {
|
|
213
|
+
// Track which component names we've already processed
|
|
214
|
+
const processed = new Set<string>()
|
|
215
|
+
|
|
216
|
+
// Build a list of work: for each page, find components we haven't processed yet
|
|
217
|
+
for (const [pagePath, pageData] of pageManifests) {
|
|
218
|
+
const componentsToProcess: Array<{
|
|
219
|
+
componentName: string
|
|
220
|
+
instance: ComponentInstance
|
|
221
|
+
}> = []
|
|
222
|
+
|
|
223
|
+
for (const instance of Object.values(pageData.components)) {
|
|
224
|
+
if (processed.has(instance.componentName)) continue
|
|
225
|
+
if (!componentDefinitions[instance.componentName]) continue
|
|
226
|
+
processed.add(instance.componentName)
|
|
227
|
+
componentsToProcess.push({ componentName: instance.componentName, instance })
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (componentsToProcess.length === 0) continue
|
|
231
|
+
|
|
232
|
+
// Resolve the HTML file path for this page
|
|
233
|
+
let htmlFilePath: string
|
|
234
|
+
if (pagePath === '/' || pagePath === '') {
|
|
235
|
+
htmlFilePath = path.join(outDir, 'index.html')
|
|
236
|
+
} else {
|
|
237
|
+
const cleanPath = pagePath.replace(/^\//, '')
|
|
238
|
+
// Try directory-style first (e.g., about/index.html)
|
|
239
|
+
const dirStyle = path.join(outDir, cleanPath, 'index.html')
|
|
240
|
+
const fileStyle = path.join(outDir, `${cleanPath}.html`)
|
|
241
|
+
try {
|
|
242
|
+
await fs.access(dirStyle)
|
|
243
|
+
htmlFilePath = dirStyle
|
|
244
|
+
} catch {
|
|
245
|
+
htmlFilePath = fileStyle
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
let pageHtml: string
|
|
250
|
+
try {
|
|
251
|
+
pageHtml = await fs.readFile(htmlFilePath, 'utf-8')
|
|
252
|
+
} catch {
|
|
253
|
+
// Page HTML not found, skip
|
|
254
|
+
continue
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const root = parse(pageHtml, { lowerCaseTagName: false, comment: true })
|
|
258
|
+
const headStyles = extractHeadStyles(root)
|
|
259
|
+
|
|
260
|
+
for (const { componentName, instance } of componentsToProcess) {
|
|
261
|
+
const def = componentDefinitions[componentName]
|
|
262
|
+
if (!def) continue
|
|
263
|
+
|
|
264
|
+
// Find the component element in the DOM
|
|
265
|
+
const componentEl = root.querySelector(
|
|
266
|
+
`[data-cms-component-id="${instance.id}"]`,
|
|
267
|
+
)
|
|
268
|
+
if (!componentEl) continue
|
|
269
|
+
|
|
270
|
+
// Clone the component HTML for annotation
|
|
271
|
+
const componentFragment = parse(componentEl.outerHTML, {
|
|
272
|
+
lowerCaseTagName: false,
|
|
273
|
+
comment: true,
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// Annotate text props for live preview
|
|
277
|
+
annotatePreviewProps(componentFragment, instance.props, def.props)
|
|
278
|
+
|
|
279
|
+
const previewHtml = generatePreviewHtml(
|
|
280
|
+
componentFragment.toString(),
|
|
281
|
+
headStyles,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
// Write preview file
|
|
285
|
+
const previewDir = path.join(outDir, '_cms-preview', componentName)
|
|
286
|
+
await fs.mkdir(previewDir, { recursive: true })
|
|
287
|
+
await fs.writeFile(path.join(previewDir, 'index.html'), previewHtml, 'utf-8')
|
|
288
|
+
|
|
289
|
+
// Set the preview URL on the component definition
|
|
290
|
+
def.previewUrl = `/_cms-preview/${componentName}/`
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|