@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 +10 -1
- package/framework/core.mjs +12 -4
- package/framework/list-runtime.js +55 -5
- package/package.json +1 -1
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
|
-
|
|
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
|
|
package/framework/core.mjs
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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:
|
|
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 => {
|