@kudzujs/core 0.6.22 → 0.6.24

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
@@ -399,15 +399,32 @@ 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 => <li key={item.id}>{item.title}</li>)}</ul>
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
- The nested collection must be `parent.<field>`, the child row must have one intrinsic root, and child handlers may capture the child item. A second child list, third nesting level, computed collection, parent-item capture from the child row, child conditions, child effects, child components, and child row-local state remain unsupported.
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
+
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.5 ms, and parent removal 0.6 ms. Svelte measured 2.2/0.8/5.2/1.1 ms, Vue 4.2/2.3/5.1/2.1 ms, React 9.2/4.3/7.5/3.8 ms, and hand-written Astro/native 0.4/0.2/3.2/0.2 ms. Kudzu shipped 6.6 KB initial JavaScript gzip and complete initial HTML. Sharing inert child-condition branches through each live child-list prototype reduced HTML from 676,086 B to 509,086 B and total deploy output from 693,806 B to 527,256 B raw; the sum of gzip-compressed files is 24,476 B. The CSR targets omit initial rows, so their artifact sizes are not architecture-equivalent.
409
426
 
410
- 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 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, nested item conditions beyond the direct one-level form above, component tags, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
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.
413
430
 
@@ -1974,10 +1974,25 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1974
1974
  ts.forEachChild(node, rejectUnsupportedRenderControl)
1975
1975
  }
1976
1976
  rejectUnsupportedRenderControl(sourceFile)
1977
- const listComponentNames = new Set(rawRenderedLists.flatMap(({ parts }) => {
1977
+ const nestedComponentCalls = new Set()
1978
+ for (const { parts } of rawRenderedLists) {
1979
+ const collectNestedComponents = node => {
1980
+ if (ts.isJsxExpression(node) && node.expression) {
1981
+ const nested = nestedKeyedListParts(node.expression, parts.item)
1982
+ if (nested) {
1983
+ const tag = jsxTagName(nested.root)
1984
+ if (tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase()) nestedComponentCalls.add(nested.root)
1985
+ return
1986
+ }
1987
+ }
1988
+ ts.forEachChild(node, collectNestedComponents)
1989
+ }
1990
+ collectNestedComponents(parts.root)
1991
+ }
1992
+ const listComponentNames = new Set([...rawRenderedLists.flatMap(({ parts }) => {
1978
1993
  const tag = jsxTagName(parts.root)
1979
1994
  return tag && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toUpperCase() ? [tag.text] : []
1980
- }))
1995
+ }), ...[...nestedComponentCalls].map(call => jsxTagName(call).text)])
1981
1996
  const keyedComponentCalls = new Set(rawRenderedLists.map(({ parts }) => parts.root))
1982
1997
  for (const call of reducerRowStateCalls) if (!keyedComponentCalls.has(call)) fail(call, "Reducer-dispatch component useState() is only supported in a direct keyed row")
1983
1998
  for (const name of listComponentNames) {
@@ -2001,6 +2016,10 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2001
2016
  ? componentSpecializations.get(call)
2002
2017
  : specializeComponentCall(call, component.function, sourceFile, factory, context, fail)
2003
2018
  if (specialization.effects.length && !keyedComponentCalls.has(call)) fail(call, "Effectful keyed row components may only be used directly as keyed map rows")
2019
+ if (!local) {
2020
+ specialization.root = mergeSpecializedImports(specialization.root, component.function.getSourceFile(), call)
2021
+ synthesizeTree(specialization.root)
2022
+ }
2004
2023
  componentSpecializations.set(call, specialization)
2005
2024
  }
2006
2025
  if (local) specializedDeclarations.add(component.declaration)
@@ -2038,7 +2057,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2038
2057
  calculation.parent = callback
2039
2058
  validateListExpression(calculation, parts.item, originalParts.root, fail)
2040
2059
  }
