@nuasite/cms-marker 0.0.98 → 0.0.101
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/types/build-processor.d.ts +9 -0
- package/dist/types/build-processor.d.ts.map +1 -1
- package/dist/types/manifest-writer.d.ts +17 -0
- package/dist/types/manifest-writer.d.ts.map +1 -1
- package/dist/types/preview-generator.d.ts +19 -0
- package/dist/types/preview-generator.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/dist/types/types.d.ts +1 -0
- package/dist/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/build-processor.ts +348 -63
- package/src/manifest-writer.ts +26 -0
- package/src/preview-generator.ts +221 -0
- package/src/types.ts +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
* Annotate elements in the component HTML with `data-cms-preview-prop` attributes.
|
|
16
|
+
* For each string prop, find the first leaf element whose trimmed text content
|
|
17
|
+
* matches the prop's rendered value, and tag it.
|
|
18
|
+
*/
|
|
19
|
+
function annotatePreviewProps(
|
|
20
|
+
componentHtml: ReturnType<typeof parse>,
|
|
21
|
+
props: Record<string, any>,
|
|
22
|
+
propDefs: ComponentDefinition['props'],
|
|
23
|
+
): void {
|
|
24
|
+
const annotated = new Set<string>()
|
|
25
|
+
|
|
26
|
+
for (const def of propDefs) {
|
|
27
|
+
// Only annotate string-type props
|
|
28
|
+
if (def.type !== 'string') continue
|
|
29
|
+
const value = props[def.name]
|
|
30
|
+
if (typeof value !== 'string' || !value.trim()) continue
|
|
31
|
+
|
|
32
|
+
const trimmedValue = value.trim()
|
|
33
|
+
|
|
34
|
+
// Find leaf text nodes whose content matches
|
|
35
|
+
const allElements = componentHtml.querySelectorAll('*')
|
|
36
|
+
for (const el of allElements) {
|
|
37
|
+
// Skip elements that already have an annotation
|
|
38
|
+
if (el.getAttribute('data-cms-preview-prop')) continue
|
|
39
|
+
|
|
40
|
+
// Check if this is a leaf element (no child elements, only text)
|
|
41
|
+
if (el.childNodes.length === 0) continue
|
|
42
|
+
const hasChildElements = el.childNodes.some(
|
|
43
|
+
(n) => n.nodeType === 1, // ELEMENT_NODE
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
// For leaf elements or elements with only text children
|
|
47
|
+
if (!hasChildElements) {
|
|
48
|
+
const textContent = el.textContent.trim()
|
|
49
|
+
if (textContent === trimmedValue && !annotated.has(def.name)) {
|
|
50
|
+
el.setAttribute('data-cms-preview-prop', def.name)
|
|
51
|
+
annotated.add(def.name)
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Generate a standalone preview HTML page for a component.
|
|
61
|
+
*/
|
|
62
|
+
function generatePreviewHtml(
|
|
63
|
+
componentOuterHtml: string,
|
|
64
|
+
headStyles: string,
|
|
65
|
+
): string {
|
|
66
|
+
return `<!DOCTYPE html>
|
|
67
|
+
<html>
|
|
68
|
+
<head>
|
|
69
|
+
<meta charset="utf-8">
|
|
70
|
+
<meta name="robots" content="noindex, nofollow">
|
|
71
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
72
|
+
${headStyles}
|
|
73
|
+
<style>
|
|
74
|
+
body { margin: 0; padding: 0; }
|
|
75
|
+
.cms-preview-container { overflow: hidden; }
|
|
76
|
+
</style>
|
|
77
|
+
</head>
|
|
78
|
+
<body>
|
|
79
|
+
<div class="cms-preview-container">${componentOuterHtml}</div>
|
|
80
|
+
<script>
|
|
81
|
+
// Notify parent that preview is ready
|
|
82
|
+
if (window.parent !== window) {
|
|
83
|
+
window.parent.postMessage({ type: 'cms-preview-ready' }, '*');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Listen for prop updates from the CMS editor
|
|
87
|
+
window.addEventListener('message', function(event) {
|
|
88
|
+
if (!event.data || event.data.type !== 'cms-preview-update') return;
|
|
89
|
+
var props = event.data.props;
|
|
90
|
+
if (!props) return;
|
|
91
|
+
|
|
92
|
+
var elements = document.querySelectorAll('[data-cms-preview-prop]');
|
|
93
|
+
for (var i = 0; i < elements.length; i++) {
|
|
94
|
+
var el = elements[i];
|
|
95
|
+
var propName = el.getAttribute('data-cms-preview-prop');
|
|
96
|
+
if (propName && props[propName] !== undefined) {
|
|
97
|
+
el.textContent = String(props[propName]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
</script>
|
|
102
|
+
</body>
|
|
103
|
+
</html>`
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extract <link rel="stylesheet"> and <style> tags from a page's <head>.
|
|
108
|
+
*/
|
|
109
|
+
function extractHeadStyles(root: ReturnType<typeof parse>): string {
|
|
110
|
+
const head = root.querySelector('head')
|
|
111
|
+
if (!head) return ''
|
|
112
|
+
|
|
113
|
+
const parts: string[] = []
|
|
114
|
+
|
|
115
|
+
// Extract <link rel="stylesheet"> tags
|
|
116
|
+
for (const link of head.querySelectorAll('link[rel="stylesheet"]')) {
|
|
117
|
+
parts.push(link.outerHTML)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Extract <style> tags
|
|
121
|
+
for (const style of head.querySelectorAll('style')) {
|
|
122
|
+
parts.push(style.outerHTML)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return parts.join('\n')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Generate standalone preview HTML files for each component that has
|
|
130
|
+
* at least one instance on a built page.
|
|
131
|
+
*
|
|
132
|
+
* Reads the built HTML, extracts the component DOM fragment, annotates
|
|
133
|
+
* text props for live preview updates, and writes a self-contained HTML
|
|
134
|
+
* page to `outDir/_cms-preview/<ComponentName>/index.html`.
|
|
135
|
+
*/
|
|
136
|
+
export async function generateComponentPreviews(
|
|
137
|
+
outDir: string,
|
|
138
|
+
pageManifests: Map<string, PageData>,
|
|
139
|
+
componentDefinitions: Record<string, ComponentDefinition>,
|
|
140
|
+
): Promise<void> {
|
|
141
|
+
// Track which component names we've already processed
|
|
142
|
+
const processed = new Set<string>()
|
|
143
|
+
|
|
144
|
+
// Build a list of work: for each page, find components we haven't processed yet
|
|
145
|
+
for (const [pagePath, pageData] of pageManifests) {
|
|
146
|
+
const componentsToProcess: Array<{
|
|
147
|
+
componentName: string
|
|
148
|
+
instance: ComponentInstance
|
|
149
|
+
}> = []
|
|
150
|
+
|
|
151
|
+
for (const instance of Object.values(pageData.components)) {
|
|
152
|
+
if (processed.has(instance.componentName)) continue
|
|
153
|
+
if (!componentDefinitions[instance.componentName]) continue
|
|
154
|
+
processed.add(instance.componentName)
|
|
155
|
+
componentsToProcess.push({ componentName: instance.componentName, instance })
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (componentsToProcess.length === 0) continue
|
|
159
|
+
|
|
160
|
+
// Resolve the HTML file path for this page
|
|
161
|
+
let htmlFilePath: string
|
|
162
|
+
if (pagePath === '/' || pagePath === '') {
|
|
163
|
+
htmlFilePath = path.join(outDir, 'index.html')
|
|
164
|
+
} else {
|
|
165
|
+
const cleanPath = pagePath.replace(/^\//, '')
|
|
166
|
+
// Try directory-style first (e.g., about/index.html)
|
|
167
|
+
const dirStyle = path.join(outDir, cleanPath, 'index.html')
|
|
168
|
+
const fileStyle = path.join(outDir, `${cleanPath}.html`)
|
|
169
|
+
try {
|
|
170
|
+
await fs.access(dirStyle)
|
|
171
|
+
htmlFilePath = dirStyle
|
|
172
|
+
} catch {
|
|
173
|
+
htmlFilePath = fileStyle
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let pageHtml: string
|
|
178
|
+
try {
|
|
179
|
+
pageHtml = await fs.readFile(htmlFilePath, 'utf-8')
|
|
180
|
+
} catch {
|
|
181
|
+
// Page HTML not found, skip
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const root = parse(pageHtml, { lowerCaseTagName: false, comment: true })
|
|
186
|
+
const headStyles = extractHeadStyles(root)
|
|
187
|
+
|
|
188
|
+
for (const { componentName, instance } of componentsToProcess) {
|
|
189
|
+
const def = componentDefinitions[componentName]
|
|
190
|
+
if (!def) continue
|
|
191
|
+
|
|
192
|
+
// Find the component element in the DOM
|
|
193
|
+
const componentEl = root.querySelector(
|
|
194
|
+
`[data-cms-component-id="${instance.id}"]`,
|
|
195
|
+
)
|
|
196
|
+
if (!componentEl) continue
|
|
197
|
+
|
|
198
|
+
// Clone the component HTML for annotation
|
|
199
|
+
const componentFragment = parse(componentEl.outerHTML, {
|
|
200
|
+
lowerCaseTagName: false,
|
|
201
|
+
comment: true,
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
// Annotate text props for live preview
|
|
205
|
+
annotatePreviewProps(componentFragment, instance.props, def.props)
|
|
206
|
+
|
|
207
|
+
const previewHtml = generatePreviewHtml(
|
|
208
|
+
componentFragment.toString(),
|
|
209
|
+
headStyles,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
// Write preview file
|
|
213
|
+
const previewDir = path.join(outDir, '_cms-preview', componentName)
|
|
214
|
+
await fs.mkdir(previewDir, { recursive: true })
|
|
215
|
+
await fs.writeFile(path.join(previewDir, 'index.html'), previewHtml, 'utf-8')
|
|
216
|
+
|
|
217
|
+
// Set the preview URL on the component definition
|
|
218
|
+
def.previewUrl = `/_cms-preview/${componentName}/`
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|