@uniweb/runtime 1.5.4 → 1.5.5
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 +89 -11
- package/dist/ssr.js.map +1 -1
- package/package.json +3 -3
- package/src/page-renderer.js +12 -3
- package/src/prepare-props.js +42 -7
- package/src/section-records.js +120 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/runtime",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.5",
|
|
4
4
|
"description": "Minimal runtime for loading Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"node": ">=22.12"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@uniweb/core": "~1.6.
|
|
39
|
+
"@uniweb/core": "~1.6.10",
|
|
40
40
|
"@uniweb/theming": "~1.3.2"
|
|
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 || ^0.28.0",
|
|
45
45
|
"vite": "^7.3.1",
|
|
46
46
|
"vitest": "^4.1.7",
|
|
47
|
-
"@uniweb/build": "1.
|
|
47
|
+
"@uniweb/build": "1.20.3"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": "^19.0.0",
|
package/src/page-renderer.js
CHANGED
|
@@ -39,6 +39,7 @@ import { hydrateDataStore } from './wire-foundation.js'
|
|
|
39
39
|
import { loadPageData } from './load-page-data.js'
|
|
40
40
|
import { requireTransport } from './require-transport.js'
|
|
41
41
|
import { whileReadingIcons } from './page-icons.js'
|
|
42
|
+
import { sectionRecords } from './section-records.js'
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* A renderer bound to one initialized Website and one shell.
|
|
@@ -81,8 +82,13 @@ export function createPageRenderer({ website, shell }) {
|
|
|
81
82
|
* parametric pages made from them (`Website#forgetData`), and files these, all in this
|
|
82
83
|
* synchronous call, so a Website reused across requests renders each against its own and
|
|
83
84
|
* no other request runs in between. Absent, the render reads whatever the store holds.
|
|
85
|
+
* @param {boolean} [options.sectionData] - also say which records each section showed, for the
|
|
86
|
+
* page's Markdown: `sections`, which `renderPageMarkdown(page, { sections })`
|
|
87
|
+
* (`@uniweb/projections`) writes after each section. Read from the store this render read, after
|
|
88
|
+
* its HTML (`section-records.js`), so a render that does not ask does no more work than before.
|
|
84
89
|
* @returns {{ outcome: 'rendered'|'notFound'|'failed', html: string|null,
|
|
85
|
-
* page: Object|null, error: {type: string, message: string}|null
|
|
90
|
+
* page: Object|null, error: {type: string, message: string}|null,
|
|
91
|
+
* sections?: Array<{ id: string, data: Object }> }} `sections` when asked, on a page with `html`
|
|
86
92
|
* `notFound` means the URL names nothing, and `html` says whether there is a page to
|
|
87
93
|
* send for it. Send `html` with your not-found status when it is set, and the shell
|
|
88
94
|
* when it is not:
|
|
@@ -95,7 +101,7 @@ export function createPageRenderer({ website, shell }) {
|
|
|
95
101
|
* - `page` set, `html: null`, `error` set — that page broke while rendering; `error`
|
|
96
102
|
* says what broke.
|
|
97
103
|
*/
|
|
98
|
-
function render(target, { inject = {}, fetchedData } = {}) {
|
|
104
|
+
function render(target, { inject = {}, fetchedData, sectionData = false } = {}) {
|
|
99
105
|
if (fetchedData !== undefined && !Array.isArray(fetchedData)) {
|
|
100
106
|
throw new Error('createPageRenderer: `fetchedData` must be the array of entries the page carries')
|
|
101
107
|
}
|
|
@@ -181,7 +187,10 @@ export function createPageRenderer({ website, shell }) {
|
|
|
181
187
|
sectionOverrideCSS: result.sectionOverrideCSS,
|
|
182
188
|
icons,
|
|
183
189
|
})
|
|
184
|
-
|
|
190
|
+
const rendered = { outcome: missing ? 'notFound' : 'rendered', html, page, error: null }
|
|
191
|
+
// In this same synchronous call, so the store holds this page's answers and no other request's.
|
|
192
|
+
if (sectionData) rendered.sections = sectionRecords(page)
|
|
193
|
+
return rendered
|
|
185
194
|
}
|
|
186
195
|
|
|
187
196
|
return { website, render, failingSection: (page, message) => failingSectionOf(page, message) }
|
package/src/prepare-props.js
CHANGED
|
@@ -142,7 +142,21 @@ export function applyDefaults(params, defaults) {
|
|
|
142
142
|
* section received every fetch that reached it and every tagged block it had.
|
|
143
143
|
*/
|
|
144
144
|
function assembleData(block, meta, entityData) {
|
|
145
|
-
const declared = block
|
|
145
|
+
const declared = declaredOf(block, meta)
|
|
146
|
+
const data = collectDeclared(block, declared, entityData)
|
|
147
|
+
// What the section holds and this leaves out — the keys the dev warning reports.
|
|
148
|
+
warnUndeclaredBlocks(block, Object.keys(block.heldData || {}).filter((key) => !Object.hasOwn(data, key)))
|
|
149
|
+
block.parsedContent.data = data
|
|
150
|
+
return declared
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** The keys a block's component receives: its own `data:`, then its foundation's. */
|
|
154
|
+
function declaredOf(block, meta) {
|
|
155
|
+
return block.website?.declaredKeys ? block.website.declaredKeys(meta) : []
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Each declared key's value, by the order `assembleData` states: held, delivered, else `null`. */
|
|
159
|
+
function collectDeclared(block, declared, entityData) {
|
|
146
160
|
const held = block.heldData || {}
|
|
147
161
|
const data = {}
|
|
148
162
|
for (const [key] of declared) {
|
|
@@ -150,10 +164,27 @@ function assembleData(block, meta, entityData) {
|
|
|
150
164
|
else if (entityData && entityData[key] !== undefined) data[key] = entityData[key]
|
|
151
165
|
else data[key] = null
|
|
152
166
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
167
|
+
return data
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* ⭐ WHAT A SECTION'S COMPONENT RECEIVES UNDER THE KEYS IT DECLARES, BEFORE ANY FOUNDATION HANDLER —
|
|
172
|
+
* `content.data` as `prepareProps` assembles it, its records linked (`linkOwnRecords`) and a
|
|
173
|
+
* `single` key holding one, worked out without touching the block. For a caller reporting what a
|
|
174
|
+
* section received, after its render: a page's Markdown (`section-records.js`). ⛔ Never for
|
|
175
|
+
* rendering, which is `prepareProps`'.
|
|
176
|
+
*
|
|
177
|
+
* @param {Object} block
|
|
178
|
+
* @param {Object|null} meta - its component's runtime meta
|
|
179
|
+
* @param {Object|null} [entityData] - what the entity store delivered for it, as the render read it
|
|
180
|
+
* @returns {{ declared: Array, data: Object }}
|
|
181
|
+
*/
|
|
182
|
+
export function receivedData(block, meta, entityData = null) {
|
|
183
|
+
const declared = declaredOf(block, meta)
|
|
184
|
+
// A stand-in carries the data through the linking, so the block's own is left as its render left it.
|
|
185
|
+
const probe = { parsedContent: { data: collectDeclared(block, declared, entityData) }, fetch: block.fetch, website: block.website }
|
|
186
|
+
block.website?.entityStore?.linkOwnRecords?.(probe)
|
|
187
|
+
return { declared, data: oneRecordEach(probe.parsedContent.data, declared) }
|
|
157
188
|
}
|
|
158
189
|
|
|
159
190
|
/**
|
|
@@ -168,14 +199,18 @@ function assembleData(block, meta, entityData) {
|
|
|
168
199
|
* object is replaced, never written into.
|
|
169
200
|
*/
|
|
170
201
|
function holdOneRecord(block, declared) {
|
|
171
|
-
|
|
202
|
+
block.parsedContent.data = oneRecordEach(block.parsedContent.data, declared)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** `data` with each `single` key's list cut to its first record, or `data` itself when none is. */
|
|
206
|
+
function oneRecordEach(data, declared) {
|
|
172
207
|
let out = null
|
|
173
208
|
for (const [key, , , single] of declared) {
|
|
174
209
|
if (single !== true || !Array.isArray(data?.[key])) continue
|
|
175
210
|
out = out || { ...data }
|
|
176
211
|
out[key] = data[key][0] ?? null
|
|
177
212
|
}
|
|
178
|
-
|
|
213
|
+
return out || data
|
|
179
214
|
}
|
|
180
215
|
|
|
181
216
|
/**
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT A PAGE'S SECTIONS SHOWED FROM RECORDS — for the page's Markdown, never for its render.
|
|
3
|
+
*
|
|
4
|
+
* ⭐ A page's Markdown (`renderPageMarkdown`, `@uniweb/projections`) carries, after each section, the
|
|
5
|
+
* records that section shows. Which records those are only the render knows: the keys its component
|
|
6
|
+
* declares, each filled by the one rule (`@uniweb/core/page-data`), as briefs or whole, one record or
|
|
7
|
+
* the set, with this page's record or without it. A rule over the site's content alone would get each
|
|
8
|
+
* of those wrong, and a host's prefetch retired that rule for the same reasons (`load-page-data.js`).
|
|
9
|
+
* So they are read back from the store the render has just read, by the reading it filled each section
|
|
10
|
+
* by, after the HTML: in L2, with no twin to keep (L3 does not change), and nowhere in the browser's
|
|
11
|
+
* bundle, since only `page-renderer.js` asks and only the SSR entry exports it.
|
|
12
|
+
*
|
|
13
|
+
* A section's records:
|
|
14
|
+
*
|
|
15
|
+
* - **The keys its component declares** in its own `data:`. Not its foundation's (`main.js` `data:`),
|
|
16
|
+
* which reach every section, since a foundation's handlers run for every section, and which of
|
|
17
|
+
* them draws one is the foundation's business.
|
|
18
|
+
* - **As delivered**, before any foundation handler (`receivedData`, `./prepare-props.js`): the
|
|
19
|
+
* site's records, never a foundation's derivation of them.
|
|
20
|
+
* - **Named by the site's word:** the fetch's `as`, which is its query's name unless the site gives
|
|
21
|
+
* another. A component's key is the foundation's word, and a fetch can fill a key of another name
|
|
22
|
+
* that has its schema (`fillDeclaredKeys`). A held answer is under its fetch's `as` already. Two
|
|
23
|
+
* keys of one section filled under one `as` with different records: the second goes by its key.
|
|
24
|
+
* - **Left out:** a key the section holds as a tagged block in its own content (` ```yaml:tag `, or
|
|
25
|
+
* a concept block), which the page's Markdown already shows as it was written; a record page's
|
|
26
|
+
* own record (a key whose fetch uses this page's record, `current: only`), which its Markdown
|
|
27
|
+
* writes once, as the page's (`renderPageMarkdown`'s `record`); and a key with no value, `null` —
|
|
28
|
+
* nothing fills it, or its fetch failed, never ran or is still out.
|
|
29
|
+
* - **Whose:** each body section and its child sections, under its own id, and the insets in a
|
|
30
|
+
* section, under the id of the section they sit in, since a page's Markdown has no place of their
|
|
31
|
+
* own for them. Not the layout's areas, whose own content the page's Markdown doesn't carry either.
|
|
32
|
+
*
|
|
33
|
+
* @module
|
|
34
|
+
*/
|
|
35
|
+
import { sectionDomId } from '@uniweb/core/section-id'
|
|
36
|
+
import { declaredKeys } from '@uniweb/core/data-keys'
|
|
37
|
+
import { blockFillsOf, blockRouteQuery, fillIsPageRecord } from '@uniweb/core/page-data'
|
|
38
|
+
import { getComponentMeta, receivedData } from './prepare-props.js'
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The records each of a page's sections showed, read after the page's render.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} page - the Page just rendered, its Website the one `globalThis.uniweb` names
|
|
44
|
+
* @returns {Array<{ id: string, data: Object }>} in page order, one entry per section, child section
|
|
45
|
+
* or inset that showed any, `id` the section's DOM id (`sectionDomId`) and `data` its records by name
|
|
46
|
+
*/
|
|
47
|
+
export function sectionRecords(page) {
|
|
48
|
+
const out = []
|
|
49
|
+
for (const block of page?.getBodyBlocks?.() ?? []) visit(block, sectionDomId(block), out)
|
|
50
|
+
return out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function visit(block, id, out) {
|
|
54
|
+
if (!block) return
|
|
55
|
+
const data = recordsOf(block)
|
|
56
|
+
if (data) out.push({ id, data })
|
|
57
|
+
for (const inset of block.insets || []) visit(inset, id, out)
|
|
58
|
+
for (const child of block.childBlocks || []) visit(child, sectionDomId(child), out)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** One block's records by name, or null when it showed none. */
|
|
62
|
+
function recordsOf(block) {
|
|
63
|
+
// A type the foundation doesn't have renders as an error box, and its render asks for no data.
|
|
64
|
+
if (!globalThis.uniweb?.getComponent?.(block.type)) return null
|
|
65
|
+
const meta = getComponentMeta(block.type)
|
|
66
|
+
const own = declaredKeys(meta?.data)
|
|
67
|
+
if (own.length === 0) return null
|
|
68
|
+
|
|
69
|
+
// The resolve the render just made, from the same store: `renderBlock` (`ssr-renderer.js`).
|
|
70
|
+
const resolved = block.website?.entityStore?.resolve(block, meta)
|
|
71
|
+
const { data } = receivedData(block, meta, resolved?.status === 'ready' ? resolved.data : null)
|
|
72
|
+
|
|
73
|
+
const held = block.heldData || {}
|
|
74
|
+
let fills = null
|
|
75
|
+
let route = null
|
|
76
|
+
let tagged = null
|
|
77
|
+
const out = {}
|
|
78
|
+
for (const [key] of own) {
|
|
79
|
+
const value = data[key]
|
|
80
|
+
if (value === undefined || value === null) continue
|
|
81
|
+
let name
|
|
82
|
+
if (held[key] !== undefined) {
|
|
83
|
+
tagged ??= taggedKeys(block)
|
|
84
|
+
if (tagged.has(key)) continue
|
|
85
|
+
// An answer a static build prerendered into the section, under its fetch's `as`.
|
|
86
|
+
name = key
|
|
87
|
+
} else {
|
|
88
|
+
if (!fills) {
|
|
89
|
+
route = blockRouteQuery(block)
|
|
90
|
+
fills = blockFillsOf(block, meta, route)
|
|
91
|
+
}
|
|
92
|
+
const cfg = fills.get(key)?.cfg
|
|
93
|
+
if (!cfg?.as || fillIsPageRecord(block, cfg, route)) continue
|
|
94
|
+
name = cfg.as
|
|
95
|
+
}
|
|
96
|
+
if (Object.hasOwn(out, name) && !sameValue(out[name], value)) name = key
|
|
97
|
+
if (Object.hasOwn(out, name) && !sameValue(out[name], value)) continue
|
|
98
|
+
out[name] = value
|
|
99
|
+
}
|
|
100
|
+
return Object.keys(out).length ? out : null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The keys a section's own content holds as tagged blocks: its content parsed alone, without the
|
|
105
|
+
* answers a build put beside it (`Block`'s constructor merges the two). `parseContent` is pure, and
|
|
106
|
+
* this runs only for a section holding a declared key.
|
|
107
|
+
*/
|
|
108
|
+
function taggedKeys(block) {
|
|
109
|
+
const parsed = typeof block.parseContent === 'function' && block.rawContent ? block.parseContent(block.rawContent) : null
|
|
110
|
+
return new Set(Object.keys(parsed?.data || {}))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function sameValue(a, b) {
|
|
114
|
+
if (a === b) return true
|
|
115
|
+
try {
|
|
116
|
+
return JSON.stringify(a) === JSON.stringify(b)
|
|
117
|
+
} catch {
|
|
118
|
+
return false
|
|
119
|
+
}
|
|
120
|
+
}
|