@kudzujs/core 0.6.23 → 0.6.25
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 +18 -1
- package/framework/core.mjs +4 -1
- package/framework/list-runtime.js +24 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -399,14 +399,31 @@ The original row component remains reusable across multiple lists and ordinary J
|
|
|
399
399
|
One nested keyed map may read a direct array property of its parent item. This supports category/item data populated after mount while preserving both parent and child DOM identity across updates and reorder:
|
|
400
400
|
|
|
401
401
|
```tsx
|
|
402
|
+
function ItemCard({ item, onSelect }: {
|
|
403
|
+
item: Item
|
|
404
|
+
onSelect: () => void
|
|
405
|
+
}) {
|
|
406
|
+
return <li>
|
|
407
|
+
<span>{item.title}</span>
|
|
408
|
+
{item.available ? <strong>Available</strong> : <small>Unavailable</small>}
|
|
409
|
+
<button onClick={() => onSelect()}>Select</button>
|
|
410
|
+
</li>
|
|
411
|
+
}
|
|
412
|
+
|
|
402
413
|
{categories.map(category => <section key={category.id}>
|
|
403
414
|
<h2>{category.title}</h2>
|
|
404
|
-
<ul>{category.items.map(item => <ItemCard
|
|
415
|
+
<ul>{category.items.map(item => <ItemCard
|
|
416
|
+
key={item.id}
|
|
417
|
+
item={item}
|
|
418
|
+
onSelect={() => setSelected(item)}
|
|
419
|
+
/>)}</ul>
|
|
405
420
|
</section>)}
|
|
406
421
|
```
|
|
407
422
|
|
|
408
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.
|
|
409
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.
|
|
426
|
+
|
|
410
427
|
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>`.
|
|
411
428
|
|
|
412
429
|
The focused wrapper fixture emits 1,393 B raw / 500 B gzip HTML and 10,719 B raw / 4,665 B gzip JavaScript across its route capabilities. After one warm-up, seven clean builds measured 314.1, 325.3, 322.3, 327.2, 336.1, 322.4, and 315.0 ms, with a 322.4 ms median.
|
package/framework/core.mjs
CHANGED
|
@@ -585,7 +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
|
-
|
|
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>`
|
|
589
592
|
}
|
|
590
593
|
if (!node || typeof node !== "object" || !("type" in node)) {
|
|
591
594
|
throw new Error(`Cannot render ${String(node)}`)
|
|
@@ -9,6 +9,7 @@ const itemParts = new WeakMap()
|
|
|
9
9
|
const listItems = new WeakMap()
|
|
10
10
|
const ownedLists = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
|
|
11
11
|
const conditionOwners = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
|
|
12
|
+
const conditionTemplates = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
|
|
12
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]" : ""}`
|
|
13
14
|
|
|
14
15
|
function commitLists(id) {
|
|
@@ -39,7 +40,12 @@ function mountLists(root) {
|
|
|
39
40
|
if (__KUDZU_LIST_ROW_STATES__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])
|
|
40
41
|
const templateRoot = start.content.firstElementChild
|
|
41
42
|
const parts = listItemPartPlan(templateRoot, descriptor.nested)
|
|
42
|
-
for (const root of roots)
|
|
43
|
+
for (const root of roots) {
|
|
44
|
+
if (__KUDZU_LIST_CONDITIONS__ && descriptor.conditions) {
|
|
45
|
+
const initialParts = listItemParts(root, descriptor.nested)
|
|
46
|
+
mapListConditionTemplates(parts.conditions, initialParts.conditions)
|
|
47
|
+
} else mapListItemParts(parts, root, descriptor.nested)
|
|
48
|
+
}
|
|
43
49
|
if (__KUDZU_LIST_SEEDS__ && descriptor.seed && !browserState.has(descriptor.state)) browserState.set(descriptor.state, roots.map((root, index) => seedListItem(root, descriptor, index)))
|
|
44
50
|
const items = browserState.get(descriptor.state)
|
|
45
51
|
const list = {
|
|
@@ -417,7 +423,8 @@ function listItemParts(root, nested = false) {
|
|
|
417
423
|
if (__KUDZU_LIST_EXPRESSIONS__ && node.hasAttribute("data-k-list-expression")) parts.expressions.push([node, JSON.parse(node.dataset.kListExpression)])
|
|
418
424
|
if (__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ && node.hasAttribute("data-k-list-expression-attrs")) parts.expressionAttributes.push([node, JSON.parse(node.dataset.kListExpressionAttrs)])
|
|
419
425
|
if (__KUDZU_LIST_CONDITIONS__ && node.hasAttribute("data-k-list-condition")) {
|
|
420
|
-
|
|
426
|
+
const encoded = node.dataset.kListCondition || conditionTemplates.get(node)?.dataset.kListCondition
|
|
427
|
+
parts.conditions.push([node, encoded ? JSON.parse(encoded) : undefined])
|
|
421
428
|
conditionOwners.set(node, root)
|
|
422
429
|
}
|
|
423
430
|
if (__KUDZU_LIST_EFFECTS__ && node.hasAttribute("data-k-effects")) parts.effects.push(node)
|
|
@@ -429,7 +436,7 @@ function listItemParts(root, nested = false) {
|
|
|
429
436
|
function listItemPartPlan(template, nested = false) {
|
|
430
437
|
const source = nested ? ownedElements(template) : [template, ...template.querySelectorAll("*")]
|
|
431
438
|
const indexes = new Map(source.map((node, index) => [node, index]))
|
|
432
|
-
const parts = listItemParts(template)
|
|
439
|
+
const parts = listItemParts(template, nested)
|
|
433
440
|
return {
|
|
434
441
|
directTexts: parts.directTexts.map(([node, field]) => [indexes.get(node), field]),
|
|
435
442
|
texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([node, field]) => [indexes.get(node), field]) : [],
|
|
@@ -437,7 +444,7 @@ function listItemPartPlan(template, nested = false) {
|
|
|
437
444
|
events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([node, events]) => [indexes.get(node), events]) : [],
|
|
438
445
|
expressions: __KUDZU_LIST_EXPRESSIONS__ ? parts.expressions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : [],
|
|
439
446
|
expressionAttributes: __KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? parts.expressionAttributes.map(([node, attributes]) => [indexes.get(node), attributes]) : [],
|
|
440
|
-
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : [],
|
|
447
|
+
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [indexes.get(node), descriptor, node]) : [],
|
|
441
448
|
effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(node => indexes.get(node)) : []
|
|
442
449
|
}
|
|
443
450
|
}
|
|
@@ -459,6 +466,15 @@ function mapListItemParts(parts, root, nested = false) {
|
|
|
459
466
|
})
|
|
460
467
|
}
|
|
461
468
|
|
|
469
|
+
function mapListConditionTemplates(planned, initial) {
|
|
470
|
+
if (planned.length !== initial.length) throw new Error("Keyed list condition markers do not match its template")
|
|
471
|
+
for (let index = 0; index < initial.length; index++) {
|
|
472
|
+
const marker = initial[index][0]
|
|
473
|
+
initial[index][1] ??= planned[index][1]
|
|
474
|
+
if (!marker.content.querySelector("template[data-k-list-true],template[data-k-list-false]")) conditionTemplates.set(marker, planned[index][2])
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
462
478
|
function updateListCondition(marker, kind, value, item) {
|
|
463
479
|
const current = listConditionKey(kind, value)
|
|
464
480
|
if (marker.dataset.kListCurrent === current) return
|
|
@@ -472,9 +488,12 @@ function updateListCondition(marker, kind, value, item) {
|
|
|
472
488
|
node = next
|
|
473
489
|
}
|
|
474
490
|
const falseText = kind === "and" && !value ? renderFalsy(value) : ""
|
|
491
|
+
const selector = value ? "template[data-k-list-true]" : "template[data-k-list-false]"
|
|
492
|
+
const branch = marker.content.querySelector(selector) ?? conditionTemplates.get(marker)?.content.querySelector(selector)
|
|
493
|
+
if (!falseText && !branch) throw new Error("Keyed list condition marker has no branch template")
|
|
475
494
|
const fragment = falseText
|
|
476
495
|
? marker.ownerDocument.createDocumentFragment()
|
|
477
|
-
:
|
|
496
|
+
: branch.content.cloneNode(true)
|
|
478
497
|
if (falseText) fragment.append(marker.ownerDocument.createTextNode(falseText))
|
|
479
498
|
const nodes = [...fragment.childNodes]
|
|
480
499
|
const revision = (revisions.get(marker) ?? 0) + 1
|