@kudzujs/core 0.6.26 → 0.6.28

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/GOAL_A.md CHANGED
@@ -31,7 +31,7 @@ In a matched one-effect navigation build with the same conditional capability, m
31
31
 
32
32
  In matched state-only and item-property keyed-row builds, targeted notification adds 821 B raw / 255 B gzip across the route effect entry, shared runtime, and list runtime. Builds without item-property dependencies retain their previous generated path.
33
33
 
34
- In the matched 1,000-row keyed-effect runtime microbenchmark, Kudzu measured 3.4 ms selected-row cleanup/update/setup, 2.9 ms unrelated-field update, and 7.8 ms reorder after all rows and effects were ready. React CSR measured 12.3, 6.8, and 19.0 ms; Vue measured 4.7, 2.3, and 10.1 ms; and Svelte measured 5.2, 3.0, and 48.1 ms. Targeted changed-root notification reduced Kudzu's selected update from 6.2 to 3.4 ms; list reconciliation remains O(n). Kudzu emits initial rows while these framework fixtures are CSR, so their JavaScript, output, and build observations are not architecture-equivalent claims.
34
+ In the latest matched 1,000-row keyed-effect runtime microbenchmark, Kudzu measured 3.6 ms selected-row cleanup/update/setup, 2.4 ms unrelated-field update, and 8.8 ms reorder after all rows and effects were ready. React CSR measured 9.8, 5.9, and 16.8 ms; Vue measured 5.8, 2.4, and 10.5 ms; and Svelte measured 5.7, 4.1, and 58.1 ms. Targeted changed-root notification avoids an extra O(n) effect-record scan; list reconciliation remains O(n). Kudzu emits initial rows while these framework fixtures are CSR, so their JavaScript, output, and build observations are not architecture-equivalent claims.
35
35
 
36
36
  A 0.6.4 release-tree desktop rerun of the matched six-route commerce fixture measured Kudzu at 486.8 ms build, 35,355 deploy bytes, 7,334 B gzip product JavaScript, 332/156 ms cold/warm LCP, 122.6 ms startup task, 4.8 ms interaction, and 5.6 ms product-cart navigation. React measured 545.4 ms build, 61,464 B gzip product JavaScript, 332/264 ms LCP, 179.9 ms startup task, 10.2 ms interaction, and 9.9 ms navigation. Kudzu built 10.7% faster and its top-level-only fixture retained byte-identical deploy, product-graph, and navigation-asset sizes after navigation-owned effects were added. Raw arrays and the consolidated report are retained under the local demo benchmark workspace.
37
37
 
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  HTML-first TSX framework with synchronous state semantics and no virtual DOM.
8
8
 
9
- Kudzu keeps the familiar function-component, props, children, event-handler, `useState`, `useReducer`, and mount-effect shape. Static components compile to HTML. Simple interactions compile to small behavior commands, while normal sync or async JavaScript handlers and mount effects compile to external ESM.
9
+ Kudzu is designed so ordinary common React-shaped TSX can migrate with minimal source restructuring. It keeps familiar function components, props, children, collection rendering, conditions, event handlers, `useState`, reduced `useReducer`, refs, and effects, preferring compiler specialization over imperative DOM rewrites. This is a general migration model, not compatibility for one application. Static components compile to HTML; interactions compile to direct DOM capabilities and external ESM only where used.
10
10
 
11
11
  > Experimental `0.6.x`: the compiler API and supported TSX surface may change.
12
12
 
