@kudzujs/core 0.7.8 → 0.7.10

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
@@ -1,187 +1,116 @@
1
- # Goal A: Static Business Applications
1
+ # Goal A: Static Application Lifecycles And Opt-In Navigation
2
2
 
3
- Goal A makes Kudzu sufficient for a conventional business application, starting with a complete commerce journey and leaving a clean path to realtime dashboards. It must preserve Kudzu's static HTML, zero-JavaScript static routes, direct DOM updates, and lack of a browser component tree.
3
+ Goal A is a completed compiler-capability milestone for general React-to-static migration. It proves that ordinary React-shaped applications can retain shared layouts, route lifetimes, asynchronous workflows, and optional same-document navigation while every route remains a complete static document.
4
4
 
5
- This is the implementation contract. `MIGRATION_ROADMAP.md` remains the source of truth for compiler invariants and the fixture-first workflow.
5
+ The six-route commerce journey is one executable validation fixture. It is not Kudzu's product scope, a commerce API, or the priority model for future development. [`MIGRATION_ROADMAP.md`](./MIGRATION_ROADMAP.md) is the source of truth for product direction and future fixture-driven work.
6
6
 
7
- The benchmark runner, framework fixtures, generated artifacts, and raw arrays are retained as local validation material and are intentionally excluded from the repository and npm package. Published numbers below are a dated product record rather than a reproducible public benchmark bundle. The matched versions were Kudzu 0.5.10, React 19.2.8 with Vite, Next.js 16.2.11, Nuxt 4.5.0 with Vue 3.5.40, and SvelteKit 2.70.1 with Svelte 5.56.7.
7
+ ## Status
8
8
 
9
- ## Implementation Status
9
+ Goal A is complete for explicitly configured emitted-route groups with shared layouts. Implemented capabilities include:
10
10
 
11
- **Goal A is complete for one explicitly configured emitted-route group with one shared layout.** Complete standalone documents, exact and runtime-parameter navigation, native fallback, persistent layout state/effects, disposable route state/effects, optimistic workflows, desktop/mobile performance gates, and the dashboard expansion seam are covered. Multiple independent shared-layout groups and conditional/keyed DOM-owned effects within layout and route lifetimes were added post-Goal-A.
11
+ - complete standalone documents for exact and runtime-parameter routes;
12
+ - native navigation fallback and opt-in same-document navigation;
13
+ - persistent layout state and effects with disposable route state and effects;
14
+ - conditional and keyed DOM ownership within layout and route lifetimes;
15
+ - validated finite prefetch caching for eligible links;
16
+ - optimistic success, rejection, rollback, and stale-write suppression;
17
+ - multiple independent shared-layout groups with overlap diagnostics.
12
18
 
13
- - **Phase 1 complete**: a local six-route commerce fixture, locked React/Next/Nuxt/SvelteKit comparisons, and a reproducible artifact/build/Chrome runner validate the implementation.
14
- - **Phase 2 complete**: effects inside conditional ranges and supported keyed row components mount with their DOM owner, unsubscribe and clean up on removal, and remount without affecting effect-free output.
15
- - **Phase 3 complete**: page-exported layouts render complete documents with compiler-owned route boundaries, collision-free layout/route IDs, and state/effect ownership metadata. Effects remain document effects and client navigation is unchanged.
16
- - **Phase 4 route/layout effects implemented**: layout effects mount once per document session, route effects remount after awaited route cleanup, and primitive dependency subscriptions exist only while their lifetime is mounted. Conditional/keyed effects use per-lifetime owner registries; route registries are fresh on cached revisits. Direct primitive keyed-item properties rerun only changed rows; reorder does not rerun and key changes remount. Disposed effect setters and queued commits are inactive.
17
- - **Phase 4 document prefetch implemented**: visible, near-visible, hovered, or focused eligible group anchors prefetch and validate complete documents without importing target capabilities. The finite in-memory full-URL cache removes the measured product-cart HTML RTT while preserving retry and native fallback.
18
- - **Phase 5 complete**: matched async cart success/rejection flows prove immediate optimistic updates, duplicate prevention, accessible errors, rollback, route-local reset, and stale-write suppression without new framework APIs.
19
- - **Phase 6 expansion probe complete**: one layout-owned mock `EventTarget` stream and one route-owned imperative chart stub use existing effects and a relative TypeScript helper across repeated navigation, with exact listener and disposal assertions. This proves only the compatibility seam; Kudzu does not provide telemetry or chart support.
20
- - **Desktop and mobile gates complete**: both profiles use seven rotating fresh Chrome runs per target and record zero cold/warm CLS.
19
+ Future migration features do not extend Goal A as a product vertical. They return to the migration roadmap and begin with a failing conventional React fixture.
21
20
 
22
- The capability-local prefetch/cache increased the commerce navigation asset from 1,887 B to 2,306 B gzip (+419 B). Routes outside the configured group remain unchanged.
21
+ ## Capability Contract
23
22
 
