@uniweb/build 0.8.19 → 0.8.21
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 +7 -5
- package/src/import-map-plugin.js +145 -0
- package/src/prerender.js +15 -0
- package/src/runtime-schema.js +8 -0
- package/src/site/config.js +61 -92
- package/src/site/content-collector.js +19 -3
- package/src/site/data-fetcher.js +12 -0
- package/src/vite-foundation-plugin.js +133 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/build",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.21",
|
|
4
4
|
"description": "Build tooling for the Uniweb Component Web Platform",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"./dev": "./src/dev/index.js",
|
|
18
18
|
"./prerender": "./src/prerender.js",
|
|
19
19
|
"./i18n": "./src/i18n/index.js",
|
|
20
|
-
"./search": "./src/search/index.js"
|
|
20
|
+
"./search": "./src/search/index.js",
|
|
21
|
+
"./import-map-plugin": "./src/import-map-plugin.js"
|
|
21
22
|
},
|
|
22
23
|
"files": [
|
|
23
24
|
"src"
|
|
@@ -47,14 +48,15 @@
|
|
|
47
48
|
"jest": "^29.7.0"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
51
|
+
"esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0",
|
|
50
52
|
"js-yaml": "^4.1.0",
|
|
51
53
|
"sharp": "^0.33.2",
|
|
52
54
|
"@uniweb/theming": "0.1.2"
|
|
53
55
|
},
|
|
54
56
|
"optionalDependencies": {
|
|
55
|
-
"@uniweb/
|
|
57
|
+
"@uniweb/runtime": "0.6.16",
|
|
56
58
|
"@uniweb/content-reader": "1.1.4",
|
|
57
|
-
"@uniweb/
|
|
59
|
+
"@uniweb/schemas": "0.2.1"
|
|
58
60
|
},
|
|
59
61
|
"peerDependencies": {
|
|
60
62
|
"vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
|
|
@@ -63,7 +65,7 @@
|
|
|
63
65
|
"@tailwindcss/vite": "^4.0.0",
|
|
64
66
|
"@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
|
|
65
67
|
"vite-plugin-svgr": "^4.0.0",
|
|
66
|
-
"@uniweb/core": "0.5.
|
|
68
|
+
"@uniweb/core": "0.5.15"
|
|
67
69
|
},
|
|
68
70
|
"peerDependenciesMeta": {
|
|
69
71
|
"vite": {
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Import Map Plugin
|
|
3
|
+
*
|
|
4
|
+
* Shared Vite plugin that emits import-map bridge modules so that
|
|
5
|
+
* foundations loaded via dynamic import() can resolve bare specifiers
|
|
6
|
+
* (react, @uniweb/core, etc.) to the same instances used by the host app.
|
|
7
|
+
*
|
|
8
|
+
* Production: emits deterministic chunks at _importmap/*.js with explicit
|
|
9
|
+
* named re-exports, and injects a <script type="importmap"> into the HTML.
|
|
10
|
+
*
|
|
11
|
+
* Used by:
|
|
12
|
+
* - Site builds (runtime mode + extensions) — packages/build/src/site/config.js
|
|
13
|
+
* - Runtime shell build — packages/runtime/vite.config.app.js
|
|
14
|
+
* - Dynamic-runtime (editor preview) — packages/uniweb-editor/dynamic-runtime/
|
|
15
|
+
*
|
|
16
|
+
* @module @uniweb/build/import-map-plugin
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Default externals shared between foundations and hosts */
|
|
20
|
+
const DEFAULT_EXTERNALS = [
|
|
21
|
+
'react',
|
|
22
|
+
'react-dom',
|
|
23
|
+
'react/jsx-runtime',
|
|
24
|
+
'react/jsx-dev-runtime',
|
|
25
|
+
'@uniweb/core',
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
const IMPORT_MAP_PREFIX = '\0importmap:'
|
|
29
|
+
|
|
30
|
+
/** Valid JS identifier — filters out non-identifier keys from CJS modules */
|
|
31
|
+
const isValidId = (k) => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k)
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Create the import map Vite plugin.
|
|
35
|
+
*
|
|
36
|
+
* @param {Object} [options]
|
|
37
|
+
* @param {string[]} [options.externals] - Package specifiers to bridge (default: react, react-dom, @uniweb/core, etc.)
|
|
38
|
+
* @param {string} [options.name] - Plugin name (default: 'uniweb:import-map')
|
|
39
|
+
* @param {string} [options.basePath] - Base path prefix for import map URLs in HTML (default: '/')
|
|
40
|
+
* @param {string} [options.resolveFrom] - Absolute path to resolve bare specifiers from inside virtual modules.
|
|
41
|
+
* Needed when the host project doesn't have the externals as direct dependencies (e.g., site builds
|
|
42
|
+
* under pnpm strict mode resolve from the foundation directory instead).
|
|
43
|
+
* @param {Object} [options.devBridges] - Map of specifier → dev-mode URL for import map injection in dev.
|
|
44
|
+
* When provided, the import map is injected in both dev and prod (with different URLs).
|
|
45
|
+
* When omitted, the import map is only injected in prod (dev uses other mechanisms like transformRequest).
|
|
46
|
+
* @returns {import('vite').Plugin}
|
|
47
|
+
*/
|
|
48
|
+
export function importMapPlugin({
|
|
49
|
+
externals = DEFAULT_EXTERNALS,
|
|
50
|
+
name = 'uniweb:import-map',
|
|
51
|
+
basePath = '/',
|
|
52
|
+
resolveFrom,
|
|
53
|
+
devBridges,
|
|
54
|
+
} = {}) {
|
|
55
|
+
let isBuild = false
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
name,
|
|
59
|
+
|
|
60
|
+
configResolved(config) {
|
|
61
|
+
isBuild = config.command === 'build'
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
resolveId(id, importer) {
|
|
65
|
+
if (id.startsWith(IMPORT_MAP_PREFIX)) return id
|
|
66
|
+
// Bare specifiers inside our virtual modules (e.g. '@uniweb/core' re-exported
|
|
67
|
+
// from '\0importmap:@uniweb/core') can't be resolved by Rollup because virtual
|
|
68
|
+
// modules have no filesystem context. When a resolveFrom path is provided,
|
|
69
|
+
// resolve from there (e.g. the foundation directory under pnpm strict mode).
|
|
70
|
+
if (resolveFrom && importer?.startsWith(IMPORT_MAP_PREFIX) && externals.includes(id)) {
|
|
71
|
+
return this.resolve(id, resolveFrom, { skipSelf: true })
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
|
|
75
|
+
async load(id) {
|
|
76
|
+
if (!id.startsWith(IMPORT_MAP_PREFIX)) return
|
|
77
|
+
const pkg = id.slice(IMPORT_MAP_PREFIX.length)
|
|
78
|
+
|
|
79
|
+
// Generate explicit named re-exports (not `export *`) because CJS
|
|
80
|
+
// packages like React only expose a default via `export *`, losing
|
|
81
|
+
// individual named exports (useState, jsx, etc.) that foundations need.
|
|
82
|
+
try {
|
|
83
|
+
const mod = await import(pkg)
|
|
84
|
+
const names = Object.keys(mod).filter((k) => k !== '__esModule' && isValidId(k))
|
|
85
|
+
const hasDefault = 'default' in mod
|
|
86
|
+
const named = names.filter((k) => k !== 'default')
|
|
87
|
+
const lines = []
|
|
88
|
+
if (named.length) {
|
|
89
|
+
lines.push(`export { ${named.join(', ')} } from '${pkg}'`)
|
|
90
|
+
}
|
|
91
|
+
if (hasDefault) {
|
|
92
|
+
lines.push(`export { default } from '${pkg}'`)
|
|
93
|
+
}
|
|
94
|
+
return lines.join('\n') || 'export {}'
|
|
95
|
+
} catch {
|
|
96
|
+
// Fallback: generic re-export (may not preserve named exports for CJS)
|
|
97
|
+
return `export * from '${pkg}'`
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
// Emit deterministic chunks for each external (production only).
|
|
102
|
+
// preserveSignature: 'exports-only' tells Rollup to preserve the original
|
|
103
|
+
// export names (useState, jsx, etc.) instead of mangling them.
|
|
104
|
+
buildStart() {
|
|
105
|
+
if (!isBuild) return
|
|
106
|
+
for (const ext of externals) {
|
|
107
|
+
this.emitFile({
|
|
108
|
+
type: 'chunk',
|
|
109
|
+
id: `${IMPORT_MAP_PREFIX}${ext}`,
|
|
110
|
+
fileName: `_importmap/${ext.replace(/\//g, '-')}.js`,
|
|
111
|
+
preserveSignature: 'exports-only',
|
|
112
|
+
})
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// Inject the import map into the HTML.
|
|
117
|
+
// In prod: always injects with basePath-prefixed _importmap/ URLs.
|
|
118
|
+
// In dev: only injects if devBridges are provided (otherwise, the consumer
|
|
119
|
+
// handles dev-mode resolution via other mechanisms like transformRequest).
|
|
120
|
+
transformIndexHtml: {
|
|
121
|
+
order: 'pre',
|
|
122
|
+
handler(html) {
|
|
123
|
+
const imports = {}
|
|
124
|
+
|
|
125
|
+
if (isBuild) {
|
|
126
|
+
for (const ext of externals) {
|
|
127
|
+
imports[ext] = `${basePath}_importmap/${ext.replace(/\//g, '-')}.js`
|
|
128
|
+
}
|
|
129
|
+
} else if (devBridges) {
|
|
130
|
+
Object.assign(imports, devBridges)
|
|
131
|
+
} else {
|
|
132
|
+
// No dev injection — consumer handles dev mode separately
|
|
133
|
+
return html
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const importMap = JSON.stringify({ imports }, null, 2)
|
|
137
|
+
const script = ` <script type="importmap">\n${importMap}\n </script>\n`
|
|
138
|
+
// Import map must appear before any module scripts
|
|
139
|
+
return html.replace('<head>', '<head>\n' + script)
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export { DEFAULT_EXTERNALS }
|
package/src/prerender.js
CHANGED
|
@@ -403,6 +403,7 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
403
403
|
prefetchIcons,
|
|
404
404
|
renderPage,
|
|
405
405
|
injectPageContent,
|
|
406
|
+
generate404Html,
|
|
406
407
|
} = await import('@uniweb/runtime/ssr')
|
|
407
408
|
|
|
408
409
|
// Load default site content
|
|
@@ -545,6 +546,20 @@ export async function prerenderSite(siteDir, options = {}) {
|
|
|
545
546
|
renderedFiles.push(outputPath)
|
|
546
547
|
onProgress(` → ${outputPath.replace(distDir, 'dist')}`)
|
|
547
548
|
}
|
|
549
|
+
|
|
550
|
+
// Write 404.html — shared logic from @uniweb/runtime/ssr
|
|
551
|
+
const fallbackBaseHtml = injectBuildData(htmlShell, siteContent)
|
|
552
|
+
const { html: notFoundHtml, hasNotFoundPage } = generate404Html({
|
|
553
|
+
baseHtml: fallbackBaseHtml,
|
|
554
|
+
website,
|
|
555
|
+
siteContent,
|
|
556
|
+
})
|
|
557
|
+
|
|
558
|
+
const fallbackDir = routePrefix ? join(distDir, routePrefix.replace(/^\//, '')) : distDir
|
|
559
|
+
await mkdir(fallbackDir, { recursive: true })
|
|
560
|
+
await writeFile(join(fallbackDir, '404.html'), notFoundHtml)
|
|
561
|
+
const fallbackNote = hasNotFoundPage ? '404 page + SPA fallback' : 'SPA fallback'
|
|
562
|
+
onProgress(` → ${routePrefix || ''}404.html (${fallbackNote})`)
|
|
548
563
|
}
|
|
549
564
|
|
|
550
565
|
onProgress(`\nPre-rendered ${renderedFiles.length} pages across ${localeConfigs.length} locale(s)`)
|
package/src/runtime-schema.js
CHANGED
|
@@ -228,6 +228,14 @@ export function extractRuntimeSchema(fullMeta) {
|
|
|
228
228
|
if (fullMeta.data.inherit !== undefined) {
|
|
229
229
|
runtime.inheritData = fullMeta.data.inherit
|
|
230
230
|
}
|
|
231
|
+
// detail: false → opt out of single-item resolution on dynamic pages,
|
|
232
|
+
// returning the collection instead (minus the active item)
|
|
233
|
+
if (fullMeta.data.detail !== undefined) {
|
|
234
|
+
runtime.inheritDetail = fullMeta.data.detail
|
|
235
|
+
}
|
|
236
|
+
if (fullMeta.data.limit !== undefined) {
|
|
237
|
+
runtime.inheritLimit = fullMeta.data.limit
|
|
238
|
+
}
|
|
231
239
|
}
|
|
232
240
|
}
|
|
233
241
|
|
package/src/site/config.js
CHANGED
|
@@ -24,6 +24,7 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
24
24
|
import { resolve, dirname, join } from 'node:path'
|
|
25
25
|
import yaml from 'js-yaml'
|
|
26
26
|
import { generateEntryPoint, shouldRegenerateForFile } from '../generate-entry.js'
|
|
27
|
+
import { importMapPlugin } from '../import-map-plugin.js'
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Normalize a base path for Vite compatibility
|
|
@@ -343,105 +344,73 @@ export async function defineSiteConfig(options = {}) {
|
|
|
343
344
|
|
|
344
345
|
if (noopFoundationPlugin) plugins.push(noopFoundationPlugin)
|
|
345
346
|
|
|
346
|
-
// Import map plugin for runtime mode production builds
|
|
347
|
+
// Import map plugin for runtime mode production builds.
|
|
347
348
|
// Emits re-export modules for each externalized package (react, @uniweb/core, etc.)
|
|
348
|
-
// so the browser can resolve bare specifiers in the dynamically-imported foundation
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
return {
|
|
362
|
-
name: 'uniweb:import-map',
|
|
363
|
-
|
|
364
|
-
configResolved(config) {
|
|
365
|
-
isBuild = config.command === 'build'
|
|
366
|
-
},
|
|
367
|
-
|
|
368
|
-
resolveId(id, importer) {
|
|
369
|
-
if (id.startsWith(IMPORT_MAP_PREFIX)) return id
|
|
370
|
-
// Bare specifiers inside our virtual modules (e.g. '@uniweb/core' re-exported
|
|
371
|
-
// from '\0importmap:@uniweb/core') can't be resolved by Rollup because virtual
|
|
372
|
-
// modules have no filesystem context. Resolve from the foundation directory where
|
|
373
|
-
// @uniweb/core is a direct dependency (the site may not have it under pnpm strict).
|
|
374
|
-
if (importer?.startsWith(IMPORT_MAP_PREFIX) && IMPORT_MAP_EXTERNALS.includes(id)) {
|
|
375
|
-
const resolveFrom = foundationInfo.path
|
|
376
|
-
? resolve(foundationInfo.path, 'package.json')
|
|
377
|
-
: resolve(siteRoot, 'main.js')
|
|
378
|
-
return this.resolve(id, resolveFrom, { skipSelf: true })
|
|
379
|
-
}
|
|
380
|
-
},
|
|
349
|
+
// so the browser can resolve bare specifiers in the dynamically-imported foundation.
|
|
350
|
+
// In dev mode, Vite's transformRequest() handles bare specifier resolution instead.
|
|
351
|
+
if (needsImportMap) {
|
|
352
|
+
plugins.push(importMapPlugin({
|
|
353
|
+
basePath: base || '/',
|
|
354
|
+
// Under pnpm strict mode, the site may not have @uniweb/core in its own
|
|
355
|
+
// node_modules. Resolve from the foundation directory where it's a direct dep.
|
|
356
|
+
resolveFrom: foundationInfo.path
|
|
357
|
+
? resolve(foundationInfo.path, 'package.json')
|
|
358
|
+
: resolve(siteRoot, 'main.js'),
|
|
359
|
+
}))
|
|
360
|
+
}
|
|
381
361
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
362
|
+
// Preload hints for runtime-loaded foundations and extensions.
|
|
363
|
+
// In runtime mode, foundation JS is loaded via import() and CSS is injected
|
|
364
|
+
// dynamically in JavaScript — the browser doesn't discover them until JS executes.
|
|
365
|
+
// These <link> tags let the browser start fetching during HTML parsing.
|
|
366
|
+
// Shell mode is excluded: URLs come from __DATA__ at serve time (unicloud handles it).
|
|
367
|
+
if (isRuntimeMode && !isShellMode) {
|
|
368
|
+
plugins.push({
|
|
369
|
+
name: 'uniweb:foundation-preload',
|
|
370
|
+
transformIndexHtml: {
|
|
371
|
+
order: 'post',
|
|
372
|
+
handler() {
|
|
373
|
+
const tags = []
|
|
374
|
+
|
|
375
|
+
// Foundation JS modulepreload
|
|
376
|
+
if (foundationConfig.url) {
|
|
377
|
+
tags.push({
|
|
378
|
+
tag: 'link',
|
|
379
|
+
attrs: { rel: 'modulepreload', href: foundationConfig.url },
|
|
380
|
+
injectTo: 'head',
|
|
381
|
+
})
|
|
400
382
|
}
|
|
401
|
-
return lines.join('\n') || `export {}`
|
|
402
|
-
} catch {
|
|
403
|
-
// Fallback: generic re-export (may not preserve named exports for CJS)
|
|
404
|
-
return `export * from '${pkg}'`
|
|
405
|
-
}
|
|
406
|
-
},
|
|
407
383
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
fileName: `_importmap/${ext.replace(/\//g, '-')}.js`,
|
|
419
|
-
preserveSignature: 'exports-only'
|
|
420
|
-
})
|
|
421
|
-
}
|
|
422
|
-
},
|
|
384
|
+
// Foundation CSS — injected as a real <link> so the browser fetches it
|
|
385
|
+
// during HTML parsing instead of waiting for loadFoundationCSS() in JS.
|
|
386
|
+
// The runtime's dynamic <link> deduplicates (same URL, already cached).
|
|
387
|
+
if (foundationConfig.cssUrl) {
|
|
388
|
+
tags.push({
|
|
389
|
+
tag: 'link',
|
|
390
|
+
attrs: { rel: 'stylesheet', href: foundationConfig.cssUrl },
|
|
391
|
+
injectTo: 'head',
|
|
392
|
+
})
|
|
393
|
+
}
|
|
423
394
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
395
|
+
// Extension JS modulepreload (CSS left to runtime — we can't reliably
|
|
396
|
+
// derive CSS URLs for all extension formats)
|
|
397
|
+
const extensions = siteConfig.extensions || []
|
|
398
|
+
for (const ext of extensions) {
|
|
399
|
+
const url = typeof ext === 'string' ? ext : ext?.url
|
|
400
|
+
if (url) {
|
|
401
|
+
tags.push({
|
|
402
|
+
tag: 'link',
|
|
403
|
+
attrs: { rel: 'modulepreload', href: url },
|
|
404
|
+
injectTo: 'head',
|
|
405
|
+
})
|
|
406
|
+
}
|
|
434
407
|
}
|
|
435
|
-
const importMap = JSON.stringify({ imports }, null, 2)
|
|
436
|
-
const script = ` <script type="importmap">\n${importMap}\n </script>\n`
|
|
437
|
-
// Import map must appear before any module scripts
|
|
438
|
-
return html.replace('<head>', '<head>\n' + script)
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
}
|
|
442
|
-
})() : null
|
|
443
408
|
|
|
444
|
-
|
|
409
|
+
return tags
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
})
|
|
413
|
+
}
|
|
445
414
|
|
|
446
415
|
// Build foundation config for runtime
|
|
447
416
|
const foundationConfig = {
|
|
@@ -1425,6 +1425,14 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1425
1425
|
const { page, assetCollection: pageAssets, iconCollection: pageIcons } = result
|
|
1426
1426
|
assetCollection = mergeAssetCollections(assetCollection, pageAssets)
|
|
1427
1427
|
iconCollection = mergeIconCollections(iconCollection, pageIcons)
|
|
1428
|
+
|
|
1429
|
+
// Modern pattern: blog/index/ (isIndex) inherits the container's fetch config
|
|
1430
|
+
// when it has no fetch of its own. Without this, EntityStore can't find the
|
|
1431
|
+
// fetch config for sections on the index page (page.parent is null for /blog).
|
|
1432
|
+
if (isIndex && !page.fetch && parentFetch) {
|
|
1433
|
+
page.fetch = parentFetch
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1428
1436
|
pages.push(page)
|
|
1429
1437
|
|
|
1430
1438
|
// Recurse into subdirectories (page mode)
|
|
@@ -1477,16 +1485,17 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1477
1485
|
changefreq: dirConfig.seo?.changefreq || null,
|
|
1478
1486
|
priority: dirConfig.seo?.priority || null
|
|
1479
1487
|
},
|
|
1480
|
-
fetch: null,
|
|
1488
|
+
fetch: parseFetchConfig(dirConfig.fetch) || null,
|
|
1481
1489
|
sections: [],
|
|
1482
1490
|
order: typeof dirConfig.order === 'number' ? dirConfig.order : undefined
|
|
1483
1491
|
}
|
|
1484
1492
|
|
|
1485
1493
|
pages.push(containerPage)
|
|
1486
1494
|
|
|
1487
|
-
// Recurse in folder mode
|
|
1495
|
+
// Recurse in folder mode — pass container's own fetch config (or fall back to parent's)
|
|
1488
1496
|
const childDirPath = mounts?.get(entry) || entryPath
|
|
1489
|
-
const
|
|
1497
|
+
const containerFetch = containerPage.fetch || parentFetch
|
|
1498
|
+
const subResult = await collectPagesRecursive(childDirPath, containerRoute, siteRoot, childOrderConfig, containerFetch, versionContext, 'pages', null, effectiveLayout)
|
|
1490
1499
|
pages.push(...subResult.pages)
|
|
1491
1500
|
assetCollection = mergeAssetCollections(assetCollection, subResult.assetCollection)
|
|
1492
1501
|
iconCollection = mergeIconCollections(iconCollection, subResult.iconCollection)
|
|
@@ -1595,6 +1604,13 @@ async function collectPagesRecursive(dirPath, parentRoute, siteRoot, orderConfig
|
|
|
1595
1604
|
assetCollection = mergeAssetCollections(assetCollection, pageAssets)
|
|
1596
1605
|
iconCollection = mergeIconCollections(iconCollection, pageIcons)
|
|
1597
1606
|
|
|
1607
|
+
// Modern pattern: articles/index/ (isIndex) inherits the container's fetch config
|
|
1608
|
+
// when it has no fetch of its own. Without this, EntityStore can't find the
|
|
1609
|
+
// fetch config for sections on the index page.
|
|
1610
|
+
if (isIndex && !page.fetch && parentFetch) {
|
|
1611
|
+
page.fetch = parentFetch
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1598
1614
|
// Handle 404 page - only at root level
|
|
1599
1615
|
if (parentRoute === '/' && entry === '404') {
|
|
1600
1616
|
notFound = page
|
package/src/site/data-fetcher.js
CHANGED
|
@@ -225,6 +225,18 @@ export function parseFetchConfig(fetch) {
|
|
|
225
225
|
// Full config object
|
|
226
226
|
if (typeof fetch !== 'object') return null
|
|
227
227
|
|
|
228
|
+
// Inherit-merge config: { inherit: true, detail: false, limit: 3 }
|
|
229
|
+
// No URL — merges with the parent fetch config at runtime; only carries override props.
|
|
230
|
+
if (fetch.inherit === true) {
|
|
231
|
+
return {
|
|
232
|
+
inherit: true,
|
|
233
|
+
...(fetch.detail !== undefined ? { detail: fetch.detail } : {}),
|
|
234
|
+
...(fetch.limit !== undefined ? { limit: fetch.limit } : {}),
|
|
235
|
+
...(fetch.sort !== undefined ? { sort: fetch.sort } : {}),
|
|
236
|
+
...(fetch.filter !== undefined ? { filter: fetch.filter } : {}),
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
228
240
|
// Collection reference: { collection: 'articles', limit: 3 }
|
|
229
241
|
if (fetch.collection) {
|
|
230
242
|
return {
|
|
@@ -34,6 +34,133 @@ async function buildSchemaWithPreviews(srcDir, outDir, isProduction, sectionPath
|
|
|
34
34
|
return schemaWithImages
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Module-level guard to prevent recursive SSR bundle builds.
|
|
39
|
+
* When buildSSRBundle calls esbuild, it should not re-trigger
|
|
40
|
+
* the foundation plugin's writeBundle hook.
|
|
41
|
+
*/
|
|
42
|
+
let _buildingSSRBundle = false
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Build a self-contained ESM bundle for edge SSR (Dynamic Workers).
|
|
46
|
+
*
|
|
47
|
+
* Produces foundation.ssr.js — a single ESM file with React, ReactDOM/server,
|
|
48
|
+
* @uniweb/core, and the foundation components all inlined. No external imports.
|
|
49
|
+
*
|
|
50
|
+
* This bundle is loaded into a Cloudflare Dynamic Worker isolate at request time
|
|
51
|
+
* via env.LOADER.get(). The isolate caches the bundle per foundation version.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} outDir - Path to dist/ directory containing foundation.js
|
|
54
|
+
*/
|
|
55
|
+
async function buildSSRBundle(outDir) {
|
|
56
|
+
if (_buildingSSRBundle) return
|
|
57
|
+
_buildingSSRBundle = true
|
|
58
|
+
|
|
59
|
+
const entryPath = join(outDir, 'foundation.js')
|
|
60
|
+
try {
|
|
61
|
+
const { build: esbuild } = await import('esbuild')
|
|
62
|
+
const { statSync } = await import('node:fs')
|
|
63
|
+
|
|
64
|
+
// Collect all node_modules directories up the tree (pnpm hoists to workspace root)
|
|
65
|
+
const { existsSync } = await import('node:fs')
|
|
66
|
+
let searchDir = resolve(outDir, '..')
|
|
67
|
+
let nodePaths = []
|
|
68
|
+
for (let i = 0; i < 10; i++) {
|
|
69
|
+
const candidate = join(searchDir, 'node_modules')
|
|
70
|
+
if (existsSync(candidate)) {
|
|
71
|
+
nodePaths.push(candidate)
|
|
72
|
+
}
|
|
73
|
+
const parent = resolve(searchDir, '..')
|
|
74
|
+
if (parent === searchDir) break
|
|
75
|
+
searchDir = parent
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Resolve workspace packages that esbuild can't find via node_modules
|
|
79
|
+
// (pnpm workspace symlinks aren't in node_modules for the foundation project)
|
|
80
|
+
const { createRequire } = await import('node:module')
|
|
81
|
+
const pluginRequire = createRequire(import.meta.url)
|
|
82
|
+
let runtimeSSRPath
|
|
83
|
+
try {
|
|
84
|
+
runtimeSSRPath = pluginRequire.resolve('@uniweb/runtime/ssr')
|
|
85
|
+
} catch {
|
|
86
|
+
// Fallback: try to find it relative to the workspace root
|
|
87
|
+
for (const np of nodePaths) {
|
|
88
|
+
const candidate = join(np, '@uniweb', 'runtime', 'dist', 'ssr.js')
|
|
89
|
+
if (existsSync(candidate)) {
|
|
90
|
+
runtimeSSRPath = candidate
|
|
91
|
+
break
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Build a self-contained ESM bundle including:
|
|
97
|
+
// - Foundation components (from the just-built ESM output)
|
|
98
|
+
// - React + ReactDOM/server (browser version, no Node.js built-ins)
|
|
99
|
+
// - @uniweb/core (Website, Page, Block classes)
|
|
100
|
+
// - @uniweb/runtime/ssr (initPrerender, renderPage, injectPageContent)
|
|
101
|
+
// - @uniweb/theming (buildSectionOverrides, used by runtime/ssr)
|
|
102
|
+
//
|
|
103
|
+
// All in a single file so the Dynamic Worker isolate has one React instance.
|
|
104
|
+
const ssrExports = runtimeSSRPath
|
|
105
|
+
? `export { initPrerender, renderPage, injectPageContent, prefetchIcons } from "${runtimeSSRPath.replace(/\\/g, '/')}";`
|
|
106
|
+
: ''
|
|
107
|
+
|
|
108
|
+
// Resolve React to a single package directory to avoid duplicate instances
|
|
109
|
+
// (foundation.js and runtime/ssr may resolve to different copies)
|
|
110
|
+
const { dirname } = await import('node:path')
|
|
111
|
+
let reactDir
|
|
112
|
+
try {
|
|
113
|
+
reactDir = dirname(pluginRequire.resolve('react/package.json'))
|
|
114
|
+
} catch {
|
|
115
|
+
// Fall back to nodePaths resolution
|
|
116
|
+
}
|
|
117
|
+
const alias = {}
|
|
118
|
+
if (reactDir) {
|
|
119
|
+
alias['react'] = reactDir
|
|
120
|
+
// Force react-dom/server imports to the browser version (no Node.js built-ins)
|
|
121
|
+
const reactDomDir = dirname(pluginRequire.resolve('react-dom/package.json'))
|
|
122
|
+
alias['react-dom'] = reactDomDir
|
|
123
|
+
alias['react-dom/server'] = join(reactDomDir, 'server.browser.js')
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const foundationPath = entryPath.replace(/\\/g, '/')
|
|
127
|
+
await esbuild({
|
|
128
|
+
stdin: {
|
|
129
|
+
contents: [
|
|
130
|
+
// Foundation components (named + default export)
|
|
131
|
+
`export * from "${foundationPath}";`,
|
|
132
|
+
`export { default } from "${foundationPath}";`,
|
|
133
|
+
// React SSR
|
|
134
|
+
`export { renderToString } from "react-dom/server.browser";`,
|
|
135
|
+
`export { createElement } from "react";`,
|
|
136
|
+
// Runtime SSR functions (initPrerender, renderPage, etc.)
|
|
137
|
+
ssrExports,
|
|
138
|
+
].join('\n'),
|
|
139
|
+
resolveDir: outDir,
|
|
140
|
+
loader: 'js',
|
|
141
|
+
},
|
|
142
|
+
bundle: true,
|
|
143
|
+
format: 'esm',
|
|
144
|
+
platform: 'browser',
|
|
145
|
+
outfile: join(outDir, 'foundation.ssr.js'),
|
|
146
|
+
minify: false,
|
|
147
|
+
external: [],
|
|
148
|
+
nodePaths,
|
|
149
|
+
alias,
|
|
150
|
+
conditions: ['browser', 'module'],
|
|
151
|
+
logLevel: 'warning',
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const ssrFile = join(outDir, 'foundation.ssr.js')
|
|
155
|
+
const size = (statSync(ssrFile).size / 1024).toFixed(1)
|
|
156
|
+
console.log(`Generated foundation.ssr.js (${size} KB)`)
|
|
157
|
+
} catch (err) {
|
|
158
|
+
console.warn(`Warning: SSR bundle build failed: ${err.message}`)
|
|
159
|
+
} finally {
|
|
160
|
+
_buildingSSRBundle = false
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
37
164
|
/**
|
|
38
165
|
* Vite plugin for foundation builds
|
|
39
166
|
*/
|
|
@@ -69,6 +196,9 @@ export function foundationBuildPlugin(options = {}) {
|
|
|
69
196
|
},
|
|
70
197
|
|
|
71
198
|
async writeBundle() {
|
|
199
|
+
// Skip if this is a recursive call from buildSSRBundle
|
|
200
|
+
if (_buildingSSRBundle) return
|
|
201
|
+
|
|
72
202
|
// After bundle is written, generate schema.json in meta folder
|
|
73
203
|
const outDir = resolve(resolvedOutDir)
|
|
74
204
|
const metaDir = join(outDir, 'meta')
|
|
@@ -87,6 +217,9 @@ export function foundationBuildPlugin(options = {}) {
|
|
|
87
217
|
await writeFile(schemaPath, JSON.stringify(schema, null, 2), 'utf-8')
|
|
88
218
|
|
|
89
219
|
console.log(`Generated meta/schema.json with ${Object.keys(schema).length - 1} components`)
|
|
220
|
+
|
|
221
|
+
// Build self-contained SSR bundle for edge rendering (Dynamic Workers)
|
|
222
|
+
await buildSSRBundle(outDir)
|
|
90
223
|
},
|
|
91
224
|
}
|
|
92
225
|
}
|