@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 +75 -146
- package/GOAL_B.md +56 -92
- package/README.md +14 -11
- package/RELEASES.md +46 -0
- package/framework/README.md +2 -2
- package/framework/build.mjs +112 -8
- package/package.json +1 -1
package/GOAL_A.md
CHANGED
|
@@ -1,187 +1,116 @@
|
|
|
1
|
-
# Goal A: Static
|
|
1
|
+
# Goal A: Static Application Lifecycles And Opt-In Navigation
|
|
2
2
|
|
|
3
|
-
Goal A
|
|
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
|
-
|
|
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
|
-
|
|
7
|
+
## Status
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Goal A is complete for explicitly configured emitted-route groups with shared layouts. Implemented capabilities include:
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21
|
+
## Capability Contract
|
|
23
22
|
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
32
|
+
The capability must preserve:
|
|
67
33
|
|
|
68
|
-
- complete indexable HTML for
|
|
34
|
+
- complete indexable HTML for every route;
|
|
69
35
|
- direct entry, reload, back, forward, query strings, and base paths;
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
-
|
|
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
|
-
|
|
42
|
+
## Invariants
|
|
79
43
|
|
|
80
|
-
|
|
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
|
-
|
|
54
|
+
## Ownership Model
|
|
83
55
|
|
|
84
|
-
|
|
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
|
-
|
|
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
|
|
108
|
-
| Layout | Enhanced navigation session | user summary,
|
|
109
|
-
| Route | Current route range | filters,
|
|
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
|
-
|
|
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
|
-
##
|
|
67
|
+
## Validation Fixture
|
|
117
68
|
|
|
118
|
-
|
|
69
|
+
The reduced commerce journey remains a useful integration fixture because it exercises the neutral capability contract in one flow:
|
|
119
70
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
+
Other applications should validate the same capabilities with domain-appropriate fixtures such as documentation, content, administration, forms, and dashboards.
|
|
132
78
|
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
+
Record at least:
|
|
143
84
|
|
|
144
|
-
- deploy
|
|
145
|
-
- HTML, CSS,
|
|
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
|
|
148
|
-
-
|
|
149
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
+
## Regression Gates
|
|
172
95
|
|
|
173
|
-
|
|
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
|
-
|
|
104
|
+
## Historical Validation Record
|
|
176
105
|
|
|
177
|
-
The
|
|
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
|
-
-
|
|
183
|
-
-
|
|
184
|
-
-
|
|
185
|
-
-
|
|
186
|
-
-
|
|
187
|
-
- Winning
|
|
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:
|
|
1
|
+
# Goal B: Relative TypeScript Worker Capability Proof
|
|
2
2
|
|
|
3
|
-
Goal B
|
|
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
|
-
|
|
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
|
-
##
|
|
7
|
+
## Status
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
+
## Capability Target
|
|
16
14
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
23
|
+
## Supported Form
|
|
38
24
|
|
|
39
|
-
|
|
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<
|
|
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
|
-
|
|
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,
|
|
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
|
-
-
|
|
69
|
-
- create the Worker only when
|
|
70
|
-
- preserve
|
|
71
|
-
- reject generated/public asset collisions
|
|
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
|
|
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 |
|
|
61
|
+
| Owner | Lifetime | Capability responsibility |
|
|
78
62
|
|---|---|---|
|
|
79
|
-
| Document | Full document |
|
|
80
|
-
| Layout | Enhanced navigation session |
|
|
81
|
-
| Route | Current
|
|
82
|
-
| DOM range |
|
|
83
|
-
| Worker | Explicit owner cleanup | parsing, bounded buffering, aggregation
|
|
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
|
-
|
|
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
|
|
73
|
+
The capability fixture proves:
|
|
90
74
|
|
|
91
|
-
- one complete
|
|
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
|
-
-
|
|
95
|
-
-
|
|
96
|
-
-
|
|
97
|
-
-
|
|
98
|
-
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
2. **Capability conformance fixture**: mock telemetry Worker, bounded buffer, downsampling, imperative DOM ownership, and route cleanup.
|
|
99
|
+
## Historical Validation Record
|
|
124
100
|
|
|
125
|
-
|
|
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
|
-
|
|
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
|
-
-
|
|
140
|
-
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
677
|
+
Selected current migration limits:
|
|
678
678
|
|
|
679
|
-
-
|
|
680
|
-
-
|
|
681
|
-
-
|
|
682
|
-
-
|
|
683
|
-
-
|
|
684
|
-
-
|
|
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.
|
package/framework/README.md
CHANGED
|
@@ -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.
|
|
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
|
|
package/framework/build.mjs
CHANGED
|
@@ -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))
|
|
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 (
|
|
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
|
|
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)
|
|
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)
|