24
- In the focused effect-enabled navigation fixture, mount support adds 171 B gzip to the same-route navigation asset and 37 B gzip to the shared runtime. The active-context guard adds 18 B gzip to `kudzu-effect.js` (257 B total); cache-safe route entries are 1,059-1,116 B gzip. The effect-free commerce specialization remains byte-for-byte unchanged.
25
-
26
- The post-Goal-A runtime-pattern matcher increases the exact-only commerce navigation asset from 2,306 B to 2,461 B gzip. The mixed exact/runtime navigation fixture emits a 3,105 B gzip navigation asset and a 703 B gzip cache-safe parameter initializer.
27
-
28
- The post-Goal-A multiple-group fixture emits a 7,448 B raw / 3,099 B gzip (`gzip -9`) mixed runtime/effect group asset, including one native exclusion for an overlapping ungrouped exact route, and a separately specialized 5,681 B raw / 2,448 B gzip exact effect-free group asset. These measurements do not revise the historical Goal A benchmark.
29
-
30
- In a matched one-effect navigation build with the same conditional capability, moving the effect from the route body into the conditional owner changes the route effect entry from 2,181 B raw / 1,036 B gzip to 4,135 B raw / 1,807 B gzip (`+1,954 B` raw / `+771 B` gzip). Owner-hook unsubscription changes the shared runtime from 1,347 B raw / 718 B gzip to 1,459 B raw / 732 B gzip (`+112 B` raw / `+14 B` gzip). Top-level-only and effect-free navigation builds retain their smaller generators.
31
-
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
-
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
-
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
-
38
- The Phase 6 chart probe's complete initial module graph is 11,902 B raw / 5,331 B gzip. It adds no framework API or package and does not change the commerce benchmark fixture.
39
-
40
- The app-mode Kudzu fixture emits 34,879 deploy bytes. Its product route loads 15,800 raw / 7,215 gzip bytes of initial JavaScript, including the 2,306 B gzip navigation capability. Routes outside a configured application group retain the static zero-JavaScript and byte-for-byte gates.
41
-
42
- The latest matched run used seven rotating production builds and seven rotating fresh Chrome profiles per target at 4x CPU slowdown, 100 ms latency, and 200 KiB/s download. Raw local results retain the full arrays, artifact groups, environment, and limitations.
43
-
44
- | Target | Product JS gzip | Cold transfer | Cold LCP | Warm LCP | Cold task | Cold heap | Interaction | Product-cart |
45
- |---|---:|---:|---:|---:|---:|---:|---:|---:|
46
- | Kudzu | 7,215 B | 34,809 B | 324 ms | 140 ms | 103.3 ms | 648,844 B | 3.7 ms | 5.7 ms |
47
- | React + Vite | 61,464 B | 202,842 B | 324 ms | 232 ms | 160.0 ms | 1,062,520 B | 10.5 ms | 8.3 ms |
48
- | Next.js | 190,090 B | 546,581 B | 316 ms | 156 ms | 357.7 ms | 2,157,020 B | 11.4 ms | 26.9 ms |
49
- | Nuxt | 67,620 B | 195,953 B | 320 ms | 204 ms | 205.1 ms | 1,721,348 B | 3.9 ms | 33.0 ms |
50
- | SvelteKit | 32,473 B | 90,929 B | 340 ms | 172 ms | 125.8 ms | 999,496 B | 4.7 ms | 20.3 ms |
51
-
52
- The navigation capability initially measured 128.7 ms because it paid one HTML round trip. Validated near-viewport document prefetch reduced the final seven-run median to 5.7 ms while Kudzu still transferred 62% less than SvelteKit and 83% less than React on the cold product load. A progressive-HTML grid shift found by the benchmark was fixed in the matched CSS; 30 focused Kudzu profiles and the final desktop/mobile cross-framework runs then recorded zero cold and warm CLS.
53
-
54
- The original final table measured 460.8 ms for Kudzu and 429.5 ms for React, and a focused follow-up confirmed a 6-7% loss primarily from TypeScript ESM/compiler startup. A post-gate startup fix now enables Node's native module compile cache before lazily loading the compiler. The first empty-cache build measured 541.4 ms against the previous 547.6 ms path; after one warm-up, 31 interleaved artifact-clean builds measured Kudzu at 425.8 ms and React at 483.4 ms, making Kudzu 11.9% faster in that run. Cache-disabled and cached builds emit byte-for-byte identical output.
55
-
56
- The mobile profile uses a 390x844 viewport, 6x CPU slowdown, 150 ms latency, and 150 KiB/s download. Kudzu measured 420 ms cold LCP, 220 ms warm LCP, 5.6 ms interaction, 8.7 ms product-cart navigation, 4.1 ms optimistic rejection, and 158 ms rollback/error.
57
-
58
- ## Product Target
59
-
60
- A user can build and deploy this flow without React, a VDOM, hydration, or a request server:
23
+ A migrated application may use familiar function components, props, children, JSX, hooks, conditions, collections, and handlers while Kudzu emits:
61
24
 
62
25
  ```text
63
- home -> category -> product -> cart -> checkout -> account
26
+ complete static document
27
+ -> optional persistent layout range
28
+ -> replaceable route range
29
+ -> only the route-specific ESM capabilities used
64
30
  ```
65
31
 
66
- The reference fixture must cover:
32
+ The capability must preserve:
67
33
 
68
- - complete indexable HTML for public pages;
34
+ - complete indexable HTML for every route;
69
35
  - direct entry, reload, back, forward, query strings, and base paths;
70
- - product filters stored in the URL;
71
- - product variants and quantity as route state;
72
- - cart and session information shared by an application layout;
73
- - async loading, empty, error, retry, and cancellation behavior;
74
- - optimistic cart updates with rollback after a rejected request;
75
- - accessible focus, title, scroll, and announcement behavior after navigation;
36
+ - URL, route, and shared-layout state with explicit lifetimes;
37
+ - async loading, empty, error, retry, cancellation, and stale-result handling;
38
+ - optimistic updates, rejection, rollback, and accessible status output;
39
+ - focus, title, scroll, and announcement behavior after enhanced navigation;
76
40
  - ordinary document navigation when JavaScript is absent, late, or fails.
77
41
 
78
- The fixture is a generic executable requirement, not a reusable commerce package. Kudzu should add only compiler features that the reduced fixture proves necessary.
42
+ ## Invariants
79
43
 
80
- ## Runtime Model
44
+ - Native `<a>` navigation is the default.
45
+ - Navigation enhancement is explicit per emitted route group.
46
+ - Every enhanced route also exists as a complete document.
47
+ - Static routes emit zero client JavaScript.
48
+ - Interactive routes emit only the capabilities they use.
49
+ - React, a VDOM, hydration, retained browser components, and browser component rerenders remain forbidden.
50
+ - External, download, modified-click, reload, malformed, unsupported, and ungrouped links keep native behavior.
51
+ - A failed or superseded transition cannot commit stale route state.
52
+ - No separate fragment protocol, router API, cache, or scheduler is added without a measured fixture requirement.
81
53
 
82
- Kudzu continues to emit a complete document for every route. Native `<a>` navigation remains the default. An explicitly configured application route group may enhance eligible same-origin links after the initial document has loaded.
54
+ ## Ownership Model
83
55
 
84
- ```text
85
- Static document
86
- -> optional persistent layout
87
- -> replaceable route range
88
- -> capability-specific route ESM
89
- ```
90
-
91
- The implementation must use the smallest measured mechanism. A separate fragment format, prefetcher, cache, or router API is not required unless the fixture and browser measurements show that parsing the existing document output is insufficient.
92
-
93
- Navigation enhancement must not become a general SPA runtime:
56
+ Goal A adds explicit ownership, not a state library or application runtime.
94
57
 
95
- - no VDOM, hydration, retained component tree, or component rerenderer;
96
- - no global router on routes outside an opted-in application group;
97
- - no interception of external, download, modified-click, reload, or explicitly native links;
98
- - no client-only route as the sole representation of indexable content;
99
- - no duplicated source component execution in the browser.
100
-
101
- ## Ownership And Cleanup
102
-
103
- Goal A needs explicit ownership, not a new state library:
104
-
105
- | Owner | Lifetime | Examples |
58
+ | Owner | Lifetime | General examples |
106
59
  |---|---|---|
107
- | Document | Full document | global listeners and document exit cleanup |
108
- | Layout | Enhanced navigation session | user summary, cart count, persistent connection |
109
- | Route | Current route range | filters, selected variant, request, subscription |
110
- | DOM range | Conditional or keyed item | listener, effect, imperative child resource |
111
-
112
- Removing an owner must dispose its effects, listeners, pending work, and descendant registrations exactly once. A failed or superseded navigation must not commit stale route state. Full document navigation remains the recovery path.
60
+ | Document | Full document | global listeners and document-exit cleanup |
61
+ | Layout | Enhanced navigation session | user summary, navigation state, shared resource |
62
+ | Route | Current route range | filters, requests, subscriptions, pending work |
63
+ | DOM range | Conditional or keyed item | listener, effect, ref, imperative child resource |
113
64
 
