@kudzujs/core 0.6.24 → 0.6.26
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 +6 -5
- package/framework/list-runtime.js +37 -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, 369,586 B after sharing.
|
|
430
|
+
- Total deploy output: 693,806 B before sharing, 388,611 B after sharing.
|
|
431
|
+
- Compressed-file sum: 23,920 B. The final prototype-sharing step saves 46,715 B raw over `v0.6.25` while adding 447 B gzip because the removed HTML compressed well and prototype lookup adds nested runtime code.
|
|
432
|
+
- Initial JavaScript: 6.8 KB gzip. Routes without nested lists compile out prototype lookup.
|
|
433
|
+
|
|
434
|
+
In the matched 100-parent/1,000-child fixture, Kudzu measured 1.1/0.4/4.7/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.3/0.2 ms, Svelte 2.4/0.9/5.6/1.0 ms, Vue 4.0/2.3/5.1/2.2 ms, and React 8.1/4.1/6.7/3.6 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
|
@@ -585,10 +585,10 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
585
585
|
? ""
|
|
586
586
|
: node.kind === "and" && !node.value ? escapeHtml(renderFalsy(node.value)) : node.value ? truthy : falsy
|
|
587
587
|
const initial = renderContext.listTemplate ? "" : ` data-k-list-current="${escapeAttribute(key)}"`
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
return `<template
|
|
588
|
+
const shared = renderContext.listInitialMarkers && !renderContext.listTemplate && owner?.descriptor.ownerField
|
|
589
|
+
const condition = shared ? " data-k-list-condition" : ` data-k-list-condition='${escapeJsonAttribute(descriptor)}'`
|
|
590
|
+
const branches = shared ? "" : `<template data-k-list-true>${truthy}</template><template data-k-list-false>${falsy}</template>`
|
|
591
|
+
return `<template${condition}${initial}>${branches}</template>${current}<template data-k-list-condition-end></template>`
|
|
592
592
|
}
|
|
593
593
|
if (!node || typeof node !== "object" || !("type" in node)) {
|
|
594
594
|
throw new Error(`Cannot render ${String(node)}`)
|
|
@@ -801,7 +801,8 @@ async function renderList(node, namespace, selectValue) {
|
|
|
801
801
|
if (!node.ownerField || ownerTemplate) renderContext.lists.push(descriptor)
|
|
802
802
|
renderContext.hasBehaviors = true
|
|
803
803
|
renderContext.hasLists = true
|
|
804
|
-
|
|
804
|
+
const prototype = node.ownerField && !ownerTemplate ? "" : template
|
|
805
|
+
return `<template data-k-list='${escapeJsonAttribute(descriptor)}'>${prototype}</template>${current}<template data-k-list-end="${id}"></template>`
|
|
805
806
|
} finally {
|
|
806
807
|
renderContext.listRoot = previousListRoot
|
|
807
808
|
renderContext.listRowRoot = previousListRowRoot
|
|
@@ -11,6 +11,7 @@ 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
|
|
14
15
|
|
|
15
16
|
function commitLists(id) {
|
|
16
17
|
const lists = listTargets.get(id)
|
|
@@ -37,8 +38,9 @@ function mountLists(root) {
|
|
|
37
38
|
const descriptor = JSON.parse(start.dataset.kList)
|
|
38
39
|
const end = findEnd(start, descriptor.id)
|
|
39
40
|
const roots = listRoots(start, end)
|
|
41
|
+
const nested = __KUDZU_NESTED_LISTS__ ? mountNestedPrototype(start, descriptor, roots) : undefined
|
|
42
|
+
const templateRoot = __KUDZU_NESTED_LISTS__ ? nested.templateRoot : start.content.firstElementChild
|
|
40
43
|
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
44
|
const parts = listItemPartPlan(templateRoot, descriptor.nested)
|
|
43
45
|
for (const root of roots) {
|
|
44
46
|
if (__KUDZU_LIST_CONDITIONS__ && descriptor.conditions) {
|
|
@@ -51,6 +53,7 @@ function mountLists(root) {
|
|
|
51
53
|
const list = {
|
|
52
54
|
start,
|
|
53
55
|
descriptor,
|
|
56
|
+
...(__KUDZU_NESTED_LISTS__ ? { templateRoot, ...(nested.childPrototype ? { childPrototype: nested.childPrototype } : {}) } : {}),
|
|
54
57
|
parts,
|
|
55
58
|
seedFields: __KUDZU_LIST_SEEDS__ && descriptor.seed && Object.keys(descriptor.seed),
|
|
56
59
|
roots: new Map(roots.map((node, index) => [keyToken(descriptor.keys[index]), node])),
|
|
@@ -58,7 +61,7 @@ function mountLists(root) {
|
|
|
58
61
|
items: undefined,
|
|
59
62
|
container: roots[0]?.parentNode,
|
|
60
63
|
boundary: end,
|
|
61
|
-
...(__KUDZU_NESTED_LISTS__ && descriptor.ownerField ? { owner:
|
|
64
|
+
...(__KUDZU_NESTED_LISTS__ && descriptor.ownerField ? { owner: nested.owner } : {})
|
|
62
65
|
}
|
|
63
66
|
if (__KUDZU_NESTED_LISTS__ && descriptor.ownerField) {
|
|
64
67
|
if (!list.owner) throw new Error("Nested keyed list has no parent row")
|
|
@@ -150,10 +153,11 @@ function updateList(list) {
|
|
|
150
153
|
for (const { item, key, token, value } of entries) {
|
|
151
154
|
let node = list.roots.get(token)
|
|
152
155
|
if (!node) {
|
|
153
|
-
node = list.start.content.firstElementChild?.cloneNode(true)
|
|
156
|
+
node = (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
|
|
154
157
|
if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
155
158
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
156
159
|
node.removeAttribute("data-k-list-root")
|
|
160
|
+
if (__KUDZU_NESTED_LISTS__ && list.childPrototype) childPrototypes.set(node, list.childPrototype)
|
|
157
161
|
if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
|
|
158
162
|
mapListItemParts(list.parts, node, list.descriptor.nested)
|
|
159
163
|
fillListItem(node, item, list.descriptor.nested)
|
|
@@ -323,10 +327,11 @@ function updateReducerList(list, items) {
|
|
|
323
327
|
}
|
|
324
328
|
|
|
325
329
|
function addListRoot(list, { item, key, token, value }) {
|
|
326
|
-
let node = list.start.content.firstElementChild?.cloneNode(true)
|
|
330
|
+
let node = (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
|
|
327
331
|
if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
328
332
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
329
333
|
node.removeAttribute("data-k-list-root")
|
|
334
|
+
if (__KUDZU_NESTED_LISTS__ && list.childPrototype) childPrototypes.set(node, list.childPrototype)
|
|
330
335
|
if (__KUDZU_LIST_ROW_STATES__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)
|
|
331
336
|
mapListItemParts(list.parts, node, list.descriptor.nested)
|
|
332
337
|
fillListItem(node, item, list.descriptor.nested)
|
|
@@ -423,7 +428,8 @@ function listItemParts(root, nested = false) {
|
|
|
423
428
|
if (__KUDZU_LIST_EXPRESSIONS__ && node.hasAttribute("data-k-list-expression")) parts.expressions.push([node, JSON.parse(node.dataset.kListExpression)])
|
|
424
429
|
if (__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ && node.hasAttribute("data-k-list-expression-attrs")) parts.expressionAttributes.push([node, JSON.parse(node.dataset.kListExpressionAttrs)])
|
|
425
430
|
if (__KUDZU_LIST_CONDITIONS__ && node.hasAttribute("data-k-list-condition")) {
|
|
426
|
-
|
|
431
|
+
const encoded = node.dataset.kListCondition || conditionTemplates.get(node)?.dataset.kListCondition
|
|
432
|
+
parts.conditions.push([node, encoded ? JSON.parse(encoded) : undefined])
|
|
427
433
|
conditionOwners.set(node, root)
|
|
428
434
|
}
|
|
429
435
|
if (__KUDZU_LIST_EFFECTS__ && node.hasAttribute("data-k-effects")) parts.effects.push(node)
|
|
@@ -469,6 +475,7 @@ function mapListConditionTemplates(planned, initial) {
|
|
|
469
475
|
if (planned.length !== initial.length) throw new Error("Keyed list condition markers do not match its template")
|
|
470
476
|
for (let index = 0; index < initial.length; index++) {
|
|
471
477
|
const marker = initial[index][0]
|
|
478
|
+
initial[index][1] ??= planned[index][1]
|
|
472
479
|
if (!marker.content.querySelector("template[data-k-list-true],template[data-k-list-false]")) conditionTemplates.set(marker, planned[index][2])
|
|
473
480
|
}
|
|
474
481
|
}
|
|
@@ -535,6 +542,31 @@ function listOwner(start) {
|
|
|
535
542
|
return owner
|
|
536
543
|
}
|
|
537
544
|
|
|
545
|
+
function mountNestedPrototype(start, descriptor, roots) {
|
|
546
|
+
const owner = descriptor.ownerField ? listOwner(start) : undefined
|
|
547
|
+
const prototypeStart = owner ? childPrototypes.get(owner) : undefined
|
|
548
|
+
const templateRoot = prototypeStart?.content.firstElementChild ?? start.content.firstElementChild
|
|
549
|
+
if (!templateRoot) throw new Error("Nested keyed list has no shared row prototype")
|
|
550
|
+
const childPrototype = descriptor.child ? findChildPrototype(templateRoot, descriptor.child.field) : undefined
|
|
551
|
+
if (descriptor.child && !childPrototype) throw new Error("Keyed list template has no nested row prototype")
|
|
552
|
+
if (childPrototype) for (const root of roots) childPrototypes.set(root, childPrototype)
|
|
553
|
+
if (prototypeStart && start !== prototypeStart) start.content.replaceChildren()
|
|
554
|
+
return { owner, childPrototype, templateRoot }
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function findChildPrototype(root, field) {
|
|
558
|
+
for (const element of root.children) {
|
|
559
|
+
if (element.matches("template[data-k-list]")) {
|
|
560
|
+
if (JSON.parse(element.dataset.kList).ownerField === field) return element
|
|
561
|
+
const nested = findChildPrototype(element.content, field)
|
|
562
|
+
if (nested) return nested
|
|
563
|
+
} else {
|
|
564
|
+
const nested = findChildPrototype(element, field)
|
|
565
|
+
if (nested) return nested
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
538
570
|
function ownedElements(root) {
|
|
539
571
|
const elements = []
|
|
540
572
|
const visit = node => {
|