@kudzujs/core 0.5.8 → 0.6.0
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 +175 -0
- package/README.md +94 -10
- package/framework/README.md +9 -3
- package/framework/build.mjs +800 -43
- package/framework/core.d.ts +10 -4
- package/framework/core.mjs +117 -32
- package/framework/dependency-runtime.js +36 -0
- package/framework/effect-runtime.js +3 -1
- package/framework/list-runtime.js +13 -6
- package/framework/navigation-runtime.js +220 -0
- package/package.json +2 -1
package/GOAL_A.md
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# Goal A: Static Business Applications
|
|
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.
|
|
4
|
+
|
|
5
|
+
This is the implementation contract. `MIGRATION_ROADMAP.md` remains the source of truth for compiler invariants and the fixture-first workflow.
|
|
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.
|
|
8
|
+
|
|
9
|
+
## Implementation Status
|
|
10
|
+
|
|
11
|
+
**Goal A is complete for one explicitly configured exact-static route group with one shared layout.** Complete standalone documents, native fallback, persistent layout state/effects, disposable route state/effects, optimistic workflows, desktop/mobile performance gates, and the dashboard expansion seam are covered. Runtime bracket navigation, multiple layout groups, and conditional/keyed DOM-owned effects inside a navigation group remain deliberate post-Goal-A limits.
|
|
12
|
+
|
|
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**: top-level layout effects mount once per document session, top-level route effects remount after awaited route cleanup, and primitive dependency subscriptions exist only while their lifetime is mounted. Disposed effect setters and queued commits are inactive. Conditional/keyed DOM-owned effects remain unsupported inside navigation groups.
|
|
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.
|
|
21
|
+
|
|
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.
|
|
23
|
+
|
|
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 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.
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
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.
|
|
31
|
+
|
|
32
|
+
| Target | Product JS gzip | Cold transfer | Cold LCP | Warm LCP | Cold task | Cold heap | Interaction | Product-cart |
|
|
33
|
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
34
|
+
| Kudzu | 7,215 B | 34,809 B | 324 ms | 140 ms | 103.3 ms | 648,844 B | 3.7 ms | 5.7 ms |
|
|
35
|
+
| 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 |
|
|
36
|
+
| 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 |
|
|
37
|
+
| Nuxt | 67,620 B | 195,953 B | 320 ms | 204 ms | 205.1 ms | 1,721,348 B | 3.9 ms | 33.0 ms |
|
|
38
|
+
| SvelteKit | 32,473 B | 90,929 B | 340 ms | 172 ms | 125.8 ms | 999,496 B | 4.7 ms | 20.3 ms |
|
|
39
|
+
|
|
40
|
+
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.
|
|
41
|
+
|
|
42
|
+
The final seven-run build table measured 460.8 ms for Kudzu and 429.5 ms for React. A focused 21-run interleaved follow-up measured 578.6 ms and 542.5 ms under different machine load, leaving a repeatable 6-7% Kudzu loss. CPU profiling attributes the small-project difference primarily to TypeScript ESM/compiler startup. Replacing that parser or hiding startup work is outside Goal A; this is an explicit build-time tradeoff, while deploy bytes and every measured browser CPU, memory, and navigation metric remain lower than React.
|
|
43
|
+
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
## Product Target
|
|
47
|
+
|
|
48
|
+
A user can build and deploy this flow without React, a VDOM, hydration, or a request server:
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
home -> category -> product -> cart -> checkout -> account
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The reference fixture must cover:
|
|
55
|
+
|
|
56
|
+
- complete indexable HTML for public pages;
|
|
57
|
+
- direct entry, reload, back, forward, query strings, and base paths;
|
|
58
|
+
- product filters stored in the URL;
|
|
59
|
+
- product variants and quantity as route state;
|
|
60
|
+
- cart and session information shared by an application layout;
|
|
61
|
+
- async loading, empty, error, retry, and cancellation behavior;
|
|
62
|
+
- optimistic cart updates with rollback after a rejected request;
|
|
63
|
+
- accessible focus, title, scroll, and announcement behavior after navigation;
|
|
64
|
+
- ordinary document navigation when JavaScript is absent, late, or fails.
|
|
65
|
+
|
|
66
|
+
The fixture is a generic executable requirement, not a reusable commerce package. Kudzu should add only compiler features that the reduced fixture proves necessary.
|
|
67
|
+
|
|
68
|
+
## Runtime Model
|
|
69
|
+
|
|
70
|
+
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.
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
Static document
|
|
74
|
+
-> optional persistent layout
|
|
75
|
+
-> replaceable route range
|
|
76
|
+
-> capability-specific route ESM
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
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.
|
|
80
|
+
|
|
81
|
+
Navigation enhancement must not become a general SPA runtime:
|
|
82
|
+
|
|
83
|
+
- no VDOM, hydration, retained component tree, or component rerenderer;
|
|
84
|
+
- no global router on routes outside an opted-in application group;
|
|
85
|
+
- no interception of external, download, modified-click, reload, or explicitly native links;
|
|
86
|
+
- no client-only route as the sole representation of indexable content;
|
|
87
|
+
- no duplicated source component execution in the browser.
|
|
88
|
+
|
|
89
|
+
## Ownership And Cleanup
|
|
90
|
+
|
|
91
|
+
Goal A needs explicit ownership, not a new state library:
|
|
92
|
+
|
|
93
|
+
| Owner | Lifetime | Examples |
|
|
94
|
+
|---|---|---|
|
|
95
|
+
| Document | Full document | global listeners and document exit cleanup |
|
|
96
|
+
| Layout | Enhanced navigation session | user summary, cart count, persistent connection |
|
|
97
|
+
| Route | Current route range | filters, selected variant, request, subscription |
|
|
98
|
+
| DOM range | Conditional or keyed item | listener, effect, imperative child resource |
|
|
99
|
+
|
|
100
|
+
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.
|
|
101
|
+
|
|
102
|
+
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.
|
|
103
|
+
|
|
104
|
+
## Delivery Order
|
|
105
|
+
|
|
106
|
+
Each phase starts with one failing fixture and ends with correctness, browser, size, and build measurements.
|
|
107
|
+
|
|
108
|
+
1. **Benchmark harness**: freeze the commerce journey, network profiles, framework versions, generated artifacts, and measurement scripts before optimizing Kudzu.
|
|
109
|
+
2. **Owned effects**: complete cleanup for conditional ranges and keyed items using the existing mount and unmount hooks. **Complete.**
|
|
110
|
+
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.**
|
|
111
|
+
4. **Opt-in navigation**: support eligible links, history, aborts, stale responses, focus, scroll, metadata, and native fallback. **Complete for exact static routes and top-level layout/route effects; conditional/keyed DOM-owned effects remain excluded.**
|
|
112
|
+
5. **Business workflows**: close only fixture-proven gaps in forms, async requests, optimistic updates, and diagnostics. **Complete for the matched cart success/rejection flow.**
|
|
113
|
+
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.**
|
|
114
|
+
|
|
115
|
+
Do not begin a later phase while the current phase has an unexplained correctness or performance regression.
|
|
116
|
+
|
|
117
|
+
## Performance Comparison
|
|
118
|
+
|
|
119
|
+
The matched comparison set is:
|
|
120
|
+
|
|
121
|
+
- Kudzu from the previous completed phase;
|
|
122
|
+
- React with an equivalent production static deployment;
|
|
123
|
+
- Next.js static export;
|
|
124
|
+
- Vue/Nuxt static generation;
|
|
125
|
+
- Svelte/SvelteKit static generation;
|
|
126
|
+
- hand-written HTML and DOM JavaScript where it provides a useful lower bound.
|
|
127
|
+
|
|
128
|
+
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.
|
|
129
|
+
|
|
130
|
+
Use identical content, CSS, local images, API payloads, cache state, browser version, CPU profile, and network profile. Record at least:
|
|
131
|
+
|
|
132
|
+
- deploy artifact bytes and initial raw/gzip JavaScript;
|
|
133
|
+
- HTML, CSS, image, and route payload transfer bytes;
|
|
134
|
+
- clean build median;
|
|
135
|
+
- cold and warm LCP, CLS, and INP;
|
|
136
|
+
- hydration or startup main-thread work;
|
|
137
|
+
- cold and warm route transition time;
|
|
138
|
+
- cart update and rollback latency;
|
|
139
|
+
- browser heap after initial load and repeated navigation;
|
|
140
|
+
- duplicate listeners, unfinished requests, and lifecycle leaks.
|
|
141
|
+
|
|
142
|
+
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.
|
|
143
|
+
|
|
144
|
+
## Performance Gates
|
|
145
|
+
|
|
146
|
+
Goal A is not complete unless:
|
|
147
|
+
|
|
148
|
+
- static routes still emit zero JavaScript;
|
|
149
|
+
- routes outside an application group remain byte-for-byte unaffected by navigation support;
|
|
150
|
+
- an application route ships only the layout and route capabilities it uses;
|
|
151
|
+
- no hydration task or browser component tree is introduced;
|
|
152
|
+
- repeated navigation leaves no growing listener, request, state, DOM, or heap ownership;
|
|
153
|
+
- Kudzu has no unexplained material regression against its previous phase;
|
|
154
|
+
- material losses against the fastest matched framework are profiled and either fixed or documented as an explicit product tradeoff;
|
|
155
|
+
- performance claims include fixture limitations and do not compare unequal initial content as equivalent.
|
|
156
|
+
|
|
157
|
+
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.
|
|
158
|
+
|
|
159
|
+
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.
|
|
160
|
+
|
|
161
|
+
## Completion Definition
|
|
162
|
+
|
|
163
|
+
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.
|
|
164
|
+
|
|
165
|
+
The final validation record must retain the generated artifacts, framework versions, commands, raw measurements, medians, known losses, and deliberate limits.
|
|
166
|
+
|
|
167
|
+
## Non-Goals
|
|
168
|
+
|
|
169
|
+
- A default SPA router.
|
|
170
|
+
- Arbitrary React application or package compatibility.
|
|
171
|
+
- Runtime component registration or client component rendering.
|
|
172
|
+
- Request-time SSR, server actions, or a hidden server.
|
|
173
|
+
- A charting, mapping, telemetry, virtual-grid, or visual-editor engine.
|
|
174
|
+
- A custom widget marketplace or plugin security model.
|
|
175
|
+
- Winning every synthetic metric at the cost of correctness, accessibility, or deploy output.
|
package/README.md
CHANGED
|
@@ -8,10 +8,12 @@ HTML-first TSX framework with synchronous state semantics and no virtual DOM.
|
|
|
8
8
|
|
|
9
9
|
Kudzu keeps the familiar function-component, props, children, event-handler, `useState`, and mount-effect shape. Static components compile to HTML. Simple interactions compile to small behavior commands, while normal sync or async JavaScript handlers and mount effects compile to external ESM.
|
|
10
10
|
|
|
11
|
-
> Experimental `0.
|
|
11
|
+
> Experimental `0.6.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
13
13
|
Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
|
|
14
14
|
|
|
15
|
+
Development target: [Goal A static business applications](./GOAL_A.md)
|
|
16
|
+
|
|
15
17
|
## Install
|
|
16
18
|
|
|
17
19
|
Create a new project:
|
|
@@ -126,17 +128,20 @@ Static trusted HTML can be rendered without a transform layer:
|
|
|
126
128
|
|
|
127
129
|
The HTML is intentionally not sanitized. Use only trusted or previously sanitized build-time content. Reactive raw HTML, children on the same element, void elements, and keyed-list raw HTML are rejected.
|
|
128
130
|
|
|
129
|
-
Every CSS file under `src` is copied to the same relative path under `dist/assets` and linked in deterministic order. Project-page deployments and post-build artifacts use `kudzu.config.mjs`:
|
|
131
|
+
Every CSS file under `src` is copied to the same relative path under `dist/assets` and linked in deterministic order. Stylesheets produced by another build step can be declared globally so Kudzu still emits them in every document `<head>`. Configured root-relative URLs receive `base`; absolute HTTP URLs are preserved. Project-page deployments and post-build artifacts use `kudzu.config.mjs`:
|
|
130
132
|
|
|
131
133
|
```js
|
|
132
134
|
export default {
|
|
133
135
|
base: "/newsletter",
|
|
136
|
+
styles: ["/assets/generated.css"],
|
|
134
137
|
async afterBuild({ outDir, routes, plans, rewrites, base }) {
|
|
135
|
-
// Write host rewrites, RSS, sitemap, or other static artifacts.
|
|
138
|
+
// Write generated.css, host rewrites, RSS, sitemap, or other static artifacts.
|
|
136
139
|
}
|
|
137
140
|
}
|
|
138
141
|
```
|
|
139
142
|
|
|
143
|
+
Do not render `<link rel="stylesheet">` from page or component JSX. Kudzu rejects direct static body stylesheets with a source location and catches computed JSX stylesheet output during rendering. Trusted `dangerouslySetInnerHTML` remains unparsed and is responsible for its own resource tags.
|
|
144
|
+
|
|
140
145
|
## State Semantics
|
|
141
146
|
|
|
142
147
|
Kudzu intentionally differs from React's state snapshot behavior:
|
|
@@ -325,11 +330,11 @@ const rows = items.map(item => <ItemRow
|
|
|
325
330
|
/>)
|
|
326
331
|
```
|
|
327
332
|
|
|
328
|
-
The original component remains reusable across multiple lists and ordinary JSX. No component function or component runtime is shipped to the browser. Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX or in one top-level immutable `const` rendered once as a JSX child. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Single-level item-local `&&` and ternary JSX conditions patch only their bounded branch and mount or unmount its handlers. Item-local handlers
|
|
333
|
+
The original component remains reusable across multiple lists and ordinary JSX. No component function or component runtime is shipped to the browser. Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX or in one top-level immutable `const` rendered once as a JSX child. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Single-level item-local `&&` and ternary JSX conditions patch only their bounded branch and mount or unmount its handlers. Item-local handlers and effects receive the latest JSON-safe item for their key. Effects mount after a row is connected, clean up when it is removed, and do not rerun during reorder. The item remains stored once in shared list state; runtime descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
|
|
329
334
|
|
|
330
|
-
Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root or top-level local or relative-imported row component, and `key={item.<field>}`. Row components accept destructured projected props
|
|
335
|
+
Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. The current syntax requires a local-state `.map`, one identifier callback parameter, one intrinsic JSX root or top-level local or relative-imported row component, and `key={item.<field>}`. Row components accept destructured projected props, top-level single-`const` calculations and inline effects before one intrinsic return. Effect dependencies inside a row may be empty or direct primitive Kudzu state identifiers; item-property dependencies remain unsupported. A list alias may only be rendered once and cannot be read by other JavaScript. Derived expressions must be pure and synchronous: item reads, literals, operators, templates, approved read-only string/array methods, deterministic `Math` methods, and `String`/`Number`/`Boolean` conversion are supported. Component state, imported helpers used inside calculations, browser globals, Promise values, mutation, arbitrary calls, and prototype-sensitive properties are rejected. Package or namespace row imports, same-file exported rows, reusable aliases, prop spreads/defaults/rest, children, nested item conditions, lists, or component tags, refs, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
|
|
331
336
|
|
|
332
|
-
##
|
|
337
|
+
## Effects
|
|
333
338
|
|
|
334
339
|
Browser-only initial work uses the familiar empty-dependency effect shape:
|
|
335
340
|
|
|
@@ -357,12 +362,30 @@ useEffect(() => {
|
|
|
357
362
|
}, [])
|
|
358
363
|
```
|
|
359
364
|
|
|
360
|
-
|
|
365
|
+
Document-owned cleanup runs once when the document leaves outside the browser back-forward cache. An effect in a conditional branch or supported keyed row mounts only while its DOM owner is present and cleans up once when that owner is removed. Effect-local resources and component state read by nested cleanup closures retain their setup-time values. Cleanup failures are isolated so later cleanups still run.
|
|
366
|
+
|
|
367
|
+
Literal arrays of direct primitive `useState` or `useParams` signal identifiers rerun after committed dependency changes:
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
const [event, setEvent] = useState("resize")
|
|
371
|
+
|
|
372
|
+
useEffect(() => {
|
|
373
|
+
const listener = () => console.log(event)
|
|
374
|
+
window.addEventListener(event, listener)
|
|
375
|
+
return () => window.removeEventListener(event, listener)
|
|
376
|
+
}, [event])
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Dependency values are limited to JSON-safe strings, finite numbers, booleans, and `null`; direct signal aliases are accepted, while expressions, property reads, ordinary props or locals, objects, spreads, and dynamic arrays fail the build. Kudzu compares dependencies with `Object.is`, coalesces multiple commits in one turn, invokes every affected previous cleanup in declaration order, awaits asynchronous cleanup, and then runs the affected setups in declaration order. The component itself is not rerun.
|
|
380
|
+
|
|
381
|
+
Effect callbacks must be inline and block-bodied. Named or dynamically obtained cleanup functions, cleanup parameters or generators, other return values, callback parameters, and non-serializable captures are rejected. Async effects cannot return cleanup functions; the cleanup itself may be async. Pages without effects receive no effect entry. Empty-dependency effects retain their smaller output, and dependency-only capability code is isolated to the routes that use `kudzu-deps.js` unless another capability already requires the shared runtime.
|
|
361
382
|
|
|
362
383
|
A matched mount-fetch benchmark renders a title and two keyed rows from local JSON. With one warm-up and seven rotating clean builds, Kudzu shipped initial HTML, 3.4 KB initial JS gzip, 8.1 KB total output, and built in 374 ms. React CSR shipped no initial content, 59.3 KB initial JS gzip, 189.2 KB total output, and built in 992 ms. Hand-written ESM shipped 534 B initial JS gzip, 1.2 KB total output, and built in 210 ms. Fresh-profile Chrome medians to loaded data were 157.9 ms, 166.5 ms, and 153.4 ms respectively.
|
|
363
384
|
|
|
364
385
|
A matched resize-listener cleanup fixture, measured with the same warm-up and seven rotating clean builds, shipped 1.2 KB JavaScript gzip and built in 402 ms with Kudzu. Svelte shipped 10.1 KB and built in 861 ms, Vue shipped 23.6 KB and built in 768 ms, and React shipped 59.1 KB and built in 1,058 ms. Kudzu and the 127 B hand-written Astro baseline emitted initial HTML; the CSR fixtures did not.
|
|
365
386
|
|
|
387
|
+
In the matched dependency-rerun fixture, Kudzu shipped 1.5 KB JavaScript gzip and built in 429 ms. Svelte shipped 9.7 KB in 995 ms, Vue 23.8 KB in 943 ms, React 59.2 KB in 1,172 ms, and the hand-written Astro baseline 196 B in 969 ms. Kudzu and Astro emitted initial HTML; the CSR fixtures did not.
|
|
388
|
+
|
|
366
389
|
## Normal JavaScript
|
|
367
390
|
|
|
368
391
|
Command-only setters use the smallest optimized path. Conditions, local variables, browser globals, events, and `async`/`await` compile to external ESM without `eval`, `new Function`, or inline executable code.
|
|
@@ -409,9 +432,36 @@ TSX
|
|
|
409
432
|
|
|
410
433
|
- Static pages ship no client JavaScript.
|
|
411
434
|
- Interactive pages receive only the runtime capabilities they use.
|
|
435
|
+
- Interactive route modules are discovered in the document head and retain deferred execution after HTML parsing, overlapping cold downloads with document transfer.
|
|
412
436
|
- Production JavaScript is minified; development output stays readable.
|
|
413
437
|
- Components are authoring units; no component tree is retained in the browser.
|
|
414
|
-
- There is no VDOM, hydration pass, router, or client application runtime.
|
|
438
|
+
- There is no VDOM, hydration pass, retained component tree, default router, or general client application runtime.
|
|
439
|
+
|
|
440
|
+
## Application Navigation
|
|
441
|
+
|
|
442
|
+
Pages may export one shared layout while continuing to emit complete standalone documents:
|
|
443
|
+
|
|
444
|
+
```tsx
|
|
445
|
+
export { Shell as layout } from "../components/Shell"
|
|
446
|
+
|
|
447
|
+
export default function ProductPage() {
|
|
448
|
+
return <main><h1>Product</h1></main>
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
Opt exact static routes into same-document navigation:
|
|
453
|
+
|
|
454
|
+
```js
|
|
455
|
+
export default {
|
|
456
|
+
navigation: { routes: ["/product", "/cart"] }
|
|
457
|
+
}
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Every configured route must be an exact static route and export the same layout function. The layout DOM, state, and top-level effects persist; route state and top-level effects reset after cleanup on each transition. Eligible ordinary anchors prefetch validated complete documents into a finite memory cache. Direct requests, reloads, JavaScript failures, unsupported links, and routes outside the group retain native document navigation.
|
|
461
|
+
|
|
462
|
+
Runtime bracket routes, multiple layout groups, and conditional or keyed effects inside a navigation group are not supported yet.
|
|
463
|
+
|
|
464
|
+
This produces fast same-document route changes, but it does not add a coordinated transition animation. CSS entry animations can style newly inserted route content; exit and shared-element View Transitions are not integrated yet.
|
|
415
465
|
|
|
416
466
|
Example Nginx configuration:
|
|
417
467
|
|
|
@@ -434,6 +484,7 @@ Supported:
|
|
|
434
484
|
- Base-path deployments, multiple CSS files, and `afterBuild`
|
|
435
485
|
- Primitive `useState` bindings
|
|
436
486
|
- Mount-only `useEffect(fn, [])` compiled to route-specific ESM
|
|
487
|
+
- Conditional and keyed-row effect ownership with cleanup on DOM removal
|
|
437
488
|
- Synchronous and async event handlers
|
|
438
489
|
- Relative imported helpers in native handlers
|
|
439
490
|
- Serializable component-local captures
|
|
@@ -446,17 +497,50 @@ Supported:
|
|
|
446
497
|
- Conditional child `&&` and ternary DOM patches
|
|
447
498
|
- Top-level and block-scoped JSX locals, terminal early returns, and exhaustive JSX assignment
|
|
448
499
|
- Direct keyed local-state lists
|
|
500
|
+
- Page-exported shared layouts with layout/route state lifetimes
|
|
501
|
+
- Opt-in exact-route navigation with complete-document prefetch and native fallback
|
|
502
|
+
- Layout- and route-lifetime effect mounts in navigation groups
|
|
449
503
|
|
|
450
504
|
Not implemented yet:
|
|
451
505
|
|
|
452
|
-
-
|
|
506
|
+
- Reusable keyed-list aliases
|
|
453
507
|
- Server actions and request-time SSR
|
|
454
508
|
- React package islands
|
|
455
509
|
- HMR and framework DevTools
|
|
510
|
+
- Conditional/keyed DOM-owned effects inside navigation groups
|
|
456
511
|
|
|
457
512
|
## Benchmarks
|
|
458
513
|
|
|
459
|
-
|
|
514
|
+
### Goal A Commerce Journey
|
|
515
|
+
|
|
516
|
+
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.
|
|
517
|
+
|
|
518
|
+
Browser medians use seven rotating fresh Chrome profiles per target with 4x CPU slowdown, 100 ms latency, and 200 KiB/s throughput. Product JavaScript includes the initial static import graph; cold transfer includes the complete initial page transfer. Lower is better.
|
|
519
|
+
|
|
520
|
+
| Target | Product JS gzip | Cold transfer | Cold LCP | Warm LCP | Startup task | Heap | Interaction | Product → cart |
|
|
521
|
+
|---|---:|---:|---:|---:|---:|---:|---:|---:|
|
|
522
|
+
| Kudzu | **7,215 B** | **34,809 B** | 324 ms | **140 ms** | **103.3 ms** | **648,844 B** | **3.7 ms** | **5.7 ms** |
|
|
523
|
+
| 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 |
|
|
524
|
+
| 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 |
|
|
525
|
+
| Nuxt | 67,620 B | 195,953 B | 320 ms | 204 ms | 205.1 ms | 1,721,348 B | 3.9 ms | 33.0 ms |
|
|
526
|
+
| SvelteKit | 32,473 B | 90,929 B | 340 ms | 172 ms | 125.8 ms | 999,496 B | 4.7 ms | 20.3 ms |
|
|
527
|
+
|
|
528
|
+
The Kudzu application emits 34,879 deploy bytes. Its 7,215 B gzip product graph includes the 2,306 B navigation capability. The first implementation paid a 128.7 ms HTML round trip during product-to-cart navigation; validated near-viewport document prefetch reduced the measured median to 5.7 ms while preserving complete documents and native fallback.
|
|
529
|
+
|
|
530
|
+
The mobile profile uses a 390x844 viewport, 6x CPU slowdown, 150 ms latency, and 150 KiB/s throughput:
|
|
531
|
+
|
|
532
|
+
| Profile | Cold LCP | Warm LCP | Interaction | Product → cart | Reject feedback | Rollback/error | CLS |
|
|
533
|
+
|---|---:|---:|---:|---:|---:|---:|---:|
|
|
534
|
+
| Desktop | 324 ms | 140 ms | 3.7 ms | 5.7 ms | 3.3 ms | 110.8 ms | 0 |
|
|
535
|
+
| Mobile | 420 ms | 220 ms | 5.6 ms | 8.7 ms | 4.1 ms | 158 ms | 0 |
|
|
536
|
+
|
|
537
|
+
Small clean builds are the known tradeoff. Seven rotating production builds measured Kudzu at 460.8 ms and React at 429.5 ms; a separate 21-run interleaved check measured 578.6 ms and 542.5 ms. The repeatable 6–7% difference is primarily TypeScript ESM/compiler startup. Attempts to replace generated-handler lowering or share one TypeScript Program did not improve the combined median and were not retained.
|
|
538
|
+
|
|
539
|
+
These results describe this six-route fixture on one machine, not framework ecosystem size or every rendering mode. Prefetch improves an eligible warm application transition; it does not hide cold transfer, and direct loads remain complete standalone documents.
|
|
540
|
+
|
|
541
|
+
### Capability Microbenchmarks
|
|
542
|
+
|
|
543
|
+
The measurements below isolate individual compiler capabilities. They were produced on the same machine from production builds. Each framework received one warm-up followed by seven clean builds in rotating order; the table reports the median. Initial JavaScript includes inline scripts, root script references, and their static import graph, compressed file-by-file with gzip level 9. Total output is the raw size of every deploy artifact.
|
|
460
544
|
|
|
461
545
|
### Interactive Counter
|
|
462
546
|
|
package/framework/README.md
CHANGED
|
@@ -4,15 +4,21 @@
|
|
|
4
4
|
- `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
|
|
5
5
|
- `jsx-runtime.mjs`: automatic JSX runtime used by TypeScript.
|
|
6
6
|
- `runtime.js`: command-only runtime for direct state-to-text patches.
|
|
7
|
+
- `dependency-runtime.js`: command runtime with commit observation for dependency effects.
|
|
7
8
|
- `shared-runtime.js`: command runtime with capability commit and DOM lifecycle hooks, emitted only when needed.
|
|
8
9
|
- `binding-runtime.js`: optional generic attributes, form properties, comment-bounded text patches, and conditional range patches.
|
|
9
|
-
- `list-runtime.js`: optional keyed list validation, external item-expression evaluation, item-local conditional ranges, dynamic styles
|
|
10
|
+
- `list-runtime.js`: optional keyed list validation, external item-expression evaluation, item-local conditional ranges, dynamic styles, item handler/effect scopes, moves, and cleanup.
|
|
10
11
|
- `serialization.js`: capture deserialization shared by binding and native handlers.
|
|
11
12
|
- `effect-runtime.js`: optional state and capture context for route-specific mount-effect entries.
|
|
12
13
|
- `native-runtime.js`: optional runtime for normal synchronous and asynchronous ESM handlers.
|
|
14
|
+
- `navigation-runtime.js`: optional exact-route complete-document prefetch, validation, finite memory caching, route-range replacement, history, focus, and native fallback for one configured shared layout.
|
|
13
15
|
- `dev-state.js`: dev-only, short-lived logical-state snapshot validation and restoration.
|
|
14
16
|
- `*.d.ts`: public TypeScript and JSX declarations.
|
|
15
17
|
|
|
16
|
-
Static routes receive no browser runtime. Command routes receive `runtime.js`; runtime bracket pages using `useParams()` add one route-specific pathname matcher; reactive attributes and conditions add `binding-runtime.js`; keyed lists add `list-runtime.js`; native handlers add `native-runtime.js`;
|
|
18
|
+
Static routes receive no browser runtime. Command routes receive `runtime.js`; dependency effects use route-specific `kudzu-deps.js` unless that route already requires shared commit hooks; runtime bracket pages using `useParams()` add one route-specific pathname matcher; reactive attributes and conditions add `binding-runtime.js`; keyed lists add `list-runtime.js`; native handlers add `native-runtime.js`; effects add `effect-runtime.js` and one route-specific entry. Generated module scripts live in the document head, so cold downloads overlap HTML transfer while standard module deferral preserves execution after parsing. A single effect with one dependency compiles to a direct runner; generic maps, sets, and ordering are reserved for larger effect graphs. Dependency commits coalesce in a microtask; affected cleanups are awaited in declaration order before replacement setups run. Document cleanup integrates with shared unmount hooks when present and otherwise disposes directly on non-persisted `pagehide`. List builds remove unused text-range, attribute, event, expression, condition, seed, and mount branches. Effect builds omit capture deserialization entirely when every effect scope is empty. Capability runtimes share state and lifecycle hooks through `shared-runtime.js`. Generated evaluators and their bundled relative TypeScript helpers live under `dist/assets/handlers/`; shared helper chunks are emitted only when multiple handler entries need them. Runtime fallback rewrites are ordered by specificity in `.kudzu/kudzu-plan.json` and passed to `afterBuild()`; exact static files take precedence in development. The dev server derives stable state identities from route-unique state variable names in each route plan; every state sharing a duplicate name is omitted. It then injects its SSE reload, short-lived full-URL-scoped logical-state snapshot, and build-error client into responses only, never into `dist/`. Snapshots are consumed even when the next page is static or broken. Reload restoration covers compatible framework state, not uncontrolled DOM state, focus, selection, or imperative mutations.
|
|
17
19
|
|
|
18
|
-
Page `metadata` can emit description, canonical, favicon, manifest, Open Graph, and Twitter Card tags without a client runtime.
|
|
20
|
+
Page `metadata` can emit description, canonical, favicon, manifest, Open Graph, and Twitter Card tags without a client runtime. Source CSS and global `kudzu.config` styles are emitted in document heads before `afterBuild()` runs; static stylesheet links in component JSX fail compilation instead of loading from the body.
|
|
21
|
+
|
|
22
|
+
`kudzu.config` may opt one exact static shared-layout group into same-document navigation with `navigation: { routes: ["/product", "/cart"] }`. The routes must be unique emitted paths and export the same layout function identity. Route effect entries export cache-safe layout and route mount functions: layout effects mount once per document session, route effects remount after each route insertion, and non-persisted page disposal cleans route before layout. Primitive dependencies and cleanup are supported; conditional/keyed DOM-owned effects fail with a source-located diagnostic in navigation groups. Eligible same-origin anchors prefetch complete documents when near the viewport, hovered, or focused; validated documents are cached only in memory by full URL while represented by the current route. Runtime bracket enhancement, multiple groups, and fragment payloads are not implemented.
|
|
23
|
+
|
|
24
|
+
The matched Goal A commerce profile emits 34,879 deploy bytes and loads 7,215 B gzip of product-route JavaScript, including 2,306 B for navigation. Validated prefetch reduced product-to-cart navigation from 128.7 ms to 5.7 ms. The known tradeoff is a repeatable 6-7% clean-build loss against the matched small React build, primarily from TypeScript startup; failed handler-lowering and shared-Program experiments were not retained.
|