@uniweb/build 0.11.8 → 0.12.0

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.11.8",
3
+ "version": "0.12.0",
4
4
  "description": "Build tooling for the Uniweb Component Web Platform",
5
5
  "type": "module",
6
6
  "exports": {
@@ -59,8 +59,8 @@
59
59
  },
60
60
  "optionalDependencies": {
61
61
  "@uniweb/content-reader": "1.1.9",
62
- "@uniweb/runtime": "0.8.9",
63
- "@uniweb/schemas": "0.2.1"
62
+ "@uniweb/schemas": "0.2.1",
63
+ "@uniweb/runtime": "0.8.9"
64
64
  },
65
65
  "peerDependencies": {
66
66
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0",
@@ -7,7 +7,8 @@
7
7
  * - Processing preview images for presets
8
8
  */
9
9
 
10
- import { writeFile, mkdir } from 'node:fs/promises'
10
+ import { writeFile, mkdir, readFile } from 'node:fs/promises'
11
+ import { existsSync } from 'node:fs'
11
12
  import { join, resolve } from 'node:path'
12
13
  import { buildSchema } from './schema.js'
13
14
  import { generateEntryPoint, shouldRegenerateForFile } from './generate-entry.js'
@@ -42,30 +43,144 @@ async function buildSchemaWithPreviews(srcDir, outDir, isProduction, sectionPath
42
43
  let _buildingSSRBundle = false
43
44
 
44
45
  /**
45
- * Build a self-contained ESM bundle for edge SSR (Cloudflare Dynamic Workers).
46
+ * Emit dist/runtime-pin.json declaring the @uniweb/runtime version this
47
+ * foundation was built against. Read by the edge isolate (under the
48
+ * Strategy S split-bundle path) to decide which runtime/{ver}/ssr.js to
49
+ * side-load from R2. See kb/platform/plans/edge-ssr-bundling-strategy.md
50
+ * and kb/platform/operations/release-workflow.md.
51
+ *
52
+ * Reads the resolved version from the foundation's node_modules/@uniweb/
53
+ * runtime/package.json so the pin reflects what was actually linked at
54
+ * build time, not what the foundation's own package.json range happens
55
+ * to allow.
56
+ *
57
+ * Silently no-ops when @uniweb/runtime isn't resolvable (e.g., the
58
+ * foundation depends on the runtime via a workspace alias that puts it
59
+ * elsewhere). The edge resolver treats the absence of a pin as the
60
+ * legacy single-bundle path, so omitting the pin is harmless during the
61
+ * dual-mode window.
62
+ *
63
+ * Optional foundation-author override: a `uniweb.runtimePolicy` field
64
+ * in the foundation's own package.json gets recorded alongside the
65
+ * runtime version so the registry's semver resolver can apply it.
66
+ *
67
+ * @param {string} outDir - dist/ directory to write to.
68
+ * @param {string} projectRoot - foundation project root (where package.json lives).
69
+ */
70
+ async function emitRuntimePin(outDir, projectRoot) {
71
+ // Resolve @uniweb/runtime via two strategies, in order:
72
+ // 1. createRequire from this plugin's location (catches the runtime
73
+ // pulled in transitively through @uniweb/build, @uniweb/core, etc.).
74
+ // 2. Walk up node_modules from the project root (catches the case
75
+ // where the foundation depends on runtime directly).
76
+ // The first covers the common case (foundations don't typically depend
77
+ // on runtime directly — it's the host environment, not a foundation
78
+ // import); the second is a safety net.
79
+ let runtimePkgPath = null
80
+
81
+ try {
82
+ // Resolve via an exported subpath, not 'package.json' directly —
83
+ // @uniweb/runtime's `exports` map doesn't include package.json, so
84
+ // require.resolve on it throws ERR_PACKAGE_PATH_NOT_EXPORTED.
85
+ // Walking back from the resolved subpath finds the package root.
86
+ const { createRequire } = await import('node:module')
87
+ const { dirname: pathDirname } = await import('node:path')
88
+ const pluginRequire = createRequire(import.meta.url)
89
+ const ssrEntry = pluginRequire.resolve('@uniweb/runtime/ssr')
90
+ let dir = pathDirname(ssrEntry)
91
+ for (let i = 0; i < 5; i++) {
92
+ const candidate = join(dir, 'package.json')
93
+ if (existsSync(candidate)) {
94
+ const pkg = JSON.parse(await readFile(candidate, 'utf-8'))
95
+ if (pkg.name === '@uniweb/runtime') {
96
+ runtimePkgPath = candidate
97
+ break
98
+ }
99
+ }
100
+ const parent = resolve(dir, '..')
101
+ if (parent === dir) break
102
+ dir = parent
103
+ }
104
+ } catch {
105
+ // Fall through to the walk-up search.
106
+ }
107
+
108
+ if (!runtimePkgPath) {
109
+ let dir = projectRoot
110
+ for (let i = 0; i < 10; i++) {
111
+ const candidate = join(dir, 'node_modules', '@uniweb', 'runtime', 'package.json')
112
+ if (existsSync(candidate)) {
113
+ runtimePkgPath = candidate
114
+ break
115
+ }
116
+ const parent = resolve(dir, '..')
117
+ if (parent === dir) break
118
+ dir = parent
119
+ }
120
+ }
121
+
122
+ if (!runtimePkgPath) {
123
+ // No runtime resolvable. Skip emission — edge will treat as legacy.
124
+ return
125
+ }
126
+
127
+ let runtimeVersion
128
+ try {
129
+ const pkg = JSON.parse(await readFile(runtimePkgPath, 'utf-8'))
130
+ runtimeVersion = pkg.version
131
+ } catch {
132
+ return
133
+ }
134
+ if (!runtimeVersion) return
135
+
136
+ // Read foundation's own package.json for an optional runtimePolicy
137
+ // field. Default policy (auto-patch) lives at the registry layer; we
138
+ // only record the foundation's override if explicitly set.
139
+ let policy = null
140
+ try {
141
+ const foundationPkgPath = join(projectRoot, 'package.json')
142
+ if (existsSync(foundationPkgPath)) {
143
+ const foundationPkg = JSON.parse(await readFile(foundationPkgPath, 'utf-8'))
144
+ policy = foundationPkg?.uniweb?.runtimePolicy ?? null
145
+ }
146
+ } catch {
147
+ // Foundation package.json malformed; skip policy. Pin still emits.
148
+ }
149
+
150
+ const pin = { runtime: runtimeVersion }
151
+ if (policy) pin.policy = policy
152
+
153
+ const pinPath = join(outDir, 'runtime-pin.json')
154
+ await writeFile(pinPath, JSON.stringify(pin, null, 2) + '\n', 'utf-8')
155
+ console.log(`Generated runtime-pin.json (runtime ${runtimeVersion}${policy ? `, policy ${policy}` : ''})`)
156
+ }
157
+
158
+ /**
159
+ * @deprecated 2026-04-27 — Strategy S Phase 2.
160
+ *
161
+ * Foundations no longer carry their own runtime bundle. Runtime + React +
162
+ * core + theming now live in R2 under `runtime/{version}/worker-runtime.js`,
163
+ * published by the platform's `/deploy-runtime` skill, and side-loaded
164
+ * by the Cloudflare isolate alongside `dist/foundation.js`.
46
165
  *
166
+ * The invocation in `writeBundle()` is commented out; this function
167
+ * definition is kept for the rollout window so it can be flipped back
168
+ * on with one line if Phase 1's edge dispatcher misbehaves in production.
169
+ * Phase 3 cleanup deletes this function entirely once the new path is
170
+ * proven healthy.
171
+ *
172
+ * See `kb/platform/plans/edge-ssr-bundling-strategy.md`.
173
+ *
174
+ * Original purpose (preserved for context):
175
+ * Build a self-contained ESM bundle for edge SSR (Cloudflare Dynamic Workers).
47
176
  * Produces `ssr-worker-bundle.js` — a single ESM file with React,
48
177
  * ReactDOM/server, `@uniweb/core`, `@uniweb/runtime/ssr`,
49
178
  * `@uniweb/theming`, and the foundation's components all inlined. No
50
- * external imports.
51
- *
52
- * This is **NOT a foundation**. The foundation is `dist/foundation.js`
53
- * a foundation-shaped ESM module that externalizes runtime and links
54
- * to it at runtime, the same as in browser SPA / framework SSG / unipress
55
- * (Node SSR). This file is something different: a self-contained SSR
56
- * pipeline shaped for the Cloudflare Workers Dynamic Worker LOADER, with
57
- * the foundation embedded as one of several inputs. The size ratio
58
- * (typical ~12× larger than `foundation.js`) reflects what's actually
59
- * inside it.
60
- *
61
- * The artifact is bundled this way because the Dynamic Worker LOADER
62
- * accepts a closed `modules` map at isolate construction (no external
63
- * resolver, no path back to npm or R2 for transitive imports). One
64
- * file in, everything resolves. A future refactor can switch to side-
65
- * loading runtime + React + core into the modules map separately so
66
- * foundations stop carrying runtime in their bundles — see
67
- * `kb/platform/plans/edge-runtime-side-loading.md`. Until then, this
68
- * build pre-bundles for the isolate's contract.
179
+ * external imports. The artifact was bundled this way because the
180
+ * Dynamic Worker LOADER accepts a closed `modules` map at isolate
181
+ * construction. The Phase -1 prototype (2026-04-27) verified the LOADER
182
+ * actually deduplicates shared modules across multiple ESM bundles, so
183
+ * a multi-entry modules map became viable that's what Strategy S uses.
69
184
  *
70
185
  * @param {string} outDir - Path to dist/ directory containing foundation.js
71
186
  */
@@ -118,8 +233,16 @@ async function buildSSRBundle(outDir) {
118
233
  // - @uniweb/theming (buildSectionOverrides, used by runtime/ssr)
119
234
  //
120
235
  // All in a single file so the Dynamic Worker isolate has one React instance.
236
+ // L2/L3 helpers from @uniweb/runtime/ssr that worker SSR + framework SSG
237
+ // both depend on. Keep this list in sync with runtime/src/ssr-renderer.js
238
+ // exports — missing one here makes the foundation bundle fail to import
239
+ // it ("module does not provide an export named X") inside the SSR isolate.
121
240
  const ssrExports = runtimeSSRPath
122
- ? `export { initPrerender, renderPage, injectPageContent, prefetchIcons } from "${runtimeSSRPath.replace(/\\/g, '/')}";`
241
+ ? `export {
242
+ initPrerender, initPrerenderForLocale,
243
+ renderPage, injectPageContent, prefetchIcons,
244
+ sliceContentForLocale, hydrateDataStore,
245
+ } from "${runtimeSSRPath.replace(/\\/g, '/')}";`
123
246
  : ''
124
247
 
125
248
  // Resolve React to a single package directory to avoid duplicate instances
@@ -191,6 +314,7 @@ export function foundationBuildPlugin(options = {}) {
191
314
 
192
315
  let resolvedSrcDir
193
316
  let resolvedOutDir
317
+ let resolvedRoot
194
318
  let isProduction
195
319
 
196
320
  return {
@@ -209,6 +333,7 @@ export function foundationBuildPlugin(options = {}) {
209
333
  async configResolved(config) {
210
334
  resolvedSrcDir = resolve(config.root, srcDir)
211
335
  resolvedOutDir = config.build.outDir
336
+ resolvedRoot = config.root
212
337
  isProduction = config.mode === 'production'
213
338
  },
214
339
 
@@ -235,8 +360,25 @@ export function foundationBuildPlugin(options = {}) {
235
360
 
236
361
  console.log(`Generated meta/schema.json with ${Object.keys(schema).length - 1} components`)
237
362
 
238
- // Build self-contained SSR bundle for edge rendering (Dynamic Workers)
239
- await buildSSRBundle(outDir)
363
+ // Emit runtime-pin.json so the edge isolate (under Strategy S) can
364
+ // side-load the matching runtime/{ver}/ssr.js. Lands silently before
365
+ // the dual-mode resolver ships; foundations published in the dual-mode
366
+ // window already have the pin and start using the split-bundle path
367
+ // automatically once the edge is updated.
368
+ await emitRuntimePin(outDir, resolvedRoot)
369
+
370
+ // Strategy S Phase 2: foundations no longer carry a self-contained
371
+ // SSR bundle. The runtime + React + core + theming live in R2 under
372
+ // runtime/{version}/worker-runtime.js (uploaded by the platform's
373
+ // /deploy-runtime skill); the Cloudflare isolate side-loads them
374
+ // alongside dist/foundation.js via the edge dual-mode dispatcher.
375
+ // See kb/platform/plans/edge-ssr-bundling-strategy.md.
376
+ //
377
+ // The buildSSRBundle() function is kept (just not invoked) so it
378
+ // can be flipped back on with one line if Phase 1's edge dispatcher
379
+ // misbehaves in production. Phase 3 cleanup deletes the function
380
+ // entirely once we're confident the new path is healthy.
381
+ // await buildSSRBundle(outDir)
240
382
  },
241
383
 
242
384
  async closeBundle() {