114
- High-frequency data is deliberately not a Goal A state feature. A later dashboard can keep WebSocket parsing, telemetry buffers, charts, maps, and editors in Workers or imperative browser modules while Kudzu owns their route or layout lifecycle.
65
+ Removing an owner disposes its effects, listeners, pending work, and descendant registrations exactly once. A non-persisted document exit cleans route ownership before layout ownership. A BFCache-persisted document retains live ownership for restoration.
115
66
 
116
- ## Delivery Order
67
+ ## Validation Fixture
117
68
 
118
- Each phase starts with one failing fixture and ends with correctness, browser, size, and build measurements.
69
+ The reduced commerce journey remains a useful integration fixture because it exercises the neutral capability contract in one flow:
119
70
 
120
- 1. **Benchmark harness**: freeze the commerce journey, network profiles, framework versions, generated artifacts, and measurement scripts before optimizing Kudzu.
121
- 2. **Owned effects**: complete cleanup for conditional ranges and keyed items using the existing mount and unmount hooks. **Complete.**
122
- 3. **Layout and route scopes**: retain only declared layout state and dispose route-owned behavior on every completed transition. **Compiler ownership complete; transition behavior belongs to Phase 4.**
123
- 4. **Opt-in navigation**: support eligible links, history, aborts, stale responses, focus, scroll, metadata, and native fallback. **Complete for emitted exact/runtime-parameter routes and layout/route effects, including conditional/keyed DOM ownership.**
124
- 5. **Business workflows**: close only fixture-proven gaps in forms, async requests, optimistic updates, and diagnostics. **Complete for the matched cart success/rejection flow.**
125
- 6. **Expansion probe**: prove that one persistent mock stream and one imperative chart stub can mount, update, navigate, and dispose without adding a component runtime. **Compatibility probe complete; real telemetry and chart engines remain outside Goal A.**
126
-
127
- Do not begin a later phase while the current phase has an unexplained correctness or performance regression.
71
+ ```text
72
+ home -> category -> product -> cart -> checkout -> account
73
+ ```
128
74
 
129
- ## Performance Comparison
75
+ Its product filters prove URL state, variants prove route state, cart/session data proves layout state, rejected updates prove rollback, and product-to-cart transitions prove enhanced navigation. These nouns belong to the fixture only; Kudzu must not add commerce-specific APIs or architecture.
130
76
 
131
- The matched comparison set is:
77
+ Other applications should validate the same capabilities with domain-appropriate fixtures such as documentation, content, administration, forms, and dashboards.
132
78
 
133
- - Kudzu from the previous completed phase;
134
- - React with an equivalent production static deployment;
135
- - Next.js static export;
136
- - Vue/Nuxt static generation;
137
- - Svelte/SvelteKit static generation;
138
- - hand-written HTML and DOM JavaScript where it provides a useful lower bound.
79
+ ## Performance Protocol
139
80
 
140
- Every implementation must render the same initial content and provide the same tested behavior. Results that omit initial HTML, navigation semantics, error handling, or accessibility are reported separately and cannot be presented as direct wins.
81
+ Matched comparisons may include React/Vite, static exports from established frameworks, and hand-written HTML/DOM code as a lower bound. Every target must provide the same initial content, behavior, errors, navigation semantics, and accessibility before results are compared directly.
141
82
 
142
- Use identical content, CSS, local images, API payloads, cache state, browser version, CPU profile, and network profile. Record at least:
83
+ Record at least:
143
84
 
144
- - deploy artifact bytes and initial raw/gzip JavaScript;
145
- - HTML, CSS, image, and route payload transfer bytes;
85
+ - deploy bytes and initial raw/gzip JavaScript;
86
+ - HTML, CSS, media, and route transfer bytes;
146
87
  - clean build median;
147
- - cold and warm LCP, CLS, and INP;
148
- - hydration or startup main-thread work;
149
- - cold and warm route transition time;
150
- - cart update and rollback latency;
151
- - browser heap after initial load and repeated navigation;
152
- - duplicate listeners, unfinished requests, and lifecycle leaks.
153
-
154
- Production builds receive one warm-up and at least seven interleaved runs. Keep raw results and record the median; use additional runs when ranges overlap or a result controls an architectural decision.
155
-
156
- ## Performance Gates
157
-
158
- Goal A is not complete unless:
159
-
160
- - static routes still emit zero JavaScript;
161
- - routes outside an application group remain byte-for-byte unaffected by navigation support;
162
- - an application route ships only the layout and route capabilities it uses;
163
- - no hydration task or browser component tree is introduced;
164
- - repeated navigation leaves no growing listener, request, state, DOM, or heap ownership;
165
- - Kudzu has no unexplained material regression against its previous phase;
166
- - material losses against the fastest matched framework are profiled and either fixed or documented as an explicit product tradeoff;
167
- - performance claims include fixture limitations and do not compare unequal initial content as equivalent.
88
+ - cold and warm LCP, CLS, INP, startup work, and heap;
89
+ - state update, optimistic rejection, rollback, and route transition latency;
90
+ - repeated-navigation listener, request, state, DOM, and heap ownership.
168
91
 
169
- For browser timings, treat a repeatable median change above 5% as material. Deterministic byte growth is reviewed regardless of percentage. Measurement noise is not a reason to optimize: repeat the interleaved run first.
92
+ Production builds receive one warm-up and at least seven interleaved runs. Browser comparisons use rotating fresh profiles and additional runs when ranges overlap. A repeatable browser median change above 5% is material; deterministic byte growth is always reviewed.
170
93
 
171
- When a gate fails, stop feature work, reduce the cause, fix the shared path, rerun `npm run check`, `npm test`, and the affected benchmark, then continue. Do not hide regressions with prefetching, omitted content, or a weaker competitor fixture.
94
+ ## Regression Gates
172
95
 
173
- ## Completion Definition
96
+ - Static routes still emit zero JavaScript.
97
+ - Routes outside configured groups remain unaffected by navigation support.
98
+ - No hydration task or retained component tree is introduced.
99
+ - Repeated navigation leaves no growing ownership.
100
+ - Material regressions against Kudzu's previous path are explained and fixed or recorded as explicit tradeoffs.
101
+ - Competitor comparisons disclose unequal rendering architecture and fixture limitations.
102
+ - `npm run check`, `npm test`, and applicable browser checks pass.
174
103
 
175
- Goal A is complete when the commerce fixture passes direct-load and enhanced-navigation browser tests, every lifecycle owner cleans up correctly, static fallback behavior remains intact, and the benchmark gates above pass on both desktop and mobile profiles.
104
+ ## Historical Validation Record
176
105
 
177
- The final validation record must retain the generated artifacts, framework versions, commands, raw measurements, medians, known losses, and deliberate limits.
106
+ The completed commerce and lifecycle measurements remain dated evidence, not current product requirements. The latest retained records include complete desktop/mobile navigation runs, zero cold/warm CLS, exact cleanup counters, capability byte deltas, and matched framework comparisons. Release notes and repository history retain version-specific numbers; new measurements must record their source revision, fixture, browser, hardware, raw arrays, and limitations.
178
107
 
179
108
  ## Non-Goals
180
109
 
181
110
  - A default SPA router.
