@uniweb/build 0.64.2 → 0.65.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.
@@ -39,9 +39,8 @@ async function buildSchemaWithPreviews(srcDir, outDir, isProduction, sectionPath
39
39
  }
40
40
 
41
41
  /**
42
- * Module-level guard to prevent recursive SSR bundle builds.
43
- * When buildSSRBundle calls esbuild, it should not re-trigger
44
- * the foundation plugin's writeBundle hook.
42
+ * Module-level guard around the `entry-ssr.js` sub-build (buildEntrySSR): while
43
+ * it runs, the foundation plugin's writeBundle hook does nothing.
45
44
  */
46
45
  let _buildingSSRBundle = false
47
46
 
@@ -68,11 +67,8 @@ let _buildingSSRBundle = false
68
67
  * ⚠️ **This used to say "the selector is `site.yml::runtime`". It is not.** That
69
68
  * key is an operator-level override, not the authoring surface: a link-mode site
70
69
  * is CODELESS, so it has nothing that binds to a runtime version and no basis for
71
- * an opinion about one. The selector is the backend's resolution (an explicit pin
72
- * → the site's current → a deployment default → newest installed). What a
70
+ * an opinion about one. The selector is whatever serves the site. What a
73
71
  * foundation declares here is the CONSTRAINT on that choice, never the choice.
74
- * the site/foundation/runtime model, § "who gets to say
75
- * whether a site accepts a newer runtime" is the authority.
76
72
  *
77
73
  * Reads the resolved version from the foundation's node_modules/@uniweb/
78
74
  * runtime/package.json so the pin reflects what was actually linked at
@@ -101,13 +97,9 @@ let _buildingSSRBundle = false
101
97
  * about every host the framework serves and framework can substantiate it for
102
98
  * none of them.
103
99
  *
104
- * 📌 **The check that refutes it, so this is not a claim with no expiry:** grep a
105
- * host's serving code for `runtime-pin` / `runtimePolicy`. Asked and answered for
106
- * the Uniweb host on **2026-08-26 — zero consumers across its delivery worker,
107
- * mirror, runtime host and capability worker**, and its runtime version comes
108
- * solely from published site metadata and is part of a render-cache key, so a
109
- * request-time policy would collide with its invalidation model. **That is one
110
- * host on one date; a static host or a foreign backend is a separate question.**
100
+ * 📌 **The check, so this is not a claim with no expiry:** grep a host's serving
101
+ * code for `runtime-pin` / `runtimePolicy`. An answer is about one host on one
102
+ * date; a static host or a foreign backend is a separate question.
111
103
  *
112
104
  * ⚖️ **This is a field with standing and no consumer — NOT a mistake to delete.**
113
105
  * The architecture assigns this declaration to the foundation on principle: each
@@ -116,7 +108,7 @@ let _buildingSSRBundle = false
116
108
  * core. So this is the designated home for the answer; nothing has asked for the
117
109
  * answer yet.
118
110
  *
119
- * ⇒ **The event that gives it a consumer** (agreed with the backend, 2026-08-16):
111
+ * ⇒ **The event that gives it a consumer:**
120
112
  * when the runtime declares what it supplies — replacing today's `>=` version
121
113
  * compare, which is a proxy that holds only while the runtime's version number
122
114
  * tracks its externals contract — **or** when the publish path starts consulting
@@ -180,7 +172,8 @@ async function emitRuntimePin(outDir, projectRoot) {
180
172
  }
181
173
 
182
174
  if (!runtimePkgPath) {
183
- // No runtime resolvable. Skip emission — edge will treat as legacy.
175
+ // No runtime resolvable. Skip emission — the foundation then states no
176
+ // floor (see the header).
184
177
  return
185
178
  }
186
179
 
