@uniweb/runtime 0.17.2 → 0.18.1
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/dist/ssr.js +10 -10
- package/dist/ssr.js.map +1 -1
- package/package.json +3 -3
- package/src/appearance.js +1 -1
- package/src/collect-records.js +2 -2
- package/src/default-fetcher.js +19 -13
- package/src/document-tracking.js +1 -1
- package/src/foundation-loader.js +2 -2
- package/src/page-renderer.js +2 -2
- package/src/prefetch.js +5 -4
- package/src/shell/main.js +2 -1
- package/src/ssr-renderer.js +4 -4
- package/src/wire-foundation.js +8 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "Minimal runtime for loading Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"node": ">=20.19"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@uniweb/core": "^0.
|
|
39
|
+
"@uniweb/core": "^0.24.0",
|
|
40
40
|
"@uniweb/theming": "^0.1.15"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"esbuild": "^0.21.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.27.0",
|
|
45
45
|
"vite": "^7.3.1",
|
|
46
46
|
"vitest": "^4.1.7",
|
|
47
|
-
"@uniweb/build": "0.
|
|
47
|
+
"@uniweb/build": "0.42.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": "^19.0.0",
|
package/src/appearance.js
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
*
|
|
29
29
|
* Environment-neutral by construction. `applyBootScheme` no-ops its DOM writes
|
|
30
30
|
* outside a browser, and `renderAppearanceBootScript` only stringifies — so
|
|
31
|
-
* ssr-renderer.js can import this module in Node and in a
|
|
31
|
+
* ssr-renderer.js can import this module in Node and in a server-side isolate.
|
|
32
32
|
*
|
|
33
33
|
* Two writers with independent resolution is the bug this replaced:
|
|
34
34
|
* WebsiteRenderer used to re-apply `appearance.default` from an effect, and
|
package/src/collect-records.js
CHANGED
|
@@ -78,7 +78,7 @@ import { createDefaultFetcher } from './default-fetcher.js'
|
|
|
78
78
|
*/
|
|
79
79
|
export async function collectSiteRecords(
|
|
80
80
|
content,
|
|
81
|
-
{ locale, fetch, signal, only = null,
|
|
81
|
+
{ locale, fetch, signal, only = null, whole = false, maxPages } = {},
|
|
82
82
|
) {
|
|
83
83
|
const config = content?.config
|
|
84
84
|
const services = config?.services ?? null
|
|
@@ -111,7 +111,7 @@ export async function collectSiteRecords(
|
|
|
111
111
|
if (!cfg.ask) return
|
|
112
112
|
// `limit` is the list page's, never the corpus's — see the header.
|
|
113
113
|
const { limit, ...population } = cfg
|
|
114
|
-
const asked = { ...population,
|
|
114
|
+
const asked = { ...population, whole, exhaustive: true }
|
|
115
115
|
if (typeof maxPages === 'number' && maxPages > 0) asked.maxPages = maxPages
|
|
116
116
|
|
|
117
117
|
const result = await fetcher.resolve(asked, { signal })
|
package/src/default-fetcher.js
CHANGED
|
@@ -230,12 +230,13 @@ export function createDefaultFetcher({ basePath = '', dev = false, fetch: fetchI
|
|
|
230
230
|
// it returned.
|
|
231
231
|
data = applyOperators(data, request, { dev })
|
|
232
232
|
|
|
233
|
-
// ⭐ Say
|
|
234
|
-
// the config asked for, echoed:
|
|
235
|
-
// a per-record source,
|
|
233
|
+
// ⭐ Say whether WHOLE records were delivered, so the record index can
|
|
234
|
+
// file them — what the config asked for, echoed: briefs when the query
|
|
235
|
+
// has a per-record source, whole records otherwise. (The service reports
|
|
236
|
+
// `whole` per
|
|
236
237
|
// key and overrides this with what it actually served.)
|
|
237
|
-
const
|
|
238
|
-
return
|
|
238
|
+
const whole = typeof request.whole === 'boolean' ? request.whole : undefined
|
|
239
|
+
return whole === undefined ? { data: data ?? [] } : { data: data ?? [], meta: { whole } }
|
|
239
240
|
} catch (error) {
|
|
240
241
|
if (error?.name === 'AbortError') {
|
|
241
242
|
return { data: [], error: 'aborted' }
|
|
@@ -270,7 +271,9 @@ function toQuestion(request) {
|
|
|
270
271
|
const sort = sortToWire(request.sort)
|
|
271
272
|
if (sort) q.sort = sort
|
|
272
273
|
if (typeof request.limit === 'number' && request.limit > 0) q.limit = request.limit
|
|
273
|
-
|
|
274
|
+
// ⛔ ONLY WHEN TRUE. The brief is the default and absent means the brief, so
|
|
275
|
+
// sending `whole: false` would be noise on every list question the client makes.
|
|
276
|
+
if (request.whole === true) q.whole = true
|
|
274
277
|
// ⭐ `cursor` is the ONLY field here that is not the author's: it is opaque and
|
|
275
278
|
// it comes from a previous answer's `cursors` (the records contract §2). ⛔ And
|
|
276
279
|
// `exhaustive` deliberately does NOT cross — it is a client instruction about
|
|
@@ -385,7 +388,10 @@ async function flushAsked(url, queue, doFetch) {
|
|
|
385
388
|
}
|
|
386
389
|
const data = parsed && typeof parsed.data === 'object' && parsed.data ? parsed.data : {}
|
|
387
390
|
const errors = parsed && typeof parsed.errors === 'object' && parsed.errors ? parsed.errors : {}
|
|
388
|
-
|
|
391
|
+
// ⭐ `whole[key]` is present only for keys delivered as WHOLE entities — so a
|
|
392
|
+
// key's ABSENCE is the brief, and a key present that never asked is the
|
|
393
|
+
// brief-less Model saying so. Absent entirely when every key is briefs.
|
|
394
|
+
const wholes = parsed && typeof parsed.whole === 'object' && parsed.whole ? parsed.whole : {}
|
|
389
395
|
// Both absent when empty, never `{}` (the records contract §5).
|
|
390
396
|
const cursors = parsed && typeof parsed.cursors === 'object' && parsed.cursors ? parsed.cursors : {}
|
|
391
397
|
const limits = parsed && typeof parsed.limits === 'object' && parsed.limits ? parsed.limits : {}
|
|
@@ -413,9 +419,9 @@ async function flushAsked(url, queue, doFetch) {
|
|
|
413
419
|
entry.resolve({ data: null, error: `the records service answered without the key "${key}"` })
|
|
414
420
|
return
|
|
415
421
|
}
|
|
416
|
-
const
|
|
417
|
-
?
|
|
418
|
-
: (
|
|
422
|
+
const whole = typeof wholes[key] === 'boolean'
|
|
423
|
+
? wholes[key]
|
|
424
|
+
: (typeof entry.request.whole === 'boolean' ? entry.request.whole : undefined)
|
|
419
425
|
|
|
420
426
|
const cursor = typeof cursors[key] === 'string' && cursors[key] ? cursors[key] : null
|
|
421
427
|
const bound = typeof limits[key] === 'number' ? limits[key] : undefined
|
|
@@ -429,17 +435,17 @@ async function flushAsked(url, queue, doFetch) {
|
|
|
429
435
|
? entry.request.maxPages
|
|
430
436
|
: DEFAULT_MAX_PAGES
|
|
431
437
|
if (page <= cap) {
|
|
432
|
-
pending.set(entry, { cursor, collected: acc, page,
|
|
438
|
+
pending.set(entry, { cursor, collected: acc, page, whole, bound })
|
|
433
439
|
return
|
|
434
440
|
}
|
|
435
441
|
// The caller's own bound, not the service's: report rather than spin.
|
|
436
|
-
entry.resolve({ data: acc, meta: withMeta({
|
|
442
|
+
entry.resolve({ data: acc, meta: withMeta({ whole, bound, partial: true, pages: cap }) })
|
|
437
443
|
return
|
|
438
444
|
}
|
|
439
445
|
|
|
440
446
|
const collected = entry.collected ? entry.collected.concat(Array.isArray(rows) ? rows : []) : rows
|
|
441
447
|
const meta = withMeta({
|
|
442
|
-
|
|
448
|
+
whole,
|
|
443
449
|
bound,
|
|
444
450
|
// ⭐ ONE FLAG, MEANING **NOT THE WHOLE POPULATION** — asked for by name, so
|
|
445
451
|
// a caller has one boolean to branch on rather than three signals to
|
package/src/document-tracking.js
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* ## No page-level opt-in, deliberately
|
|
55
55
|
*
|
|
56
56
|
* `section_view` needs one (`trackSections`) because its dimension is unbounded
|
|
57
|
-
* — a site's section types run to the hundreds and
|
|
57
|
+
* — a site's section types run to the hundreds and a host capped the
|
|
58
58
|
* cardinality. **An outbound hostname is bounded by how many external sites a
|
|
59
59
|
* page links to**, which is small and does not grow with the site.
|
|
60
60
|
*
|
package/src/foundation-loader.js
CHANGED
|
@@ -32,8 +32,8 @@ function resolveAgainstDocument(url) {
|
|
|
32
32
|
* ⭐ **Exported for the test, and the test exists because a HOST builds on this.**
|
|
33
33
|
* The skip below is what lets a shell put the stylesheet in the head itself —
|
|
34
34
|
* so the sheet applies during HTML parse instead of waiting for the runtime to
|
|
35
|
-
* boot, parse `__DATA__` and inject it.
|
|
36
|
-
* this guard (
|
|
35
|
+
* boot, parse `__DATA__` and inject it. A host shipped that on the strength of
|
|
36
|
+
* this guard (2026-08-19); until then nothing
|
|
37
37
|
* asserted it, so a refactor would have produced two `<link>` tags and two
|
|
38
38
|
* fetches on their lane with every test here still green.
|
|
39
39
|
*
|
package/src/page-renderer.js
CHANGED
|
@@ -128,8 +128,8 @@ export async function prefetchAndHydrate({ website, content, route, locale = nul
|
|
|
128
128
|
// ⛔ THE TRANSPORT IS REQUIRED HERE, unlike on `prefetchPageData`, and this is the
|
|
129
129
|
// one place the difference matters.
|
|
130
130
|
//
|
|
131
|
-
// A function does NOT survive every isolate boundary. Measured by
|
|
132
|
-
//
|
|
131
|
+
// A function does NOT survive every isolate boundary. Measured by a host against
|
|
132
|
+
// a real isolate loader, 2026-09-03: passed through an
|
|
133
133
|
// entrypoint's `fetch(Request)` with a JSON body the transport arrives
|
|
134
134
|
// **`undefined`**; passed as an argument to an RPC method it arrives as a callable
|
|
135
135
|
// function and the isolate invokes it. Only the RPC shape carries it.
|
package/src/prefetch.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* shape the datastore expects — a copy of the runtime's logic, in another repo, drifting
|
|
12
12
|
* (the records envelope went silently unread that way on 2026-09-02). [Diego, 2026-09-03]:
|
|
13
13
|
* *the backend sets the records service; the fetch comes from the runtime.* The host now calls
|
|
14
|
-
* this and carries no copy
|
|
14
|
+
* this and carries no copy; the host that renders in an isolate agreed to exactly
|
|
15
|
+
* that shape the same day.
|
|
15
16
|
*
|
|
16
17
|
* ⛔ Contract with the host, deliberately small:
|
|
17
18
|
* - `content` the render payload (`site-content.json` / `__DATA__`), config included —
|
|
@@ -22,8 +23,8 @@
|
|
|
22
23
|
* decides how a site-relative one is reached (its origin, a binding).
|
|
23
24
|
* ⛔ **Crossing an isolate boundary, this survives only as an RPC method
|
|
24
25
|
* argument.** Through an entrypoint's `fetch(Request)` with a serialized
|
|
25
|
-
* body it arrives `undefined` (
|
|
26
|
-
*
|
|
26
|
+
* body it arrives `undefined` (measured by a host against a real
|
|
27
|
+
* isolate loader, 2026-09-03) — and the fetcher then falls
|
|
27
28
|
* back to `globalThis.fetch`, so the request leaves from the isolate,
|
|
28
29
|
* outside whatever budget the host wrapped around it. `prefetchAndHydrate`
|
|
29
30
|
* refuses a non-function for exactly this reason; this entry keeps the
|
|
@@ -43,7 +44,7 @@
|
|
|
43
44
|
* deferred it to the browser with `prerender: false`). `hydrateDataStore`
|
|
44
45
|
* takes the list as-is and hydrates only `fetched` entries — a host reads the
|
|
45
46
|
* outcomes to tell "nothing was tried" from "everything tried failed", which
|
|
46
|
-
* is a different cache decision (
|
|
47
|
+
* is a different cache decision (measured by a host, 2026-09-03).
|
|
47
48
|
*
|
|
48
49
|
* It resolves nothing the host owns and models no host route layout: every address is
|
|
49
50
|
* `{base}/…` from the payload, or the records service the host itself
|
package/src/shell/main.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Runtime Shell Entry Point
|
|
3
3
|
*
|
|
4
4
|
* Standalone browser app that boots a Uniweb site from __DATA__ injected
|
|
5
|
-
* by a dynamic backend (
|
|
5
|
+
* by a dynamic backend (a Uniweb backend, PHP, anything that can render a
|
|
6
|
+
* page shell).
|
|
6
7
|
*
|
|
7
8
|
* This calls the same start() function that all sites use. When no
|
|
8
9
|
* __FOUNDATION_CONFIG__ is embedded in the page, start() checks for
|
package/src/ssr-renderer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* SSR Renderer
|
|
3
3
|
*
|
|
4
|
-
* Hook-free rendering pipeline for SSG (build) and
|
|
4
|
+
* Hook-free rendering pipeline for SSG (build) and server-side SSR.
|
|
5
5
|
* Mirrors BlockRenderer.jsx + Background.jsx using React.createElement
|
|
6
6
|
* directly — no hooks, no JSX, no browser APIs.
|
|
7
7
|
*
|
|
@@ -363,7 +363,7 @@ export function renderLayout(page, website) {
|
|
|
363
363
|
* Construct a Uniweb singleton scoped to a single locale.
|
|
364
364
|
*
|
|
365
365
|
* Combines the three steps that every SSR consumer (browser SPA, Node
|
|
366
|
-
* SSG,
|
|
366
|
+
* SSG, a server-side isolate) needs in the same order: slice the
|
|
367
367
|
* multi-locale content payload, run `initPrerender` (which builds the
|
|
368
368
|
* Website + wires foundation capabilities), then `setActiveLocale` so
|
|
369
369
|
* `website.activeLang` stays in sync with what the page is rendering
|
|
@@ -396,10 +396,10 @@ export function initPrerenderForLocale(content, foundation, locale, extensionsOr
|
|
|
396
396
|
*
|
|
397
397
|
* Returns the configured uniweb instance. Consumers can add extras after:
|
|
398
398
|
* - Build: pre-populate DataStore, load extensions
|
|
399
|
-
* -
|
|
399
|
+
* - A server-side renderer: (none needed — the payload is complete)
|
|
400
400
|
*
|
|
401
401
|
* NOTE: Does NOT clone content. Cloning is the consumer's responsibility
|
|
402
|
-
* (build modifies content before init;
|
|
402
|
+
* (build modifies content before init; a server-side renderer clones upfront).
|
|
403
403
|
*
|
|
404
404
|
* @param {Object} content - Site content JSON (pages, config, hierarchy)
|
|
405
405
|
* @param {Object} foundation - Loaded foundation module
|
package/src/wire-foundation.js
CHANGED
|
@@ -119,10 +119,10 @@ export function wireFoundationCapabilities(uniweb, foundation) {
|
|
|
119
119
|
* each `dist/{locale}/site-content.json` separately) get pass-through
|
|
120
120
|
* behavior.
|
|
121
121
|
*
|
|
122
|
-
* The shape comes from the editor's publish payload, which is the
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
122
|
+
* The shape comes from the editor's publish payload, which is the canonical
|
|
123
|
+
* form for multi-locale content — a server-side renderer consumes it
|
|
124
|
+
* directly. Build-time SSG pre-flattens to one file per locale and so falls
|
|
125
|
+
* into the pass-through case.
|
|
126
126
|
*
|
|
127
127
|
* @param {Object} content - Site content payload, possibly multi-locale.
|
|
128
128
|
* @param {string} locale - Requested locale code.
|
|
@@ -152,10 +152,10 @@ export function sliceContentForLocale(content, locale) {
|
|
|
152
152
|
* value MUST be wrapped as `{ data }` — otherwise the dispatcher's
|
|
153
153
|
* lookup at `_dataStore.get(deriveCacheKey(request))` misses every
|
|
154
154
|
* time and `cached.data` reads `undefined`. Three call sites used to
|
|
155
|
-
* inline this loop independently (browser SPA, Node SSG,
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
155
|
+
* inline this loop independently (browser SPA, Node SSG, and a server-side
|
|
156
|
+
* renderer); one of the three was using the wrong shape, silently killing
|
|
157
|
+
* prefetched-data reuse wherever it ran. This helper is the one canonical
|
|
158
|
+
* implementation.
|
|
159
159
|
*
|
|
160
160
|
* @param {import('@uniweb/core').Website} website
|
|
161
161
|
* @param {Array<{config: Object, data: any}>} fetchedData
|