182
- - Arbitrary React application or package compatibility.
183
- - Runtime component registration or client component rendering.
184
- - Request-time SSR, server actions, or a hidden server.
185
- - A charting, mapping, telemetry, virtual-grid, or visual-editor engine.
186
- - A custom widget marketplace or plugin security model.
187
- - Winning every synthetic metric at the cost of correctness, accessibility, or deploy output.
111
+ - A commerce framework or reusable commerce package.
112
+ - Arbitrary React package compatibility.
113
+ - Runtime component registration, hydration, or client component rendering.
114
+ - Request-time SSR, server actions, or a hidden application server.
115
+ - Charting, mapping, telemetry, virtual-grid, or editor engines.
116
+ - Winning synthetic metrics by omitting equivalent content, behavior, accessibility, or fallback.
package/GOAL_B.md CHANGED
@@ -1,44 +1,28 @@
1
- # Goal B: Static Realtime Dashboards
1
+ # Goal B: Relative TypeScript Worker Capability Proof
2
2
 
3
- Goal B makes Kudzu sufficient for a ThingsBoard-shaped realtime device dashboard without turning Kudzu into a stream runtime, widget framework, or server platform. It preserves complete static documents, zero-JavaScript routes that use no browser capabilities, direct DOM ownership, native navigation fallback, and the absence of React, a VDOM, hydration, or a retained browser component tree.
3
+ Goal B is a completed cross-application compiler-capability proof for one browser-only pattern used by realtime React-shaped applications: a route-owned relative TypeScript module Worker created and cleaned up from an inline `useEffect`.
4
4
 
5
- `MIGRATION_ROADMAP.md` remains the source of truth for compiler invariants and fixture-first development. This document is the implementation contract for realtime dashboard work.
5
+ The realtime dashboard is a stress and conformance fixture. It is not a Kudzu product vertical, dashboard API, stream runtime, widget framework, server platform, or claim of ThingsBoard compatibility. [`MIGRATION_ROADMAP.md`](./MIGRATION_ROADMAP.md) remains the source of truth for future fixture-driven migration work.
6
6
 
7
- ## Product Target
7
+ ## Status
8
8
 
9
- A user can statically deploy this flow:
9
+ The exact relative TypeScript Worker capability is implemented and verified. Kudzu recognizes one statically analyzable constructor form, emits its validated module graph separately, rewrites the URL for the configured base, ties creation and termination to effect ownership, and adds no Worker bytes to routes that do not use it.
10
10
 
11
- ```text
12
- dashboard -> devices -> device detail -> alarms -> settings
13
- ```
11
+ Further Worker, transport, effect, or React compatibility work must begin with a reduced conventional React migration fixture that fails.
14
12
 
15
- The first vertical slice is deliberately smaller:
13
+ ## Capability Target
16
14
 
17
- ```text
18
- plain route <-> realtime device dashboard
19
- ```
20
-
21
- The dashboard receives a logical 1,000 telemetry samples per second, keeps a bounded history in a module Worker, downsamples it, and updates one imperative chart without routing samples through `useState()`.
22
-
23
- ## Runtime Model
15
+ The focused proof uses one Worker-enabled route, one plain companion route, and one unrelated static route:
24
16
 
25
17
  ```text
26
- Complete static dashboard shell
27
- -> route-specific effect ESM
28
- -> route-owned module Worker
29
- -> bounded telemetry buffer and downsampling
30
- -> batched imperative chart updates
18
+ plain route <-> Worker-enabled route
31
19
  ```
32
20
 
33
- Kudzu state is for low-frequency UI state such as selected device, time range, filters, tabs, connection status, alarm status, and widget configuration. High-frequency samples belong in a Worker or imperative browser module.
34
-
35
- The Worker is a capability, not a framework runtime. Routes that do not create one must not load its graph. Static routes must remain JavaScript-free.
21
+ The stress fixture generates a logical 1,000 samples per second, keeps bounded history in a module Worker, downsamples it, and updates one imperative chart. Telemetry and chart details validate Worker isolation, throughput, and ownership; they are not Kudzu product features or advice to rewrite ordinary declarative UI imperatively.
36
22
 
37
- ## Milestone 1: Relative TypeScript Workers
23
+ ## Supported Form
38
24
 
39
- Status: implemented and verified. The compiler recognizes only the exact inline-effect form below, emits its validated graph separately, rewrites the constructor to the base-aware same-origin asset, and leaves routes without this capability on their existing output paths.
40
-
41
- Support this exact shape inside a compiled inline `useEffect` callback:
25
+ The Worker must be constructed directly inside a compiled inline effect:
42
26
 
43
27
  ```tsx
44
28
  useEffect(() => {
@@ -47,13 +31,13 @@ useEffect(() => {
47
31
  { type: "module" },
48
32
  )
49
33
 
50
- const onMessage = (event: MessageEvent<ChartFrame>) => chart.render(event.data)
34
+ const onMessage = (event: MessageEvent<Frame>) => resource.render(event.data)
51
35
  worker.addEventListener("message", onMessage)
52
36
 
53
37
  return () => {
54
38
  worker.removeEventListener("message", onMessage)
55
39
  worker.terminate()
56
- chart.dispose()
40
+ resource.dispose()
57
41
  }
58
42
  }, [])
59
43
  ```
@@ -63,87 +47,67 @@ Compiler requirements:
63
47
  - accept only an unshadowed `Worker` with `new URL(relativeLiteral, import.meta.url)` and literal `{ type: "module" }`;
64
48
  - resolve one `.worker.ts` entry under project source and bundle its relative TypeScript graph separately from window code;
65
49
  - emit deterministic content-hashed ESM under `assets/workers` and rewrite the constructor to the base-aware emitted URL;
66
- - reject package imports, JSX, TypeScript import-equals declarations, dynamic imports, `require()`, paths outside source, malformed options, and unsupported Worker forms with source locations;
50
+ - reject package imports, JSX, import-equals declarations, dynamic imports, `require()`, paths outside source, malformed options, and unsupported Worker forms with source locations;
67
51
  - reject ordinary runtime imports or re-exports of `.worker.ts`; type-only imports may erase normally;
68
- - do not mark Worker files as navigation capability scripts or import them into the window;
69
- - create the Worker only when the owning effect mounts;
70
- - preserve byte-for-byte generated shared/list/effect paths for builds without relative TypeScript Workers where practical;
71
- - reject generated/public asset collisions instead of silently overwriting output.
52
+ - never mark Worker files as document capability scripts or import them into the window;
53
+ - create the Worker only when its owning effect mounts;
54
+ - preserve zero-cost exclusion for builds without relative TypeScript Workers;
55
+ - reject generated/public asset collisions.
72
56
 
73
- Worker construction inside imported helpers or imported keyed-row effects, `SharedWorker`, classic workers, inline Blob workers, and arbitrary dynamic Worker URLs are outside Milestone 1. Imported keyed rows must move Worker ownership to a directly compiled page or local component effect so lexical global analysis remains tied to the original source tree.
57
+ Worker construction in imported helpers or imported keyed-row effects, `SharedWorker`, classic workers, Blob workers, arbitrary dynamic URLs, and package runtime graphs remain outside this completed capability.
74
58
 
