@uniweb/build 0.4.8 → 0.4.9
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 +2 -2
- package/src/generate-entry.js +1 -1
- package/src/prerender.js +188 -49
- package/src/runtime-schema.js +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.9",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
},
|
|
52
52
|
"optionalDependencies": {
|
|
53
53
|
"@uniweb/content-reader": "1.1.2",
|
|
54
|
-
"@uniweb/runtime": "0.5.
|
|
54
|
+
"@uniweb/runtime": "0.5.10"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
package/src/generate-entry.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - `meta` - Per-component runtime metadata extracted from meta.js files
|
|
10
10
|
*
|
|
11
11
|
* The `meta` export contains only properties needed at runtime:
|
|
12
|
-
* - `background` -
|
|
12
|
+
* - `background` - 'self' opt-out when component handles its own background
|
|
13
13
|
* - `data` - CMS entity binding ({ type, limit })
|
|
14
14
|
* - `defaults` - Param default values
|
|
15
15
|
* - `context` - Static capabilities for cross-block coordination
|
package/src/prerender.js
CHANGED
|
@@ -11,13 +11,13 @@
|
|
|
11
11
|
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
12
12
|
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
13
13
|
import { join, dirname, resolve } from 'node:path'
|
|
14
|
-
import { pathToFileURL } from 'node:url'
|
|
15
14
|
import { createRequire } from 'node:module'
|
|
15
|
+
import { pathToFileURL } from 'node:url'
|
|
16
16
|
import { executeFetch, mergeDataIntoContent, singularize } from './site/data-fetcher.js'
|
|
17
17
|
|
|
18
18
|
// Lazily loaded dependencies
|
|
19
19
|
let React, renderToString, createUniweb
|
|
20
|
-
let preparePropsSSR, getComponentMetaSSR
|
|
20
|
+
let preparePropsSSR, getComponentMetaSSR
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Execute all data fetches for prerender
|
|
@@ -229,25 +229,24 @@ async function processSectionFetches(sections, cascadedData, fetchOptions, onPro
|
|
|
229
229
|
async function loadDependencies(siteDir) {
|
|
230
230
|
if (React) return // Already loaded
|
|
231
231
|
|
|
232
|
-
//
|
|
233
|
-
// This ensures we get the same React instance
|
|
232
|
+
// Load React from the site's node_modules using createRequire.
|
|
233
|
+
// This ensures we get the same React instance as the foundation
|
|
234
|
+
// components (which are loaded via pathToFileURL and externalize React
|
|
235
|
+
// to the same node_modules). Using bare import('react') would resolve
|
|
236
|
+
// from @uniweb/build's context, creating a dual-React instance problem.
|
|
234
237
|
const absoluteSiteDir = resolve(siteDir)
|
|
235
238
|
const siteRequire = createRequire(join(absoluteSiteDir, 'package.json'))
|
|
236
239
|
|
|
237
240
|
try {
|
|
238
|
-
// Try to load React from site's node_modules
|
|
239
241
|
const reactMod = siteRequire('react')
|
|
240
242
|
const serverMod = siteRequire('react-dom/server')
|
|
241
|
-
|
|
242
243
|
React = reactMod.default || reactMod
|
|
243
244
|
renderToString = serverMod.renderToString
|
|
244
245
|
} catch {
|
|
245
|
-
// Fallback to dynamic import if require fails
|
|
246
246
|
const [reactMod, serverMod] = await Promise.all([
|
|
247
247
|
import('react'),
|
|
248
248
|
import('react-dom/server')
|
|
249
249
|
])
|
|
250
|
-
|
|
251
250
|
React = reactMod.default || reactMod
|
|
252
251
|
renderToString = serverMod.renderToString
|
|
253
252
|
}
|
|
@@ -256,11 +255,12 @@ async function loadDependencies(siteDir) {
|
|
|
256
255
|
const coreMod = await import('@uniweb/core')
|
|
257
256
|
createUniweb = coreMod.createUniweb
|
|
258
257
|
|
|
259
|
-
// Load
|
|
258
|
+
// Load pure utility functions from runtime SSR bundle.
|
|
259
|
+
// These are plain functions (no hooks), so they work even if the SSR
|
|
260
|
+
// bundle resolves a different React instance internally.
|
|
260
261
|
const runtimeMod = await import('@uniweb/runtime/ssr')
|
|
261
262
|
preparePropsSSR = runtimeMod.prepareProps
|
|
262
263
|
getComponentMetaSSR = runtimeMod.getComponentMeta
|
|
263
|
-
guaranteeContentStructureSSR = runtimeMod.guaranteeContentStructure
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
/**
|
|
@@ -301,8 +301,155 @@ async function prefetchIcons(siteContent, uniweb, onProgress) {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
/**
|
|
304
|
-
*
|
|
305
|
-
|
|
304
|
+
* Valid color contexts for section theming
|
|
305
|
+
*/
|
|
306
|
+
const VALID_CONTEXTS = ['light', 'medium', 'dark']
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Build wrapper props from block configuration
|
|
310
|
+
* Mirrors getWrapperProps in BlockRenderer.jsx
|
|
311
|
+
*/
|
|
312
|
+
function getWrapperProps(block) {
|
|
313
|
+
const theme = block.themeName
|
|
314
|
+
const blockClassName = block.state?.className || ''
|
|
315
|
+
|
|
316
|
+
let contextClass = ''
|
|
317
|
+
if (theme && VALID_CONTEXTS.includes(theme)) {
|
|
318
|
+
contextClass = `context-${theme}`
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
let className = contextClass
|
|
322
|
+
if (blockClassName) {
|
|
323
|
+
className = className ? `${className} ${blockClassName}` : blockClassName
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const { background = {} } = block.standardOptions
|
|
327
|
+
const style = {}
|
|
328
|
+
if (background.mode) {
|
|
329
|
+
style.position = 'relative'
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const sectionId = block.stableId || block.id
|
|
333
|
+
return { id: `section-${sectionId}`, style, className, background }
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Render a background element for SSR
|
|
338
|
+
* Mirrors the Background component in Background.jsx (image, color, gradient only)
|
|
339
|
+
* Video backgrounds are skipped in SSR (they require JS for autoplay)
|
|
340
|
+
*/
|
|
341
|
+
function renderBackground(background) {
|
|
342
|
+
if (!background?.mode) return null
|
|
343
|
+
|
|
344
|
+
const containerStyle = {
|
|
345
|
+
position: 'absolute',
|
|
346
|
+
inset: '0',
|
|
347
|
+
overflow: 'hidden',
|
|
348
|
+
zIndex: 0,
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const children = []
|
|
352
|
+
|
|
353
|
+
// Resolve URL against basePath for subdirectory deployments
|
|
354
|
+
const basePath = globalThis.uniweb?.activeWebsite?.basePath || ''
|
|
355
|
+
function resolveUrl(url) {
|
|
356
|
+
if (!url || !url.startsWith('/')) return url
|
|
357
|
+
if (!basePath) return url
|
|
358
|
+
if (url.startsWith(basePath + '/') || url === basePath) return url
|
|
359
|
+
return basePath + url
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (background.mode === 'color' && background.color) {
|
|
363
|
+
children.push(
|
|
364
|
+
React.createElement('div', {
|
|
365
|
+
key: 'bg-color',
|
|
366
|
+
className: 'background-color',
|
|
367
|
+
style: { position: 'absolute', inset: '0', backgroundColor: background.color },
|
|
368
|
+
'aria-hidden': 'true'
|
|
369
|
+
})
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (background.mode === 'gradient' && background.gradient) {
|
|
374
|
+
const g = background.gradient
|
|
375
|
+
const angle = g.angle || 0
|
|
376
|
+
const start = g.start || 'transparent'
|
|
377
|
+
const end = g.end || 'transparent'
|
|
378
|
+
const startPos = g.startPosition || 0
|
|
379
|
+
const endPos = g.endPosition || 100
|
|
380
|
+
children.push(
|
|
381
|
+
React.createElement('div', {
|
|
382
|
+
key: 'bg-gradient',
|
|
383
|
+
className: 'background-gradient',
|
|
384
|
+
style: {
|
|
385
|
+
position: 'absolute', inset: '0',
|
|
386
|
+
background: `linear-gradient(${angle}deg, ${start} ${startPos}%, ${end} ${endPos}%)`
|
|
387
|
+
},
|
|
388
|
+
'aria-hidden': 'true'
|
|
389
|
+
})
|
|
390
|
+
)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (background.mode === 'image' && background.image?.src) {
|
|
394
|
+
const img = background.image
|
|
395
|
+
children.push(
|
|
396
|
+
React.createElement('div', {
|
|
397
|
+
key: 'bg-image',
|
|
398
|
+
className: 'background-image',
|
|
399
|
+
style: {
|
|
400
|
+
position: 'absolute', inset: '0',
|
|
401
|
+
backgroundImage: `url(${resolveUrl(img.src)})`,
|
|
402
|
+
backgroundPosition: img.position || 'center',
|
|
403
|
+
backgroundSize: img.size || 'cover',
|
|
404
|
+
backgroundRepeat: 'no-repeat'
|
|
405
|
+
},
|
|
406
|
+
'aria-hidden': 'true'
|
|
407
|
+
})
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Overlay
|
|
412
|
+
if (background.overlay?.enabled) {
|
|
413
|
+
const ov = background.overlay
|
|
414
|
+
let overlayStyle
|
|
415
|
+
|
|
416
|
+
if (ov.gradient) {
|
|
417
|
+
const g = ov.gradient
|
|
418
|
+
overlayStyle = {
|
|
419
|
+
position: 'absolute', inset: '0', pointerEvents: 'none',
|
|
420
|
+
background: `linear-gradient(${g.angle || 180}deg, ${g.start || 'rgba(0,0,0,0.7)'} ${g.startPosition || 0}%, ${g.end || 'rgba(0,0,0,0)'} ${g.endPosition || 100}%)`,
|
|
421
|
+
opacity: ov.opacity ?? 0.5
|
|
422
|
+
}
|
|
423
|
+
} else {
|
|
424
|
+
const baseColor = ov.type === 'light' ? '255, 255, 255' : '0, 0, 0'
|
|
425
|
+
overlayStyle = {
|
|
426
|
+
position: 'absolute', inset: '0', pointerEvents: 'none',
|
|
427
|
+
backgroundColor: `rgba(${baseColor}, ${ov.opacity ?? 0.5})`
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
children.push(
|
|
432
|
+
React.createElement('div', {
|
|
433
|
+
key: 'bg-overlay',
|
|
434
|
+
className: 'background-overlay',
|
|
435
|
+
style: overlayStyle,
|
|
436
|
+
'aria-hidden': 'true'
|
|
437
|
+
})
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (children.length === 0) return null
|
|
442
|
+
|
|
443
|
+
return React.createElement('div', {
|
|
444
|
+
className: `background background--${background.mode}`,
|
|
445
|
+
style: containerStyle,
|
|
446
|
+
'aria-hidden': 'true'
|
|
447
|
+
}, ...children)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Render a single block for SSR
|
|
452
|
+
* Mirrors BlockRenderer.jsx but without hooks (no runtime data fetching in SSR)
|
|
306
453
|
*/
|
|
307
454
|
function renderBlock(block) {
|
|
308
455
|
const Component = block.initComponent()
|
|
@@ -318,14 +465,10 @@ function renderBlock(block) {
|
|
|
318
465
|
let content, params
|
|
319
466
|
|
|
320
467
|
if (block.parsedContent?._isPoc) {
|
|
321
|
-
// Simple PoC format - content was passed directly
|
|
322
468
|
content = block.parsedContent._pocContent
|
|
323
469
|
params = block.properties
|
|
324
470
|
} else {
|
|
325
|
-
// Get runtime metadata for this component
|
|
326
471
|
const meta = getComponentMetaSSR(block.type)
|
|
327
|
-
|
|
328
|
-
// Prepare props with runtime guarantees
|
|
329
472
|
const prepared = preparePropsSSR(block, meta)
|
|
330
473
|
params = prepared.params
|
|
331
474
|
content = {
|
|
@@ -335,28 +478,33 @@ function renderBlock(block) {
|
|
|
335
478
|
}
|
|
336
479
|
}
|
|
337
480
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
481
|
+
// Background handling (mirrors BlockRenderer.jsx)
|
|
482
|
+
const { background, ...wrapperProps } = getWrapperProps(block)
|
|
483
|
+
const meta = getComponentMetaSSR(block.type)
|
|
484
|
+
const hasBackground = background?.mode && meta?.background !== 'self'
|
|
485
|
+
|
|
486
|
+
if (hasBackground) {
|
|
487
|
+
params = { ...params, _hasBackground: true }
|
|
342
488
|
}
|
|
343
489
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
490
|
+
const componentProps = { content, params, block }
|
|
491
|
+
|
|
492
|
+
if (hasBackground) {
|
|
493
|
+
return React.createElement('section', wrapperProps,
|
|
494
|
+
renderBackground(background),
|
|
495
|
+
React.createElement('div', { className: 'relative z-10' },
|
|
496
|
+
React.createElement(Component, componentProps)
|
|
497
|
+
)
|
|
498
|
+
)
|
|
351
499
|
}
|
|
352
500
|
|
|
353
|
-
return React.createElement('
|
|
501
|
+
return React.createElement('section', wrapperProps,
|
|
354
502
|
React.createElement(Component, componentProps)
|
|
355
503
|
)
|
|
356
504
|
}
|
|
357
505
|
|
|
358
506
|
/**
|
|
359
|
-
*
|
|
507
|
+
* Render an array of blocks for SSR
|
|
360
508
|
*/
|
|
361
509
|
function renderBlocks(blocks) {
|
|
362
510
|
if (!blocks || blocks.length === 0) return null
|
|
@@ -368,7 +516,7 @@ function renderBlocks(blocks) {
|
|
|
368
516
|
}
|
|
369
517
|
|
|
370
518
|
/**
|
|
371
|
-
*
|
|
519
|
+
* Render page layout for SSR
|
|
372
520
|
*/
|
|
373
521
|
function renderLayout(page, website) {
|
|
374
522
|
const RemoteLayout = website.getRemoteLayout()
|
|
@@ -387,34 +535,25 @@ function renderLayout(page, website) {
|
|
|
387
535
|
|
|
388
536
|
if (RemoteLayout) {
|
|
389
537
|
return React.createElement(RemoteLayout, {
|
|
390
|
-
page,
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
footer: footerElement,
|
|
395
|
-
left: leftElement,
|
|
396
|
-
right: rightElement,
|
|
397
|
-
leftPanel: leftElement,
|
|
398
|
-
rightPanel: rightElement
|
|
538
|
+
page, website,
|
|
539
|
+
header: headerElement, body: bodyElement, footer: footerElement,
|
|
540
|
+
left: leftElement, right: rightElement,
|
|
541
|
+
leftPanel: leftElement, rightPanel: rightElement
|
|
399
542
|
})
|
|
400
543
|
}
|
|
401
544
|
|
|
402
|
-
// Default layout
|
|
403
545
|
return React.createElement(React.Fragment, null,
|
|
404
|
-
headerElement,
|
|
405
|
-
bodyElement,
|
|
406
|
-
footerElement
|
|
546
|
+
headerElement && React.createElement('header', null, headerElement),
|
|
547
|
+
bodyElement && React.createElement('main', null, bodyElement),
|
|
548
|
+
footerElement && React.createElement('footer', null, footerElement)
|
|
407
549
|
)
|
|
408
550
|
}
|
|
409
551
|
|
|
410
552
|
/**
|
|
411
|
-
*
|
|
412
|
-
* Uses React from prerender's scope
|
|
553
|
+
* Create a page element for SSR
|
|
413
554
|
*/
|
|
414
555
|
function createPageElement(page, website) {
|
|
415
|
-
return
|
|
416
|
-
renderLayout(page, website)
|
|
417
|
-
)
|
|
556
|
+
return renderLayout(page, website)
|
|
418
557
|
}
|
|
419
558
|
|
|
420
559
|
/**
|
|
@@ -585,7 +724,7 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
585
724
|
// Set this as the active page
|
|
586
725
|
uniweb.activeWebsite.setActivePage(page.route)
|
|
587
726
|
|
|
588
|
-
// Create the page element
|
|
727
|
+
// Create the page element for SSR
|
|
589
728
|
const element = createPageElement(page, website)
|
|
590
729
|
|
|
591
730
|
// Render to HTML string
|
package/src/runtime-schema.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* The runtime schema is optimized for size and contains only what's
|
|
6
6
|
* needed at render time:
|
|
7
7
|
*
|
|
8
|
-
* - background:
|
|
8
|
+
* - background: 'self' when component handles its own background
|
|
9
9
|
* - data: { type, limit } for CMS entity binding
|
|
10
10
|
* - defaults: param default values
|
|
11
11
|
* - context: static capabilities for cross-block coordination
|
|
@@ -190,7 +190,8 @@ export function extractRuntimeSchema(fullMeta) {
|
|
|
190
190
|
|
|
191
191
|
const runtime = {}
|
|
192
192
|
|
|
193
|
-
// Background
|
|
193
|
+
// Background opt-out: 'self' means the component renders its own background
|
|
194
|
+
// layer (solid colors, insets, effects), so the runtime skips its Background.
|
|
194
195
|
if (fullMeta.background) {
|
|
195
196
|
runtime.background = fullMeta.background
|
|
196
197
|
}
|