@kudzujs/core 0.8.23 → 0.8.25

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_B.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Goal B: Relative TypeScript Worker Capability Proof
2
2
 
3
+ This historical capability milestone is distinct from the active [Goal B optimization program](./docs/next-architecture/goal-b-optimization-benchmarks.md).
4
+
3
5
  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
6
 
5
7
  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.
@@ -102,11 +104,11 @@ The focused fixture emitted a 907 B raw / 477 B gzip Worker graph and an 11,388
102
104
 
103
105
  These numbers are dated conformance evidence. They do not define a dashboard product target.
104
106
 
105
- ## Reproducible 0.7.12 Rerun
107
+ ## Historical 0.7.12 Rerun
106
108
 
107
- At commit `05e5cc2` on Apple M3 / macOS 26.5.2 / Node 25.6.1, `npm run benchmark` measured clean build times of 404.2, 401.3, 408.2, 404.4, 399.9, 408.8, and 402.0 ms after one warm-up, for a 404.2 ms median. The current Worker graph is 907 B raw / 475 B gzip; the complete dashboard window graph is 11,960 B raw / 5,365 B aggregate gzip.
109
+ At commit `05e5cc2` on Apple M3 / macOS 26.5.2 / Node 25.6.1, `npm run benchmark` measured clean build times of 404.2, 401.3, 408.2, 404.4, 399.9, 408.8, and 402.0 ms after one warm-up, for a 404.2 ms median. That rerun's Worker graph was 907 B raw / 475 B gzip; its complete dashboard window graph was 11,960 B raw / 5,365 B aggregate gzip.
108
110
 
109
- Chrome 150.0.7871.187 passed the tracked throughput, cadence, stale-write, bounded-history, and 30-cycle start/termination checks through the focused Worker browser test. These current values differ from the historical completion snapshot because shared window capabilities and the local toolchain changed; the historical values above remain release provenance, not current artifact claims.
111
+ Chrome 150.0.7871.187 passed the tracked throughput, cadence, stale-write, bounded-history, and 30-cycle start/termination checks through the focused Worker browser test. Those rerun values differ from the historical completion snapshot because shared window capabilities and the local toolchain changed; both sets remain release provenance, not current artifact claims.
110
112
 
111
113
  ## Non-Goals
112
114
 
@@ -243,6 +243,19 @@ This queue orders the next investigations by general migration value. Start only
243
243
  - Source graph resolution and shared path conversion have focused owners, while Worker compilation directly owns Worker graph emission.
244
244
  - Complete-site and representative deploy artifacts remain byte-identical to `v0.8.22`; diagnostics, RouteIR, CapabilityIR, browser ownership, and accepted syntax are unchanged.
245
245
 
246
+ ### Completed In 0.8.24
247
+
248
+ - Goal B adds a maintained 2,000-row keyed browser benchmark with fresh-profile timing and ownership checks.
249
+ - Safe large flat-list restoration batches mount-hook discovery, reducing the measured median by 19.77% while retained-heavy append, filter, reverse, identity, state reset, and handler behavior remain unchanged.
250
+ - No-op normalization passes skip redundant recursive parent repair, reducing the measured 1,000-product build median by 6.26% with equivalent output.
251
+ - Identical generated route-entry transformation remains the next evidence-backed Goal B investigation; it is not implemented without an isolated benchmark.
252
+
253
+ ### Completed In 0.8.25
254
+
255
+ - Seven alternating builds of the 1,000-product, 1,011-page fixture authorized build-local exact-source reuse for identical generated native, parameter, and effect route-entry transforms, improving the median by 9.17% with identical deploy paths and bytes.
256
+ - Normalization pass results and finalized ModuleIR cross-slot references now fail closed at their compiler boundaries.
257
+ - Node 22 compatibility, required Chrome coverage, packed-package installation, version alignment, and post-publish registry checks are explicit release gates.
258
+
246
259
  ## Cross-Cutting Performance Gates
247
260
 
248
261
  Every migration feature must preserve:
package/PERFORMANCE.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Performance Records
2
2
 