75
59
  ## Ownership
76
60
 
77
- | Owner | Lifetime | Dashboard responsibility |
61
+ | Owner | Lifetime | Capability responsibility |
78
62
  |---|---|---|
79
- | Document | Full document | authentication expiry and global diagnostics |
80
- | Layout | Enhanced navigation session | tenant session or shared transport |
81
- | Route | Current dashboard/device | Worker, telemetry subscription, request cancellation |
82
- | DOM range | Current widget | chart, gauge, map, table, animation frame |
83
- | Worker | Explicit owner cleanup | parsing, bounded buffering, aggregation, downsampling |
63
+ | Document | Full document | global diagnostics and document-exit cleanup |
64
+ | Layout | Enhanced navigation session | explicitly shared browser resource |
65
+ | Route | Current route range | Worker, subscription, request cancellation |
66
+ | DOM range | Connected conditional/keyed range | imperative resource and animation work |
67
+ | Worker | Explicit owner cleanup | parsing, bounded buffering, aggregation |
84
68
 
85
- Leaving a route must remove message listeners, stop chart work, terminate its Worker, and invalidate stale UI writes before another route mounts. BFCache-preserved documents must retain their live ownership until a real document exit.
69
+ On an enhanced same-document transition, route cleanup removes listeners, stops imperative work, terminates its Worker, and invalidates stale UI writes before the next route mounts. Non-persisted document exit performs cleanup; a BFCache-persisted document retains live ownership for restoration.
86
70
 
87
71
  ## Fixture Contract
88
72
 
89
- The first fixture must provide:
73
+ The capability fixture proves:
90
74
 
91
- - one complete realtime dashboard document and one complete plain document in an opt-in navigation group;
75
+ - one complete Worker-enabled document and one complete plain document in an opt-in navigation group;
92
76
  - one unrelated static route with zero JavaScript;
93
77
  - a relative TypeScript Worker importing at least one relative helper;
94
- - a fixed-capacity ring buffer with deterministic eviction;
95
- - logical 1,000 samples/second input in batches rather than a 1 ms browser timer;
96
- - bounded downsampled frames delivered at no more than display cadence;
97
- - one imperative canvas or DOM chart updated without sample-level Kudzu setters;
98
- - direct load, dashboard-to-plain navigation, back/forward, and repeated cached revisits;
99
- - exact counters for Worker starts, terminations, messages, renders, listeners, and stale post-cleanup work;
78
+ - bounded deterministic buffering and batched sample generation;
79
+ - bounded frames delivered no faster than display cadence;
80
+ - one imperative resource updated without sample-level Kudzu setters;
81
+ - direct load, enhanced navigation, back/forward, and repeated cached revisits;
82
+ - exact starts, terminations, messages, renders, listeners, disposal, and stale-write counters;
100
83
  - native document fallback when JavaScript or Worker creation fails.
101
84
 
102
- ## Acceptance Criteria
103
-
104
- - the Worker graph is absent from static and plain route HTML and is fetched only after the dashboard effect mounts;
105
- - two unchanged production builds emit identical Worker names and bytes;
106
- - changing Worker source changes its content hash;
107
- - base-prefixed deployment produces a valid same-origin Worker URL;
108
- - 30 dashboard/plain cycles create and terminate exactly 30 route Workers with no growing listener or chart ownership;
109
- - messages arriving after cleanup cannot update removed route DOM;
110
- - the ring buffer remains at its configured capacity under sustained input;
111
- - chart rendering is batched and sample ingestion does not call `useState()`;
112
- - routes without Worker capabilities remain byte-for-byte unaffected;
113
- - output raw/gzip cost, clean build time, sample throughput, render cadence, and lifecycle counters are recorded;
114
- - `npm run check`, `npm test`, package dry-run, and browser checks pass.
115
-
116
- Verified measurements for the focused `/dash` fixture: the minified Worker graph is `assets/workers/telemetry.worker-BVG2SA55.js`, 907 B raw and 477 B gzip. The dashboard window graph is 11,388 B raw and 5,148 B gzip across its shared runtime, effect runtime, navigation, route effect entry, and handler module; the Worker is not part of that graph. Seven clean minified builds measured 455.1, 459.9, 460.7, 463.5, 467.6, 472.6, and 475.2 ms, with a 463.5 ms median.
117
-
118
- The real-Worker browser check uses real wall time and requires sustained generation beyond 1,130 samples at 700-1,300 logical samples/second, an exact 128-sample ring bound, batches of 10, exactly 24 displayed points, multiple renders, and a render ceiling below 25 Hz. Delayed Worker ticks catch up in batches to the logical 1,000 samples/second clock; frames emit no more often than every 50 ms. The imperative chart performs a minimal canvas path draw. The navigation ownership check completed 30 dashboard/plain cycles with exactly 30 starts and 30 terminations, exactly 60 listener additions and removals across message and error listeners, zero retained listeners after every cleanup, disposed every old chart canvas, fresh ownership on back/forward and cached revisits, and no render from a removed message listener invoked after cleanup. Dashboard, plain, and static HTML contain no Worker asset URL; plain does not load the route effect graph, and static contains no script, capability marker, or state payload. A no-Worker equivalent emitted byte-identical shared runtime, effect runtime, navigation, and route effect entry files with no `assets/workers` directory. An unreachable imported-row effect referencing `unused.worker.ts` emitted no Worker asset. Two unchanged builds emitted identical Worker names and bytes, and a controlled downsample-source change changed the emitted hash.
85
+ ## Acceptance And Performance Gates
119
86
 
120
- ## Delivery Order
87
+ - The Worker graph is absent from static and plain route HTML and fetched only after its effect mounts.
88
+ - Two unchanged builds emit identical Worker names and bytes; source changes alter the content hash.
89
+ - Base-prefixed output constructs a valid same-origin Worker URL.
90
+ - Repeated route cycles create and terminate exactly one route Worker each with no growing ownership.
91
+ - Messages after cleanup cannot update removed route DOM.
92
+ - Worker memory remains bounded by declared capacity.
93
+ - High-frequency ingestion does not create one main-thread task or Kudzu state commit per sample.
94
+ - Imperative rendering is batched to its declared cadence.
95
+ - Builds without this capability remain unaffected.
96
+ - Raw/gzip output, build time, throughput, cadence, and lifecycle counters are recorded.
97
+ - `npm run check`, `npm test`, package dry-run, and applicable browser checks pass.
121
98
 
122
- 1. **Worker compiler capability**: exact syntax, graph bundling, hashing, base rewriting, diagnostics, and zero-cost exclusion.
123
- 2. **Capability conformance fixture**: mock telemetry Worker, bounded buffer, downsampling, imperative DOM ownership, and route cleanup.
99
+ ## Historical Validation Record
124
100
 
