@uniweb/runtime 0.28.9 → 0.29.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/dist/ssr.js +18 -96
- package/dist/ssr.js.map +1 -1
- package/package.json +4 -4
- package/src/isolate-api.js +3 -2
- package/src/prepare-props.js +54 -222
- package/src/ssr.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniweb/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"description": "Minimal runtime for loading Uniweb foundations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -36,15 +36,15 @@
|
|
|
36
36
|
"node": ">=20.19"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@uniweb/
|
|
40
|
-
"@uniweb/
|
|
39
|
+
"@uniweb/theming": "^0.2.0",
|
|
40
|
+
"@uniweb/core": "^0.37.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@vitejs/plugin-react": "^4.5.2",
|
|
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.75.1"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"react": "^19.0.0",
|
package/src/isolate-api.js
CHANGED
|
@@ -74,9 +74,10 @@ export const UNRELEASED = 'UNRELEASED'
|
|
|
74
74
|
|
|
75
75
|
/** Every export of `@uniweb/runtime/ssr`, with the version it first shipped in. */
|
|
76
76
|
export const ISOLATE_API = Object.freeze({
|
|
77
|
-
// props preparation
|
|
77
|
+
// props preparation. ⛔ `applySchemas` was here (0.2.15) until 2026-10-05, when the runtime
|
|
78
|
+
// stopped filling `content.data` fields from schema defaults; no host imported it (checked
|
|
79
|
+
// that day).
|
|
78
80
|
prepareProps: '0.2.15',
|
|
79
|
-
applySchemas: '0.2.15',
|
|
80
81
|
applyDefaults: '0.2.15',
|
|
81
82
|
guaranteeContentStructure: '0.2.15',
|
|
82
83
|
getComponentMeta: '0.2.15',
|
package/src/prepare-props.js
CHANGED
|
@@ -4,13 +4,11 @@
|
|
|
4
4
|
* Prepares props for foundation components with:
|
|
5
5
|
* - Param defaults from runtime schema
|
|
6
6
|
* - Guaranteed content structure (no null checks needed)
|
|
7
|
-
* -
|
|
7
|
+
* - `content.data` holding the keys the component declares, each as it arrived
|
|
8
8
|
*
|
|
9
9
|
* This enables simpler component code by ensuring predictable prop shapes.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { isRichSchema } from '@uniweb/core'
|
|
13
|
-
|
|
14
12
|
/**
|
|
15
13
|
* Guarantee item has flat content structure
|
|
16
14
|
*
|
|
@@ -104,211 +102,6 @@ export function guaranteeContentStructure(parsedContent) {
|
|
|
104
102
|
}
|
|
105
103
|
}
|
|
106
104
|
|
|
107
|
-
/**
|
|
108
|
-
* Apply a schema to a single object
|
|
109
|
-
* Only processes fields defined in the schema, preserves unknown fields
|
|
110
|
-
*
|
|
111
|
-
* @param {Object} obj - The object to process
|
|
112
|
-
* @param {Object} schema - Schema definition (fieldName -> fieldDef)
|
|
113
|
-
* @returns {Object} Object with schema defaults applied
|
|
114
|
-
*/
|
|
115
|
-
function applySchemaToObject(obj, schema) {
|
|
116
|
-
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
117
|
-
return obj
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const result = { ...obj }
|
|
121
|
-
|
|
122
|
-
for (const [field, fieldDef] of Object.entries(schema)) {
|
|
123
|
-
// Get the default value - handle both shorthand and full form
|
|
124
|
-
const defaultValue = typeof fieldDef === 'object' ? fieldDef.default : undefined
|
|
125
|
-
|
|
126
|
-
// Apply default if field is missing and default exists
|
|
127
|
-
if (result[field] === undefined && defaultValue !== undefined) {
|
|
128
|
-
result[field] = defaultValue
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
// Bare type strings ('string', 'decimal', …) carry nothing more to apply.
|
|
132
|
-
if (typeof fieldDef !== 'object') continue
|
|
133
|
-
|
|
134
|
-
// Inline picklist (`enum`): if the value is set but not among the allowed
|
|
135
|
-
// values, fall back to the default. By value: an entry may be `{ value, label }`, as a
|
|
136
|
-
// foundation built before its build lowered it to values still carries.
|
|
137
|
-
if (Array.isArray(fieldDef.enum)) {
|
|
138
|
-
const allowed = fieldDef.enum.map((e) => (e && typeof e === 'object' && 'value' in e ? e.value : e))
|
|
139
|
-
if (result[field] !== undefined && !allowed.includes(result[field]) && defaultValue !== undefined) {
|
|
140
|
-
result[field] = defaultValue
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Nested object → recurse into its field map.
|
|
145
|
-
if (fieldDef.type === 'object' && fieldDef.fields && result[field]) {
|
|
146
|
-
result[field] = applySchemaToObject(result[field], fieldDef.fields)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Array of objects → apply the element field map to each item.
|
|
150
|
-
if (fieldDef.type === 'array' && fieldDef.items && Array.isArray(result[field])) {
|
|
151
|
-
const items = fieldDef.items
|
|
152
|
-
if (items && typeof items === 'object' && items.type === 'object' && items.fields) {
|
|
153
|
-
result[field] = result[field].map((item) => applySchemaToObject(item, items.fields))
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return result
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Apply a schema to a value (object or array of objects)
|
|
163
|
-
*
|
|
164
|
-
* @param {Object|Array} value - The value to process
|
|
165
|
-
* @param {Object} schema - Schema definition
|
|
166
|
-
* @returns {Object|Array} Value with schema defaults applied
|
|
167
|
-
*/
|
|
168
|
-
function applySchemaToValue(value, schema) {
|
|
169
|
-
if (Array.isArray(value)) {
|
|
170
|
-
return value.map(item => applySchemaToObject(item, schema))
|
|
171
|
-
}
|
|
172
|
-
return applySchemaToObject(value, schema)
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Apply field defaults from a rich form `fields` array to an object.
|
|
177
|
-
*
|
|
178
|
-
* Recurses into `type: 'form'` (composite arrays with childSchema) and
|
|
179
|
-
* `type: 'nestedObject'` / `type: 'object'` (single nested objects).
|
|
180
|
-
*
|
|
181
|
-
* Conditional visibility (`field.condition`) is not yet applied here —
|
|
182
|
-
* components receive all fields the author filled plus defaults; hiding
|
|
183
|
-
* is a later pass that requires the shared evaluateCondition util.
|
|
184
|
-
*
|
|
185
|
-
* @param {Object} obj - Row data (object keyed by field id)
|
|
186
|
-
* @param {Array} fields - Rich field definitions
|
|
187
|
-
* @returns {Object} - obj with defaults filled in
|
|
188
|
-
*/
|
|
189
|
-
function applyRichFieldDefaults(obj, fields) {
|
|
190
|
-
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) return obj
|
|
191
|
-
if (!Array.isArray(fields)) return obj
|
|
192
|
-
|
|
193
|
-
const result = { ...obj }
|
|
194
|
-
|
|
195
|
-
for (const field of fields) {
|
|
196
|
-
if (!field || typeof field !== 'object' || !field.id) continue
|
|
197
|
-
const id = field.id
|
|
198
|
-
|
|
199
|
-
if (result[id] === undefined && field.default !== undefined) {
|
|
200
|
-
result[id] = field.default
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (field.type === 'form' && field.childSchema && Array.isArray(result[id])) {
|
|
204
|
-
result[id] = result[id].map(item =>
|
|
205
|
-
applyRichFieldDefaults(item, field.childSchema.fields)
|
|
206
|
-
)
|
|
207
|
-
} else if (
|
|
208
|
-
(field.type === 'nestedObject' || field.type === 'object') &&
|
|
209
|
-
Array.isArray(field.fields) &&
|
|
210
|
-
result[id] &&
|
|
211
|
-
typeof result[id] === 'object'
|
|
212
|
-
) {
|
|
213
|
-
result[id] = applyRichFieldDefaults(result[id], field.fields)
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return result
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Apply a rich form schema to its stored value.
|
|
222
|
-
*
|
|
223
|
-
* Shape rules:
|
|
224
|
-
* - composite (isComposite=true) → value is array of childSchema rows
|
|
225
|
-
* - when `childRecords` is set, value may be `{ [childRecords]: [...] }`
|
|
226
|
-
* - non-composite → value is a single object keyed by field id
|
|
227
|
-
*/
|
|
228
|
-
function applyRichSchemaToValue(value, schema) {
|
|
229
|
-
if (value == null) return value
|
|
230
|
-
|
|
231
|
-
if (schema.isComposite && schema.childSchema) {
|
|
232
|
-
const childFields = schema.childSchema.fields
|
|
233
|
-
const queryKey = schema.childRecords
|
|
234
|
-
|
|
235
|
-
if (queryKey && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
236
|
-
const arr = Array.isArray(value[queryKey]) ? value[queryKey] : []
|
|
237
|
-
return {
|
|
238
|
-
...value,
|
|
239
|
-
[queryKey]: arr.map(row => applyRichFieldDefaults(row, childFields)),
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (Array.isArray(value)) {
|
|
244
|
-
return value.map(row => applyRichFieldDefaults(row, childFields))
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return value
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (Array.isArray(schema.fields)) {
|
|
251
|
-
return applyRichFieldDefaults(value, schema.fields)
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
return value
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Apply schemas to content.data
|
|
259
|
-
* Only processes tags that have a matching schema, leaves others untouched
|
|
260
|
-
*
|
|
261
|
-
* ## Two orders of schema — what a `data:` declaration may describe
|
|
262
|
-
*
|
|
263
|
-
* A component's `data:` is 1st order: a DEVELOPER says what shape the section
|
|
264
|
-
* consumes. An authored form (```yaml:form```) is 2nd order: an AUTHOR says what
|
|
265
|
-
* shape a VISITOR will submit. It is schema-shaped, but it is content.
|
|
266
|
-
*
|
|
267
|
-
* Declaring a schema for such a tag is legitimate, and it is worth being precise
|
|
268
|
-
* about what it may describe:
|
|
269
|
-
*
|
|
270
|
-
* OK the DEFINITION's envelope — `title?`, `description?`, `fields: <map>`.
|
|
271
|
-
* That asks "is this a well-formed form?", which is what build-time
|
|
272
|
-
* validation is for (`build/src/validate-data.js` pairs a section's data
|
|
273
|
-
* input with the schema its meta.js binds to that key).
|
|
274
|
-
* WRONG a schema whose fields are THE FORM'S fields (`name`, `email`, …).
|
|
275
|
-
* Those are author-chosen and unknowable at build time. A form-rendering
|
|
276
|
-
* component receives its fields; it does not declare them.
|
|
277
|
-
*
|
|
278
|
-
* The mechanism is bounded and does not punish the mistake loudly:
|
|
279
|
-
* `applySchemaToObject` recurses only where the schema declares structure
|
|
280
|
-
* (`type: object` + `fields`, `type: array` + `items.fields`), so a
|
|
281
|
-
* form-definition schema — which cannot name the author's fields — can never
|
|
282
|
-
* reach into them. It fills the envelope defaults its own author declared.
|
|
283
|
-
*
|
|
284
|
-
* (Established with the editor team, 2026-07-31, channel frontend↔framework.
|
|
285
|
-
* The editor shadows a foundation's `form` declaration with its own builder via
|
|
286
|
-
* `builtinSchemas()`; that is about the EDITING UI and is orthogonal to whether a
|
|
287
|
-
* foundation declares a schema for validation.)
|
|
288
|
-
*
|
|
289
|
-
* @param {Object} data - The data object from content
|
|
290
|
-
* @param {Object} schemas - Schema definitions from runtime meta
|
|
291
|
-
* @returns {Object} Data with schemas applied
|
|
292
|
-
*/
|
|
293
|
-
export function applySchemas(data, schemas) {
|
|
294
|
-
if (!schemas || !data || typeof data !== 'object') {
|
|
295
|
-
return data || {}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const result = { ...data }
|
|
299
|
-
|
|
300
|
-
for (const [tag, rawValue] of Object.entries(data)) {
|
|
301
|
-
const schema = schemas[tag]
|
|
302
|
-
if (!schema) continue // No schema for this tag - leave as-is
|
|
303
|
-
|
|
304
|
-
result[tag] = isRichSchema(schema)
|
|
305
|
-
? applyRichSchemaToValue(rawValue, schema)
|
|
306
|
-
: applySchemaToValue(rawValue, schema)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
return result
|
|
310
|
-
}
|
|
311
|
-
|
|
312
105
|
/**
|
|
313
106
|
* Apply param defaults from runtime schema
|
|
314
107
|
*
|
|
@@ -426,8 +219,18 @@ function runDataHandler(block) {
|
|
|
426
219
|
* Skipped when the block is still waiting on async data
|
|
427
220
|
* (`block.dataLoading`), when no handler is registered, when the
|
|
428
221
|
* block has no raw content, when the handler returns a no-change
|
|
429
|
-
* signal (undefined, null, or the same reference as rawContent),
|
|
430
|
-
*
|
|
222
|
+
* signal (undefined, null, or the same reference as rawContent), when
|
|
223
|
+
* it returns anything but a ProseMirror document, or when the handler
|
|
224
|
+
* throws. Errors are logged via `console.error`.
|
|
225
|
+
*
|
|
226
|
+
* ⛔ ONLY A DOCUMENT IS RE-PARSED (2026-10-05). The handler is given the section's
|
|
227
|
+
* DATA, so the natural no-op — `(x) => x` — returns the data map. That is not
|
|
228
|
+
* `rawContent`, so it passed the no-change check; `parseContent` takes a plain
|
|
229
|
+
* object as content already parsed; and the section's content became its data
|
|
230
|
+
* map — title, paragraphs and items gone — with `data.data` pointing back at
|
|
231
|
+
* itself, so a `JSON.stringify(content.data)` threw far from the handler (and
|
|
232
|
+
* failed a static build's prerender of the page). Any result that is not a
|
|
233
|
+
* document is now ignored, and said once per handler.
|
|
431
234
|
*/
|
|
432
235
|
function runContentHandler(block) {
|
|
433
236
|
if (block.dataLoading) return
|
|
@@ -438,6 +241,10 @@ function runContentHandler(block) {
|
|
|
438
241
|
try {
|
|
439
242
|
const transformed = handler(block.parsedContent.data, block)
|
|
440
243
|
if (!transformed || transformed === block.rawContent) return
|
|
244
|
+
if (!isDocument(transformed)) {
|
|
245
|
+
warnNotADocument(handler, transformed, block.parsedContent.data)
|
|
246
|
+
return
|
|
247
|
+
}
|
|
441
248
|
const reparsed = block.parseContent(transformed)
|
|
442
249
|
reparsed.data = block.parsedContent.data
|
|
443
250
|
block.parsedContent = reparsed
|
|
@@ -447,11 +254,37 @@ function runContentHandler(block) {
|
|
|
447
254
|
}
|
|
448
255
|
}
|
|
449
256
|
|
|
257
|
+
/** A ProseMirror document — bare, or wrapped as `{ doc }`, the two forms `rawContent` takes. */
|
|
258
|
+
function isDocument(value) {
|
|
259
|
+
return value?.type === 'doc' || value?.doc?.type === 'doc'
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Say, once per handler, that a content handler returned something that is not a
|
|
264
|
+
* document — naming the likely mistake when it returned the very data it was given.
|
|
265
|
+
*/
|
|
266
|
+
const warnedContentHandlers = new WeakSet()
|
|
267
|
+
function warnNotADocument(handler, value, data) {
|
|
268
|
+
if (warnedContentHandlers.has(handler)) return
|
|
269
|
+
warnedContentHandlers.add(handler)
|
|
270
|
+
const what =
|
|
271
|
+
value === data
|
|
272
|
+
? 'the data it was given'
|
|
273
|
+
: Array.isArray(value)
|
|
274
|
+
? 'an array'
|
|
275
|
+
: `${typeof value === 'object' ? 'an object' : typeof value}`
|
|
276
|
+
console.warn(
|
|
277
|
+
`[uniweb] handlers.content returned ${what}, not a ProseMirror document — ignored, and the ` +
|
|
278
|
+
`section keeps its content. The handler is called (data, block) and returns the section's ` +
|
|
279
|
+
`raw content transformed (block.rawContent, as Loom's does), or null for no change.`
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
450
283
|
/**
|
|
451
284
|
* Run the foundation-level props handler on the final { content, params }
|
|
452
285
|
* before they reach the component. Runs after content parsing, param
|
|
453
|
-
* defaults
|
|
454
|
-
*
|
|
286
|
+
* defaults and content guarantees — the handler sees the exact shape the
|
|
287
|
+
* component would receive and can modify it.
|
|
455
288
|
*
|
|
456
289
|
* The handler receives `(content, params, block)` and returns a new
|
|
457
290
|
* `{ content, params }` object, or null/undefined for no change.
|
|
@@ -490,13 +323,18 @@ function runPropsHandler(content, params, block) {
|
|
|
490
323
|
* instantiated version.
|
|
491
324
|
* 4. Apply param defaults from meta.
|
|
492
325
|
* 5. Build the guaranteed content structure.
|
|
493
|
-
* 6.
|
|
494
|
-
* 7. Run the foundation props handler (if registered) for
|
|
326
|
+
* 6. Run the foundation props handler (if registered) for
|
|
495
327
|
* post-processing of the final { content, params }.
|
|
496
328
|
*
|
|
497
|
-
* Steps 1–3 mutate the block (vanilla JS layer). Steps 4–
|
|
329
|
+
* Steps 1–3 mutate the block (vanilla JS layer). Steps 4–6 are
|
|
498
330
|
* pure derivations of the block's now-assembled state.
|
|
499
331
|
*
|
|
332
|
+
* ⛔ No field defaults for `content.data` — ruled 2026-10-05 [Diego]. A step between 5 and 6
|
|
333
|
+
* filled each missing field from its schema's `default`, and replaced a value its `enum`
|
|
334
|
+
* rejected, from the `schemas` a foundation built before then still carries in its meta —
|
|
335
|
+
* read no more. A record reaches the component as it is: an absent field is its own fact,
|
|
336
|
+
* and what it renders as is the component's choice.
|
|
337
|
+
*
|
|
500
338
|
* @param {Object} block - The block instance
|
|
501
339
|
* @param {Object} meta - Runtime metadata for the component (from meta[componentName])
|
|
502
340
|
* @param {Object|null} [entityData] - Entity data resolved by EntityStore (null if none)
|
|
@@ -516,13 +354,7 @@ export function prepareProps(block, meta, entityData = null) {
|
|
|
516
354
|
const params = applyDefaults(block.properties, defaults)
|
|
517
355
|
|
|
518
356
|
// Guarantee content structure
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
// Apply schemas to content.data
|
|
522
|
-
const schemas = meta?.schemas || null
|
|
523
|
-
if (schemas && content.data) {
|
|
524
|
-
content.data = applySchemas(content.data, schemas)
|
|
525
|
-
}
|
|
357
|
+
const content = guaranteeContentStructure(block.parsedContent)
|
|
526
358
|
|
|
527
359
|
// Post-process hook
|
|
528
360
|
const adjusted = runPropsHandler(content, params, block)
|