3
+ Reproducibility classes: `npm run benchmark` and `npm run benchmark:keyed` are maintained in this repository; `npm run benchmark:commerce` is a maintained paired runner over the public external storefront; older excluded-workspace sections are historical provenance only and are not current framework rankings.
4
+
5
+ ## 0.8.25 Route Entry Transform Reuse
6
+
7
+ Measured UTC 2026-08-10 on an Intel Core i5-9500 with 6 cores, Linux 6.17.0-19-generic, Node 24.14.0, and npm 11.9.0. The public 1,000-product storefront fixture at `f2d5be1` generated 1,011 pages against the `0.8.24` tree plus the same compiler-boundary safety changes in both targets.
8
+
9
+ One warm-up followed by seven alternating clean builds compared repeated esbuild transformation with a build-local exact-source result map used only by generated native, parameter, and effect route entries. The median decreased from 13,851.0 ms to 12,581.4 ms, a 9.17% improvement. Every emitted path and SHA-256 hash matched. The catalog was generated once before timing; `dist` and `.kudzu` cleanup and manifest hashing remained outside timing.
10
+
11
+ ```text
12
+ repeated transform: [13437.4,13851.0,13844.9,13440.8,13906.1,14310.0,14901.0]
13
+ exact-source reuse: [12480.2,12826.9,12777.9,12615.9,11869.7,12121.3,12581.4]
14
+ ```
15
+
16
+ The map lasts for one build and keys the complete generated source after route-relative URLs are resolved. It is not a persistent or generalized JavaScript transform cache. The result measures the large repeated-route fixture on this Linux machine and is not directly comparable to the Apple M3 `0.8.24` build medians below.
17
+
18
+ ## 0.8.24 Measured Goal B Optimizations
19
+
20
+ Measured UTC 2026-08-09 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 25.6.1, npm 11.18.0, and Chrome 151.0.7922.76. Baseline tag `v0.8.23` at `39a065b` and the `0.8.24` candidate used the same local volume and dependencies.
21
+
22
+ The implementation and maintained benchmark patch over `v0.8.23` had SHA-256 `c6c39b0c64f7a5d3271cb9f0286a4c6787aaf7e293744457629e6ea249e9ef76`, produced by:
23
+
24
+ ```bash
25
+ git diff --binary v0.8.23 -- framework/compiler/normalization-pipeline.mjs framework/list-runtime.js test/compiler-passes.test.mjs test/keyed-performance.mjs test/fixtures/keyed-performance/src/pages/index.tsx test/commerce-build-performance.mjs package.json | shasum -a 256
26
+ ```
27
+
28
+ Twenty-one fresh profiles measured 2,000-row restoration at 21.1 ms versus 26.3 ms, a 19.77% improvement. Twenty-one alternating clean builds of the external 1,000-product fixture measured 6,266.5 ms versus 6,684.7 ms, a 6.26% improvement. The keyed route adds 127 B raw / 35 B aggregate gzip JavaScript; the normalization optimization adds no output bytes. Complete methodology, arrays, artifacts, correctness checks, and limitations are in [Goal B Measurement Details](#goal-b-measurement-details).
29
+
30
+ Before release-content updates, the complete site plus `lists`, `keyed-row-hooks`, and `navigation` retained identical deploy file lists. Only `assets/kudzu-list.js` changed; every other deploy file was byte-identical. Their normalized `.kudzu` trees were identical to `v0.8.23`.
31
+
3
32
  ## 0.8.23 Source Compiler Boundary
4
33
 
5
34
  Measured UTC 2026-08-09 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 22.23.2, and npm 11.18.0. Baseline tag `v0.8.22` at `60b9bff` and the current `0.8.23` compiler candidate used the same local volume and identical installed dependencies.
@@ -24,16 +53,111 @@ Both targets received one warm-up followed by seven clean `worker-effects` produ
24
53
 
25
54
  Before release-content updates, the complete 135-page site and the `bindings`, `keyed-row-hooks`, `effect-dependencies`, `worker-effects`, `runtime-params`, `navigation`, `config-authoring`, and `event-package` deploy trees had identical file lists and bytes. Their `.kudzu` trees also matched after replacing only each checkout root in existing source-location strings. The invalid-reducer fixture retained the same source file, line, column, and diagnostic text. `build.mjs` decreased from 3,732 to 744 lines; this source-organization metric is not a runtime performance claim.
26
55
 
56
+ ## Goal B Measurement Details
57
+
58
+ Goal B started from clean commit `39a065b4284c74e7bf8ee5e39647ef771f2ba6f6`. On the same Apple M3 environment with Node 22.23.2, the maintained `worker-effects` benchmark measured a 289.9 ms clean-build median, 907 B raw / 477 B gzip Worker graph, and 12,148 B raw / 5,411 B gzip window graph:
59
+
60
+ ```text
61
+ build: [284.3,287.1,293.6,289.9,291.3,286.4,290.7]
62
+ ```
63
+
64
+ After the two candidate optimizations, the same maintained benchmark measured a 286.2 ms median and identical graph sizes. This is a regression check rather than evidence for either optimization:
65
+
66
+ ```text
67
+ build: [286.2,282.1,287.1,286.4,285.4,284.4,287.2]
68
+ ```
69
+
70
+ Chrome 151.0.7922.76 passed the tracked throughput, cadence, bounded-history, stale-write, and 30-cycle ownership checks. The first Node 22 browser attempt returned no Worker data; immediate Node 25 and Node 22 retries passed, so this is recorded as a startup flake rather than a performance result.
71
+
72
+ The preserved external cross-framework workspace then rebuilt its 1,000-row Kudzu fixture against clean `0.8.23` to locate a possible keyed-list loss. This exploratory harness is not maintained in the repository and its historical framework results were not reused. Seven fresh unthrottled Chrome profiles all passed row visibility, retained identity, removal, and fresh re-add checks:
73
+
74
+ | Operation | Median | Raw runs |
75
+ |---|---:|---|
76
+ | Edit row 500 | 0.5 ms | 1.1, 0.6, 0.5, 0.5, 0.6, 0.5, 0.5 |
77
+ | Reverse 1,000 rows | 4.0 ms | 4.8, 3.8, 3.8, 4.2, 4.0, 4.1, 3.8 |
78
+ | Remove row 500 | 1.2 ms | 1.5, 1.1, 1.1, 1.3, 1.2, 1.2, 1.2 |
79
+ | Re-add row 500 | 1.3 ms | 2.3, 1.0, 1.3, 1.5, 1.1, 1.2, 1.4 |
80
+
81
+ That fixture's seven clean builds were `[390.891,389.698,393.034,390.798,389.477,390.258,388.714]` ms for a 390.258 ms median. It emitted 28,243 B raw / 9,245 B aggregate gzip initial JavaScript across eight files and 943,075 B total output across nine files. The absolute build result has no current matched control and does not establish a regression. No browser operation in this exploratory fixture established a material loss.
82
+
83
+ #### Maintained Keyed Restoration Benchmark
84
+
85
+ The repository-owned `npm run benchmark:keyed` fixture renders 2,000 keyed rows with row-local state, a reactive slice, and a reactive string filter. Each revision received one clean build warm-up, 21 measured clean builds, and 21 unthrottled fresh Chrome 151.0.7922.76 profiles. In-page `MutationObserver` completion includes the synchronous state commit and terminal DOM mutation. Every run retained row 1000 and its selected state, appended 33 rows beside 1,967 retained rows, filtered to one row, released row 1, restored row 1 with fresh identity/state and a working handler, and retained row 1000 through reversal.
86
+
87
+ The baseline uses the identical `0.8.24` benchmark harness and fixture copied into a clean `v0.8.23` worktree; only compiler/runtime source differs:
88
+
89
+ ```bash
90
+ git worktree add --detach /tmp/kudzu-0.8.23 v0.8.23
91
+ mkdir -p /tmp/kudzu-0.8.23/test/fixtures/keyed-performance/src/pages
92
+ cp test/keyed-performance.mjs /tmp/kudzu-0.8.23/test/keyed-performance.mjs
93
+ cp test/fixtures/keyed-performance/src/pages/index.tsx /tmp/kudzu-0.8.23/test/fixtures/keyed-performance/src/pages/index.tsx
94
+ ln -s "$PWD/node_modules" /tmp/kudzu-0.8.23/node_modules
95
+ RUNS=21 CHROME_BIN="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" node /tmp/kudzu-0.8.23/test/keyed-performance.mjs
96
+ RUNS=21 CHROME_BIN="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" npm run benchmark:keyed
97
+ ```
98
+
99
+ The measured loss was restoration from one retained row to 2,000 rows. The baseline called `mountDom()` separately for 1,999 connected additions. For top-level flat lists, the candidate calls it once on the connected list parent when more than 32 additions are a majority of both the next rows and their parent children. Nested/parent lists, shared containers dominated by unrelated siblings, and small or retained-heavy updates keep per-root mounting. This reduces repeated mount-hook selector traversal without adding a scheduler, cache, tree, or public API.
100
+
101
+ | Target | Build median | Append 33 | Filter median | Restore median | Reverse median | JavaScript raw / gzip |
102
+ |---|---:|---:|---:|---:|---:|---:|
103
+ | `0.8.23` baseline | 263.0 ms | 2.6 ms | 4.6 ms | 26.3 ms | 6.2 ms | 28,308 B / 10,937 B |
104
+ | Goal B candidate | 265.6 ms | 2.7 ms | 4.6 ms | 21.1 ms | 6.1 ms | 28,435 B / 10,972 B |
105
+
106
+ Restore improved 19.77%, and its 20.4-21.9 ms candidate range did not overlap the 25.7-27.3 ms baseline range. Append, filter, and reverse distributions overlap; no change is claimed. Build distributions overlap; no build improvement is claimed. The deterministic cost is 127 B raw / 35 B aggregate gzip in `kudzu-list.js`.
107
+
108
+ ```text
109
+ baseline build: [261.7,265.9,263.0,263.9,259.4,265.5,261.5,263.4,260.4,261.8,264.7,263.4,260.7,262.7,263.6,264.4,261.5,264.0,261.7,263.5,262.3]
110
+ candidate build: [265.4,271.2,265.2,263.6,266.1,266.7,264.4,317.3,298.2,269.4,269.2,264.1,266.9,268.9,273.6,265.5,262.4,259.4,262.1,265.6,262.0]
111
+ baseline append: [2.8,2.5,2.5,3.0,2.5,2.6,2.8,2.7,2.7,2.8,2.8,2.5,2.6,2.9,2.7,2.6,2.5,2.4,2.7,2.4,2.4]
112
+ candidate append: [2.8,3.0,2.7,2.5,2.7,2.6,2.8,2.8,2.7,2.6,2.8,2.8,3.0,2.8,2.4,2.4,2.4,2.9,2.9,2.7,2.6]
113
+ baseline filter: [4.5,4.5,5.0,4.6,4.5,4.5,4.9,4.8,4.5,4.6,4.7,4.3,4.7,4.8,4.7,4.7,4.6,4.5,4.9,4.4,4.2]
114
+ candidate filter: [4.7,4.8,4.6,4.6,4.7,4.4,4.7,4.5,4.6,4.4,4.7,4.9,4.9,4.6,4.4,4.6,4.6,4.8,4.8,4.3,4.4]
115
+ baseline restore: [26.5,27.3,26.5,26.5,25.8,26.7,26.3,26.4,26.2,26.4,26.6,26.2,27.0,26.6,25.9,25.7,25.9,26.3,26.0,25.9,26.3]
116
+ candidate restore: [21.3,21.7,21.8,21.6,20.4,20.8,20.6,21.5,20.4,20.8,21.8,21.4,21.9,21.0,21.5,20.9,20.8,21.0,21.1,21.1,21.2]
117
+ baseline reverse: [6.2,6.3,6.0,6.4,6.6,7.1,6.4,6.0,6.8,6.3,5.8,5.8,6.4,5.8,6.2,7.1,5.7,6.1,5.8,6.0,5.5]
118
+ candidate reverse: [5.8,6.0,6.2,5.8,5.8,6.1,6.2,6.2,6.3,6.0,6.1,6.2,6.2,6.0,6.3,6.2,5.9,6.3,6.0,5.9,6.3]
119
+ ```
120
+
121
+ #### External 1,000-Product Build
122
+
123
+ The public [`SimYunSup/kudzu-based-bench`](https://github.com/SimYunSup/kudzu-based-bench) commerce fixture at `f2d5be1` generated 1,000 deterministic products and 1,011 complete Kudzu pages. On the same Apple M3 / Node 25.6.1 machine, clean `0.8.23` and the Goal B candidate received one warm-up and 21 alternating clean builds. Output and `.kudzu` cleanup remained outside timing.
124
+
125
+ The paired runner generates the catalog once, alternates the two compiler roots, cleans `dist` and `.kudzu` outside timing, compares relative output manifests and hashes, permits only the recorded `assets/kudzu-list.js` delta, and restores the external app's package symlink afterward:
126
+
127
+ ```bash
128
+ git clone https://github.com/SimYunSup/kudzu-based-bench.git /tmp/kudzu-based-bench
129
+ git -C /tmp/kudzu-based-bench checkout --detach f2d5be1a516c539e30f7125f6870d42b1dd02ecd
130
+ pnpm --dir /tmp/kudzu-based-bench install --force
131
+ pnpm --dir /tmp/kudzu-based-bench run build:commerce
132
+ git worktree add --detach /tmp/kudzu-0.8.23 v0.8.23
133
+ ln -s "$PWD/node_modules" /tmp/kudzu-0.8.23/node_modules
134
+ APP_ROOT=/tmp/kudzu-based-bench/apps/shop-kudzu \
135
+ BASELINE_ROOT=/tmp/kudzu-0.8.23 \
136
+ CANDIDATE_ROOT="$PWD" RUNS=21 CATALOG_SIZE=1000 \
137
+ npm run benchmark:commerce
138
+ ```
139
+
140
+ `applyNormalizationPasses()` previously called TypeScript's recursive parent-pointer repair after every pass, including validators and no-op passes returning the identical `SourceFile`. Every current pass was audited to use immutable factory updates for structural changes. Repairing only a changed root reduced the build median from 6,684.7 ms to 6,266.5 ms, a 6.26% improvement. Both builds emitted 3,056 files; only `assets/kudzu-list.js` differed because of the independently measured keyed restoration optimization. The normalization change itself adds no browser bytes.
141
+
142
+ ```text
143
+ baseline: [6488.0,7095.6,6861.0,6647.7,6563.4,6613.3,6536.1,6529.2,6462.6,6655.9,6684.7,6623.2,6504.9,7618.5,8174.5,9309.1,7611.4,7942.3,8148.2,7191.0,7380.7]
144
+ candidate: [5898.6,5903.9,6108.5,6088.1,5966.4,5913.8,6266.5,6115.1,6014.6,6022.0,6223.1,6357.5,6429.5,6950.8,7887.1,8275.9,7208.9,7306.3,7861.9,6798.8,6859.3]
145
+ ```
146
+
147
+ This external fixture is a candidate-finding and paired Kudzu regression benchmark, not a framework leaderboard result. It uses Kudzu-authored source; the catalog is generated once before timing and both revisions run the same complete Kudzu build. Its published cross-framework runs are sequential with only three build samples, and none of those framework numbers are used for this optimization claim. The paired command requires an existing symlink at `apps/shop-kudzu/node_modules/@kudzujs/core`. The external commit's pnpm 10.20 lock cannot install with `--frozen-lockfile`; `--force` accepts the unchanged lock and is a reproducibility limitation, although baseline and candidate use the same resulting dependency graph.
148
+
27
149
  ## 0.8.22 Versioned Compiler Foundation
28
150
 
29
- Measured UTC 2026-08-09 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 25.6.1, and npm 11.18.0. Baseline tag `v0.8.21` at `ff38092` and the current `0.8.22` compiler candidate used the same local volume and identical installed dependencies.
151
+ Measured UTC 2026-08-09 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 25.6.1, and npm 11.18.0. Baseline tag `v0.8.21` at `ff38092` and the then-current `0.8.22` compiler candidate used the same local volume and identical installed dependencies.
30
152
 
31
- The candidate implementation patch over `v0.8.21` had SHA-256 `e1d89c97dd8dfb60d41ae9c14ac64dfbb50466e024f80076f430ae9e725fb28f`, produced by:
153
+ The final `v0.8.21` to `v0.8.22` tagged implementation patch has reproducible SHA-256 `5fbdc3658b8c0d4d568c7ccdbf89c2c1c20c3a275ff9d0fd20e439b479d60530`, produced by:
32
154
 
33
155
  ```bash
34
- git diff --binary v0.8.21 -- framework/build.mjs framework/compiler/list-runtime-codegen.mjs framework/compiler/param-codegen.mjs framework/compiler/route-capability-planner.mjs framework/compiler/runtime-codegen.mjs framework/core.mjs framework/core.d.ts | shasum -a 256
156
+ git diff --binary v0.8.21 v0.8.22 -- framework/build.mjs framework/compiler/list-runtime-codegen.mjs framework/compiler/param-codegen.mjs framework/compiler/route-capability-planner.mjs framework/compiler/runtime-codegen.mjs framework/core.mjs framework/core.d.ts | shasum -a 256
35
157
  ```
36
158
 
159
+ The previously recorded candidate hash does not match this tagged patch, and no intermediate commit exists between the two release tags. The measured candidate is therefore not independently identifiable as the final `v0.8.22` tree from repository history.
160
+
37
161
  Both targets received one warm-up followed by seven clean `worker-effects` production builds in alternating round-robin order. Cleanup remained outside timing. Both medians were exactly 250.1 ms; the distributions overlap and establish no material change.
38
162
 
39
163
  | Target | Build median | Worker raw / gzip | Window raw / gzip |
@@ -50,14 +174,16 @@ Before release-content updates, the complete 135-page site and the `bindings`, `
50
174
 
51
175
  ## 0.8.21 Explicit Effect Ownership
52
176
 
53
- Measured UTC 2026-08-09 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 25.6.1, and npm 11.18.0. Baseline tag `v0.8.20` at `7fb6e37` and the current `0.8.21` compiler candidate used the same local volume and identical installed dependencies.
177
+ Measured UTC 2026-08-09 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 25.6.1, and npm 11.18.0. Baseline tag `v0.8.20` at `7fb6e37` and the then-current `0.8.21` compiler candidate used the same local volume and identical installed dependencies.
54
178
 
55
- The candidate implementation patch over `v0.8.20` had SHA-256 `ac0b1921bbbfb72f45d9b53338bec96bf9ab3d1680446148dfd8548b871bcbe4`, produced by:
179
+ The final `v0.8.20` to `v0.8.21` tagged implementation patch has reproducible SHA-256 `c78159ccce1f88a5ed06445d5e0b113953576529a5845e7772ab386b8adf166a`, produced by:
56
180
 
57
181
  ```bash
58
- git diff --binary v0.8.20 -- framework/build.mjs framework/compiler/descriptor-session.mjs framework/compiler/effect-analysis.mjs framework/compiler/ir/module-ir.mjs framework/compiler/worker-compiler.mjs framework/core.d.ts | shasum -a 256
182
+ git diff --binary v0.8.20 v0.8.21 -- framework/build.mjs framework/compiler/descriptor-session.mjs framework/compiler/effect-analysis.mjs framework/compiler/ir/module-ir.mjs framework/compiler/worker-compiler.mjs framework/core.d.ts | shasum -a 256
59
183
  ```
60
184
 
185
+ The previously recorded candidate hash does not match this tagged patch, and no intermediate commit exists between the two release tags. The measured candidate is therefore not independently identifiable as the final `v0.8.21` tree from repository history.
186
+
61
187
  Both targets received one warm-up followed by seven clean `worker-effects` production builds in alternating round-robin order. Cleanup remained outside timing. The distributions overlap; the 1.02% lower candidate median does not establish a material change.
62
188
 
63
189
  | Target | Build median | Worker raw / gzip | Window raw / gzip |
@@ -420,4 +546,4 @@ React toggle: [5.7, 6.1, 5.8, 5.8, 5.6, 5.8, 5.7]
420
546
  Kudzu toggle: [5.2, 5.4, 5.3, 5.6, 5.3, 5.2, 5.3]
421
547
  ```
422
548
 
423
- The reproducible fixture, benchmark harness, environment record, and JSON results are under `/home/kft/Documents/etc/demo/large-benchmark`.
549
+ The original fixture and harness were stored in an excluded local workspace and are unavailable from this checkout. The arrays above remain historical provenance, not an independently reproducible current claim.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
14
14
 
15
15
  > Experimental `0.8.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Latest release: 0.8.23 - Source compiler boundary.** Source normalization, TSX analysis, ModuleIR finalization, handler generation, and build-module generation now belong to a no-write source compiler. `build.mjs` coordinates discovery, source results, RouteIR, CapabilityIR, and artifact emission without analyzing TSX. Read the [release notes](./RELEASES.md#0823---source-compiler-boundary), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.23), or follow the [architecture packet](./docs/next-architecture/README.md).
17
+ **Latest release: 0.8.25 - Exact route-entry reuse.** Byte-identical generated route entries share one build-local transform result, compiler boundaries fail closed, and package release gates verify what is installed and published. The 1,011-page benchmark measures a 9.17% build improvement with identical deploy output. Read the [release notes](./RELEASES.md#0825---exact-route-entry-reuse), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.25), or follow the [architecture packet](./docs/next-architecture/README.md).
18
18
 
19
19
  - [Documentation](https://kudzujs.cloud/docs)
20
20
  - [Installation guide](https://kudzujs.cloud/docs#install)
@@ -123,7 +123,7 @@ React-shaped TypeScript/TSX
123
123
  -> complete HTML + route-specific capability ESM
124
124
  ```
125
125
 
126
- - `framework/compiler/normalization-pipeline.mjs` owns pass order and repairs AST parent pointers after every transform.
126
+ - `framework/compiler/normalization-pipeline.mjs` owns pass order and repairs AST parent pointers after structurally changed transforms.
127
127
  - Focused passes own React, React Router, browser-signal, animation-frame, timer, render-control, and Worker validation.
128
128
  - The main transformer produces descriptors; effect and handler codegen modules turn them into route-specific ESM.
129
129
  - Unsupported nearby syntax fails with source-located diagnostics instead of falling back to React or a generic runtime.
package/RELEASES.md CHANGED
@@ -1,5 +1,70 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.25 - Exact route-entry reuse
4
+
5
+ Kudzu 0.8.25 removes measured repeated esbuild work from large multi-route builds, strengthens compiler boundary validation, and makes package publication gates explicit without changing generated deploy bytes or browser behavior.
6
+
7
+ ### Changed in 0.8.25
8
+
9
+ - Generated native, parameter, and effect route entries reuse one build-local esbuild result when their complete source is byte-identical after route-relative URLs are resolved.
10
+ - Normalization passes now declare an immutable-root contract and reject any result that is not a TypeScript `SourceFile`.
11
+ - Finalized ModuleIR rejects invalid local slots and dangling signal, handler, derived, keyed-parent, keyed-child, keyed-selector, and ownership references.
12
+ - CI covers the minimum Node 22 runtime and the Node 24 Chrome suite, while release jobs exercise the packed package and verify package, lockfile, tag, and registry versions.
13
+ - Missing `0.8.21`, `0.8.22`, and `0.8.23` release pages and their canonical sitemap entries are restored.
14
+
15
+ ### Performance
16
+
17
+ - Seven alternating clean builds of the public 1,000-product, 1,011-page fixture measured 12,581.4 ms with exact-source reuse versus 13,851.0 ms with repeated transformation, a 9.17% improvement.
18
+ - Every deploy path and SHA-256 hash matched. The map is scoped to one build and only the three generated route-entry families; no persistent or generalized transform cache was added.
19
+ - Raw arrays, environment, fixture revision, methodology, and limitations are recorded in `PERFORMANCE.md`.
20
+
21
+ ### Validation
22
+
23
+ - `npm run check`, `npm run test:package`, and all 174 tests pass.
24
+ - The packed-package smoke test installs the produced tarball into a temporary consumer, imports both public entry points, runs the packed CLI, and verifies generated HTML.
25
+ - The external 1,011-page benchmark produced byte-identical deploy manifests across both targets.
26
+ - No accepted syntax, public runtime API, VDOM, hydration, scheduler, retained browser component tree, or `create-kudzu` template change was added.
27
+
28
+ ### Upgrade
29
+
30
+ ```bash
31
+ npm install @kudzujs/core@^0.8.25
32
+ ```
33
+
34
+ ## 0.8.24 - Measured Goal B optimizations
35
+
36
+ Kudzu 0.8.24 starts the architecture optimization Goal B with two benchmark-proven optimizations for large keyed restoration and large multi-route builds while preserving complete HTML, direct DOM ownership, and existing compiler output. This is distinct from the completed historical Worker capability milestone in `GOAL_B.md`.
37
+
38
+ ### Changed in 0.8.24
39
+
40
+ - A maintained `npm run benchmark:keyed` fixture measures 2,000 keyed rows with row-local state, reactive slicing/search, bulk restoration, retained-heavy append, reversal, DOM identity, reset state, and restored handlers across fresh Chrome profiles.
41
+ - Top-level flat lists batch mount-hook discovery through their connected parent only when more than 32 additions dominate both the next list and parent children. Nested lists, shared containers, and small or retained-heavy additions keep per-root mounting.
42
+ - `applyNormalizationPasses()` repairs TypeScript parent pointers only after a pass returns a structurally changed `SourceFile`; validators and no-op passes no longer walk the complete AST again.
43
+ - A focused test proves changed normalization output has repaired parent links before the next pass runs.
44
+ - Goal B records identical generated route entries as the next measured esbuild-transform investigation; no cache or generalized build system was added without isolated evidence.
45
+
46
+ ### Performance
47
+
48
+ - Twenty-one fresh Chrome profiles measured 2,000-row restoration at 21.1 ms versus 26.3 ms for `0.8.23`, a 19.77% improvement. A retained-heavy 33-row append showed no material change at 2.7 ms versus 2.6 ms.
49
+ - The keyed route adds 127 B raw / 35 B aggregate gzip JavaScript. Filter, reverse, and clean-build distributions establish no material regression.
50
+ - Twenty-one alternating clean builds of [`SimYunSup/kudzu-based-bench`](https://github.com/SimYunSup/kudzu-based-bench)'s 1,000-product, 1,011-page Kudzu fixture measured 6,266.5 ms versus 6,684.7 ms for `0.8.23`, a 6.26% improvement.
51
+ - Both commerce builds emitted the same 3,056 files. Only `assets/kudzu-list.js` changed for the independently measured runtime optimization; normalization changed no output bytes.
52
+ - Exact keyed worktree setup and the checked-in `benchmark:commerce` paired runner are documented in `PERFORMANCE.md` with raw arrays and limitations.
53
+
54
+ ### Validation
55
+
56
+ - `npm run check` and all 172 tests pass, including the new normalization boundary check.
57
+ - Node 22 focused compiler checks and the maintained Worker benchmark pass.
58
+ - Chrome-backed keyed list, selector, row-hook, Worker ownership, and the new large keyed benchmark checks pass.
59
+ - Before release-content updates, the complete site plus representative list, keyed-row-hook, and navigation builds retained identical file lists; only `assets/kudzu-list.js` changed, and normalized `.kudzu` trees remained identical to `v0.8.23`.
60
+ - No accepted syntax, public API behavior, VDOM, hydration, scheduler, cache, retained browser component tree, or `create-kudzu` template change was added.
61
+
62
+ ### Upgrade
63
+
64
+ ```bash
65
+ npm install @kudzujs/core@^0.8.24
66
+ ```
67
+
3
68
  ## 0.8.23 - Source compiler boundary
4
69
 
5
70
  Kudzu 0.8.23 completes the Goal A compiler foundation by moving source normalization, TSX semantic analysis, ModuleIR finalization, handler generation, and build-module generation behind one explicit no-write source compiler result.
@@ -7,7 +7,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
7
7
  | Goal | Decision | Start condition |
8
8
  |---|---|---|
9
9
  | A: compiler foundation | Complete in `0.8.23` | The no-write source compiler, RouteIR v1, CapabilityIR v1, generator boundaries, and output baseline are recorded. |
10
- | B: optimization benchmarks | Deferred | Start only by explicit decision from the recorded `0.8.23` baseline. |
10
+ | B: optimization benchmarks | Active | The first measured bulk keyed restoration optimization is retained; continue one reproduced loss at a time. |
11
11
  | C: state/resource model | Research only | Reduced fixtures expose a limitation |
12
12
  | D: routing compatibility | Current behavior preserved | Revisit only with migration evidence and invariant review |
13
13
 
@@ -27,7 +27,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
27
27
  2. [`goal-a-compiler-foundation.md`](./goal-a-compiler-foundation.md): approved extraction and generator work.
28
28
  3. [`versioning.md`](./versioning.md): planned patch sequence and completion rules.
29
29
  4. [`performance-gates.md`](./performance-gates.md): output, build, and browser gates.
30
- 5. [`goal-b-optimization-benchmarks.md`](./goal-b-optimization-benchmarks.md): deferred measured optimization.
30
+ 5. [`goal-b-optimization-benchmarks.md`](./goal-b-optimization-benchmarks.md): active measurements, retained optimizations, and continuation rules.
31
31
  6. [`goal-c-state-resource-research.md`](./goal-c-state-resource-research.md): research boundary only.
32
32
  7. [`goal-d-routing-compatibility-decisions.md`](./goal-d-routing-compatibility-decisions.md): routing and islands decisions.
33
33
 
@@ -1,6 +1,6 @@
1
1
  # Current Compiler Architecture
2
2
 
3
- This maps the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
3
+ This maps the current `0.8.24` architecture built on the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
4
4
 
5
5
  ## Responsibility Map
6
6
 
@@ -9,7 +9,7 @@ This maps the completed `0.8.23` Goal A compiler foundation. File and function n
9
9
  | CLI entry | [`bin/kudzu.mjs`](../../bin/kudzu.mjs) | Dispatches build and development commands. |
10
10
  | Build orchestration | [`framework/build.mjs`](../../framework/build.mjs), `build()` | Coordinates config, discovery, source compilation, RouteIR rendering, CapabilityIR projection, generator invocation, artifact emission, and `afterBuild`. |
11
11
  | Reachability/import resolution | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `reachableSourceFiles()`; [`framework/compiler/source-graph.mjs`](../../framework/compiler/source-graph.mjs), `resolveSourceImport()` | Starts from page entries, follows relative runtime imports/re-exports and validated Worker references, and excludes unreachable migration source. |
12
- | Ordered normalization | [`framework/compiler/normalization-pipeline.mjs`](../../framework/compiler/normalization-pipeline.mjs), `applyNormalizationPasses()`; [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `normalizeCompilerSource()` | Applies migration/resource passes in order and repairs TypeScript parent pointers after every pass. Imported source uses the same pipeline. |
12
+ | Ordered normalization | [`framework/compiler/normalization-pipeline.mjs`](../../framework/compiler/normalization-pipeline.mjs), `applyNormalizationPasses()`; [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `normalizeCompilerSource()` | Applies migration/resource passes in order and repairs TypeScript parent pointers after every structural change. Imported source uses the same pipeline. |
13
13
  | Focused normalization passes | [`framework/compiler/`](../../framework/compiler/) | React, Router, browser signals, animation-frame refs, custom-hook timers, Zustand, and render control each validate and lower a narrow source shape. |
14
14
  | Shared AST/scope helpers | [`framework/compiler/ast-helpers.mjs`](../../framework/compiler/ast-helpers.mjs) | Binding, scope, reference, effect-return, and source-location analysis. |
15
15
  | Pure collection language | [`framework/compiler/collection-analysis.mjs`](../../framework/compiler/collection-analysis.mjs) | Analyzes collection roots/selectors and serializes the allowed pure expression language used by lists and derived dependencies. |
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- Deferred until Goal A is complete. Do not combine optimization with the compiler-boundary patches: structural movement would invalidate attribution and make output drift harder to review.
5
+ Active from the completed Goal A baseline at `0.8.23` commit `39a065b`. Maintained keyed restoration and external 1,000-product build measurements authorized the first two narrow optimizations.
6
6
 
7
7
  ## Purpose
8
8
 
@@ -21,6 +21,18 @@ At Goal A completion, record one source revision and the current maintained fixt
21
21
 
22
22
  Historical figures in [`PERFORMANCE.md`](../../PERFORMANCE.md) and the top-level Goal records are provenance, not an automatic current baseline. Reproduce a fixture before using it to approve work.
23
23
 
24
+ ### First Baseline Pass
25
+
26
+ The maintained Worker benchmark measured a 289.9 ms clean-build median, 907 B raw / 477 B gzip Worker graph, and 12,148 B raw / 5,411 B gzip window graph on Node 22. Its Chrome throughput, cadence, stale-write, bounded-history, and repeated ownership gate passed.
27
+
28
+ The preserved external 1,000-row keyed fixture was used only to find a candidate, not as a maintained approval benchmark. Its operations were fast, so a repository-owned benchmark was added for 2,000 rows with local state and reactive search. It measures filtering to one retained row, restoring 1,999 fresh rows, and reversing all rows in fresh Chrome profiles while checking retained identity, released identity, reset state, and restored handlers.
29
+
30
+ The maintained benchmark measured a 26.3 ms restoration median. For top-level flat lists where more than 32 additions are a majority of both the next rows and their parent children, mounting the connected list parent once rather than invoking every mount hook for every new row reduced the median to 21.1 ms (19.77%). A retained-heavy 33-row append stayed on the per-root path and showed no material change at 2.7 ms versus 2.6 ms. The route added 127 B raw / 35 B aggregate gzip JavaScript.
31
+
32
+ [`SimYunSup/kudzu-based-bench`](https://github.com/SimYunSup/kudzu-based-bench) then supplied a 1,000-product, 1,011-page build fixture. Twenty-one alternating clean builds measured 6,684.7 ms for clean `0.8.23` and 6,266.5 ms after skipping parent-pointer repair for normalization passes that return the unchanged AST, a 6.26% improvement. The output retained 3,056 files and only the separately optimized `kudzu-list.js` changed. Full raw arrays and limitations are recorded in [`PERFORMANCE.md`](../../PERFORMANCE.md).
33
+
34
+ The `0.8.25` investigation isolated repeated esbuild work by comparing ordinary transformation with build-local reuse keyed by the complete generated route-entry source. Seven alternating 1,011-page builds measured 13,851.0 ms and 12,581.4 ms medians, a 9.17% improvement, with identical emitted paths and hashes. The retained implementation applies only to native, parameter, and effect route entries and keeps no data beyond one build.
35
+
24
36
  ## Candidate Order
25
37
 
26
38
  1. Profile the largest measured loss in a maintained fixture.
@@ -30,6 +42,8 @@ Historical figures in [`PERFORMANCE.md`](../../PERFORMANCE.md) and the top-level
30
42
 
31
43
  Known historical pressure points include large keyed-list removal/reconciliation and broad runtime specialization, but neither is authorized without a reproduced current loss.
32
44
 
45
+ Repeated esbuild transformation of byte-identical generated route entries cleared the materiality and output gates. Further optimization is not authorized until another current fixture isolates a measured loss; do not broaden the route-entry map into a generalized cache.
46
+
33
47
  ## Benchmark Contract
34
48
 
35
49
  - Match visible content, behavior, errors, accessibility, and navigation semantics.
@@ -51,9 +65,9 @@ Reject an optimization that:
51
65
 
52
66
  ## Continuation Checklist
53
67
 
54
- - [ ] Verify Goal A acceptance is complete.
55
- - [ ] Freeze and record the post-Goal-A baseline revision.
56
- - [ ] Select one measured loss, not a speculative hotspot.
57
- - [ ] Add one minimal benchmark or reuse a maintained one.
58
- - [ ] Record before/after raw arrays and artifacts.
68
+ - [x] Verify Goal A acceptance is complete.
69
+ - [x] Freeze and record the post-Goal-A baseline revision.
70
+ - [x] Select one measured loss, not a speculative hotspot.
71
+ - [x] Add one minimal benchmark or reuse a maintained one.
72
+ - [x] Record before/after raw arrays and artifacts.
59
73
  - [ ] Revert experiments that do not clear correctness and materiality gates.
@@ -1,6 +1,6 @@
1
1
  # Planned Version Sequence
2
2
 
3
- This is an execution sequence, not release history. `0.8.16` through `0.8.23` are completed scopes represented by package/release records.
3
+ This is an execution sequence, not release history. `0.8.16` through `0.8.24` are completed scopes represented by package/release records.
4
4
 
5
5
  Keep each patch behavior-preserving and independently reviewable. If a boundary proves inseparable, revise this plan before combining releases; do not silently broaden a patch.
6
6
 
@@ -14,10 +14,11 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
14
14
  | `0.8.21` | Replace effect AST side tables with EffectIR covering setup, cleanup, dependencies, ownership, source, and Worker edges. | Route/layout/conditional/list lifetime, stale-write isolation, and exact resource cleanup remain unchanged. |
15
15
  | `0.8.22` | Version the existing RouteIR and CapabilityIR, formalize numeric slots plus readable debug metadata, and extract focused runtime generators. | RouteIR and CapabilityIR reject unsupported versions; runtime generator contracts, output gates, and the recorded benchmark pass. |
16
16
  | `0.8.23` | Forward-fix Goal A by moving source normalization, semantic analysis, ModuleIR finalization, handler generation, and build-module generation behind one no-write source compiler result. | `build.mjs` contains no TSX feature analysis; source results are JSON-safe and project-relative; diagnostics, deploy bytes, tests, and build performance remain equivalent. |
17
+ | `0.8.24` | Start Goal B with measured large keyed restoration and no-op normalization optimizations. | Fresh-profile keyed restoration improves materially; alternating 1,000-product builds improve materially; correctness, output, and byte deltas are recorded. |
17
18
 
18
19
  ## Sequence Rules
19
20
 
20
- - Goal A is complete. Goal B optimization remains deferred until explicitly started from the recorded `0.8.23` baseline.
21
+ - Goal A is complete. Goal B proceeds from the `0.8.23` baseline one repeatable measured loss at a time.
21
22
  - Keep Goal C research out of these patches and publish no store/resource API.
22
23
  - Keep Goal D behavior unchanged: complete HTML, native default, opt-in groups, no islands.
23
24
  - A planned patch may ship under a different actual version only if this table and release-facing documentation are updated before release.
@@ -38,7 +38,7 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
38
38
  - `compiler/collection-analysis.mjs`: pure collection expression IR, selector pipeline, alias, and imported-transform analysis.
39
39
  - `compiler/descriptor-session.mjs`: private source-local descriptor registration and deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, EffectIR, import, and client-root finalization.
40
40
  - `compiler/effect-analysis.mjs`: ordered effect dependency classification and cleanup-owned browser resource validation before EffectIR registration.
41
- - `compiler/normalization-pipeline.mjs`: ordered source normalization with parent-pointer repair after every compiler pass.
41
+ - `compiler/normalization-pipeline.mjs`: ordered source normalization with parent-pointer repair after each structurally changed source root.
42
42
  - `compiler/param-codegen.mjs`: runtime pathname and search-parameter entry generation from rendered route descriptors.
43
43
  - `compiler/react-migration-pass.mjs`: React import, memo, callback, `forwardRef`, and `useId` migration validation and lowering.
44
44
  - `compiler/custom-hook-timer-pass.mjs`: private custom-hook timeout-ref validation and compiler-owned timer-state lowering.
@@ -90,4 +90,4 @@ A direct setter may cross one same-file or relative-imported component boundary
90
90
 
91
91
  `kudzu.config` may opt one emitted shared-layout group into same-document navigation with legacy `navigation: { routes: ["/product", "/items/[id]"] }`, or multiple groups with `navigation: { groups: [{ routes: [...] }, { routes: [...] }] }`. The forms are mutually exclusive. Identities are globally unique emitted exact paths or `runtimeParams` patterns; each group uses one page-exported layout function identity. Runtime records securely match concrete pathnames under `base`, and their cache-safe parameter initializer runs before route DOM/effects mount on every transition. Each group receives a deterministic route-hashed asset specialized to only its records, pattern decoder, and effect/parameter lifecycle needs. Cross-group and ungrouped anchors remain native and are not prefetched; overlapping path domains across groups fail the build. Route effect entries export cache-safe layout and route mount functions: layout effects, including conditional/keyed DOM-owned effects, persist for the group session; route effects receive a fresh owner registry after each route insertion; and non-persisted page disposal cleans route before layout. Direct primitive state, runtime parameter, and keyed-item property dependencies and cleanup are supported. Fragment payloads and coordinated View Transitions are not implemented.
92
92
 
93
- Cross-framework performance tables are historical snapshots from an excluded local benchmark workspace; the current checkout does not contain their competitor fixtures, runners, or raw arrays. The maintained provenance warning and tables live in the web docs. `npm run benchmark` is the tracked reproducible Worker fixture measurement, while the focused Chrome Worker test verifies throughput, cadence, bounded history, stale-write isolation, and route lifecycle behavior.
93
+ Cross-framework performance tables are historical snapshots from excluded workspaces and are not current rankings. `npm run benchmark` tracks the Worker build/graph fixture; `RUNS=21 npm run benchmark:keyed` tracks large keyed restoration with fresh Chrome profiles. Current methodology, raw arrays, external-fixture limits, and artifact deltas live in `PERFORMANCE.md` and the web docs.
@@ -6,7 +6,7 @@ import { build as bundle, transform } from "esbuild"
6
6
  import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
7
7
  import { generateListRuntime } from "./compiler/list-runtime-codegen.mjs"
8
8
  import { assetPath, browserPath, relativeModulePath, withBase } from "./compiler/path-helpers.mjs"
9
- import { clientModulePath, collectClientModules, compileClientModule, compiledPath, compileSource, layoutExportError, orderSourceStyles, reachableSourceFiles, safeStaticFiles } from "./compiler/source-compiler.mjs"
9
+ import { collectClientModules, compileClientModule, compiledPath, compileSource, layoutExportError, orderSourceStyles, reachableSourceFiles, safeStaticFiles } from "./compiler/source-compiler.mjs"
10
10
  import { createParamCodegen } from "./compiler/param-codegen.mjs"
11
11
  import { planRouteCapabilities, usesRouteDependencyRuntime } from "./compiler/route-capability-planner.mjs"
12
12
  import { generateBindingRuntime, generateCoreRuntime, generateEffectRuntime, generateNativeRuntime, generateNavigationRuntime, specializeRuntime } from "./compiler/runtime-codegen.mjs"
@@ -95,6 +95,7 @@ export async function build({ quiet = false, minify = true } = {}) {
95
95
  const effectEntries = []
96
96
  const nativeEntries = []
97
97
  const paramEntries = []
98
+ const routeEntryTransforms = new Map()
98
99
  const rewrites = []
99
100
  const emittedRoutes = new Set()
100
101
  const emittedApplicationRoutes = new Set()
@@ -264,7 +265,7 @@ export async function build({ quiet = false, minify = true } = {}) {
264
265
  if (hasNativeHandlers) {
265
266
  const generated = generateNativeRuntime(await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"), capabilityIR)
266
267
  await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), generated.source, minify, generated.define)
267
- for (const entry of nativeEntries) await printNativeEntry(entry, assetsDirectory, base, minify)
268
+ for (const entry of nativeEntries) await printNativeEntry(entry, assetsDirectory, base, minify, routeEntryTransforms)
268
269
  }
269
270
  if (navigationGroups.length) {
270
271
  const navigationSource = await readFile(new URL("./navigation-runtime.js", import.meta.url), "utf8")
@@ -280,12 +281,12 @@ export async function build({ quiet = false, minify = true } = {}) {
280
281
  for (const entry of paramEntries) {
281
282
  const output = join(assetsDirectory, entry.path)
282
283
  await mkdir(dirname(output), { recursive: true })
283
- await writeJavaScript(output, printParamEntry(entry.schema, entry.params, entry.searchParams, entry.searchParamsWritable, output, assetsDirectory, base, runtimeName(entry.usesDependencyRuntime), entry.navigable), minify)
284
+ await writeRouteEntry(output, printParamEntry(entry.schema, entry.params, entry.searchParams, entry.searchParamsWritable, output, assetsDirectory, base, runtimeName(entry.usesDependencyRuntime), entry.navigable), minify, routeEntryTransforms)
284
285
  }
285
286
  for (const entry of effectEntries) {
286
287
  const output = join(assetsDirectory, entry.path)
287
288
  await mkdir(dirname(output), { recursive: true })
288
- await writeJavaScript(output, printEffectEntry(entry.effects, output, emittedHandlerModules, assetsDirectory, base, entry.paramPath, runtimeName(entry.usesDependencyRuntime), entry.navigable), minify)
289
+ await writeRouteEntry(output, printEffectEntry(entry.effects, output, emittedHandlerModules, assetsDirectory, base, entry.paramPath, runtimeName(entry.usesDependencyRuntime), entry.navigable), minify, routeEntryTransforms)
289
290
  }
290
291
  const clientModules = await collectClientModules(emittedHandlerModules.flatMap(module => module.clientImports).map(file => resolve(root, file)), sourceFileSet)
291
292
  for (const file of clientModules) {
@@ -358,13 +359,13 @@ function preloadModules(html) {
358
359
  return html.replace(scripts[0][0], `${links}${scripts[0][0]}`)
359
360
  }
360
361
 
361
- async function printNativeEntry(entry, assetsDirectory, base, minify) {
362
+ async function printNativeEntry(entry, assetsDirectory, base, minify, transforms) {
362
363
  const output = join(assetsDirectory, entry.path)
363
364
  await mkdir(dirname(output), { recursive: true })
364
365
  const imports = entry.modules.map((module, index) => `import * as __kNativeModule${index} from ${JSON.stringify(module)}`).join("\n")
365
366
  const registrations = entry.modules.map((module, index) => `[${JSON.stringify(module)}, __kNativeModule${index}]`).join(",")
366
367
  const runtime = assetPath(base, "assets/kudzu-native.js")
367
- await writeJavaScript(output, `import { registerNativeModules } from ${JSON.stringify(runtime)}\n${imports}\nregisterNativeModules([${registrations}])`, minify)
368
+ await writeRouteEntry(output, `import { registerNativeModules } from ${JSON.stringify(runtime)}\n${imports}\nregisterNativeModules([${registrations}])`, minify, transforms)
368
369
  }
369
370
 
370
371
  function runtimeEffects(effects, lifetimes = false) {
@@ -388,6 +389,15 @@ async function writeJavaScript(file, source, minify, define) {
388
389
  await writeFile(file, code)
389
390
  }
390
391
 
392
+ async function writeRouteEntry(file, source, minify, transforms) {
393
+ let code = transforms.get(source)
394
+ if (code === undefined) {
395
+ code = minify ? (await transform(source, { format: "esm", legalComments: "none", minify, target: "es2022" })).code : source
396
+ transforms.set(source, code)
397
+ }
398
+ await writeFile(file, code)
399
+ }
400
+
391
401
  async function writeBundledJavaScript(file, source, minify, define) {
392
402
  const result = await bundle({
393
403
  stdin: { contents: source, resolveDir: dirname(file), sourcefile: file },
@@ -2,7 +2,7 @@ import ts from "typescript"
2
2
  import { createComponentAnalysis } from "./analysis/component-analysis.mjs"
3
3
  import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
4
4
  import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
5
- import { createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerEffect, registerKeyedBlock, registerModuleHandler } from "./ir/module-ir.mjs"
5
+ import { assertModuleIRReferences, createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerEffect, registerKeyedBlock, registerModuleHandler } from "./ir/module-ir.mjs"
6
6
 
7
7
  export function createSemanticArtifact(file) {
8
8
  return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file) }
@@ -222,6 +222,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
222
222
  const imports = [...callbacks, ...reactiveBindings].flatMap(entry => entry.imports ?? []).map(importRecord)
223
223
  moduleIR.imports = [...new Map(imports.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry])).values()]
224
224
  moduleIR.clientModules = [...clientModules]
225
+ assertModuleIRReferences(moduleIR)
225
226
  }
226
227
 
227
228
  function source(node) {
@@ -2,6 +2,33 @@ export function createModuleIR(file) {
2
2
  return { version: 1, file, signals: [], handlers: [], bindings: [], derived: [], effects: [], keyedBlocks: [], imports: [], clientModules: [] }
3
3
  }
4
4
 
5
+ export function assertModuleIRReferences(moduleIR) {
6
+ const slot = (records, value, label) => {
7
+ if (!Number.isInteger(value) || value < 0 || value >= records.length) throw new Error(`ModuleIR ${label} references missing slot ${JSON.stringify(value)}`)
8
+ }
9
+ for (const [name, records] of [["signal", moduleIR.signals], ["handler", moduleIR.handlers], ["binding", moduleIR.bindings], ["derived", moduleIR.derived], ["effect", moduleIR.effects], ["keyed block", moduleIR.keyedBlocks]]) {
10
+ records.forEach((record, index) => {
11
+ if (record.slot !== index) throw new Error(`ModuleIR ${name} slot ${JSON.stringify(record.slot)} must equal its index ${index}`)
12
+ })
13
+ }
14
+ for (const handler of moduleIR.handlers) {
15
+ for (const command of handler.commands ?? []) slot(moduleIR.signals, command.signal, `handler ${handler.slot} command signal`)
16
+ if (handler.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, handler.keyedBlock, `handler ${handler.slot} keyed block`)
17
+ }
18
+ for (const binding of moduleIR.bindings) if (binding.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, binding.keyedBlock, `binding ${binding.slot} keyed block`)
19
+ for (const effect of moduleIR.effects) {
20
+ slot(moduleIR.handlers, effect.setup?.handler, `effect ${effect.slot} setup handler`)
21
+ for (const dependency of effect.dependencies ?? []) if (dependency.kind === "derived") slot(moduleIR.derived, dependency.derived, `effect ${effect.slot} derived dependency`)
22
+ if (effect.ownership?.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, effect.ownership.keyedBlock, `effect ${effect.slot} keyed block`)
23
+ }
24
+ for (const block of moduleIR.keyedBlocks) {
25
+ if (block.parent !== undefined) slot(moduleIR.keyedBlocks, block.parent, `keyed block ${block.slot} parent`)
26
+ for (const child of block.children ?? []) slot(moduleIR.keyedBlocks, child, `keyed block ${block.slot} child`)
27
+ if (block.selector !== undefined) slot(moduleIR.derived, block.selector, `keyed block ${block.slot} selector`)
28
+ }
29
+ return moduleIR
30
+ }
31
+
5
32
  export function registerCommandHandler(moduleIR, commands, source, scope = "module") {
6
33
  const slots = new Map(moduleIR.signals.map(signal => [signal.key, signal.slot]))
7
34
  for (const { state, owner = scope } of commands) {
@@ -1,9 +1,12 @@
1
1
  import ts from "typescript"
2
2
 
3
3
  export function applyNormalizationPasses(sourceFile, passes) {
4
- for (const pass of passes) {
5
- sourceFile = pass(sourceFile)
6
- ts.setParentRecursive(sourceFile, false)
4
+ // Passes treat their input root as immutable and return the SourceFile used by the next pass.
5
+ for (const [index, pass] of passes.entries()) {
6
+ const next = pass(sourceFile)
7
+ if (!next || !ts.isSourceFile(next)) throw new TypeError(`Normalization pass ${index + 1} must return a TypeScript SourceFile`)
8
+ if (next !== sourceFile) ts.setParentRecursive(next, false)
9
+ sourceFile = next
7
10
  }
8
11
  return sourceFile
9
12
  }
@@ -282,7 +282,8 @@ function updateList(list) {
282
282
  if (run.firstChild) parent.insertBefore(run, list.boundary)
283
283
  ordered = true
284
284
  } else parent.insertBefore(additions, list.boundary)
285
- if (addedNodes) for (const node of addedNodes) mountDom(node)
285
+ if (addedNodes?.length > 32 && addedNodes.length * 2 > next.length && addedNodes.length * 2 > parent.children.length && !list.descriptor.children && !list.descriptor.ownerField) mountDom(parent)
286
+ else if (addedNodes) for (const node of addedNodes) mountDom(node)
286
287
  list.container ??= parent
287
288
  }
288
289
  let anchor = list.boundary
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.23",
3
+ "version": "0.8.25",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,7 +54,10 @@
54
54
  "dev": "node ./bin/kudzu.mjs dev",
55
55
  "check": "tsc --noEmit && tsc -p test/fixtures/tsconfig.json --noEmit && node ./bin/kudzu.mjs build",
56
56
  "test": "node --test test/*.test.mjs",
57
+ "test:package": "node test/package-smoke.mjs",
57
58
  "benchmark": "node test/performance.mjs",
59
+ "benchmark:keyed": "node test/keyed-performance.mjs",
60
+ "benchmark:commerce": "node test/commerce-build-performance.mjs",
58
61
  "prepublishOnly": "npm run check && npm test",
59
62
  "deploy": "wrangler deploy",
60
63
  "preview": "wrangler dev"