125
- Further work belongs to the React migration roadmap and starts from a reduced compatibility fixture that fails. Kudzu does not implement device, alarm, transport, or widget product features.
101
+ The focused fixture emitted a 907 B raw / 477 B gzip Worker graph and an 11,388 B raw / 5,148 B gzip window graph. Seven clean minified builds measured a 463.5 ms median. Thirty route cycles produced exactly 30 starts and terminations, balanced message/error listener registration, bounded 128-sample history, no retained chart ownership, no stale post-cleanup render, deterministic unchanged builds, and a changed hash after a controlled Worker source edit.
126
102
 
127
- ## Performance Gates
128
-
129
- - sustained 1,000 samples/second does not create one main-thread task or Kudzu state commit per sample;
130
- - Worker memory is bounded by declared buffer capacity;
131
- - chart updates are batched to at most one per display frame;
132
- - dashboard departure stops observable messages and renders before the next route mounts;
133
- - repeated navigation leaves no growing Worker, timer, listener, chart, state, DOM, or heap ownership;
134
- - Worker support adds no bytes to routes and builds that do not use it;
135
- - material losses are profiled and fixed or documented as explicit tradeoffs using matched initial content and behavior.
103
+ These numbers are dated conformance evidence. They do not define a dashboard product target.
136
104
 
137
105
  ## Non-Goals
138
106
 
139
- - implementing ThingsBoard's server, protocol, rule engine, database, or complete UI;
140
- - storing telemetry samples in Kudzu component state;
141
- - adding a general observable, scheduler, stream, state, or widget runtime;
142
- - retaining a browser component tree;
143
- - request-time SSR, Server Actions, or a hidden application server;
144
- - a plugin marketplace or arbitrary third-party React widgets;
145
- - claiming Worker isolation as a security sandbox.
146
-
147
- ## Completion Definition
148
-
149
- Goal B Milestone 1 is complete when the focused realtime fixture proves deterministic relative TypeScript Worker emission, bounded high-frequency processing, imperative chart updates, exact route ownership and cleanup across repeated navigation, native/static fallback, zero-cost exclusion, source diagnostics, and recorded production measurements.
107
+ - Device, alarm, tenant, transport, chart, map, or widget product features.
108
+ - A general stream, observable, scheduler, state, or plugin runtime.
109
+ - Storing high-frequency samples in Kudzu component state.
110
+ - Retaining a browser component tree.
111
+ - Request-time SSR, server actions, or a hidden server.
112
+ - Arbitrary third-party React widgets.
113
+ - Treating Worker isolation as a security sandbox.
package/README.md CHANGED
@@ -10,11 +10,11 @@ Kudzu is designed so ordinary common React-shaped TSX can migrate with minimal s
10
10
 
11
11
  > Experimental `0.7.x`: the compiler API and supported TSX surface may change.
12
12
 