@@ -198,7 +198,7 @@ function Controls({ dispatch }: { dispatch: Dispatch<TodoAction> }) {
198
198
  return <Controls dispatch={dispatch} />
199
199
  ```
200
200
 
201
- Kudzu specializes that call at build time; no function prop or child component survives in the browser. Reducers follow React's pure reducer contract. The direct child may also be a keyed row such as `todos.map(todo => <Item key={todo.id} todo={todo} dispatch={dispatch} />)`. Its inline or simple `const` event handler receives the latest keyed item. That row may declare one top-level `useState` with a primitive literal initial value; the existing list key owns its state across item updates and reorder, and removal releases it so a later re-add starts from the initializer. Reactive attributes and conditional DOM use the existing binding capability. Relative TypeScript constants and helpers used inside the handler are renamed for call-site safety and bundled into the parent handler graph. Lazy state or reducer initializers, multiple or non-keyed specialized local states, package, namespace, local, async, and generator reducers, package imports or child imports used outside event handlers, further dispatch forwarding, reducer-dispatch keyed-row effects, and reducer dispatch through context remain unsupported.
201
+ Kudzu specializes that call at build time; no function prop or child component survives in the browser. Reducers follow React's pure reducer contract. The direct child may also be a keyed row such as `todos.map(todo => <Item key={todo.id} todo={todo} dispatch={dispatch} />)`. Its inline or simple `const` event handler receives the latest keyed item, and it has the same multiple serializable state, effect, condition, and object-ref support as other keyed rows. The list key path owns those hooks across item updates and reorder; removal cleans up and releases them. Relative TypeScript constants and helpers used inside the handler are renamed for call-site safety and bundled into the parent handler graph. Lazy state or reducer initializers, non-keyed specialized local state, package, namespace, local, async, and generator reducers, package imports or child imports used outside event handlers, further dispatch forwarding, and reducer dispatch through context remain unsupported.
202
202
 
203
203
  That specialized component may pass one inline or simple `const` callback containing dispatch to one relative-imported synchronous child with an intrinsic root:
204
204
 
@@ -264,7 +264,7 @@ return <>
264
264
  </>
265
265
  ```
266
266
 
267
- Kudzu resolves `current` when the handler reads it, so removed conditional elements return `null` without a component runtime. Refs must initialize with `null`; callback refs, mutable value refs, and refs inside keyed lists are not supported.
267
+ Kudzu resolves `current` when the handler reads it, so removed conditional elements return `null` without a component runtime. Keyed row components may also declare object refs; the row key path scopes the ref and removal releases it. Refs must initialize directly with `null`; callback refs and mutable value refs are not supported.
268
268
 
269
269
  ## Context
270
270
 
@@ -394,9 +394,11 @@ function ItemList({ items }: { items: Item[] }) {
394
394
  return <ItemList items={items} />
395
395
  ```
396
396
 
397
- The original row component remains reusable across multiple lists and ordinary JSX. State-backed list wrappers and row components are specialized to intrinsic JSX at build time; no component function or component runtime is shipped to the browser. Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX, in one top-level immutable `const` rendered once as a JSX child, or in one synchronous wrapper receiving the state identifier as a direct prop. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Single-level item-local `&&` and ternary JSX conditions patch only their bounded branch and mount or unmount its handlers. Item-local handlers and effects receive the latest JSON-safe item for their key. Effects mount after a row is connected and clean up when it is removed. A direct primitive item dependency such as `[item.name]`, optionally mixed with state as `[version, item.name]`, reruns only rows whose selected value changed; the replacement setup receives the complete latest item. Unrelated fields and reorder do not rerun it, while a key change removes and mounts the row. The item remains stored once in shared list state; runtime descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
397
+ The original row component remains reusable across multiple lists and ordinary JSX. State-backed list wrappers and row components are specialized to intrinsic JSX at build time; no component function or component runtime is shipped to the browser. Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX, in one top-level immutable `const` rendered once as a JSX child, or in one synchronous wrapper receiving the state identifier as a direct prop. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Nested item-local `&&` and ternary conditions patch bounded branches and mount or unmount their handlers. Item-local handlers and effects receive the latest JSON-safe item for their key. Effects mount after a row is connected and clean up when it is removed. A direct primitive item dependency such as `[item.name]`, optionally mixed with row or page state, reruns only rows whose selected values changed; unrelated fields and reorder do not rerun it, while removal cleans it up. The item remains stored once in shared list state; runtime descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
398
398
 
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:
399
+ Rendered collections may use one-use top-level aliases and analyzable pipelines over local array state. Inline arrow callbacks accept `(item)` or `(item, index)`; `filter()` supports pure synchronous expressions, `flatMap()` projects one direct array property, `Array.from()` accepts an optional pure mapper, and the final `map()` may use `key={item.field}` or positional `key={index}`. Field keys preserve the matching DOM node through filtering and reorder. Positional keys deliberately preserve the DOM node at each position while its item changes, matching React key semantics.
400
+
401
+ A keyed row may contain multiple keyed maps over direct array properties of its item at any nesting depth. This supports recursively nested data populated after mount while preserving keyed DOM identity across updates and reorder:
400
402
 
401
403
  ```tsx
402
404
  function ItemCard({ item, onSelect }: {
@@ -407,6 +409,12 @@ function ItemCard({ item, onSelect }: {
407
409
  <span>{item.title}</span>
408
410
  {item.available ? <strong>Available</strong> : <small>Unavailable</small>}
409
411
  <button onClick={() => onSelect()}>Select</button>
412
+ <ul>{item.groups.map(group => <li key={group.id}>
413
+ <strong>{group.title}</strong>
414
+ {group.options.map(option => <button key={option.id} onClick={() => console.log(option.title)}>
415
+ {option.title}
416
+ </button>)}
417
+ </li>)}</ul>
410
418
  </li>
411
419
  }
412
420
 
@@ -420,25 +428,27 @@ function ItemCard({ item, onSelect }: {
420
428
  </section>)}
421
429
  ```
422
430
 
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.
431
+ Each nested collection must be `parent.<field>`, and a row may own multiple sibling child maps. There is no numeric nesting-depth limit. Nested rows may be intrinsic or recursively specialized through same-file and relative-imported components to one intrinsic root. They support nested item conditions, latest-item handlers, effects, object refs initialized with `null`, and multiple `useState` declarations whose initial values are directly serializable primitives, arrays, or plain objects. The structural list site plus ancestor key path owns each hook slot across updates and reorder; removal cleans up effects and releases state/ref ownership, so re-adding the key starts from its initial values. Computed nested collections, parent-item capture from a child row, component cycles, package or namespace row imports, and arbitrary collection callbacks remain unsupported.
424
432
 
425
433
  #### Nested list output
426
434
 
427
435
  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
436
 
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.
437
+ - HTML: 339,601 B.
438
+ - Total deploy output: 359,271 B.
439
+ - Compressed-file sum: 24,173 B.
440
+ - Initial JavaScript: 7,204 B gzip. Routes without nested lists compile out prototype and marker lookup.
433
441
 
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.
442
+ In the matched 100-parent/1,000-child fixture, Kudzu measured 1.3/0.4/5.0/0.7 ms for child update and condition change, child reverse, parent reverse, and parent removal. Hand-written Astro/native measured 0.5/0.4/3.9/0.2 ms, Svelte 2.7/1.2/6.7/1.3 ms, Vue 4.9/2.5/6.1/2.2 ms, and React 11.8/5.0/8.2/4.4 ms. Kudzu and Astro emit initial rows while the CSR targets do not, so artifact sizes are not architecture-equivalent.
435
443
 
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>`.
444
+ 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. Collections must remain anchored to local array state; inline callbacks accept one or two identifier parameters, and row roots must be intrinsic JSX or supported same-file/relative components with `key={item.<field>}` or `key={index}`. State-backed list wrappers use one destructured props parameter, an intrinsic return root, no effects, and a direct local-state prop. Whole-item, computed, nested, derived, `__proto__`, `prototype`, and `constructor` effect dependencies are rejected. A collection alias may only be rendered once and cannot be read by other JavaScript. Collection callbacks and derived expressions must be pure and synchronous: supported reads, operators, templates, approved read-only methods, deterministic `Math`, and primitive conversion compile; imported callbacks, browser globals, promises, mutation, arbitrary calls, and prototype-sensitive properties fail. Lazy or dynamic keyed-row state initializers, non-`null` refs, callback refs, package/namespace/star row imports, same-file exported rows, reusable aliases, prop spreads/defaults/rest, children, fragments, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
437
445
 
438
446
  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.
439
447
 
440
448
  The three-wrapper relative-import fixture emits 2,279 B raw / 629 B gzip HTML and 11,370 B raw / 4,828 B gzip JavaScript, including one imported-wrapper item expression; its unused component handler module is not emitted. After one warm-up, seven clean builds measured 340.2, 349.4, 352.8, 335.0, 354.1, 364.3, and 349.5 ms, with a 349.5 ms median.
441
449
 
450
+ The compact neutral integration fixture combines the ordinary migration shapes above: a `flatMap`/`filter` alias, `(item, index)`, stable and positional keys, sibling/deep same-file and relative component lists, three nested conditions, and keyed-row state/effect/ref lifecycles. It emits 13,898 B HTML and 39,387 B raw/14,655 B gzip JavaScript across 11 files (16,514 B total file-by-file gzip at level 9). Seven artifact-clean builds after one warm-up measured 515.185, 516.919, 486.594, 517.121, 436.251, 454.066, and 444.403 ms, with a 486.594 ms median. Seven fresh Chrome profiles measured 2.2 ms filter update, 1.2 ms flatMap reorder, 0.6 ms keyed-row state/effect rerun, 3.5 ms ref focus/read, 1.0 ms nested-condition re-entry, 0.8 ms removal cleanup, 2.1 ms re-add/reset, and 0.8/0.6/0.5 ms sibling-list update/add/reorder.
451
+
442
452
  ## Effects
443
453
 
444
454
  Browser-only initial work uses the familiar empty-dependency effect shape:
@@ -631,6 +641,10 @@ Supported:
631
641
  - Conditional child `&&` and ternary DOM patches
632
642
  - Top-level and block-scoped JSX locals, terminal early returns, and exhaustive JSX assignment
633
643
  - Direct keyed local-state lists
644
+ - Analyzable `filter`/direct-property `flatMap`/`Array.from` collection pipelines with item or positional keys
645
+ - Multiple sibling and recursively deep direct-property child lists
646
+ - Recursive same-file/relative keyed-row specialization with nested conditions and latest-item handlers
647
+ - Keyed-row multiple serializable state slots, effects, and `null`-initialized object refs
634
648
  - Page-exported shared layouts with layout/route state lifetimes
635
649
  - Opt-in exact/runtime-route navigation with complete-document prefetch and native fallback
636
650
  - Layout- and route-lifetime effect mounts in navigation groups
@@ -639,8 +653,10 @@ Supported:
639
653
  Not implemented yet:
640
654
 
641
655
  - Reusable keyed-list aliases
656
+ - Arbitrary, imported, mutating, or asynchronous collection callbacks
657
+ - Lazy/dynamic keyed-row state initializers and callback refs
642
658
  - Server actions and request-time SSR
643
- - React package islands
659
+ - React package/runtime/ecosystem compatibility or React islands
644
660
  - HMR and framework DevTools
645
661
 
646
662
  ## Benchmarks
@@ -752,21 +768,21 @@ Same content and CSS across every fixture:
752
768
 
753
769
  ### 1,000-item Keyed List
754
770
 
755
- The list starts with 1,000 keyed items, then updates every label, reverses the order, removes odd IDs, and adds 500 items. Browser timings are medians from seven fresh headless Chrome runs, measured when a DOM observer sees each expected result rather than at the next animation frame.
771
+ The list starts with 1,000 keyed items, then updates every label, reverses the order, removes odd IDs, and adds 500 items. Kudzu, Next, React, Vue, and Svelte browser timings are medians from 31 rotating fresh headless Chrome profiles, measured when a DOM observer sees each expected result rather than at the next animation frame. The Astro and Qwik rows retain their earlier seven-profile measurements.
756
772
 
757
773
  | Framework | Initial content | Initial JS gzip | Total output | Build | Update | Reverse | Remove | Add | Operations total |
758
774
  |---|---:|---:|---:|---:|---:|---:|---:|---:|---:|
759
- | Astro | Yes | **324 B** | **43.6 KB** | 834 ms | **4.3 ms** | **3.8 ms** | **1.3 ms** | **3.1 ms** | **12.5 ms** |
760
- | Kudzu | Yes | 5.1 KB | 60.3 KB | **438 ms** | 7.5 ms | 7.0 ms | 1.8 ms | 6.9 ms | 23.2 ms |
761
- | Next.js | Yes | 182.2 KB | 695.2 KB | 2983 ms | 7.0 ms | 12.0 ms | 3.9 ms | 6.7 ms | 29.6 ms |
762
- | React CSR | No | 59.3 KB | 189.4 KB | 1020 ms | 9.5 ms | 11.7 ms | 3.8 ms | 5.3 ms | 30.3 ms |
763
- | Vue CSR | No | 24.3 KB | 61.3 KB | 773 ms | 11.4 ms | 9.5 ms | 4.1 ms | 6.6 ms | 31.6 ms |
764
- | Svelte CSR | No | 12.9 KB | 33.1 KB | 828 ms | 5.8 ms | 38.9 ms | 4.0 ms | 5.9 ms | 54.6 ms |
765
- | Qwik CSR | No | 22.2 KB | 64.1 KB | 594 ms | 9.1 ms | 22.2 ms | 30.8 ms | 19.0 ms | 81.1 ms |
775
+ | Astro | Yes | **324 B** | **43.6 KB** | 920 ms | **4.4 ms** | **4.1 ms** | **1.5 ms** | **3.6 ms** | **13.6 ms** |
776
+ | Kudzu | Yes | 6.7 KB | 65.9 KB | **380 ms** | **5.7 ms** | **7.3 ms** | **2.8 ms** | **5.6 ms** | **21.4 ms** |
777
+ | Next.js | Yes | 182.2 KB | 695.2 KB | 3142 ms | 8.0 ms | 13.0 ms | 4.4 ms | 7.4 ms | 32.8 ms |
778
+ | React CSR | No | 59.3 KB | 189.4 KB | 1074 ms | 9.7 ms | 12.6 ms | 4.3 ms | 5.9 ms | 32.5 ms |
779
+ | Vue CSR | No | 24.3 KB | 61.3 KB | 791 ms | 11.0 ms | 9.7 ms | 4.4 ms | 6.7 ms | 31.8 ms |
780
+ | Svelte CSR | No | 12.9 KB | 33.1 KB | 910 ms | 6.0 ms | 42.3 ms | 4.5 ms | 6.2 ms | 59.0 ms |
781
+ | Qwik CSR | No | 22.2 KB | 64.1 KB | 634 ms | 10.6 ms | 25.6 ms | 36.5 ms | 22.1 ms | 94.8 ms |
766
782
 
767
- An intrinsic-root versus projected-prop row-component A/B build produced byte-for-byte identical `dist` output: 5,175 B JS gzip and 61,731 B total. Seven interleaved clean builds measured 467 ms and 455 ms. Browser operation medians totaled 23.7 ms and 23.9 ms respectively; because the deployed HTML and JavaScript are identical, the 0.2 ms difference is measurement variance rather than component runtime overhead.
783
+ An intrinsic-root versus projected-prop row-component A/B build produced byte-for-byte identical `dist` output: 6,817 B JS gzip and 67,495 B total. Seven rotating clean builds measured 380 ms and 384 ms. In 31 paired fresh-profile rounds, no browser operation differed significantly; component specialization adds no deployed runtime overhead.
768
784
 
769
- Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu's keyed-list operations total 23.2 ms, 10.7 ms behind the hand-authored Astro baseline and 7.1 ms ahead of React across all four operations.
785
+ Astro is the hand-authored native DOM baseline in the interactive fixtures. React, Vue, Svelte, and Qwik used client-rendered fixtures, while Kudzu and Astro emitted initial HTML; Qwik therefore did not exercise its SSR resumability advantage. Kudzu has the lowest median for every operation among the 31-profile framework targets, and every exact paired sign test is significant.
770
786
 
771
787
  ### 1,000-item Keyed Effect
772
788
 
@@ -774,29 +790,29 @@ Each keyed row owns one effect depending on `item.name`. The measured actions re
774
790
 
775
791
  | Framework | Initial rows | Initial JS gzip | Total output | Build | Selected update | Unrelated update | Reverse |
776
792
  |---|---:|---:|---:|---:|---:|---:|---:|
777
- | Astro native | Yes | **381 B** | **90,734 B** | 1,022 ms | **0.4 ms** | **0.2 ms** | **5.5 ms** |
778
- | Kudzu | Yes | 7,070 B | 221,056 B | **437 ms** | 3.4 ms | 2.9 ms | 7.8 ms |
779
- | Vue CSR | No | 25,091 B | 63,368 B | 893 ms | 4.7 ms | **2.3 ms** | 10.1 ms |
780
- | Svelte CSR | No | 12,848 B | 33,222 B | 1,012 ms | 5.2 ms | 3.0 ms | 48.1 ms |
781
- | React CSR | No | 60,921 B | 194,301 B | 1,132 ms | 12.3 ms | 6.8 ms | 19.0 ms |
793
+ | Astro native | Yes | **381 B** | **90,734 B** | 998 ms | **0.4 ms** | **0.2 ms** | **5.7 ms** |
794
+ | Kudzu | Yes | 8,264 B | 225,248 B | **426 ms** | 3.6 ms | 2.4 ms | 8.8 ms |
795
+ | Vue CSR | No | 25,091 B | 63,368 B | 935 ms | 5.8 ms | 2.4 ms | 10.5 ms |
796
+ | Svelte CSR | No | 12,848 B | 33,222 B | 1,040 ms | 5.7 ms | 4.1 ms | 58.1 ms |
797
+ | React CSR | No | 60,921 B | 194,301 B | 1,198 ms | 9.8 ms | 5.9 ms | 16.8 ms |
782
798
 
783
- This is a post-initialization runtime microbenchmark, not an architecture-equivalent loading comparison. Kudzu and Astro emit all 1,000 rows in HTML while React, Vue, and Svelte use empty CSR shells, so their JavaScript, output, and build columns are observations rather than framework-size or startup claims. Once every target has 1,000 rows and effects ready, targeted changed-root notification reduces Kudzu's selected update from 6.2 to 3.4 ms, versus Vue at 4.7 ms, Svelte at 5.2 ms, and React at 12.3 ms. It adds 126 B gzip to Kudzu's initial graph. List reconciliation remains O(n), which dominates unrelated-field updates; Vue measures 2.3 ms there versus Kudzu's 2.9 ms. Astro is the hand-written direct-DOM lower bound.
799
+ This is a post-initialization runtime microbenchmark, not an architecture-equivalent loading comparison. Kudzu and Astro emit all 1,000 rows in HTML while React, Vue, and Svelte use empty CSR shells, so their JavaScript, output, and build columns are observations rather than framework-size or startup claims. Once every target has 1,000 rows and effects ready, Kudzu's targeted changed-root path measures 3.6 ms versus Vue at 5.8 ms, Svelte at 5.7 ms, and React at 9.8 ms. List reconciliation remains O(n); Kudzu and Vue both measure 2.4 ms for the unrelated detail update. Astro is the hand-written direct-DOM lower bound.
784
800
 
785
801
  ### 1,000-item Keyed Row State
786
802
 
787
- Row 500 enters local edit state, the list reverses while preserving that row and its input DOM identity, then the row is removed and the same key is re-added with fresh non-editing state. Every target passed seven fresh-profile correctness runs; timings start at click and stop only after row order, unique IDs, labels, local state, and DOM identity match.
803
+ Row 500 enters local edit state, the list reverses while preserving that row and its input DOM identity, then the row is removed and the same key is re-added with fresh non-editing state. Framework browser timings use 31 rotating fresh profiles; the native baseline retains its latest seven-profile run. Timings start at click and stop only after row order, unique IDs, labels, local state, and DOM identity match.
788
804
 
789
805
  | Framework | Initial rows | Initial JS gzip | Total output | Build | Edit | Reverse | Remove | Re-add |
790
806
  |---|---:|---:|---:|---:|---:|---:|---:|---:|
791
- | Astro native | Yes | **373 B** | **83.7 KB** | 926 ms | **1.6 ms** | **5.9 ms** | **1.2 ms** | **1.4 ms** |
792
- | Kudzu | Yes | 8.5 KB | 570.7 KB | **449 ms** | 2.7 ms | 10.1 ms | 3.0 ms | 3.1 ms |
793
- | Vue CSR | No | 24.4 KB | 61.6 KB | 820 ms | 2.8 ms | 11.7 ms | 4.0 ms | 4.2 ms |
794
- | Svelte CSR | No | 13.1 KB | 33.8 KB | 918 ms | 2.6 ms | 47.9 ms | 4.5 ms | 5.4 ms |
795
- | React CSR | No | 59.4 KB | 189.5 KB | 1,074 ms | 5.9 ms | 26.4 ms | 9.0 ms | 6.5 ms |
807
+ | Astro native | Yes | **373 B** | **83.7 KB** | 903 ms | **1.6 ms** | **5.7 ms** | **1.2 ms** | **1.3 ms** |
808
+ | Kudzu | Yes | 9.2 KB | 572.9 KB | **434 ms** | **2.7 ms** | 10.8 ms | 3.0 ms | 3.7 ms |
809
+ | Vue CSR | No | 24.4 KB | 61.6 KB | 793 ms | 3.0 ms | 12.1 ms | 4.2 ms | 4.1 ms |
810
+ | Svelte CSR | No | 13.1 KB | 33.8 KB | 911 ms | 2.8 ms | 50.4 ms | 4.6 ms | 5.9 ms |
811
+ | React CSR | No | 59.4 KB | 189.5 KB | 1,054 ms | 6.2 ms | 25.8 ms | 9.1 ms | 6.7 ms |
796
812
 
797
- Kudzu builds fastest and beats React, Vue, and Svelte on all four operations. Pure reducer identity fast paths skip unchanged-item validation for reorder, one removal, and append while preserving direct keyed DOM identity. Astro remains the hand-written native lower bound. Kudzu's 570.7 KB output includes complete initial HTML plus per-row direct-patch descriptors; React, Vue, and Svelte ship CSR shells, so deploy size and loading architecture are not equivalent comparisons.
813
+ Kudzu builds fastest and has the lowest framework median for every operation. The 0.1 ms displayed edit lead over Svelte is not statistically significant (p = 0.572); every other framework comparison is significant. Stable identity fast paths preserve direct keyed DOM identity across reorder, removal, and append. Astro remains the hand-written native lower bound. Kudzu's 572.9 KB output includes complete initial HTML plus per-row direct-patch descriptors; React, Vue, and Svelte ship CSR shells, so deploy size and loading architecture are not equivalent comparisons.
798
814
 
799
- The general benchmark snapshot was collected on July 22, 2026, the keyed-effect comparison on July 27, and keyed-row-state on July 28 with Node 24.14.0 on an Intel i5-9500. These results compare the selected one-page fixtures, not ecosystem maturity, browser interaction speed beyond the listed operations, or each framework's full rendering options. Build times vary with machine load and filesystem cache.
815
+ The general benchmark snapshot was collected on July 22, 2026, the keyed-effect comparison on July 27, and the 31-profile keyed-list and keyed-row-state comparisons on July 30 with Node 24.14.0 on an Intel i5-9500. These results compare the selected one-page fixtures, not ecosystem maturity, browser interaction speed beyond the listed operations, or each framework's full rendering options. Build times vary with machine load and filesystem cache.
800
816
 
801
817
  ## Development
802
818
 
@@ -1,5 +1,7 @@
1
1
  # Framework Internals
2
2
 
3
+ Kudzu specializes ordinary common React-shaped TSX so migrations need minimal source restructuring. Declarative components, collection pipelines, conditions, hooks, and handlers should be lowered at build time rather than replaced with application-owned imperative DOM code. This principle applies across migrations and is not Stay-specific; it does not imply a React package, VDOM, hydration, or ecosystem runtime.
4
+
3
5
  - `build.mjs`: TSX compilation, static, `getStaticPaths`, and runtime-fallback routes, base paths, CSS collection, post-build hooks, behavior extraction, static HTML output, and the development server.
4
6
  - `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
5
7
  - `jsx-runtime.mjs`: automatic JSX runtime used by TypeScript.
@@ -23,9 +25,11 @@ Page `metadata` can emit description, canonical, favicon, manifest, Open Graph,
23
25
 
24
26
  Inline SVG rendering normalizes an explicit set of common React presentation aliases before static serialization and binding descriptor creation. Reactive aliases therefore use the existing generic `setAttribute` path; static SVG adds no JavaScript and reactive SVG adds no SVG-specific runtime.
25
27
 
26
- Same-file and relative-imported components receiving a direct local-state array prop are specialized to intrinsic JSX before keyed-list analysis, so their component function is not retained in the browser. Handler modules are emitted only when a rendered descriptor references them, preventing specialized imported components from adding dead browser assets. Direct JSON-safe primitive keyed-item dependencies subscribe each row record to its owning list commit and compare selected fields after `list-runtime.js` synchronously refreshes the row marker. Only changed rows rerun with the complete latest item; reorder compares equal, unrelated fields do nothing, and key changes remain remove plus mount. Builds without item dependencies emit no item reader or list-state subscription code.
28
+ Same-file and relative-imported component chains receiving a direct local-state array or keyed item are recursively specialized to intrinsic JSX before keyed-list analysis, so their component functions are not retained in the browser. Rows may own multiple direct-property child maps recursively, nested conditions, latest-item handlers, multiple directly serializable state slots, effects, and `null`-initialized object refs. Structural list sites and ancestor key paths scope hooks across updates and reorder and release them on removal. Handler modules are emitted only when a rendered descriptor references them. Direct JSON-safe primitive keyed-item dependencies subscribe each row record to its owning list commit and compare selected fields after `list-runtime.js` synchronously refreshes the row marker. Only changed rows rerun with the complete latest item; reorder compares equal and unrelated fields do nothing. Builds without item dependencies emit no item reader or list-state subscription code.
29
+
30
+ Rendered collection selectors compile one-use aliases and inline `(item)` or `(item, index)` pipelines over local array state. Supported selectors are pure `filter`, direct-property `flatMap`, and `Array.from` before a final keyed `map`; field keys retain item identity while `key={index}` retains positional identity. Arbitrary callbacks, mutation, asynchronous selectors, imported callback functions, prototype-sensitive reads, lazy/dynamic row state initializers, non-`null` or callback refs, and recursive component cycles fail during compilation.
27
31
 
28
- The reduced `useReducer` form reuses ordinary state slots and React's pure reducer contract. A direct dispatch in a compiled handler becomes a functional `set` whose reducer is bundled from a relative TypeScript module into that handler graph. Pure reducer-owned keyed lists reuse unchanged item identities for reorder, one removal, and append fast paths; ordinary `useState` lists retain full validation. One direct dispatch prop into a same-file or relative-imported synchronous component, including a direct keyed row, is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. A keyed-row handler reads the latest item through the existing list scope. It may declare one top-level primitive-literal `useState`; compiler-generated state IDs combine its hook slot and list key, preserving state across updates and reorder while row unmount deletes it. Existing event, binding, and conditional capabilities consume those IDs. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Missing primitive literal defaults in these reducer specializations are substituted at the same call site. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
32
+ The reduced `useReducer` form reuses ordinary state slots and React's pure reducer contract. A direct dispatch in a compiled handler becomes a functional `set` whose reducer is bundled from a relative TypeScript module into that handler graph. Pure reducer-owned keyed lists reuse unchanged item identities for reorder, one removal, and append fast paths; ordinary `useState` lists retain full validation. One direct dispatch prop into a same-file or relative-imported synchronous component, including a direct keyed row, is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. A reducer row reads the latest item through the existing list scope and uses the same multiple serializable state, effect, condition, and object-ref specialization as other keyed rows. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Missing primitive literal defaults in these reducer specializations are substituted at the same call site. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
29
33
 
30
34
  `kudzu.config` may opt one emitted shared-layout group into same-document navigation with legacy `navigation: { routes: ["/product", "/items/[id]"] }`, or multiple groups with `navigation: { groups: [{ routes: [...] }, { routes: [...] }] }`. The forms are mutually exclusive. Identities are globally unique emitted exact paths or `runtimeParams` patterns; each group uses one page-exported layout function identity. Runtime records securely match concrete pathnames under `base`, and their cache-safe parameter initializer runs before route DOM/effects mount on every transition. Each group receives a deterministic route-hashed asset specialized to only its records, pattern decoder, and effect/parameter lifecycle needs. Cross-group and ungrouped anchors remain native and are not prefetched; overlapping path domains across groups fail the build. Route effect entries export cache-safe layout and route mount functions: layout effects, including conditional/keyed DOM-owned effects, persist for the group session; route effects receive a fresh owner registry after each route insertion; and non-persisted page disposal cleans route before layout. Direct primitive state, runtime parameter, and keyed-item property dependencies and cleanup are supported. Fragment payloads and coordinated View Transitions are not implemented.
31
35
 
@@ -33,4 +37,4 @@ The current matched commerce profile emits 35,355 deploy bytes and loads 7,334 B
33
37
 
34
38
  In matched state-only and item-property keyed-row builds, the minified route effect entry changes from 3,829 B raw/1,667 B gzip to 4,392 B raw/1,823 B gzip, the shared runtime from 1,291 B raw/671 B gzip to 1,503 B raw/751 B gzip, and the list runtime from 6,606 B raw/2,474 B gzip to 6,652 B raw/2,493 B gzip. The complete targeted-notification capability costs +821 B raw/+255 B gzip and remains absent from builds without item dependencies. Seven clean builds of the expanded three-route fixture measured 420-440 ms with a 430 ms median; this records current build cost rather than claiming a cross-version speed change.
35
39
 
36
- The matched 1,000-row cross-framework effect fixture measured Kudzu at 7,070 B initial JavaScript gzip, 437 ms build, 3.4 ms selected-row cleanup/update/setup, 2.9 ms unrelated-field update, and 7.8 ms reorder. React CSR measured 60,921 B, 1,132 ms, 12.3 ms, 6.8 ms, and 19.0 ms respectively; Vue measured 4.7, 2.3, and 10.1 ms for the browser operations, and Svelte measured 5.2, 3.0, and 48.1 ms. Browser operations begin only after all targets have 1,000 rows and effects ready. Kudzu emits those rows in HTML while the framework CSR fixtures begin from empty shells, so JavaScript, output, and build values are not architecture-equivalent comparisons. Targeted changed-root notification removed the extra O(n) effect-record scan and reduced Kudzu's selected update from 6.2 to 3.4 ms. List validation, serialization, and reconciliation remain O(n).
40
+ The matched 1,000-row cross-framework effect fixture measured Kudzu at 8,264 B initial JavaScript gzip, 426 ms build, 3.6 ms selected-row cleanup/update/setup, 2.4 ms unrelated-field update, and 8.8 ms reorder. React CSR measured 60,921 B, 1,198 ms, 9.8 ms, 5.9 ms, and 16.8 ms respectively; Vue measured 5.8, 2.4, and 10.5 ms for the browser operations, and Svelte measured 5.7, 4.1, and 58.1 ms. Browser operations begin only after all targets have 1,000 rows and effects ready. Kudzu emits those rows in HTML while the framework CSR fixtures begin from empty shells, so JavaScript, output, and build values are not architecture-equivalent comparisons. Targeted changed-root notification avoids an extra O(n) effect-record scan; list validation, serialization, and reconciliation remain O(n).
@@ -157,8 +157,10 @@ function updateCondition(condition) {
157
157
  condition.current = next
158
158
  if (condition.mount) for (const node of nodes) mountDom(node)
159
159
  const select = condition.start.closest("select[data-k-bind-value]")
160
- for (const binding of new Set((bindingRegistrations.get(select) ?? []).map(([, entry]) => entry))) {
161
- patchBinding(binding.node, binding.target, binding.read())
160
+ if (select) {
161
+ for (const binding of new Set((bindingRegistrations.get(select) ?? []).map(([, entry]) => entry))) {
162
+ patchBinding(binding.node, binding.target, binding.read())
163
+ }
162
164
  }
163
165
  }
164
166