@uniweb/build 0.1.26 → 0.1.27
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 +3 -2
- package/src/prerender.js +16 -257
- package/src/runtime-schema.js +102 -0
- package/src/site/asset-processor.js +32 -0
- package/src/site/assets.js +82 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.27",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
"sharp": "^0.33.2"
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
|
-
"@uniweb/content-reader": "1.0.
|
|
53
|
+
"@uniweb/content-reader": "1.0.4",
|
|
54
|
+
"@uniweb/runtime": "0.2.14"
|
|
54
55
|
},
|
|
55
56
|
"peerDependencies": {
|
|
56
57
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
package/src/prerender.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Renders each page to static HTML at build time.
|
|
5
5
|
* The output includes full HTML with hydration support.
|
|
6
|
+
*
|
|
7
|
+
* Uses @uniweb/runtime/ssr for rendering components, ensuring
|
|
8
|
+
* the same code path for both SSG and client-side rendering.
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
@@ -11,8 +14,8 @@ import { join, dirname, resolve } from 'node:path'
|
|
|
11
14
|
import { pathToFileURL } from 'node:url'
|
|
12
15
|
import { createRequire } from 'node:module'
|
|
13
16
|
|
|
14
|
-
// Lazily loaded dependencies
|
|
15
|
-
let React, renderToString, createUniweb
|
|
17
|
+
// Lazily loaded dependencies
|
|
18
|
+
let React, renderToString, createUniweb, PageElement
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Load dependencies dynamically from the site's context
|
|
@@ -25,7 +28,6 @@ async function loadDependencies(siteDir) {
|
|
|
25
28
|
|
|
26
29
|
// Create a require function that resolves from the site's perspective
|
|
27
30
|
// This ensures we get the same React instance that the foundation uses
|
|
28
|
-
// Note: createRequire requires an absolute path
|
|
29
31
|
const absoluteSiteDir = resolve(siteDir)
|
|
30
32
|
const siteRequire = createRequire(join(absoluteSiteDir, 'package.json'))
|
|
31
33
|
|
|
@@ -47,9 +49,13 @@ async function loadDependencies(siteDir) {
|
|
|
47
49
|
renderToString = serverMod.renderToString
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
// @uniweb/core
|
|
52
|
+
// Load @uniweb/core
|
|
51
53
|
const coreMod = await import('@uniweb/core')
|
|
52
54
|
createUniweb = coreMod.createUniweb
|
|
55
|
+
|
|
56
|
+
// Load @uniweb/runtime/ssr for rendering components
|
|
57
|
+
const ssrMod = await import('@uniweb/runtime/ssr')
|
|
58
|
+
PageElement = ssrMod.PageElement
|
|
53
59
|
}
|
|
54
60
|
|
|
55
61
|
/**
|
|
@@ -108,14 +114,15 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
108
114
|
const uniweb = createUniweb(siteContent)
|
|
109
115
|
uniweb.setFoundation(foundation)
|
|
110
116
|
|
|
111
|
-
// Set foundation
|
|
112
|
-
if (foundation.
|
|
113
|
-
uniweb.setFoundationConfig(foundation.
|
|
117
|
+
// Set foundation capabilities (Layout, props, etc.)
|
|
118
|
+
if (foundation.capabilities) {
|
|
119
|
+
uniweb.setFoundationConfig(foundation.capabilities)
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
// Pre-render each page
|
|
117
123
|
const renderedFiles = []
|
|
118
124
|
const pages = uniweb.activeWebsite.pages
|
|
125
|
+
const website = uniweb.activeWebsite
|
|
119
126
|
|
|
120
127
|
for (const page of pages) {
|
|
121
128
|
// Determine which routes to render this page at
|
|
@@ -134,10 +141,8 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
134
141
|
// Set this as the active page
|
|
135
142
|
uniweb.activeWebsite.setActivePage(page.route)
|
|
136
143
|
|
|
137
|
-
// Create the page element
|
|
138
|
-
|
|
139
|
-
// components to strings. The routing context isn't needed for static HTML.
|
|
140
|
-
const element = React.createElement(PageRenderer, { page, foundation })
|
|
144
|
+
// Create the page element using the runtime's SSR components
|
|
145
|
+
const element = React.createElement(PageElement, { page, website })
|
|
141
146
|
|
|
142
147
|
// Render to HTML string
|
|
143
148
|
let renderedContent
|
|
@@ -173,249 +178,6 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
173
178
|
}
|
|
174
179
|
}
|
|
175
180
|
|
|
176
|
-
/**
|
|
177
|
-
* Render an array of blocks
|
|
178
|
-
*/
|
|
179
|
-
function BlocksRenderer({ blocks, foundation }) {
|
|
180
|
-
if (!blocks || blocks.length === 0) return null
|
|
181
|
-
|
|
182
|
-
return blocks.map((block, index) =>
|
|
183
|
-
React.createElement(BlockRenderer, {
|
|
184
|
-
key: block.id || index,
|
|
185
|
-
block,
|
|
186
|
-
foundation
|
|
187
|
-
})
|
|
188
|
-
)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Default layout - renders header, body, footer in sequence
|
|
193
|
-
*/
|
|
194
|
-
function DefaultLayout({ header, body, footer }) {
|
|
195
|
-
return React.createElement(React.Fragment, null, header, body, footer)
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* Layout component for SSG
|
|
200
|
-
* Supports foundation-provided custom Layout via runtime.Layout
|
|
201
|
-
*/
|
|
202
|
-
function Layout({ page, website, foundation }) {
|
|
203
|
-
const RemoteLayout = foundation.runtime?.Layout || null
|
|
204
|
-
|
|
205
|
-
// Get block groups from page
|
|
206
|
-
const headerBlocks = page.getHeaderBlocks()
|
|
207
|
-
const bodyBlocks = page.getBodyBlocks()
|
|
208
|
-
const footerBlocks = page.getFooterBlocks()
|
|
209
|
-
const leftBlocks = page.getLeftBlocks()
|
|
210
|
-
const rightBlocks = page.getRightBlocks()
|
|
211
|
-
|
|
212
|
-
// Pre-render each area
|
|
213
|
-
const headerElement = headerBlocks
|
|
214
|
-
? React.createElement(BlocksRenderer, { blocks: headerBlocks, foundation })
|
|
215
|
-
: null
|
|
216
|
-
const bodyElement = bodyBlocks
|
|
217
|
-
? React.createElement(BlocksRenderer, { blocks: bodyBlocks, foundation })
|
|
218
|
-
: null
|
|
219
|
-
const footerElement = footerBlocks
|
|
220
|
-
? React.createElement(BlocksRenderer, { blocks: footerBlocks, foundation })
|
|
221
|
-
: null
|
|
222
|
-
const leftElement = leftBlocks
|
|
223
|
-
? React.createElement(BlocksRenderer, { blocks: leftBlocks, foundation })
|
|
224
|
-
: null
|
|
225
|
-
const rightElement = rightBlocks
|
|
226
|
-
? React.createElement(BlocksRenderer, { blocks: rightBlocks, foundation })
|
|
227
|
-
: null
|
|
228
|
-
|
|
229
|
-
// Use foundation's custom Layout if provided
|
|
230
|
-
if (RemoteLayout) {
|
|
231
|
-
return React.createElement(RemoteLayout, {
|
|
232
|
-
page,
|
|
233
|
-
website,
|
|
234
|
-
header: headerElement,
|
|
235
|
-
body: bodyElement,
|
|
236
|
-
footer: footerElement,
|
|
237
|
-
left: leftElement,
|
|
238
|
-
right: rightElement,
|
|
239
|
-
leftPanel: leftElement,
|
|
240
|
-
rightPanel: rightElement
|
|
241
|
-
})
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Default layout
|
|
245
|
-
return React.createElement(DefaultLayout, {
|
|
246
|
-
header: headerElement,
|
|
247
|
-
body: bodyElement,
|
|
248
|
-
footer: footerElement
|
|
249
|
-
})
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Page renderer for SSG
|
|
254
|
-
* Uses Layout component for proper orchestration of layout areas
|
|
255
|
-
*/
|
|
256
|
-
function PageRenderer({ page, foundation }) {
|
|
257
|
-
const website = globalThis.uniweb?.activeWebsite
|
|
258
|
-
|
|
259
|
-
return React.createElement(
|
|
260
|
-
'main',
|
|
261
|
-
null,
|
|
262
|
-
React.createElement(Layout, { page, website, foundation })
|
|
263
|
-
)
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Guarantee item has flat content structure
|
|
268
|
-
*/
|
|
269
|
-
function guaranteeItemStructure(item) {
|
|
270
|
-
return {
|
|
271
|
-
title: item.title || '',
|
|
272
|
-
pretitle: item.pretitle || '',
|
|
273
|
-
subtitle: item.subtitle || '',
|
|
274
|
-
paragraphs: item.paragraphs || [],
|
|
275
|
-
links: item.links || [],
|
|
276
|
-
imgs: item.imgs || [],
|
|
277
|
-
lists: item.lists || [],
|
|
278
|
-
icons: item.icons || [],
|
|
279
|
-
videos: item.videos || [],
|
|
280
|
-
buttons: item.buttons || [],
|
|
281
|
-
properties: item.properties || {},
|
|
282
|
-
cards: item.cards || [],
|
|
283
|
-
documents: item.documents || [],
|
|
284
|
-
forms: item.forms || [],
|
|
285
|
-
quotes: item.quotes || [],
|
|
286
|
-
headings: item.headings || [],
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Guarantee content structure exists (mirrors runtime/prepare-props.js)
|
|
292
|
-
* Returns a flat content object with all standard fields guaranteed to exist
|
|
293
|
-
*/
|
|
294
|
-
function guaranteeContentStructure(parsedContent) {
|
|
295
|
-
const content = parsedContent || {}
|
|
296
|
-
|
|
297
|
-
return {
|
|
298
|
-
// Flat header fields
|
|
299
|
-
title: content.title || '',
|
|
300
|
-
pretitle: content.pretitle || '',
|
|
301
|
-
subtitle: content.subtitle || '',
|
|
302
|
-
subtitle2: content.subtitle2 || '',
|
|
303
|
-
alignment: content.alignment || null,
|
|
304
|
-
|
|
305
|
-
// Flat body fields
|
|
306
|
-
paragraphs: content.paragraphs || [],
|
|
307
|
-
links: content.links || [],
|
|
308
|
-
imgs: content.imgs || [],
|
|
309
|
-
lists: content.lists || [],
|
|
310
|
-
icons: content.icons || [],
|
|
311
|
-
videos: content.videos || [],
|
|
312
|
-
buttons: content.buttons || [],
|
|
313
|
-
properties: content.properties || {},
|
|
314
|
-
propertyBlocks: content.propertyBlocks || [],
|
|
315
|
-
cards: content.cards || [],
|
|
316
|
-
documents: content.documents || [],
|
|
317
|
-
forms: content.forms || [],
|
|
318
|
-
quotes: content.quotes || [],
|
|
319
|
-
headings: content.headings || [],
|
|
320
|
-
|
|
321
|
-
// Items with guaranteed structure
|
|
322
|
-
items: (content.items || []).map(guaranteeItemStructure),
|
|
323
|
-
|
|
324
|
-
// Sequence for ordered rendering
|
|
325
|
-
sequence: content.sequence || [],
|
|
326
|
-
|
|
327
|
-
// Preserve raw content if present
|
|
328
|
-
raw: content.raw,
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
/**
|
|
333
|
-
* Apply param defaults from runtime schema
|
|
334
|
-
*/
|
|
335
|
-
function applyDefaults(params, defaults) {
|
|
336
|
-
if (!defaults || Object.keys(defaults).length === 0) {
|
|
337
|
-
return params || {}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return {
|
|
341
|
-
...defaults,
|
|
342
|
-
...(params || {}),
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Block renderer - maps block to foundation component
|
|
348
|
-
*/
|
|
349
|
-
function BlockRenderer({ block, foundation }) {
|
|
350
|
-
// Get component from foundation
|
|
351
|
-
const componentName = block.type
|
|
352
|
-
let Component = null
|
|
353
|
-
|
|
354
|
-
if (typeof foundation.getComponent === 'function') {
|
|
355
|
-
Component = foundation.getComponent(componentName)
|
|
356
|
-
} else if (foundation[componentName]) {
|
|
357
|
-
Component = foundation[componentName]
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (!Component) {
|
|
361
|
-
// Return placeholder for unknown components
|
|
362
|
-
return React.createElement(
|
|
363
|
-
'div',
|
|
364
|
-
{
|
|
365
|
-
className: 'block-placeholder',
|
|
366
|
-
'data-component': componentName,
|
|
367
|
-
style: { display: 'none' }
|
|
368
|
-
},
|
|
369
|
-
`Component: ${componentName}`
|
|
370
|
-
)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// Get runtime schema for defaults (from foundation.runtimeSchema)
|
|
374
|
-
const runtimeSchema = foundation.runtimeSchema || {}
|
|
375
|
-
const schema = runtimeSchema[componentName] || null
|
|
376
|
-
const defaults = schema?.defaults || {}
|
|
377
|
-
|
|
378
|
-
// Build content and params with runtime guarantees (same as runtime's BlockRenderer)
|
|
379
|
-
let content, params
|
|
380
|
-
if (block.parsedContent?._isPoc) {
|
|
381
|
-
// Simple PoC format - content was passed directly
|
|
382
|
-
content = block.parsedContent._pocContent
|
|
383
|
-
params = block.properties
|
|
384
|
-
} else {
|
|
385
|
-
// Apply param defaults from meta.js
|
|
386
|
-
params = applyDefaults(block.properties, defaults)
|
|
387
|
-
|
|
388
|
-
// Guarantee content structure + merge with properties for backward compat
|
|
389
|
-
content = {
|
|
390
|
-
...guaranteeContentStructure(block.parsedContent),
|
|
391
|
-
...block.properties,
|
|
392
|
-
_prosemirror: block.parsedContent
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Build wrapper props
|
|
397
|
-
const theme = block.themeName
|
|
398
|
-
const className = theme || ''
|
|
399
|
-
const wrapperProps = {
|
|
400
|
-
id: `Section${block.id}`,
|
|
401
|
-
className
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
// Component props
|
|
405
|
-
const componentProps = {
|
|
406
|
-
content,
|
|
407
|
-
params,
|
|
408
|
-
block,
|
|
409
|
-
input: block.input
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
return React.createElement(
|
|
413
|
-
'div',
|
|
414
|
-
wrapperProps,
|
|
415
|
-
React.createElement(Component, componentProps)
|
|
416
|
-
)
|
|
417
|
-
}
|
|
418
|
-
|
|
419
181
|
/**
|
|
420
182
|
* Inject rendered content into HTML shell
|
|
421
183
|
*/
|
|
@@ -423,7 +185,6 @@ function injectContent(shell, renderedContent, page, siteContent) {
|
|
|
423
185
|
let html = shell
|
|
424
186
|
|
|
425
187
|
// Replace the empty root div with pre-rendered content
|
|
426
|
-
// Handle various formats of root div
|
|
427
188
|
html = html.replace(
|
|
428
189
|
/<div id="root">[\s\S]*?<\/div>/,
|
|
429
190
|
`<div id="root">${renderedContent}</div>`
|
|
@@ -454,7 +215,6 @@ function injectContent(shell, renderedContent, page, siteContent) {
|
|
|
454
215
|
}
|
|
455
216
|
|
|
456
217
|
// Inject site content as JSON for hydration
|
|
457
|
-
// This allows the client-side React to hydrate with the same data
|
|
458
218
|
const contentScript = `<script id="__SITE_CONTENT__" type="application/json">${JSON.stringify(siteContent)}</script>`
|
|
459
219
|
if (!html.includes('__SITE_CONTENT__')) {
|
|
460
220
|
html = html.replace(
|
|
@@ -470,7 +230,6 @@ function injectContent(shell, renderedContent, page, siteContent) {
|
|
|
470
230
|
* Get output path for a route
|
|
471
231
|
*/
|
|
472
232
|
function getOutputPath(distDir, route) {
|
|
473
|
-
// Normalize route
|
|
474
233
|
let normalizedRoute = route
|
|
475
234
|
|
|
476
235
|
// Handle root route
|
package/src/runtime-schema.js
CHANGED
|
@@ -35,6 +35,99 @@ function parseDataString(dataString) {
|
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Extract lean schema field for runtime
|
|
40
|
+
* Strips editor-only fields (label, hint, description)
|
|
41
|
+
* Keeps runtime fields (type, default, options, of, schema)
|
|
42
|
+
*
|
|
43
|
+
* @param {string|Object} field - Schema field definition
|
|
44
|
+
* @returns {string|Object} - Lean field definition
|
|
45
|
+
*/
|
|
46
|
+
function extractSchemaField(field) {
|
|
47
|
+
// Shorthand: 'string', 'number', 'boolean'
|
|
48
|
+
if (typeof field === 'string') {
|
|
49
|
+
return field
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!field || typeof field !== 'object') {
|
|
53
|
+
return field
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const lean = {}
|
|
57
|
+
|
|
58
|
+
// Keep runtime-relevant fields
|
|
59
|
+
if (field.type) lean.type = field.type
|
|
60
|
+
if (field.default !== undefined) lean.default = field.default
|
|
61
|
+
if (field.options) lean.options = field.options
|
|
62
|
+
|
|
63
|
+
// Handle array 'of' - can be string, schema name, or inline object
|
|
64
|
+
if (field.of !== undefined) {
|
|
65
|
+
if (typeof field.of === 'string') {
|
|
66
|
+
lean.of = field.of
|
|
67
|
+
} else if (typeof field.of === 'object') {
|
|
68
|
+
// Inline schema definition
|
|
69
|
+
lean.of = extractSchemaFields(field.of)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Handle nested object 'schema'
|
|
74
|
+
if (field.schema && typeof field.schema === 'object') {
|
|
75
|
+
lean.schema = extractSchemaFields(field.schema)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// If we only have 'type' and it's a simple type, use shorthand
|
|
79
|
+
const keys = Object.keys(lean)
|
|
80
|
+
if (keys.length === 1 && keys[0] === 'type' && ['string', 'number', 'boolean'].includes(lean.type)) {
|
|
81
|
+
return lean.type
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return keys.length > 0 ? lean : null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Extract lean schema fields for an entire schema object
|
|
89
|
+
*
|
|
90
|
+
* @param {Object} schemaFields - Map of fieldName -> field definition
|
|
91
|
+
* @returns {Object} - Map of fieldName -> lean field definition
|
|
92
|
+
*/
|
|
93
|
+
function extractSchemaFields(schemaFields) {
|
|
94
|
+
if (!schemaFields || typeof schemaFields !== 'object') {
|
|
95
|
+
return {}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const lean = {}
|
|
99
|
+
for (const [name, field] of Object.entries(schemaFields)) {
|
|
100
|
+
const leanField = extractSchemaField(field)
|
|
101
|
+
if (leanField !== null) {
|
|
102
|
+
lean[name] = leanField
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return lean
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extract lean schemas from meta.js schemas object
|
|
110
|
+
* Strips editor-only fields while preserving structure
|
|
111
|
+
*
|
|
112
|
+
* @param {Object} schemas - The schemas object from meta.js
|
|
113
|
+
* @returns {Object|null} - Lean schemas or null if empty
|
|
114
|
+
*/
|
|
115
|
+
function extractSchemas(schemas) {
|
|
116
|
+
if (!schemas || typeof schemas !== 'object') {
|
|
117
|
+
return null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const lean = {}
|
|
121
|
+
for (const [schemaName, schemaFields] of Object.entries(schemas)) {
|
|
122
|
+
const leanSchema = extractSchemaFields(schemaFields)
|
|
123
|
+
if (Object.keys(leanSchema).length > 0) {
|
|
124
|
+
lean[schemaName] = leanSchema
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return Object.keys(lean).length > 0 ? lean : null
|
|
129
|
+
}
|
|
130
|
+
|
|
38
131
|
/**
|
|
39
132
|
* Extract param defaults from params object
|
|
40
133
|
*
|
|
@@ -102,6 +195,15 @@ export function extractRuntimeSchema(fullMeta) {
|
|
|
102
195
|
runtime.initialState = fullMeta.initialState
|
|
103
196
|
}
|
|
104
197
|
|
|
198
|
+
// Schemas - lean version for runtime validation/defaults
|
|
199
|
+
// Strips editor-only fields (label, hint, description)
|
|
200
|
+
if (fullMeta.schemas) {
|
|
201
|
+
const schemas = extractSchemas(fullMeta.schemas)
|
|
202
|
+
if (schemas) {
|
|
203
|
+
runtime.schemas = schemas
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
105
207
|
return Object.keys(runtime).length > 0 ? runtime : null
|
|
106
208
|
}
|
|
107
209
|
|
|
@@ -166,6 +166,33 @@ export async function processAssets(assetManifest, options = {}) {
|
|
|
166
166
|
return { pathMapping, results }
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Recursively rewrite asset paths in a data object
|
|
171
|
+
*
|
|
172
|
+
* @param {any} data - Parsed JSON/YAML data
|
|
173
|
+
* @param {Object} pathMapping - Map of original paths to new paths
|
|
174
|
+
* @returns {any} Data with rewritten paths
|
|
175
|
+
*/
|
|
176
|
+
function rewriteDataPaths(data, pathMapping) {
|
|
177
|
+
if (typeof data === 'string') {
|
|
178
|
+
return pathMapping[data] || data
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (Array.isArray(data)) {
|
|
182
|
+
return data.map(item => rewriteDataPaths(item, pathMapping))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (data && typeof data === 'object') {
|
|
186
|
+
const result = {}
|
|
187
|
+
for (const [key, value] of Object.entries(data)) {
|
|
188
|
+
result[key] = rewriteDataPaths(value, pathMapping)
|
|
189
|
+
}
|
|
190
|
+
return result
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return data
|
|
194
|
+
}
|
|
195
|
+
|
|
169
196
|
/**
|
|
170
197
|
* Rewrite asset paths in ProseMirror content
|
|
171
198
|
*
|
|
@@ -188,6 +215,11 @@ export function rewriteContentPaths(content, pathMapping) {
|
|
|
188
215
|
}
|
|
189
216
|
}
|
|
190
217
|
|
|
218
|
+
// Rewrite paths in data blocks (structured data parsed at build time)
|
|
219
|
+
if (node.type === 'dataBlock' && node.attrs?.data) {
|
|
220
|
+
node.attrs.data = rewriteDataPaths(node.attrs.data, pathMapping)
|
|
221
|
+
}
|
|
222
|
+
|
|
191
223
|
// Recurse into content
|
|
192
224
|
if (node.content && Array.isArray(node.content)) {
|
|
193
225
|
node.content.forEach(walk)
|
package/src/site/assets.js
CHANGED
|
@@ -50,6 +50,72 @@ function isPdfPath(src) {
|
|
|
50
50
|
return src.toLowerCase().endsWith(PDF_EXTENSION)
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Check if a string looks like a local asset path
|
|
55
|
+
*
|
|
56
|
+
* @param {string} value - String to check
|
|
57
|
+
* @returns {boolean} True if it looks like a local asset path
|
|
58
|
+
*/
|
|
59
|
+
function isLocalAssetPath(value) {
|
|
60
|
+
if (typeof value !== 'string' || !value) return false
|
|
61
|
+
|
|
62
|
+
// Skip external URLs
|
|
63
|
+
if (isExternalUrl(value)) return false
|
|
64
|
+
|
|
65
|
+
// Must start with ./, ../, or / (absolute site path)
|
|
66
|
+
if (!value.startsWith('./') && !value.startsWith('../') && !value.startsWith('/')) {
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Must have a media extension
|
|
71
|
+
return isImagePath(value) || isVideoPath(value) || isPdfPath(value)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Recursively walk a parsed data object and collect asset paths
|
|
76
|
+
*
|
|
77
|
+
* @param {any} data - Parsed JSON/YAML data
|
|
78
|
+
* @param {Function} visitor - Callback for each asset path: (path) => void
|
|
79
|
+
*/
|
|
80
|
+
function walkDataAssets(data, visitor) {
|
|
81
|
+
if (typeof data === 'string') {
|
|
82
|
+
if (isLocalAssetPath(data)) {
|
|
83
|
+
visitor(data)
|
|
84
|
+
}
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (Array.isArray(data)) {
|
|
89
|
+
data.forEach(item => walkDataAssets(item, visitor))
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (data && typeof data === 'object') {
|
|
94
|
+
Object.values(data).forEach(value => walkDataAssets(value, visitor))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Walk ProseMirror content and collect assets from data blocks
|
|
100
|
+
* Data blocks have pre-parsed structured data (parsed at content-reader build time)
|
|
101
|
+
*
|
|
102
|
+
* @param {Object} doc - ProseMirror document
|
|
103
|
+
* @param {Function} visitor - Callback for each asset: (path) => void
|
|
104
|
+
*/
|
|
105
|
+
function walkDataBlockAssets(doc, visitor) {
|
|
106
|
+
if (!doc) return
|
|
107
|
+
|
|
108
|
+
// dataBlock nodes have pre-parsed data in attrs.data
|
|
109
|
+
if (doc.type === 'dataBlock' && doc.attrs?.data) {
|
|
110
|
+
walkDataAssets(doc.attrs.data, visitor)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Recurse into content
|
|
114
|
+
if (doc.content && Array.isArray(doc.content)) {
|
|
115
|
+
doc.content.forEach(child => walkDataBlockAssets(child, visitor))
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
53
119
|
/**
|
|
54
120
|
* Resolve an asset path to absolute file system path
|
|
55
121
|
*
|
|
@@ -215,6 +281,22 @@ export function collectSectionAssets(section, markdownPath, siteRoot) {
|
|
|
215
281
|
}
|
|
216
282
|
}
|
|
217
283
|
|
|
284
|
+
// Collect from tagged code blocks (JSON/YAML data)
|
|
285
|
+
if (section.content) {
|
|
286
|
+
walkDataBlockAssets(section.content, (assetPath) => {
|
|
287
|
+
const result = resolveAssetPath(assetPath, markdownPath, siteRoot)
|
|
288
|
+
if (!result.external && result.resolved) {
|
|
289
|
+
assets[assetPath] = {
|
|
290
|
+
original: assetPath,
|
|
291
|
+
resolved: result.resolved,
|
|
292
|
+
isImage: result.isImage,
|
|
293
|
+
isVideo: result.isVideo,
|
|
294
|
+
isPdf: result.isPdf
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
218
300
|
return { assets, hasExplicitPoster, hasExplicitPreview }
|
|
219
301
|
}
|
|
220
302
|
|