13
- **0.7.8:** Static collection fast paths. Compiler-owned static filters reuse validated item metadata and detached row prototypes, making 1,000-row filtering match Vue and restoring filtered rows faster than React, Vue, and Svelte in the matched benchmark. See [release notes](./RELEASES.md#078---static-collection-fast-paths).
13
+ **0.7.10:** Component composition. Existing specialized collection wrappers and keyed rows now retain direct analyzable prop spreads and forwarded JSX children without adding a browser component runtime. See [release notes](./RELEASES.md#0710---component-composition).
14
14
 
15
15
  Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
16
16
 
17
- Development target: [Goal A static business applications](./GOAL_A.md)
17
+ Development direction: [React migration roadmap](./MIGRATION_ROADMAP.md)
18
18
 
19
19
  ## Install
20
20
 
@@ -465,7 +465,7 @@ Initial child rows remain complete HTML. Kudzu stores one child row prototype, i
465
465
 
466
466
  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.
467
467
 
468
- 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 or a supported static named import; 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>`.
468
+ 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 or a supported static named import; 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}`. Specialized wrappers and keyed rows accept forwarded JSX children and source-ordered prop spreads from inline object literals or one direct `const` object literal declared in the calling component. Specialized keyed row components accept missing destructured string, finite-number, boolean, or `null` props with literal defaults. 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, dynamic/computed prop spreads, rest props, non-primitive defaults, fragments, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
469
469
 
470
470
  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.
471
471
 
@@ -674,18 +674,21 @@ Supported:
674
674
  - Layout- and route-lifetime effect mounts in navigation groups
675
675
  - Conditional/keyed DOM-owned effects in navigation groups
676
676
 
677
- Not implemented yet:
677
+ Selected current migration limits:
678
678
 
679
- - Reusable keyed-list aliases
680
- - Arbitrary, imported, mutating, or asynchronous collection callbacks
681
- - Lazy/dynamic keyed-row state initializers and callback refs
682
- - Server actions and request-time SSR
683
- - React package/runtime/ecosystem compatibility or React islands
684
- - HMR and framework DevTools
679
+ - Non-primitive prop defaults/rest, exported reusable specialized rows, `forwardRef`, and `useId` still need fixture-driven component specialization. Direct analyzable prop spreads and forwarded JSX children are supported across existing specialized collection boundaries.
680
+ - Hookful non-keyed imported components and pure lazy state/reducer initializers remain narrower than ordinary React.
681
+ - Reusable collection aliases, computed child collections, imported pure transforms, and common immutable pagination/sorting forms need failing fixtures before expansion.
682
+ - Effect dependencies remain limited to directly analyzable primitive signals and supported keyed-item properties.
683
+ - Conditional/keyed ranges inside SVG and router-shaped package source remain separate compatibility work.
684
+ - Arbitrary, mutating, asynchronous, or package collection callbacks remain deliberate static-analysis boundaries.
685
+ - Server actions, request-time SSR, a React runtime/ecosystem layer, React islands, and a default SPA router are non-goals.
686
+
687
+ See the [active fixture queue](./MIGRATION_ROADMAP.md#active-fixture-queue) for development order. Unsupported syntax is not automatically backlog work; a real React migration fixture must fail first.
685
688
 
686
689
  ## Benchmarks
687
690
 
688
- ### Goal A Commerce Journey
691
+ ### Goal A Validation Fixture: Commerce Journey
689
692
 
690
693
  The matched fixture covers home, category, product, cart, checkout, and account routes with complete initial HTML, shared application layout state, product options, optimistic cart success and rejection, accessible errors, rollback, and product-to-cart navigation. Kudzu, React + Vite, Next.js, Nuxt, and SvelteKit render the same tested content and interactions.
691
694
 
package/RELEASES.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.7.10 - Component composition
4
+
5
+ Kudzu 0.7.10 preserves common component composition across existing compiler-specialized collection boundaries.
6
+
7
+ ### New in 0.7.10
8
+
9
+ - State-backed collection wrappers, keyed rows, and reducer specializations accept prop spreads from direct inline object literals or one direct `const` object literal declared in the calling component.
10
+ - Spread and explicit props apply in source order, preserving ordinary override behavior.
11
+ - Specialized components accept forwarded JSX children, including mixed text and element children, while still lowering to intrinsic DOM.
12
+ - Non-self-closing specialized component tags are counted as one JSX use instead of treating the closing tag as another reference.
13
+ - A conventional React-shaped landing fixture proves imported layout children, component spreads, keyed row children, interaction output, and a zero-JavaScript static route.
14
+ - Dynamic, computed, circular, prototype-sensitive, and method/accessor spread shapes fail with source-located diagnostics.
15
+
16
+ ### Boundary
17
+
18
+ Spread sources must be inline object literals or one direct `const` object literal declared in the calling component. State collections still cross the specialization boundary as direct props. Rest bindings, non-primitive defaults, exported reusable specialized rows, `forwardRef`, and `useId` remain fixture-driven work.
19
+
20
+ ### Upgrade
21
+
22
+ ```bash
23
+ npm install @kudzujs/core@^0.7.10
24
+ ```
25
+
26
+ ## 0.7.9 - Keyed-row prop defaults
27
+
28
+ Kudzu 0.7.9 lets ordinary keyed row components retain destructured primitive literal prop defaults instead of restructuring every call site.
29
+
30
+ ### New in 0.7.9
31
+
32
+ - Same-file and relative-imported keyed row components accept string, finite-number, boolean, and `null` defaults.
33
+ - Missing props receive their default literals during existing component specialization.
34
+ - The component call lowers to intrinsic keyed JSX; React imports and the component call do not enter browser output.
35
+ - Rest and nested destructuring remain source-diagnosed, while object, array, computed, and call defaults remain rejected.
36
+ - No browser runtime branch or shared asset bytes are added.
37
+ - The product documents now define commerce and realtime dashboards as validation fixtures under one general React migration roadmap, not separate product verticals.
38
+
39
+ ### Boundary
40
+
41
+ This release supports primitive literal defaults on compiler-specialized keyed rows. General prop spreads, rest bindings, non-primitive defaults, specialized `children`, `forwardRef`, and `useId` remain fixture-driven composition work.
42
+
43
+ ### Upgrade
44
+
45
+ ```bash
46
+ npm install @kudzujs/core@^0.7.9
47
+ ```
48
+
3
49
  ## 0.7.8 - Static collection fast paths
4
50
 
5
51
  Kudzu 0.7.8 specializes compiler-owned static `filter` collections so repeated category changes avoid general keyed-list validation and reconciliation.
@@ -31,9 +31,9 @@ Page `metadata` can emit description, canonical, favicon, manifest, Open Graph,
31
31
 
32
32
  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.
33
33
 
34
- 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.
34
+ 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. Missing destructured string, finite-number, boolean, or `null` props use their literal defaults during specialization. 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.
35
35
 
36
- Rendered collection selectors compile one-use aliases and inline `(item)` or `(item, index)` pipelines over local array state or supported static named imports. Supported selectors are pure `filter` with direct local-state reads, direct-property `flatMap`, and `Array.from` before a final keyed `map`; dependency commits re-evaluate the selector against the immutable build-time collection while field keys retain item identity and `key={index}` retains positional identity. Compiler-owned static filters over structural keyed rows validate source references and keys once, retain removed rows as detached prototypes, clone fresh restoration nodes, and insert only new runs without moving retained DOM. This route-specific path is compiled out elsewhere. Compiler-owned collection state is excluded from development snapshot restoration. 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.
36
+ Rendered collection selectors compile one-use aliases and inline `(item)` or `(item, index)` pipelines over local array state or supported static named imports. Supported selectors are pure `filter` with direct local-state reads, direct-property `flatMap`, and `Array.from` before a final keyed `map`; dependency commits re-evaluate the selector against the immutable build-time collection while field keys retain item identity and `key={index}` retains positional identity. Compiler-owned static filters over structural keyed rows validate source references and keys once, retain removed rows as detached prototypes, clone fresh restoration nodes, and insert only new runs without moving retained DOM. Specialized collection wrappers and keyed rows inline direct object-literal or calling-component `const` object prop spreads in source order and forward JSX children into intrinsic output. This route-specific path is compiled out elsewhere. Compiler-owned collection state is excluded from development snapshot restoration. Dynamic/computed prop spreads, 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.
37
37
 
38
38
  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.
39
39
 
@@ -3358,17 +3358,113 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
3358
3358
  visit(root)
3359
3359
  }
3360
3360
 
3361
+ function directConstObjectLiteral(expression, call, sourceFile) {
3362
+ expression = unwrapExpression(expression)
3363
+ if (ts.isObjectLiteralExpression(expression)) return expression
3364
+ if (!ts.isIdentifier(expression)) return
3365
+ const owner = nearestFunction(call)
3366
+ const scope = owner?.body ?? sourceFile
3367
+ if (!scope || !ts.isBlock(scope) && !ts.isSourceFile(scope)) return
3368
+ const declarations = []
3369
+ for (const statement of scope.statements) {
3370
+ if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0) continue
3371
+ for (const declaration of statement.declarationList.declarations) {
3372
+ if (ts.isIdentifier(declaration.name) && declaration.name.text === expression.text && declaration.initializer && declaration.end < call.pos) declarations.push(declaration)
3373
+ }
3374
+ }
3375
+ if (declarations.length !== 1) return
3376
+ const initializer = unwrapExpression(declarations[0].initializer)
3377
+ if (ts.isObjectLiteralExpression(initializer)) return initializer
3378
+ }
3379
+
3380
+ function specializedSpreadEntries(expression, call, sourceFile, fail, label, seen = new Set()) {
3381
+ const object = directConstObjectLiteral(expression, call, sourceFile)
3382
+ if (!object) fail(expression, `${label} component prop spreads must use an inline object literal or one direct const object literal declared in the calling component`)
3383
+ if (seen.has(object)) fail(expression, `${label} component prop spreads cannot be circular`)
3384
+ seen.add(object)
3385
+ const entries = []
3386
+ for (const property of object.properties) {
3387
+ if (ts.isSpreadAssignment(property)) {
3388
+ entries.push(...specializedSpreadEntries(property.expression, call, sourceFile, fail, label, seen))
3389
+ continue
3390
+ }
3391
+ if (ts.isShorthandPropertyAssignment(property)) {
3392
+ entries.push([property.name.text, property.name, property])
3393
+ continue
3394
+ }
3395
+ if (!ts.isPropertyAssignment(property) || ts.isComputedPropertyName(property.name) || !ts.isIdentifier(property.name) && !ts.isStringLiteral(property.name) && !ts.isNumericLiteral(property.name)) {
3396
+ fail(property, `${label} component prop spreads must contain only direct properties`)
3397
+ }
3398
+ entries.push([property.name.text, property.initializer, property])
3399
+ }
3400
+ seen.delete(object)
3401
+ return entries
3402
+ }
3403
+
3404
+ function specializedCallChildren(call, factory) {
3405
+ if (!ts.isJsxElement(call)) return []
3406
+ return call.children.flatMap(child => {
3407
+ if (ts.isJsxText(child)) {
3408
+ const lines = child.text.split(/\r\n|\n|\r/)
3409
+ const text = lines.length === 1
3410
+ ? child.text
3411
+ : lines.map((line, index) => {
3412
+ let text = line.replace(/\t/g, " ")
3413
+ if (index) text = text.trimStart()
3414
+ if (index < lines.length - 1) text = text.trimEnd()
3415
+ return text
3416
+ }).filter(Boolean).join(" ")
3417
+ return text ? [factory.createStringLiteral(text)] : []
3418
+ }
3419
+ if (ts.isJsxExpression(child)) return child.expression ? [child.expression] : []
3420
+ return [child]
3421
+ })
3422
+ }
3423
+
3424
+ function flattenForwardedComponentChildren(root, factory, context) {
3425
+ const forwarded = expression => {
3426
+ const value = unwrapExpression(expression)
3427
+ if (ts.isJsxElement(value) || ts.isJsxSelfClosingElement(value)) return [value]
3428
+ if (ts.isJsxFragment(value)) return [...value.children]
3429
+ if (ts.isArrayLiteralExpression(value) && !value.elements.some(ts.isSpreadElement)) {
3430
+ return value.elements.flatMap(element => {
3431
+ if (ts.isJsxFragment(element)) return [...element.children]
3432
+ if (ts.isJsxElement(element) || ts.isJsxSelfClosingElement(element)) return [element]
3433
+ return [factory.createJsxExpression(undefined, element)]
3434
+ })
3435
+ }
3436
+ }
3437
+ const visit = node => {
3438
+ if (ts.isJsxElement(node)) {
3439
+ const children = node.children.flatMap(child => {
3440
+ const values = ts.isJsxExpression(child) && child.expression ? forwarded(child.expression) : undefined
3441
+ return (values ?? [child]).map(entry => ts.visitNode(entry, visit))
3442
+ })
3443
+ return factory.updateJsxElement(node, ts.visitNode(node.openingElement, visit), children, ts.visitNode(node.closingElement, visit))
3444
+ }
3445
+ return ts.visitEachChild(node, visit, context)
3446
+ }
3447
+ return ts.visitNode(root, visit)
3448
+ }
3449
+
3361
3450
  function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list", allowComponentRoot = false) {
3362
3451
  if (component.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || component.asteriskToken) fail(component, `${label} components must be synchronous`)
3363
3452
  if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, `${label} components must use one destructured props parameter`)
