@kudzujs/core 0.6.25 → 0.6.27

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/README.md CHANGED
@@ -422,7 +422,16 @@ function ItemCard({ item, onSelect }: {
422
422
 
423
423
  The nested collection must be `parent.<field>`. Its child row may be intrinsic or a same-file or relative-imported component that specializes to one intrinsic root. Child handlers receive the latest child item, and one level of child-local `&&` or ternary JSX conditions patches bounded DOM without remounting the child row. A second child list, third nesting level, computed collection, parent-item capture from the child row, conditions inside child conditions, child effects, child component tags below the specialized row root, and child row-local state remain unsupported.
424
424
 
425
- In the matched 100-parent/1,000-child component benchmark, a selected child text and condition update measured 1.2 ms, child reverse 0.4 ms, parent reverse 4.6 ms, and parent removal 0.6 ms. Svelte measured 2.2/0.8/5.4/1.1 ms, Vue 4.3/2.3/5.0/2.0 ms, React 8.9/4.2/7.4/4.1 ms, and hand-written Astro/native 0.5/0.3/3.1/0.1 ms. Kudzu shipped 6.6 KB initial JavaScript gzip and complete initial HTML. Sharing inert child-condition branches and descriptors through each live child-list prototype reduced HTML from 676,086 B to 417,086 B and total deploy output from 693,806 B to 435,326 B raw; the sum of gzip-compressed files is 23,473 B. The CSR targets omit initial rows, so their artifact sizes are not architecture-equivalent.
425
+ #### Nested list output
426
+
427
+ Initial child rows remain complete HTML. Kudzu stores one child row prototype, including inert condition branches and descriptors, and reuses it across parent rows.
428
+
429
+ - HTML: 676,086 B before sharing, 339,586 B after sharing.
430
+ - Total deploy output: 693,806 B before sharing, 359,035 B after sharing.
431
+ - Compressed-file sum: 24,067 B. Sharing text and attribute patch metadata saves 29,576 B raw over `v0.6.26` while adding 147 B gzip because the removed metadata compressed well.
432
+ - Initial JavaScript: 6.9 KB gzip. Routes without nested lists compile out prototype and marker lookup.
433
+
434
+ In the matched 100-parent/1,000-child fixture, Kudzu measured 1.4/0.5/5.4/0.7 ms for child update and condition change, child reverse, parent reverse, and parent removal. Hand-written Astro/native measured 0.5/0.3/3.6/0.2 ms, Svelte 2.2/0.9/5.4/0.9 ms, Vue 4.0/2.1/4.6/1.8 ms, and React 8.5/3.9/6.8/3.8 ms. Kudzu and Astro emit initial rows while the CSR targets do not, so artifact sizes are not architecture-equivalent.
426
435
 
427
436
  Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root or top-level local or relative-imported row component, and `key={item.<field>}`. State-backed list wrappers use one destructured props parameter, an intrinsic return root, no effects, and a direct local-state prop. Same-file wrappers must be unexported and state-backed at every call; relative default, named/aliased, and direct named re-export wrappers are specialized per qualifying call. Row components accept destructured projected props, top-level single-`const` calculations and inline effects before one intrinsic return. Effect dependencies inside a direct outer row may be empty, direct primitive Kudzu state identifiers, or direct `item.<field>` properties whose selected values remain JSON-safe primitives. Whole-item, computed, nested, derived, `__proto__`, `prototype`, and `constructor` dependencies are rejected. A list alias may only be rendered once and cannot be read by other JavaScript. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, imported helpers used inside calculations, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Package, namespace, and star-export list wrappers, package or namespace row imports, same-file exported rows, reusable aliases, prop spreads/defaults/rest, children, conditions nested inside item conditions, component tags below a specialized row root, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
428
437
 
@@ -546,7 +546,9 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
546
546
  if (node?.[listMarker]) return renderList(node, namespace, selectValue)
547
547
  if (node?.[listFieldMarker]) {
548
548
  if (renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) renderContext.listFields?.add(node.field)
549
- const marker = renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch ? ` data-k-list-text="${escapeAttribute(node.field)}"` : ""
549
+ const marker = renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch
550
+ ? sharedInitialListMarker() ? " data-k-list-text" : ` data-k-list-text="${escapeAttribute(node.field)}"`
551
+ : ""
550
552
  return `<template${marker}></template>${escapeHtml(node.value ?? "")}<template data-k-list-text-end></template>`
551
553
  }
552
554
  if (node?.[listExpressionMarker]) {
@@ -727,12 +729,12 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
727
729
  }
728
730
 
729
731
  if (attributeBindings.length) attributes += ` data-k-bind-attrs='${escapeJsonAttribute(attributeBindings)}'`
730
- if ((renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) && listAttributes.length) attributes += ` data-k-list-attrs='${escapeJsonAttribute(listAttributes)}'`
732
+ if ((renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) && listAttributes.length) attributes += sharedInitialListMarker() ? " data-k-list-attrs" : ` data-k-list-attrs='${escapeJsonAttribute(listAttributes)}'`
731
733
  if ((renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) && listExpressionAttributes.length) attributes += ` data-k-list-expression-attrs='${escapeJsonAttribute(listExpressionAttributes)}'`
732
734
  if ((renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) && listEvents.length) attributes += ` data-k-list-events='${escapeJsonAttribute(listEvents)}'`
733
735
  if ((renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) && directListText) {
734
736
  renderContext.listFields?.add(directListText.field)
735
- attributes += ` data-k-list-text="${escapeAttribute(directListText.field)}"`
737
+ attributes += sharedInitialListMarker() ? " data-k-list-text" : ` data-k-list-text="${escapeAttribute(directListText.field)}"`
736
738
  }
737
739
 
738
740
  if (tag === "option" && selectValue !== noSelectValue && String(optionValue(props)) === (selectValue == null ? "" : String(selectValue))) attributes += " selected"
@@ -746,6 +748,11 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
746
748
  return `<${tag}${attributes}>${children}</${tag}>`
747
749
  }
748
750
 
751
+ function sharedInitialListMarker() {
752
+ const owner = renderContext.listRoot ?? renderContext.listRowRoot
753
+ return renderContext.listInitialMarkers && !renderContext.listTemplate && !renderContext.listConditionalBranch && owner?.descriptor.ownerField
754
+ }
755
+
749
756
  async function renderList(node, namespace, selectValue) {
750
757
  if (namespace) throw new Error(`Reactive keyed lists are not supported inside ${namespace}`)
751
758
  const ownerRoot = node.ownerField ? renderContext.listRoot ?? renderContext.listRowRoot : undefined
@@ -801,7 +808,8 @@ async function renderList(node, namespace, selectValue) {
801
808
  if (!node.ownerField || ownerTemplate) renderContext.lists.push(descriptor)
802
809
  renderContext.hasBehaviors = true
803
810
  renderContext.hasLists = true
804
- return `<template data-k-list='${escapeJsonAttribute(descriptor)}'>${template}</template>${current}<template data-k-list-end="${id}"></template>`
811
+ const prototype = node.ownerField && !ownerTemplate ? "" : template
812
+ return `<template data-k-list='${escapeJsonAttribute(descriptor)}'>${prototype}</template>${current}<template data-k-list-end="${id}"></template>`
805
813
  } finally {
806
814
  renderContext.listRoot = previousListRoot
807
815
  renderContext.listRowRoot = previousListRowRoot
@@ -11,6 +11,8 @@ const ownedLists = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
11
11
  const conditionOwners = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
12
12
  const conditionTemplates = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
13
13
  const itemPartsSelector = `[data-k-list-text]${__KUDZU_LIST_ATTRIBUTES__ ? ",[data-k-list-attrs]" : ""}${__KUDZU_LIST_EVENTS__ ? ",[data-k-list-events]" : ""}${__KUDZU_LIST_EXPRESSIONS__ ? ",[data-k-list-expression]" : ""}${__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? ",[data-k-list-expression-attrs]" : ""}${__KUDZU_LIST_CONDITIONS__ ? ",[data-k-list-condition]" : ""}${__KUDZU_LIST_EFFECTS__ ? ",[data-k-effects]" : ""}`
14
+ const childPrototypes = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
15
+ const itemPartPlans = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
14
16
 
15
17
  function commitLists(id) {
16
18
  const lists = listTargets.get(id)
@@ -37,11 +39,13 @@ function mountLists(root) {
37
39
  const descriptor = JSON.parse(start.dataset.kList)
38
40
  const end = findEnd(start, descriptor.id)
39
41
  const roots = listRoots(start, end)
42
+ const nested = __KUDZU_NESTED_LISTS__ ? mountNestedPrototype(start, descriptor, roots) : undefined
43
+ const templateRoot = __KUDZU_NESTED_LISTS__ ? nested.templateRoot : start.content.firstElementChild
40
44
  if (__KUDZU_LIST_ROW_STATES__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])
41
- const templateRoot = start.content.firstElementChild
42
45
  const parts = listItemPartPlan(templateRoot, descriptor.nested)
43
46
  for (const root of roots) {
44
47
  if (__KUDZU_LIST_CONDITIONS__ && descriptor.conditions) {
48
+ if (__KUDZU_NESTED_LISTS__ && descriptor.ownerField) itemPartPlans.set(root, parts)
45
49
  const initialParts = listItemParts(root, descriptor.nested)
46
50
  mapListConditionTemplates(parts.conditions, initialParts.conditions)
47
51
  } else mapListItemParts(parts, root, descriptor.nested)
@@ -51,6 +55,7 @@ function mountLists(root) {
51
55
  const list = {
52
56
  start,
53
57
  descriptor,
58
+ ...(__KUDZU_NESTED_LISTS__ ? { templateRoot, ...(nested.childPrototype ? { childPrototype: nested.childPrototype } : {}) } : {}),
54
59
  parts,
55
60
  seedFields: __KUDZU_LIST_SEEDS__ && descriptor.seed && Object.keys(descriptor.seed),
56
61
  roots: new Map(roots.map((node, index) => [keyToken(descriptor.keys[index]), node])),
@@ -58,7 +63,7 @@ function mountLists(root) {
58
63
  items: undefined,
59
64
  container: roots[0]?.parentNode,
60
65
  boundary: end,
61
- ...(__KUDZU_NESTED_LISTS__ && descriptor.ownerField ? { owner: listOwner(start) } : {})
66
+ ...(__KUDZU_NESTED_LISTS__ && descriptor.ownerField ? { owner: nested.owner } : {})
62
67
  }
63
68
  if (__KUDZU_NESTED_LISTS__ && descriptor.ownerField) {
64
69
  if (!list.owner) throw new Error("Nested keyed list has no parent row")
@@ -150,10 +155,11 @@ function updateList(list) {
150
155
  for (const { item, key, token, value } of entries) {
151
156
  let node = list.roots.get(token)
152
157
  if (!node) {
153
- node = list.start.content.firstElementChild?.cloneNode(true)
158
+ node = (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
154
159
  if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
155
160
  if (!node) throw new Error("Keyed list template has no root element")
156
161
  node.removeAttribute("data-k-list-root")
162
+ if (__KUDZU_NESTED_LISTS__ && list.childPrototype) childPrototypes.set(node, list.childPrototype)
157
163
  if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
158
164
  mapListItemParts(list.parts, node, list.descriptor.nested)
159
165
  fillListItem(node, item, list.descriptor.nested)
@@ -323,10 +329,11 @@ function updateReducerList(list, items) {
323
329
  }
324
330
 
325
331
  function addListRoot(list, { item, key, token, value }) {
326
- let node = list.start.content.firstElementChild?.cloneNode(true)
332
+ let node = (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
327
333
  if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
328
334
  if (!node) throw new Error("Keyed list template has no root element")
329
335
  node.removeAttribute("data-k-list-root")
336
+ if (__KUDZU_NESTED_LISTS__ && list.childPrototype) childPrototypes.set(node, list.childPrototype)
330
337
  if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
331
338
  mapListItemParts(list.parts, node, list.descriptor.nested)
332
339
  fillListItem(node, item, list.descriptor.nested)
@@ -418,7 +425,7 @@ function listItemParts(root, nested = false) {
418
425
  parts = { directTexts: [], texts: [], attributes: [], events: [], expressions: [], expressionAttributes: [], conditions: [], effects: [] }
419
426
  for (const node of nested ? ownedElements(root).filter(node => node.matches(itemPartsSelector)) : matching(root, itemPartsSelector)) {
420
427
  if (node.hasAttribute("data-k-list-text")) (node.tagName === "TEMPLATE" ? parts.texts : parts.directTexts).push([node, node.dataset.kListText])
421
- if (__KUDZU_LIST_ATTRIBUTES__ && node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, JSON.parse(node.dataset.kListAttrs)])
428
+ if (__KUDZU_LIST_ATTRIBUTES__ && node.hasAttribute("data-k-list-attrs")) parts.attributes.push([node, node.dataset.kListAttrs ? JSON.parse(node.dataset.kListAttrs) : undefined])
422
429
  if (__KUDZU_LIST_EVENTS__ && node.hasAttribute("data-k-list-events")) parts.events.push([node, node.dataset.kListEvents])
423
430
  if (__KUDZU_LIST_EXPRESSIONS__ && node.hasAttribute("data-k-list-expression")) parts.expressions.push([node, JSON.parse(node.dataset.kListExpression)])
424
431
  if (__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ && node.hasAttribute("data-k-list-expression-attrs")) parts.expressionAttributes.push([node, JSON.parse(node.dataset.kListExpressionAttrs)])
@@ -429,10 +436,28 @@ function listItemParts(root, nested = false) {
429
436
  }
430
437
  if (__KUDZU_LIST_EFFECTS__ && node.hasAttribute("data-k-effects")) parts.effects.push(node)
431
438
  }
439
+ if (__KUDZU_NESTED_LISTS__) hydrateListItemParts(itemPartPlans.get(root), parts)
432
440
  itemParts.set(root, parts)
433
441
  return parts
434
442
  }
435
443
 
444
+ function hydrateListItemParts(planned, initial) {
445
+ if (!planned) return
446
+ hydrateListItemPartValues(planned.directTexts, initial.directTexts, value => value === "")
447
+ if (__KUDZU_LIST_TEXT_RANGES__) hydrateListItemPartValues(planned.texts, initial.texts, value => value === "")
448
+ if (__KUDZU_LIST_ATTRIBUTES__) hydrateListItemPartValues(planned.attributes, initial.attributes, value => value === undefined)
449
+ }
450
+
451
+ function hydrateListItemPartValues(planned, initial, missing) {
452
+ let index = 0
453
+ for (const entry of initial) {
454
+ if (!missing(entry[1])) continue
455
+ if (!planned[index]) throw new Error("Keyed list item markers do not match its template")
456
+ entry[1] = planned[index++][1]
457
+ }
458
+ if (index !== planned.length) throw new Error("Keyed list item markers do not match its template")
459
+ }
460
+
436
461
  function listItemPartPlan(template, nested = false) {
437
462
  const source = nested ? ownedElements(template) : [template, ...template.querySelectorAll("*")]
438
463
  const indexes = new Map(source.map((node, index) => [node, index]))
@@ -537,6 +562,31 @@ function listOwner(start) {
537
562
  return owner
538
563
  }
539
564
 
565
+ function mountNestedPrototype(start, descriptor, roots) {
566
+ const owner = descriptor.ownerField ? listOwner(start) : undefined
567
+ const prototypeStart = owner ? childPrototypes.get(owner) : undefined
568
+ const templateRoot = prototypeStart?.content.firstElementChild ?? start.content.firstElementChild
569
+ if (!templateRoot) throw new Error("Nested keyed list has no shared row prototype")
570
+ const childPrototype = descriptor.child ? findChildPrototype(templateRoot, descriptor.child.field) : undefined
571
+ if (descriptor.child && !childPrototype) throw new Error("Keyed list template has no nested row prototype")
572
+ if (childPrototype) for (const root of roots) childPrototypes.set(root, childPrototype)
573
+ if (prototypeStart && start !== prototypeStart) start.content.replaceChildren()
574
+ return { owner, childPrototype, templateRoot }
575
+ }
576
+
577
+ function findChildPrototype(root, field) {
578
+ for (const element of root.children) {
579
+ if (element.matches("template[data-k-list]")) {
580
+ if (JSON.parse(element.dataset.kList).ownerField === field) return element
581
+ const nested = findChildPrototype(element.content, field)
582
+ if (nested) return nested
583
+ } else {
584
+ const nested = findChildPrototype(element, field)
585
+ if (nested) return nested
586
+ }
587
+ }
588
+ }
589
+
540
590
  function ownedElements(root) {
541
591
  const elements = []
542
592
  const visit = node => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.6.25",
3
+ "version": "0.6.27",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",