@@ -194,10 +187,10 @@ async function emitRuntimePin(outDir, projectRoot) {
194
187
  if (!runtimeVersion) return
195
188
 
196
189
  // Read foundation's own package.json for an optional runtimePolicy
197
- // field. Default policy (auto-minor) is applied platform-side when
198
- // the field is omitted; we only record the foundation's override
199
- // here if explicitly set. See framework/docs/reference/foundation-config.md
200
- // for the full set of `uniweb.*` fields foundations can declare.
190
+ // field, recorded only when explicitly set. What a host does when it is
191
+ // omitted is that host's to say (see the header). See
192
+ // framework/docs/reference/foundation-config.md for the full set of
193
+ // `uniweb.*` fields foundations can declare.
201
194
  let policy = null
202
195
  try {
203
196
  const foundationPkgPath = join(projectRoot, 'package.json')
@@ -227,150 +220,6 @@ async function emitRuntimePin(outDir, projectRoot) {
227
220
  }
228
221
  }
229
222
 
230
- /**
231
- * @deprecated 2026-04-27 — Strategy S Phase 2.
232
- *
233
- * Foundations no longer carry their own runtime bundle. Runtime + React +
234
- * core + theming now live in R2 under `runtime/{version}/worker-runtime.js`,
235
- * published by the platform's `/deploy-runtime` skill, and side-loaded
236
- * by the Cloudflare isolate alongside `dist/entry.js`.
237
- *
238
- * The invocation in `writeBundle()` is commented out; this function
239
- * definition is kept for the rollout window so it can be flipped back
240
- * on with one line if Phase 1's edge dispatcher misbehaves in production.
241
- * Phase 3 cleanup deletes this function entirely once the new path is
242
- * proven healthy.
243
- *
244
- * Original purpose (preserved for context):
245
- * Build a self-contained ESM bundle for edge SSR (Cloudflare Dynamic Workers).
246
- * Produces `ssr-worker-bundle.js` — a single ESM file with React,
247
- * ReactDOM/server, `@uniweb/core`, `@uniweb/runtime/ssr`,
248
- * `@uniweb/theming`, and the foundation's components all inlined. No
249
- * external imports. The artifact was bundled this way because the
250
- * Dynamic Worker LOADER accepts a closed `modules` map at isolate
251
- * construction. The Phase -1 prototype (2026-04-27) verified the LOADER
252
- * actually deduplicates shared modules across multiple ESM bundles, so
253
- * a multi-entry modules map became viable — that's what Strategy S uses.
254
- *
255
- * @param {string} outDir - Path to dist/ directory containing entry.js
256
- */
257
- async function buildSSRBundle(outDir) {
258
- if (_buildingSSRBundle) return
259
- _buildingSSRBundle = true
260
-
261
- const entryPath = join(outDir, 'entry.js')
262
- try {
263
- const { build: esbuild } = await import('esbuild')
264
- const { statSync } = await import('node:fs')
265
-
266
- // Collect all node_modules directories up the tree (pnpm hoists to workspace root)
267
- const { existsSync } = await import('node:fs')
268
- let searchDir = resolve(outDir, '..')
269
- let nodePaths = []
270
- for (let i = 0; i < 10; i++) {
271
- const candidate = join(searchDir, 'node_modules')
272
- if (existsSync(candidate)) {
273
- nodePaths.push(candidate)
274
- }
275
- const parent = resolve(searchDir, '..')
276
- if (parent === searchDir) break
277
- searchDir = parent
278
- }
279
-
280
- // Resolve workspace packages that esbuild can't find via node_modules
281
- // (pnpm workspace symlinks aren't in node_modules for the foundation project)
282
- const { createRequire } = await import('node:module')
283
- const pluginRequire = createRequire(import.meta.url)
284
- let runtimeSSRPath
285
- try {
286
- runtimeSSRPath = pluginRequire.resolve('@uniweb/runtime/ssr')
287
- } catch {
288
- // Fallback: try to find it relative to the workspace root
289
- for (const np of nodePaths) {
290
- const candidate = join(np, '@uniweb', 'runtime', 'dist', 'ssr.js')
291
- if (existsSync(candidate)) {
292
- runtimeSSRPath = candidate
293
- break
294
- }
295
- }
296
- }
297
-
298
- // Build a self-contained ESM bundle including:
299
- // - Foundation components (from the just-built ESM output)
300
- // - React + ReactDOM/server (browser version, no Node.js built-ins)
301
- // - @uniweb/core (Website, Page, Block classes)
302
- // - @uniweb/runtime/ssr (initPrerender, renderPage, injectPageContent)
303
- // - @uniweb/theming (buildSectionOverrides, used by runtime/ssr)
304
- //
305
- // All in a single file so the Dynamic Worker isolate has one React instance.
306
- // L2/L3 helpers from @uniweb/runtime/ssr that worker SSR + framework SSG
307
- // both depend on. Keep this list in sync with runtime/src/ssr-renderer.js
308
- // exports — missing one here makes the foundation bundle fail to import
309
- // it ("module does not provide an export named X") inside the SSR isolate.
310
- const ssrExports = runtimeSSRPath
311
- ? `export {
312
- initPrerender, initPrerenderForLocale,
313
- renderPage, injectPageContent, prefetchIcons,
314
- sliceContentForLocale, hydrateDataStore,
315
- } from "${runtimeSSRPath.replace(/\\/g, '/')}";`
316
- : ''
317
-
318
- // Resolve React to a single package directory to avoid duplicate instances
319
- // (entry.js and runtime/ssr may resolve to different copies)
320
- const { dirname } = await import('node:path')
321
- let reactDir
322
- try {
323
- reactDir = dirname(pluginRequire.resolve('react/package.json'))
324
- } catch {
325
- // Fall back to nodePaths resolution
326
- }
327
- const alias = {}
328
- if (reactDir) {
329
- alias['react'] = reactDir
330
- // Force react-dom/server imports to the browser version (no Node.js built-ins)
331
- const reactDomDir = dirname(pluginRequire.resolve('react-dom/package.json'))
332
- alias['react-dom'] = reactDomDir
333
- alias['react-dom/server'] = join(reactDomDir, 'server.browser.js')
334
- }
335
-
336
- const foundationPath = entryPath.replace(/\\/g, '/')
337
- await esbuild({
338
- stdin: {
339
- contents: [
340
- // Foundation components (named + default export)
341
- `export * from "${foundationPath}";`,
342
- `export { default } from "${foundationPath}";`,
343
- // React SSR
344
- `export { renderToString } from "react-dom/server.browser";`,
345
- `export { createElement } from "react";`,
346
- // Runtime SSR functions (initPrerender, renderPage, etc.)
347
- ssrExports,
348
- ].join('\n'),
349
- resolveDir: outDir,
350
- loader: 'js',
351
- },
352
- bundle: true,
353
- format: 'esm',
354
- platform: 'browser',
355
- outfile: join(outDir, 'ssr-worker-bundle.js'),
356
- minify: false,
357
- external: [],
358
- nodePaths,
359
- alias,
360
- conditions: ['browser', 'module'],
361
- logLevel: 'warning',
362
- })
363
-
364
- const ssrFile = join(outDir, 'ssr-worker-bundle.js')
365
- const size = (statSync(ssrFile).size / 1024).toFixed(1)
366
- console.log(`Generated ssr-worker-bundle.js (${size} KB)`)
367
- } catch (err) {
368
- console.warn(`Warning: SSR bundle build failed: ${err.message}`)
369
- } finally {
370
- _buildingSSRBundle = false
371
- }
372
- }
373
-
374
223
  /**
375
224
  * Append the foundation's theme-variable DEFAULTS as a `:root{}` baseline to the
376
225
  * built CSS (`assets/style.css`), so a runtime-loaded foundation carries its own
@@ -414,18 +263,18 @@ async function emitFoundationVarsCss(outDir, schema) {
414
263
  * Externals for the SSR bundle (`dist/entry-ssr.js`).
415
264
  *
416
265
  * Same set the browser foundation build externalizes, and now literally the
417
- * same array — `DEFAULT_EXTERNALS`, imported rather than re-typed. The isolate
418
- * resolves these to the SHARED runtime's React/core (worker-runtime.js) via its
419
- * shims, so React stays deduped and runtime patches still propagate without a
266
+ * same array — `DEFAULT_EXTERNALS`, imported rather than re-typed. A host
267
+ * rendering the SSR bundle resolves these to the runtime's shared React/core,
268
+ * so React stays deduped and runtime patches still propagate without a
420
269
  * foundation rebuild. "Same set" was a comment three copies made a promise
421
270
  * rather than a fact; deriving it makes drift unrepresentable.
422
271
  *
423
272
  * PLUS the client-only library kit code-splits via dynamic import:
424
273
  * - shiki / shiki/bundle/full — syntax highlighting (kit Code renderer)
425
- * It hydrates in the browser and never runs during renderToString (CLAUDE.md
426
- * gotcha #12). Keeping it external drops the ~10 MB Shiki language graph from
427
- * the SSR bundle and leaves it a DORMANT dynamic import the isolate never
428
- * awaits — so no extra modules-map entry is needed for it edge-side.
274
+ * It hydrates in the browser and never runs during renderToString. Keeping it
275
+ * external drops the ~10 MB Shiki language graph from the SSR bundle and leaves
276
+ * it a DORMANT dynamic import that server rendering never awaits — so a host
277
+ * has nothing to supply for it.
429
278
  *
430
279
  * ⛔ `fuse.js` was listed here too, until the local search ranker became
431
280
  * `@uniweb/projections/search` (2026-09-06). Nothing imports fuse now, so the
@@ -447,20 +296,20 @@ function isSSRExternal(id) {
447
296
  *
448
297
  * The modern browser `entry.js` is a facade that re-exports from
449
298
  * `_entry.generated-*.js` and lazily code-splits kit's client-only features
450
- * (Shiki) into hundreds of chunks — a graph the Cloudflare Dynamic Worker
451
- * isolate can't resolve (it loads a single `foundation` module). This builds the
452
- * SAME source entry into ONE file, inlining the foundation's own graph and
453
- * externalizing the runtime/React set (→ the isolate's shared worker-runtime)
454
- * and the client-only Shiki lib. Result: a ~foundation-sized ESM module
455
- * (no React, no Shiki) the edge loads as `foundation` for request-time SSR.
299
+ * (Shiki) into hundreds of chunks — a graph that a request-time renderer loading
300
+ * the foundation as a single module can't resolve. This builds the SAME source
301
+ * entry into ONE file, inlining the foundation's own graph and externalizing the
302
+ * runtime/React set (→ the runtime's shared build) and the client-only Shiki
303
+ * lib. Result: a ~foundation-sized ESM module (no React, no Shiki) that a host
304
+ * can load for request-time SSR.
456
305
  *
457
306
  * Built from source (not by re-bundling the built `entry.js`, whose Shiki
458
307
  * specifier is already rewritten to a relative chunk path that couldn't be
459
308
  * externalized) via a secondary Vite build into a temp dir; only the JS is
460
309
  * copied out (the throwaway CSS is discarded — the SSR bundle needs no styles).
461
310
  *
462
- * Best-effort: a failure warns and emits nothing, so the edge simply serves
463
- * the client-render shell for this foundation (existence-gated) — no regression.
311
+ * Best-effort: a failure warns and emits nothing. The browser `entry.js` is
312
+ * unaffected, so the foundation still renders client-side.
464
313
  *
465
314
  * @param {string} foundationRoot - foundation project root (vite `root`).
466
315
  * @param {string} entrySourcePath - absolute path to `_entry.generated.js`.
@@ -589,7 +438,7 @@ export function foundationBuildPlugin(options = {}) {
589
438
  },
590
439
 
591
440
  async writeBundle(_options, bundle) {
592
- // Skip if this is a recursive call from buildSSRBundle
441
+ // Skip while the entry-ssr.js sub-build (buildEntrySSR) is running
593
442
  if (_buildingSSRBundle) return
594
443
 
595
444
  // What host services this foundation actually reaches for, read off the
@@ -637,26 +486,23 @@ export function foundationBuildPlugin(options = {}) {
637
486
  // site build's theme.css — this is harmless redundancy there.
638
487
  await emitFoundationVarsCss(outDir, schema)
639
488
 
640
- // Record which @uniweb/runtime this build linked against. A compatibility
641
- // floor for a validation step that does not exist yet, and read by nothing
642
- // today — see emitRuntimePin's header before assuming otherwise.
489
+ // Record which @uniweb/runtime this build linked against: a compatibility
490
+ // floor that `register` carries as `info.runtime` and that nothing in the
491
+ // framework enforces — see emitRuntimePin's header before assuming more.
643
492
  await emitRuntimePin(outDir, resolvedRoot)
644
493
 
645
494
  // Emit dist/entry-ssr.js — the single-file SSR twin of the (code-split)
646
- // browser dist/entry.js — for the Cloudflare edge isolate. React + the
647
- // runtime stay externalized (resolved to the isolate's SHARED
648
- // worker-runtime, so runtime patches propagate without a rebuild — the
649
- // Strategy S win); the client-only Shiki lib are externalized so the
650
- // ~10 MB Shiki graph stays out. The edge loads this as its single
651
- // `foundation` module for request-time SSR, gated on its presence.
495
+ // browser dist/entry.js — for a host that renders at request time. React
496
+ // + the runtime stay externalized (resolved to the runtime's shared build,
497
+ // so runtime patches propagate without a foundation rebuild); the
498
+ // client-only Shiki lib is externalized so the ~10 MB Shiki graph stays
499
+ // out.
652
500
  //
653
- // (The legacy self-contained buildSSRBundle() — React + runtime INLINED,
654
- // ~14 MB with Shiki — is retained below, unused, for reference only.)
655
501
  // Skipped on a DEV rebuild: this is a second full Vite pass, it runs on
656
502
  // every save, and nothing in the dev loop reads its output. The local SSR
657
503
  // lanes load `entry.js` (SSG prerender via `import()`, unipress the same);
658
- // only the edge isolate needs the single-file twin, and dev ships nothing
659
- // to it.
504
+ // only a request-time host needs the single-file twin, and dev ships
505
+ // nothing to one.
660
506
  //
661
507
  // Shipping lanes are unaffected — `uniweb build`, and `register`/`publish`
662
508
  // which build through it. `register`'s build-if-stale check also requires