2041
- validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization?.rowState, nestedLists)
2060
+ validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, settersForNode(originalParts.root, settersByFunction), specialization?.rowState, nestedLists, componentSpecializations, factory)
2042
2061
  if (specialization?.effects.length) {
2043
2062
  usesListEffects = true
2044
2063
  const statements = specialization.effects.map(entry => {
@@ -2520,7 +2539,7 @@ function insideJsxEventHandler(node, root) {
2520
2539
  return false
2521
2540
  }
2522
2541
 
2523
- function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowState, nestedLists) {
2542
+ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions, setters, rowState, nestedLists, componentSpecializations, factory) {
2524
2543
  const fail = (node, message) => {
2525
2544
  throw sourceNodeError(node, sourceFile, message)
2526
2545
  }
@@ -2550,14 +2569,33 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2550
2569
  if (nestedList) fail(expression, "Keyed list rows support one nested keyed list")
2551
2570
  if (referenceIdentifiers(nested.callback, item).length) fail(nested.root, "Nested keyed list rows cannot capture the parent item")
2552
2571
  nestedList = nested
2553
- const nestedParts = { ...nested, state: parts.state, nested: true }
2572
+ const specialization = componentSpecializations.get(nested.root)
2573
+ const root = specialization?.root ?? nested.root
2574
+ const callback = root === nested.root ? nested.callback : factory.updateArrowFunction(
2575
+ nested.callback,
2576
+ nested.callback.modifiers,
2577
+ nested.callback.typeParameters,
2578
+ nested.callback.parameters,
2579
+ nested.callback.type,
2580
+ nested.callback.equalsGreaterThanToken,
2581
+ root
2582
+ )
2583
+ if (callback !== nested.callback) {
2584
+ ts.setParentRecursive(callback, false)
2585
+ callback.parent = nested.callback.parent
2586
+ }
2587
+ const nestedParts = { ...nested, root, callback, state: parts.state, nested: true }
2588
+ for (const calculation of specialization?.calculations ?? []) {
2589
+ ts.setParentRecursive(calculation, false)
2590
+ calculation.parent = callback
2591
+ validateListExpression(calculation, nested.item, nested.root, fail)
2592
+ }
2554
2593
  nestedLists.set(expression, nestedParts)
2555
- validateKeyedList(nestedParts, sourceFile, listValues, listEventItems, listConditions, setters, undefined, nestedLists)
2594
+ validateKeyedList(nestedParts, sourceFile, listValues, listEventItems, listConditions, setters, undefined, nestedLists, componentSpecializations, factory)
2556
2595
  return
2557
2596
  }
2558
2597
  const condition = conditionalParts(expression)
2559
2598
  if (condition && containsJsx(expression)) {
2560
- if (parts.nested) fail(node, "Nested keyed list item conditions are not supported")
2561
2599
  if (conditionDepth) fail(node, "Nested item conditions are not supported in keyed lists")
2562
2600
  if (rowState && referencedStateNames(condition.condition, setters).has(rowState.state)) {
2563
2601
  conditionDepth++
@@ -568,6 +568,8 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
568
568
  }
569
569
  if (node?.[listConditionalMarker]) {
570
570
  const descriptor = { kind: node.kind, module: node.module, handler: node.handler }
571
+ const owner = renderContext.listRoot ?? renderContext.listRowRoot
572
+ if (owner) owner.conditions = true
571
573
  const previousBranch = renderContext.listConditionalBranch
572
574
  renderContext.listConditionalBranch = true
573
575
  let truthy
@@ -583,7 +585,10 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
583
585
  ? ""
584
586
  : node.kind === "and" && !node.value ? escapeHtml(renderFalsy(node.value)) : node.value ? truthy : falsy
585
587
  const initial = renderContext.listTemplate ? "" : ` data-k-list-current="${escapeAttribute(key)}"`
586
- return `<template data-k-list-condition='${escapeJsonAttribute(descriptor)}'${initial}><template data-k-list-true>${truthy}</template><template data-k-list-false>${falsy}</template></template>${current}<template data-k-list-condition-end></template>`
588
+ const branches = renderContext.listInitialMarkers && !renderContext.listTemplate && owner?.descriptor.ownerField
589
+ ? ""
590
+ : `<template data-k-list-true>${truthy}</template><template data-k-list-false>${falsy}</template>`
591
+ return `<template data-k-list-condition='${escapeJsonAttribute(descriptor)}'${initial}>${branches}</template>${current}<template data-k-list-condition-end></template>`
587
592
  }
588
593
  if (!node || typeof node !== "object" || !("type" in node)) {
589
594
  throw new Error(`Cannot render ${String(node)}`)
@@ -765,13 +770,14 @@ async function renderList(node, namespace, selectValue) {
765
770
  renderContext.listRowConditions = []
766
771
  renderContext.listRowLists = []
767
772
  renderContext.listFields = new Set([node.keyField])
768
- renderContext.listRoot = { id, state: node.items.id, descriptor, template: true, effects: [], item: {}, rowIndexes: { s: 0, c: 0, l: 0 } }
769
- renderContext.listRowRoot = renderContext.listRoot
773
+ const templateRoot = { id, state: node.items.id, descriptor, template: true, effects: [], item: {}, rowIndexes: { s: 0, c: 0, l: 0 } }
774
+ renderContext.listRoot = templateRoot
775
+ renderContext.listRowRoot = templateRoot
770
776
  const template = await renderNode(node.render({}), namespace, selectValue)
771
777
  if (template.includes("data-k-native-") || template.includes("data-k-effects=") || template.includes("data-k-list=")) descriptor.mount = true
772
778
  if (template.includes("data-k-list=")) descriptor.nested = true
773
779
  if (template.includes("data-k-effects=")) descriptor.effects = true
774
- if (template.includes("data-k-list-condition")) descriptor.conditions = true
780
+ if (templateRoot.conditions) descriptor.conditions = true
775
781
  if (template.includes("data-k-list-text-end")) descriptor.textRanges = true
776
782
  if (template.includes("data-k-list-attrs")) descriptor.attributes = true
777
783
  if (template.includes("data-k-list-events")) descriptor.events = true
@@ -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) __KUDZU_LIST_CONDITIONS__ && descriptor.conditions ? listItemParts(root, descriptor.nested) : mapListItemParts(parts, root, descriptor.nested)
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 = {
@@ -429,7 +435,7 @@ function listItemParts(root, nested = false) {
429
435
  function listItemPartPlan(template, nested = false) {
430
436
  const source = nested ? ownedElements(template) : [template, ...template.querySelectorAll("*")]
431
437
  const indexes = new Map(source.map((node, index) => [node, index]))
432
- const parts = listItemParts(template)
438
+ const parts = listItemParts(template, nested)
433
439
  return {
434
440
  directTexts: parts.directTexts.map(([node, field]) => [indexes.get(node), field]),
435
441
  texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([node, field]) => [indexes.get(node), field]) : [],
@@ -437,7 +443,7 @@ function listItemPartPlan(template, nested = false) {
437
443
  events: __KUDZU_LIST_EVENTS__ ? parts.events.map(([node, events]) => [indexes.get(node), events]) : [],
438
444
  expressions: __KUDZU_LIST_EXPRESSIONS__ ? parts.expressions.map(([node, descriptor]) => [indexes.get(node), descriptor]) : [],
439
445
  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]) : [],
446
+ conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [indexes.get(node), descriptor, node]) : [],
441
447
  effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(node => indexes.get(node)) : []
442
448
  }
443
449
  }
@@ -459,6 +465,14 @@ function mapListItemParts(parts, root, nested = false) {
459
465
  })
460
466
  }
461
467
 
468
+ function mapListConditionTemplates(planned, initial) {
469
+ if (planned.length !== initial.length) throw new Error("Keyed list condition markers do not match its template")
470
+ for (let index = 0; index < initial.length; index++) {
471
+ const marker = initial[index][0]
472
+ if (!marker.content.querySelector("template[data-k-list-true],template[data-k-list-false]")) conditionTemplates.set(marker, planned[index][2])
473
+ }
474
+ }
475
+
462
476
  function updateListCondition(marker, kind, value, item) {
463
477
  const current = listConditionKey(kind, value)
464
478
  if (marker.dataset.kListCurrent === current) return
@@ -472,9 +486,12 @@ function updateListCondition(marker, kind, value, item) {
472
486
  node = next
473
487
  }
474
488
  const falseText = kind === "and" && !value ? renderFalsy(value) : ""
489
+ const selector = value ? "template[data-k-list-true]" : "template[data-k-list-false]"
490
+ const branch = marker.content.querySelector(selector) ?? conditionTemplates.get(marker)?.content.querySelector(selector)
491
+ if (!falseText && !branch) throw new Error("Keyed list condition marker has no branch template")
475
492
  const fragment = falseText
476
493
  ? marker.ownerDocument.createDocumentFragment()
477
- : marker.content.querySelector(value ? "template[data-k-list-true]" : "template[data-k-list-false]").content.cloneNode(true)
494
+ : branch.content.cloneNode(true)
478
495
  if (falseText) fragment.append(marker.ownerDocument.createTextNode(falseText))
479
496
  const nodes = [...fragment.childNodes]
480
497
  const revision = (revisions.get(marker) ?? 0) + 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.6.22",
3
+ "version": "0.6.24",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",