3364
- if (ts.isJsxElement(call) && call.children.some(child => !ts.isJsxText(child) || child.text.trim())) fail(call, `${label} component children are not supported`)
3365
3453
  const callAttributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
3366
3454
  const props = new Map()
3455
+ const directProps = new Set()
3367
3456
  let key
3368
3457
  for (const attribute of callAttributes.properties) {
3369
- if (ts.isJsxSpreadAttribute(attribute)) fail(attribute, `${label} component prop spreads are not supported`)
3458
+ if (ts.isJsxSpreadAttribute(attribute)) {
3459
+ for (const [name, value, property] of specializedSpreadEntries(attribute.expression, call, sourceFile, fail, label)) {
3460
+ if (["__proto__", "constructor", "prototype"].includes(name)) fail(property, `${label} component prop spread property ${JSON.stringify(name)} is not supported`)
3461
+ if (name === "key") fail(property, `${label} component prop spreads cannot declare key`)
3462
+ props.set(name, value)
3463
+ }
3464
+ continue
3465
+ }
3370
3466
  const name = attribute.name.text
3371
- if (props.has(name) || name === "key" && key) fail(attribute, `Duplicate ${label.toLowerCase()} component prop "${name}"`)
3467
+ if (directProps.has(name) || name === "key" && key) fail(attribute, `Duplicate ${label.toLowerCase()} component prop "${name}"`)
3372
3468
  const value = !attribute.initializer
3373
3469
  ? factory.createTrue()
3374
3470
  : ts.isStringLiteral(attribute.initializer)
@@ -3377,12 +3473,20 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
3377
3473
  ? attribute.initializer.expression
3378
3474
  : factory.createIdentifier("undefined")
3379
3475
  if (name === "key") key = attribute
3380
- else props.set(name, value)
3476
+ else {
3477
+ props.set(name, value)
3478
+ directProps.add(name)
3479
+ }
3480
+ }
3481
+ const children = specializedCallChildren(call, factory)
3482
+ if (children.length) {
3483
+ if (directProps.has("children")) fail(call, `Duplicate ${label.toLowerCase()} component prop "children"`)
3484
+ props.set("children", children.length === 1 ? children[0] : factory.createArrayLiteralExpression(children))
3381
3485
  }
3382
3486
  const substitutions = new Map()
3383
3487
  const acceptedProps = new Set()
3384
3488
  for (const element of component.parameters[0].name.elements) {
3385
- if (element.dotDotDotToken || !ts.isIdentifier(element.name) || (element.initializer && label === "Keyed list")) fail(element, `${label} component props cannot use rest, defaults, or nested destructuring`)
3489
+ if (element.dotDotDotToken || !ts.isIdentifier(element.name)) fail(element, `${label} component props cannot use rest or nested destructuring`)
3386
3490
  if (element.initializer && !isPrimitiveDefaultLiteral(element.initializer)) fail(element.initializer, `${label} component prop defaults must be primitive literals`)
3387
3491
  const prop = (element.propertyName ?? element.name).text
3388
3492
  acceptedProps.add(prop)
@@ -3454,7 +3558,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
3454
3558
  findUnsupportedHook(returned)
3455
3559
  for (const calculation of calculations) findUnsupportedHook(calculation.expression)
3456
3560
  if (unsupportedHook) throw sourceNodeError(unsupportedHook, component.getSourceFile(), `Keyed row ${unsupportedHook.expression.text}() must be one top-level const declaration`)
3457
- let root = unwrapExpression(substituteClone(returned, substitutions, factory, context))
3561
+ let root = unwrapExpression(flattenForwardedComponentChildren(substituteClone(returned, substitutions, factory, context), factory, context))
3458
3562
  if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, `${label} component must return one JSX element`)
3459
3563
  const tag = jsxTagName(root)
3460
3564
  if (!ts.isIdentifier(tag) || !allowComponentRoot && tag.text[0] !== tag.text[0].toLowerCase()) fail(returned, `${label} component must directly return an intrinsic JSX element`)
@@ -3663,7 +3767,7 @@ function validateListExpression(expression, item, source, fail, index) {
3663
3767
  fail(source, "Derived keyed list item expressions cannot call arbitrary functions")
3664
3768
  }
3665
3769
  }
3666
- if (ts.isIdentifier(node) && isReferenceIdentifier(node) && node.text !== item && node.text !== index && !pureListGlobals.has(node.text)) {
3770
+ if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isJsxSyntaxIdentifier(node) && node.text !== item && node.text !== index && !pureListGlobals.has(node.text)) {
3667
3771
  fail(source, `Derived keyed list item expression identifier "${node.text}" is not allowed`)
3668
3772
  }
3669
3773
  ts.forEachChild(node, visit)
@@ -3731,7 +3835,7 @@ function referencesIdentifier(root, name) {
3731
3835
  function identifierReferenceCount(root, name) {
3732
3836
  let count = 0
3733
3837
  const visit = node => {
3734
- if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node)) count++
3838
+ if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !ts.isJsxClosingElement(node.parent)) count++
3735
3839
  ts.forEachChild(node, visit)
3736
3840
  }
3737
3841
  visit(root)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.7.8",
3
+ "version": "0.7.10",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",