@uniweb/runtime 0.23.0 → 0.25.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.
@@ -313,27 +313,54 @@ export function applyDefaults(params, defaults) {
313
313
  }
314
314
 
315
315
  /**
316
- * Merge entity data onto a block's parsedContent.data.
316
+ * ⭐ `content.data` HOLDS THE KEYS THE COMPONENT DECLARES, AND NOTHING ELSE — ruled 2026-09-14
317
+ * [Diego]. Every key in its `meta.js` `data:` and its foundation's `main.js` `data:`
318
+ * (`website.declaredKeys`), each filled, in order, from:
317
319
  *
318
- * Section-level data already on the block (from prerender fetches via
319
- * blockData.parsedContent.data in the Block constructor) takes priority;
320
- * entity data only fills missing keys. Mutates `block.parsedContent.data`
321
- * in place so the vanilla JS layer holds the assembled data and
322
- * subsequent reads see the same shape.
320
+ * 1. what the section holds under that key (`block.heldData`) — a tagged data block, or
321
+ * its own fetch's answer a static build prerendered into it;
322
+ * 2. what the entity store delivered for it — the fetch that fills it (`fillDeclaredKeys`);
323
+ * 3. `null` — nothing fills it, or its fetch is still out (`block.dataLoading`) or failed
324
+ * (`block.dataError[key]`). ⭐ Never `[]`, which is an answer with no records.
325
+ *
326
+ * A tagged data block under a key the component does not declare is left out, and said so
327
+ * in dev (`warnUndeclaredBlocks`); it stays in `content.sequence`. The data object is
328
+ * rebuilt on each render from those sources, never from the last render's.
329
+ *
330
+ * ⛔ Until 2026-09-14 this merged every delivered key into what the section held, so a
331
+ * section received every fetch that reached it and every tagged block it had.
323
332
  */
324
- function mergeEntityData(block, entityData) {
325
- if (!entityData) return
326
- const current = block.parsedContent.data || {}
327
- let changed = false
328
- const merged = { ...current }
329
- for (const key of Object.keys(entityData)) {
330
- if (merged[key] === undefined) {
331
- merged[key] = entityData[key]
332
- changed = true
333
- }
333
+ function assembleData(block, meta, entityData) {
334
+ const declared = block.website?.declaredKeys ? block.website.declaredKeys(meta) : []
335
+ const held = block.heldData || {}
336
+ const data = {}
337
+ for (const [key] of declared) {
338
+ if (held[key] !== undefined) data[key] = held[key]
339
+ else if (entityData && entityData[key] !== undefined) data[key] = entityData[key]
340
+ else data[key] = null
334
341
  }
335
- if (changed) {
336
- block.parsedContent.data = merged
342
+ warnUndeclaredBlocks(block, declared)
343
+ block.parsedContent.data = data
344
+ }
345
+
346
+ /**
347
+ * Say once, in dev, that a tagged data block did not reach its section's component — its
348
+ * key is not declared. The block is still in `content.sequence`, where a component that
349
+ * renders the sequence finds it.
350
+ */
351
+ const warnedBlocks = new Set()
352
+ function warnUndeclaredBlocks(block, declared) {
353
+ if (!block.website?.entityStore?.dev) return
354
+ const keys = new Set(declared.map(([key]) => key))
355
+ for (const item of block.parsedContent?.sequence || []) {
356
+ if (item?.type !== 'dataBlock' || typeof item.tag !== 'string' || keys.has(item.tag)) continue
357
+ const memo = `${block.type}::${item.tag}`
358
+ if (warnedBlocks.has(memo)) continue
359
+ warnedBlocks.add(memo)
360
+ console.warn(
361
+ `[uniweb] ${block.type}: the \`${item.tag}\` data block is not in content.data — ${block.type} does not ` +
362
+ `declare \`${item.tag}\` in its meta.js \`data:\`. It stays in content.sequence.`
363
+ )
337
364
  }
338
365
  }
339
366
 
@@ -434,8 +461,9 @@ function runPropsHandler(content, params, block) {
434
461
  * renderers (`BlockRenderer.jsx` CSR and `ssr-renderer.js` SSG) share
435
462
  * the same code path:
436
463
  *
437
- * 1. Merge entity data (resolved by EntityStore) onto
438
- * `block.parsedContent.data`.
464
+ * 1. Assemble `block.parsedContent.data` from the keys the component
465
+ * declares: what the section holds, else what EntityStore delivered,
466
+ * else `null` (`assembleData`).
439
467
  * 2. Run the foundation data handler (if registered) to filter or
440
468
  * reshape the assembled data.
441
469
  * 3. Run the foundation content handler (if registered) on the
@@ -456,7 +484,11 @@ function runPropsHandler(content, params, block) {
456
484
  * @returns {Object} Prepared props: { content, params }
457
485
  */
458
486
  export function prepareProps(block, meta, entityData = null) {
459
- mergeEntityData(block, entityData)
487
+ assembleData(block, meta, entityData)
488
+ // ⭐ A list the section held before the store answered — its own fetch, prerendered
489
+ // into its content by a static build — gets its records' `$route` by the store's rule,
490
+ // which the store did not deliver it by (2026-09-14).
491
+ block.website?.entityStore?.linkOwnRecords?.(block)
460
492
  runDataHandler(block)
461
493
  runContentHandler(block)
462
494
 
@@ -231,8 +231,8 @@ export function renderBlock(block, { as = 'section' } = {}) {
231
231
  }, `Component not found: ${block.type}`)
232
232
  }
233
233
 
234
- // Resolve inherited entity data synchronously (SSG has no async).
235
- // EntityStore walks page/site hierarchy to find data matching meta.inheritData.
234
+ // Resolve entity data synchronously (SSG has no async): EntityStore fills the keys
235
+ // the component declares (`meta.data`) from the fetches that reach the block.
236
236
  const meta = getComponentMeta(block.type)
237
237
  const entityStore = block.website?.entityStore
238
238
  let entityData = null