@uniweb/build 0.8.20 → 0.8.22

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uniweb/build",
3
- "version": "0.8.20",
3
+ "version": "0.8.22",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -48,14 +48,15 @@
48
48
  "jest": "^29.7.0"
49
49
  },
50
50
  "dependencies": {
51
+ "esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0",
51
52
  "js-yaml": "^4.1.0",
52
53
  "sharp": "^0.33.2",
53
54
  "@uniweb/theming": "0.1.2"
54
55
  },
55
56
  "optionalDependencies": {
56
57
  "@uniweb/content-reader": "1.1.4",
57
- "@uniweb/runtime": "0.6.15",
58
- "@uniweb/schemas": "0.2.1"
58
+ "@uniweb/schemas": "0.2.1",
59
+ "@uniweb/runtime": "0.6.18"
59
60
  },
60
61
  "peerDependencies": {
61
62
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -64,7 +65,7 @@
64
65
  "@tailwindcss/vite": "^4.0.0",
65
66
  "@vitejs/plugin-react": "^4.0.0 || ^5.0.0",
66
67
  "vite-plugin-svgr": "^4.0.0",
67
- "@uniweb/core": "0.5.14"
68
+ "@uniweb/core": "0.5.15"
68
69
  },
69
70
  "peerDependenciesMeta": {
70
71
  "vite": {
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)`)
@@ -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
 
@@ -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 subResult = await collectPagesRecursive(childDirPath, containerRoute, siteRoot, childOrderConfig, parentFetch, versionContext, 'pages', null, effectiveLayout)
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
@@ -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
  }