@kudzujs/core 0.8.40 → 0.8.42
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/MIGRATION_ROADMAP.md +1 -1
- package/PERFORMANCE.md +70 -0
- package/README.md +1 -1
- package/RELEASES.md +71 -0
- package/docs/next-architecture/README.md +2 -2
- package/docs/next-architecture/compiler-current-architecture.md +3 -3
- package/docs/next-architecture/goal-b-optimization-benchmarks.md +1 -1
- package/docs/next-architecture/large-application-ai-native-roadmap.md +2 -2
- package/docs/next-architecture/versioning.md +3 -1
- package/framework/README.md +2 -2
- package/framework/build.mjs +59 -26
- package/framework/compiler/source-compiler.mjs +36 -16
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This document is the source of truth for Kudzu's product direction, architecture invariants, and future development order. Read it before extending React-shaped syntax or browser capabilities.
|
|
4
4
|
|
|
5
|
-
The executable post-`0.8.
|
|
5
|
+
The executable post-`0.8.42` compiler and large-application sequence is maintained in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md). Follow its PR dependencies for implementation work; this document remains authoritative when selecting or accepting a migration capability.
|
|
6
6
|
|
|
7
7
|
[`GOAL_A.md`](./GOAL_A.md) and [`GOAL_B.md`](./GOAL_B.md) are completed capability-validation records. Their commerce and realtime dashboard fixtures prove general lifecycle, navigation, async-workflow, and Worker capabilities; they are not separate product verticals or future priority lists.
|
|
8
8
|
|
package/PERFORMANCE.md
CHANGED
|
@@ -2,6 +2,76 @@
|
|
|
2
2
|
|
|
3
3
|
Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm run benchmark:native`, and `npm run benchmark:module-cache` 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
4
|
|
|
5
|
+
## Current 0.8.42 Release Snapshot
|
|
6
|
+
|
|
7
|
+
Kudzu 0.8.42 retains the tracked 0.8.41 runtime and six-route commerce matrices below and adds two focused external-fixture optimizations. Those focused samples compare Kudzu before and after the patch; they do not establish a current cross-framework ranking.
|
|
8
|
+
|
|
9
|
+
The maintained external 1,000-product fixture exposed 1,011 byte-identical native route entries and 1,011 byte-identical effect route entries. A three-run check against clean `v0.8.41` produced:
|
|
10
|
+
|
|
11
|
+
| Target | Cold build | Warm build | Output |
|
|
12
|
+
|---|---:|---:|---:|
|
|
13
|
+
| `v0.8.41` | 13,866 ms | 13,560 ms | 10.48 MB |
|
|
14
|
+
| Route-entry sharing candidate | 13,203 ms | 13,087 ms | 9.53 MB |
|
|
15
|
+
|
|
16
|
+
Cold build is 4.8% lower, warm build is 3.5% lower, and output is 9.1% smaller in this sample. A 100-product artifact inspection reduced 101 native and 101 effect route files to three native and five effect files because only byte-identical generated sources share paths. Single-route URLs and nonidentical entries retain their existing route paths. A seven-run alternating measurement is still required before making a stronger build-time attribution claim.
|
|
17
|
+
|
|
18
|
+
The external form fixture then replaced effect-delayed query carry with direct read-only query bindings on hidden `value` and `disabled` properties. Five Slow 4G sessions measured readiness at 348 ms versus the prior 783 ms, a 55.6% reduction. This is a narrow critical form path, not a general inline-capability policy; writable search state, handlers, effects, conditions, lists, unrelated bindings, and nonmatching markup retain the existing parameter capability path.
|
|
19
|
+
|
|
20
|
+
A static-catalog same-document navigation experiment was rejected. After compressing concrete route records to remove the first implementation's payload blowup, three Slow 4G sessions still favored native document navigation:
|
|
21
|
+
|
|
22
|
+
| Navigation | Detail | Back | Session transfer | Degraded capabilities |
|
|
23
|
+
|---|---:|---:|---:|---:|
|
|
24
|
+
| Native document | 314 ms | 107 ms | 322.2 KB | 15/18 |
|
|
25
|
+
| Enhanced candidate | 575 ms | 297 ms | 511 KB | 12/18 |
|
|
26
|
+
|
|
27
|
+
The static `getStaticPaths()` pattern expansion was removed. Existing exact and `runtimeParams` enhanced-navigation groups are unchanged, and native document navigation remains the default.
|
|
28
|
+
|
|
29
|
+
## Maintained 0.8.41 Cross-Framework Snapshot
|
|
30
|
+
|
|
31
|
+
Measured UTC 2026-08-12 on the Intel Core i5-9500 Linux x64 host, Node 24.14.0, and Chrome 142.0.7444.175. The tracked runtime matrix used one warm-up, seven interleaved clean builds, and seven rotating fresh Chrome profiles. Every correctness, accessibility, identity, effect-cleanup, and browser-error gate passed. Kudzu emits complete initial HTML while React, Vue, and Svelte start from client-rendered shells, so initial delivery and artifact totals are not architecture-equivalent.
|
|
32
|
+
|
|
33
|
+
| Runtime matrix target | Build median | HTML raw / gzip B | JS raw / gzip B | Total raw / gzip B | Complete initial DOM |
|
|
34
|
+
|---|---:|---:|---:|---:|---:|
|
|
35
|
+
| Kudzu `0.8.41` release tree | 1,475.480 ms | 179,388 / 37,008 | 33,575 / 12,928 | 212,963 / 49,936 | 532.9 ms |
|
|
36
|
+
| React 19.2.8 + Vite 8.1.5 | 856.231 ms | 282 / 219 | 193,685 / 60,043 | 193,967 / 60,262 | 510.6 ms |
|
|
37
|
+
| Vue 3.5.40 + Vite 8.1.5 | 1,053.062 ms | 281 / 221 | 64,023 / 24,772 | 64,304 / 24,993 | 357.6 ms |
|
|
38
|
+
| Svelte 5.56.7 + Vite 8.1.5 | 1,733.637 ms | 281 / 219 | 40,726 / 15,659 | 41,007 / 15,878 | 401.1 ms |
|
|
39
|
+
|
|
40
|
+
Raw runtime arrays, quartiles, checkout metadata, source hash, and validation results are checked in at `benchmarks/runtime-matrix/results/raw.json`. The maintained 2,000-row keyed run used one warm-up, seven clean builds, and seven fresh Chrome profiles: build 901.4 ms, append 14.7 ms, filter 25.4 ms, restore 126.2 ms, reverse 25.1 ms, and JavaScript 28,450 B raw / 11,036 B gzip. A requested 21-profile keyed run exceeded the 600-second limit and produced no result.
|
|
41
|
+
|
|
42
|
+
The maintained Worker benchmark recorded a 1,823.2 ms build median, 907 B raw / 477 B gzip Worker graph, and 12,148 B raw / 5,411 B gzip aggregate window graph. The tracked six-route commerce sources used one warm-up and seven rotating clean builds:
|
|
43
|
+
|
|
44
|
+
| Commerce target | Build median | Files | HTML raw / gzip B | JS raw / gzip B | Total raw / gzip B |
|
|
45
|
+
|---|---:|---:|---:|---:|---:|
|
|
46
|
+
| Kudzu | 867.188 ms | 17 | 17,123 / 5,376 | 18,428 / 8,261 | 37,434 / 14,689 |
|
|
47
|
+
| React 19.2.8 SSR + Vite hydration | 859.125 ms | 10 | 9,304 / 4,265 | 198,261 / 61,464 | 209,270 / 66,741 |
|
|
48
|
+
| Next.js 16.2.11 static export | 7,290.533 ms | 74 | 72,890 / 19,857 | 643,484 / 191,844 | 814,186 / 247,596 |
|
|
49
|
+
| Nuxt 4.5.0 generation | 7,334.287 ms | 26 | 17,557 / 7,850 | 191,758 / 70,925 | 215,851 / 82,067 |
|
|
50
|
+
| SvelteKit 2.70.1 static export | 4,725.279 ms | 19 | 15,832 / 6,511 | 85,095 / 33,477 | 102,659 / 41,047 |
|
|
51
|
+
|
|
52
|
+
The commerce targets share initial content and behavior contracts but use materially different architectures. The browser suite timed out in Kudzu's existing in-flight rejection-navigation wait before cross-target sampling, so no commerce browser timing is claimed.
|
|
53
|
+
|
|
54
|
+
## P1 Direct Two-Boundary Callback And Ref Dataflow
|
|
55
|
+
|
|
56
|
+
Measured UTC 2026-08-12 on the Intel Core i5-9500 Linux x64 host with 6 physical cores, Node 24.14.0, and npm 11.9.0. The baseline was clean tag `v0.8.40` at `c88b94f91b40d76fad58a208f3261de399c6d2b4`. The compiler and migration checks had SHA-256 `f80ec9c532376a90805c6d99b8cbfa162578592c71c5e62f1cb92e16a64a5ba3`.
|
|
57
|
+
|
|
58
|
+
One warm-up and seven alternating clean fresh-process builds produced byte-identical 176-file deploy graphs, 3,850,245 raw bytes, 1,984,991 aggregate gzip bytes, deploy SHA-256 `d24f9d4608b9ae096fa5e334cf8c6556e51d8588bff02d15e3eacf0c6711db81`, and `kudzu-plan.json` SHA-256 `302d75dac6f58306c139b398a436480cf60cdd5743e6100ab1c728bd255e16be`.
|
|
59
|
+
|
|
60
|
+
| Target | Clean build median | Range | Peak RSS median |
|
|
61
|
+
|---|---:|---:|---:|
|
|
62
|
+
| `v0.8.40` | 3,565.236 ms | 2,960.844-3,995.094 ms | 364.6 MiB |
|
|
63
|
+
| Two-boundary candidate | 3,470.277 ms | 3,099.259-3,842.591 ms | 362.0 MiB |
|
|
64
|
+
|
|
65
|
+
The candidate unpaired median is 2.66% lower; round-paired candidate-minus-baseline differences have a +59.834 ms median. Peak RSS is 0.74% lower. Ranges overlap and no material performance change is claimed.
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
v0.8.40: [3671.298,3995.094,3776.594,3295.688,2960.844,3565.236,3563.737]
|
|
69
|
+
candidate: [3099.259,3674.641,3842.591,3400.780,3418.781,3470.277,3623.571]
|
|
70
|
+
paired candidate-baseline: [-572.039,-320.453,65.997,105.092,457.937,-94.959,59.834]
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The FIRE-derived callback/ref fixture now forwards both a direct setter adapter and a simple state callback through one imported presentation component. Browser checks preserve parent state updates, child-local state/effects/IDs, parent ref resolution, conditional cleanup, fresh remount, and a zero-JavaScript static sibling. ComponentAnalysis retains the same parent SignalIR on each nested specialization. A third callback-carrying boundary remains rejected. No browser runtime, callback registry, component function, route artifact, or deploy byte was added.
|
|
74
|
+
|
|
5
75
|
## P1 Property-Level Object-State Effect Dependencies
|
|
6
76
|
|
|
7
77
|
Measured UTC 2026-08-12 on the Intel Core i5-9500 Linux x64 host with 6 physical cores, Node 24.14.0, npm 11.9.0, and Chrome 142.0.7444.175. The baseline was clean tag `v0.8.39` at `090b42124d596260bdd6a0814c014e5b906dc0eb`. The focused implementation and migration checks had SHA-256 `c097c10e786ff5d09537b52384c1bf3d32cb46998ef62125c23dda3355b62277`, produced by:
|
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.
|
|
17
|
+
**Latest release: 0.8.42 - Large-route artifact sharing.** Byte-identical generated route entries now share one emitted file within a build, and a narrow read-only query form shape initializes hidden carry fields before deferred route capabilities arrive. Native document navigation remains the default after the measured static-catalog experiment lost on latency, transfer, and resilience. Read the [release notes](./RELEASES.md#0842---large-route-artifact-sharing), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.42), 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)
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.8.42 - Large-route artifact sharing
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.42 reduces generated filesystem work for large static route sets and accelerates one proven hidden query form carry shape without adding a general runtime or changing the native-navigation default.
|
|
6
|
+
|
|
7
|
+
### Changed in 0.8.42
|
|
8
|
+
|
|
9
|
+
- Byte-identical native, parameter, and effect route-entry sources share the first generated route path and one emitted file within the current build.
|
|
10
|
+
- Existing single-route artifact URLs remain stable; deduplication is build-local and does not add a persistent or generalized cache.
|
|
11
|
+
- A read-only literal query shape bound only to matching hidden input `value` and `disabled` properties initializes those controls from `location.search` in a small inline script before deferred parameter ESM arrives.
|
|
12
|
+
- The inline form specialization requires exactly the proven hidden carry shape and falls back to existing parameter capabilities when handlers, effects, conditions, lists, writable query state, or unrelated bindings are present.
|
|
13
|
+
- Static siblings remain complete zero-JavaScript documents.
|
|
14
|
+
|
|
15
|
+
### Performance
|
|
16
|
+
|
|
17
|
+
- On the maintained external 1,000-product fixture, a three-run candidate check reduced cold build time from 13,866 ms to 13,203 ms, warm build time from 13,560 ms to 13,087 ms, and output from 10.48 MB to 9.53 MB.
|
|
18
|
+
- The generated catalog changed from 1,011 native plus 1,011 effect route entries to three native plus five effect files in the 100-product candidate inspection, while route HTML continued to reference valid complete capabilities.
|
|
19
|
+
- Five Slow 4G form sessions reduced hidden query carry readiness from 783 ms to 348 ms, about 56%.
|
|
20
|
+
- These are focused Kudzu before/after measurements, not a cross-framework fastest claim. A seven-run alternating catalog measurement remains the stronger follow-up for precise build attribution.
|
|
21
|
+
|
|
22
|
+
### Navigation decision
|
|
23
|
+
|
|
24
|
+
- A compressed static-catalog same-document navigation experiment was measured and removed rather than released.
|
|
25
|
+
- In three Slow 4G sessions, native navigation beat the enhanced candidate on detail latency (314 ms versus 575 ms), back latency (107 ms versus 297 ms), transfer (322.2 KB versus 511 KB), and degraded-capability survival (15/18 versus 12/18).
|
|
26
|
+
- Native document navigation therefore remains the default for `getStaticPaths()` catalogs; existing explicit exact and `runtimeParams` navigation groups are unchanged.
|
|
27
|
+
|
|
28
|
+
### Validation
|
|
29
|
+
|
|
30
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 208 tests and 156 generated pages.
|
|
31
|
+
- A three-route static-path fixture proves one shared native entry and one shared effect entry.
|
|
32
|
+
- A focused query form fixture proves three hidden fields initialize through the narrow inline path while a static sibling emits no script.
|
|
33
|
+
- Sites whose Content Security Policy forbids inline scripts should not rely on this optional fast path without an allowed policy; existing deferred parameter ESM remains in the document.
|
|
34
|
+
|
|
35
|
+
### Upgrade
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install @kudzujs/core@^0.8.42
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 0.8.41 - Two-boundary callback ownership
|
|
42
|
+
|
|
43
|
+
Kudzu 0.8.41 extends ordinary React-shaped component migration through one additional proven callback/ref forwarding boundary without retaining component functions or adding browser runtime machinery.
|
|
44
|
+
|
|
45
|
+
### Changed in 0.8.41
|
|
46
|
+
|
|
47
|
+
- A direct setter or inline/simple `const` state callback may cross a child component and one additional direct forwarding component before one intrinsic handler invokes it.
|
|
48
|
+
- The forwarding component must destructure the callback and pass it directly once as an `on*` JSX prop; aliases, spreads, intermediate adapters, repeated uses, and a third callback boundary remain fail-closed.
|
|
49
|
+
- Parent SignalIR remains stable through nested specialization, including direct setter input adapters and simple callbacks that capture parent state.
|
|
50
|
+
- Specialized children retain independent local state, effects, deterministic IDs, and `null`-initialized object refs through conditional cleanup and fresh remount.
|
|
51
|
+
- A parent-owned object ref may follow the same proven component tree to one direct intrinsic root.
|
|
52
|
+
- Static siblings still emit complete HTML with zero JavaScript; no callback registry, retained component tree, or browser runtime module was added.
|
|
53
|
+
|
|
54
|
+
### Performance
|
|
55
|
+
|
|
56
|
+
- Seven alternating clean builds measured `v0.8.40` and candidate medians of 3,565.236 ms and 3,470.277 ms. The +59.834 ms paired candidate-minus-baseline median and overlapping ranges establish no material build change.
|
|
57
|
+
- Peak RSS medians were 364.6 MiB and 362.0 MiB.
|
|
58
|
+
- Both targets emit byte-identical 176-file deploy graphs: 3,850,245 raw bytes, 1,984,991 aggregate gzip bytes, and identical route plans and deploy digests.
|
|
59
|
+
- Current maintained Worker, keyed-list, runtime-matrix, and commerce build measurements are recorded in `PERFORMANCE.md`; architecture and timeout limitations are explicit.
|
|
60
|
+
|
|
61
|
+
### Validation
|
|
62
|
+
|
|
63
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 206 tests and 156 generated pages.
|
|
64
|
+
- Browser coverage proves parent updates, child-local state/effects/IDs, parent ref resolution, conditional cleanup, and fresh remount across two boundaries.
|
|
65
|
+
- Compiler-result checks prove nested specializations retain the original parent signal, while a dedicated fixture rejects a third callback boundary at the authored source.
|
|
66
|
+
- Broader prop, callback, ref, and Context graphs remain migration-led work.
|
|
67
|
+
|
|
68
|
+
### Upgrade
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
npm install @kudzujs/core@^0.8.41
|
|
72
|
+
```
|
|
73
|
+
|
|
3
74
|
## 0.8.40 - Property-level effect dependencies
|
|
4
75
|
|
|
5
76
|
Kudzu 0.8.40 starts the large-application foundation sequence by allowing ordinary object state to expose property-level effect dependencies without artificial primitive-state decomposition.
|
|
@@ -7,11 +7,11 @@ 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 | Active | Keyed restoration, no-op normalization,
|
|
10
|
+
| B: optimization benchmarks | Active | Keyed restoration, no-op normalization, exact route-entry transform reuse, and identical route-entry file sharing are retained; continue only when another current fixture reproduces a material loss. |
|
|
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
|
|
|
14
|
-
The active post-`0.8.
|
|
14
|
+
The active post-`0.8.42` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.12 validation, property-level object-state dependencies, the first direct two-boundary callback/ref slice, and the measured route-entry output optimization are complete; broader multi-boundary component/prop/callback/ref/context dataflow remains the next migration-backed investigation. The plan orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
|
|
15
15
|
|
|
16
16
|
## Required Invariants
|
|
17
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Current Compiler Architecture
|
|
2
2
|
|
|
3
|
-
This maps the current `0.8.
|
|
3
|
+
This maps the current `0.8.42` 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
|
|
|
@@ -17,7 +17,7 @@ This maps the current `0.8.40` architecture, built on the completed `0.8.23` Goa
|
|
|
17
17
|
| Source-local binding index | [`framework/compiler/analysis/binding-index.mjs`](../../framework/compiler/analysis/binding-index.mjs) | After normalization, assigns deterministic lexical slots and classifies local, parameter, import, capture, global, and unresolved references. Native handler, effect, binding, list evaluator, optimized-command, and effect-resource consumers use complete index-owned AST; synthesized expressions retain the existing fallback. |
|
|
18
18
|
| 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. |
|
|
19
19
|
| Main semantic analysis | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `createKudzuTransformer()` | Produces transformed source plus explicit component, handler, binding, derived, keyed, and effect ownership results. |
|
|
20
|
-
| Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Produces ComponentAnalysis v2 with ordered JSON-safe owner and specialization slots for state, setters, props, refs, IDs, direct SignalIR links, structural OwnerRefs, source-local SiteIds, and source provenance; AST identity remains private to its source-local session. |
|
|
20
|
+
| Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Produces ComponentAnalysis v2 with ordered JSON-safe owner and specialization slots for state, setters, props, refs, IDs, direct SignalIR links, structural OwnerRefs, source-local SiteIds, and source provenance. One additional proven callback/ref forwarding component specializes into the same parent signal and intrinsic ownership; AST identity remains private to its source-local session. |
|
|
21
21
|
| Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes ModuleIR v2 with deterministic SymbolRef, SignalIR, HandlerIR, BindingIR, DerivedIR, EffectIR, KeyedBlockIR, and ImportIR slots. One fail-closed boundary validates every source-local and component ownership edge before build-module generation. |
|
|
22
22
|
| Route artifact graph | [`framework/compiler/route-build-record.mjs`](../../framework/compiler/route-build-record.mjs), `createRouteBuildRecord()`, `planRouteArtifacts()` | Validates each rendered route's RouteIR, capabilities, entry paths, styles, and exact handler/effect references. Handler modules, Workers, package-client modules, and bundle entry/chunk closure derive from structural edges without serialized output searches or formatted composite keys. |
|
|
23
23
|
| Route contract validation | [`framework/compiler/route-ir.mjs`](../../framework/compiler/route-ir.mjs), `assertRouteIR()` | Fails before artifact selection for invalid state/parameter identity, commands, native/effect captures and dependencies, reactive descriptors, conditions, keyed-list identity/ownership, marker fields, or JSON safety. Immutable in-memory contracts validate once by identity. |
|
|
@@ -32,7 +32,7 @@ This maps the current `0.8.40` architecture, built on the completed `0.8.23` Goa
|
|
|
32
32
|
| Route capability projection | [`framework/compiler/route-capability-planner.mjs`](../../framework/compiler/route-capability-planner.mjs), `planRouteCapabilities()` | Validates RouteBuildRecord and RouteIR v1, purely folds their plans and capability facts into CapabilityIR v1, then checks standalone implications and exact projection equality before codegen. |
|
|
33
33
|
| Effect entry generation | [`framework/compiler/effect-codegen.mjs`](../../framework/compiler/effect-codegen.mjs) | Generates ordinary, dependency, owned, and navigable effect entries from rendered descriptors. |
|
|
34
34
|
| Runtime generation | [`framework/compiler/runtime-codegen.mjs`](../../framework/compiler/runtime-codegen.mjs), [`framework/compiler/list-runtime-codegen.mjs`](../../framework/compiler/list-runtime-codegen.mjs), [`framework/compiler/param-codegen.mjs`](../../framework/compiler/param-codegen.mjs) | Consumes versioned contracts, specializes authored capability sources with fail-closed anchors, and returns source/define results without filesystem ownership. |
|
|
35
|
-
| Artifact emission | `framework/build.mjs` | Selects route artifacts from RouteBuildRecord edges and shared runtimes from CapabilityIR, writes route HTML in bounded batches, writes and bundles the complete generation in a project-local staging sibling, copies public subtrees without replacing generated paths, runs `afterBuild`, then promotes with rollback so failed builds preserve the prior `dist`. Byte-identical native, parameter, and effect route entries
|
|
35
|
+
| Artifact emission | `framework/build.mjs` | Selects route artifacts from RouteBuildRecord edges and shared runtimes from CapabilityIR, writes route HTML in bounded batches, writes and bundles the complete generation in a project-local staging sibling, copies public subtrees without replacing generated paths, runs `afterBuild`, then promotes with rollback so failed builds preserve the prior `dist`. Byte-identical native, parameter, and effect route entries share the first existing route path and one emitted file within the current build. |
|
|
36
36
|
| Browser capabilities | [`framework/*.js`](../../framework/) | Small optional modules for commands, bindings, lists, effects, native handlers, serialization, parameters, and navigation; native contexts invalidate writes and refs at DOM ownership release, with no component runtime. |
|
|
37
37
|
| Opt-in navigation | [`framework/navigation-runtime.js`](../../framework/navigation-runtime.js) plus `framework/build.mjs` navigation configuration/emission | Fetches and validates complete same-origin documents, replaces only the marked route range, manages route/layout disposal, history, focus, finite prefetch retention, and native fallback. |
|
|
38
38
|
| Development serving | [`framework/dev-server.mjs`](../../framework/dev-server.mjs) and [`framework/dev-state.js`](../../framework/dev-state.js) | Rebuild/watch/SSE and response-only short-lived state restoration; failed rebuilds show the existing error overlay while preserving the previous on-disk output. |
|
|
@@ -55,7 +55,7 @@ The broad baseline list is an investigation inventory, not a requirement to manu
|
|
|
55
55
|
|
|
56
56
|
Known historical pressure points include large keyed-list removal/reconciliation and broad runtime specialization, but neither is authorized without a reproduced current loss.
|
|
57
57
|
|
|
58
|
-
Repeated esbuild transformation of byte-identical generated route entries cleared the materiality and output gates.
|
|
58
|
+
Repeated esbuild transformation of byte-identical generated route entries cleared the materiality and output gates. The later maintained 1,000-product fixture isolated filesystem scaling from 2,022 byte-identical native/effect route-entry files, authorizing build-local source deduplication to the first existing route path. This remains limited to generated route entries and is not a generalized cache.
|
|
59
59
|
|
|
60
60
|
## Benchmark Contract
|
|
61
61
|
|
|
@@ -378,7 +378,7 @@ function increment(value) { setCount(value + 1) }; increment(count)
|
|
|
378
378
|
After the relevant P0 foundations, investigate capabilities in this order:
|
|
379
379
|
|
|
380
380
|
1. Property-level derived dependencies over ordinary object state. **Completed in `0.8.40`:** direct property paths and top-level immutable primitive locals over object state reuse tagged DerivedIR, subscribe to the source signal, and compare selected values with `Object.is`; whole-object and dynamic dependencies remain rejected.
|
|
381
|
-
2. Multi-boundary component/prop/callback/ref/context dataflow. **
|
|
381
|
+
2. Multi-boundary component/prop/callback/ref/context dataflow. **In progress after `0.8.42`:** one additional direct setter-callback/ref forwarding component preserves the parent SignalIR and final intrinsic ownership; `0.8.42` separately completed measured route-entry sharing and hidden query carry without broadening this dataflow scope. Broader prop, callback, ref, and Context graphs remain migration-led work.
|
|
382
382
|
3. Package-neutral shared state/actions and migration of current Zustand internals.
|
|
383
383
|
4. Browser-only package imports in owned effect/resource modules.
|
|
384
384
|
5. ResourceIR from at least two independent WebSocket/SSE/SDK fixtures with the same semantics.
|
|
@@ -480,4 +480,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
480
480
|
|
|
481
481
|
## Immediate Decision
|
|
482
482
|
|
|
483
|
-
PR 1 through PR 12
|
|
483
|
+
PR 1 through PR 12, the `0.8.40` property-dependency slice, the `0.8.41` direct multi-boundary callback/ref slice, and the `0.8.42` measured route-output optimization are complete. Continue **multi-boundary component/prop/callback/ref/context dataflow** from the next real migration blocker. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
|
|
@@ -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.
|
|
3
|
+
This is an execution sequence, not release history. `0.8.16` through `0.8.42` 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
|
|
|
@@ -31,6 +31,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
31
31
|
| `0.8.38` | Replace serialized route artifact discovery with validated RouteBuildRecord edges. | Handler, effect, Worker, CSS, package-client, and chunk retention is structural while deploy files, bytes, plans, and browser behavior remain unchanged. |
|
|
32
32
|
| `0.8.39` | Deeply validate concrete RouteIR, RouteBuildRecord, and CapabilityIR contracts. | Invalid state, command, effect, binding, condition, list, ownership, capability, and JSON references fail before codegen without changing deploy output. |
|
|
33
33
|
| `0.8.40` | Add property-level derived effect dependencies over ordinary object state. | Direct property paths and immutable primitive locals reuse existing DerivedIR and `Object.is`; whole-object/dynamic dependencies fail, runtime JavaScript is unchanged, and measured build/RSS changes remain below 5%. |
|
|
34
|
+
| `0.8.41` | Preserve callback, setter, and ref ownership through one additional direct presentation-component boundary. | Parent signals, child hooks, IDs, effects, refs, conditional cleanup, static zero-JavaScript output, and byte-identical deploy graphs pass; a third callback boundary fails closed. |
|
|
35
|
+
| `0.8.42` | Share byte-identical generated route entries and accelerate one proven read-only query form carry shape. | A static-path fixture emits one native/effect file for three routes; hidden GET carry initializes before deferred parameter ESM; static siblings remain zero-JavaScript; the losing catalog navigation experiment is excluded. |
|
|
34
36
|
|
|
35
37
|
## Sequence Rules
|
|
36
38
|
|
package/framework/README.md
CHANGED
|
@@ -20,7 +20,7 @@ A named or aliased `Link` import from `react-router-dom` may render directly wit
|
|
|
20
20
|
|
|
21
21
|
A named or aliased React Router `useParams` import may also be called directly without runtime arguments on a bracket route exporting `runtimeParams = true`. The compiler redirects that local binding to `@kudzujs/core`, preserving one optional TypeScript type argument, and reuses the existing route-specific pathname matcher. Indirect calls and other router hooks remain unsupported. Build-known `getStaticPaths()` routes use page props instead because no browser pathname capability is needed.
|
|
22
22
|
|
|
23
|
-
A named or aliased React Router `useSearchParams` import may initialize one top-level `const [params]` or `const [params, setParams]` binding. Each top-level `const value = params.get("literal")` lowers to one cached nullable `useSearchParam()` signal. Direct setter calls inside nested browser callbacks accept one synchronous inline updater over native `URLSearchParams`; no options pushes history and exactly `{ replace: true }` replaces it. The route parameter asset recommits changed query signals and follows `popstate`. Missing keys remain `null`, direct text renders blank, and nullable attributes are removed. Dynamic names, direct-value setters, other methods on the outer params object, aliases, wrapped reads, and layout ownership are rejected. Routes without query reads or writes do not emit this branch or a parameter asset.
|
|
23
|
+
A named or aliased React Router `useSearchParams` import may initialize one top-level `const [params]` or `const [params, setParams]` binding. Each top-level `const value = params.get("literal")` lowers to one cached nullable `useSearchParam()` signal. New Kudzu source may call that same literal reader directly; binding nullable query values to hidden input `value` and `disabled` attributes initializes native GET form carry without a generic mount effect. Direct setter calls inside nested browser callbacks accept one synchronous inline updater over native `URLSearchParams`; no options pushes history and exactly `{ replace: true }` replaces it. The route parameter asset recommits changed query signals and follows `popstate`. Missing keys remain `null`, direct text renders blank, and nullable attributes are removed. Dynamic names, direct-value setters, other methods on the outer params object, aliases, wrapped reads, and layout ownership are rejected. Routes without query reads or writes do not emit this branch or a parameter asset.
|
|
24
24
|
|
|
25
25
|
A named or aliased React Router `useNavigate` import may initialize one top-level `const` identifier. A direct call from a nested browser callback with one safe static root-relative string lowers to `location.assign()` after applying `base`; exactly `{ replace: true }` lowers to `location.replace()`. This deliberately performs native document navigation even when enhanced navigation is configured. Dynamic or relative destinations, render-time calls, aliases passed as values, and options such as `state`, `relative`, or `preventScrollReset` are rejected. Routes without an actual navigation handler emit no browser JavaScript.
|
|
26
26
|
|
|
@@ -86,7 +86,7 @@ Rendered collection selectors compile immutable local aliases and inline `(item)
|
|
|
86
86
|
|
|
87
87
|
The reduced `useReducer` form reuses ordinary state slots and React's pure reducer contract. An optional inline, same-file, or relative-imported synchronous one-parameter initializer may derive a directly serializable literal only from its directly serializable initial argument; the compiler substitutes that argument and lowers the call to the ordinary two-argument ownership path. A direct dispatch in a compiled handler becomes a functional `set` whose reducer is bundled from a relative TypeScript module into that handler graph. Pure reducer-owned keyed lists reuse unchanged item identities for reorder, one removal, and append fast paths; ordinary `useState` lists retain full validation. One direct dispatch prop into a same-file or relative-imported synchronous component, including a direct keyed row, is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. A reducer row reads the latest item through the existing list scope and uses the same multiple serializable state, effect, condition, and object-ref specialization as other keyed rows. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Missing directly serializable literal defaults and direct intrinsic rest props in these reducer specializations are substituted at the same call site. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
|
|
88
88
|
|
|
89
|
-
A direct setter may cross one same-file or relative-imported component boundary
|
|
89
|
+
A direct setter or inline/simple `const` setter callback may cross one same-file or relative-imported component boundary and one additional direct forwarding component before an intrinsic handler invokes it exactly once. Value adapters such as `event => onValueChange(event.currentTarget.value)` specialize into the parent setter graph instead of serializing a function. The intermediate component must destructure and directly forward the callback once as an `on*` JSX prop; third-boundary forwarding, aliases, spreads, intermediate adapters, and repeated uses remain unsupported. Specialized children may own directly serializable `useState()` values or initialize string state with one direct primitive state prop's zero-argument `.toString()`, plus `useId()` values, supported effects, and object refs initialized with `null`. Same-file and relative-imported presentation components recursively specialize away; nested hooks join the same generated owner only on unconditional or statically truthy paths. A parent-owned `null`-initialized object ref may follow the proven tree to the direct intrinsic root. These remain compiler-owned descriptors: conditional removal drops the handler with the element, deletes child state, resolves refs to `null`, and runs effect cleanup, while remount creates fresh ownership and dependency effects synchronize prop-derived state without retaining a component instance.
|
|
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
|
|
package/framework/build.mjs
CHANGED
|
@@ -116,6 +116,8 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
116
116
|
|
|
117
117
|
const routeRecords = []
|
|
118
118
|
const routeEntryTransforms = new Map()
|
|
119
|
+
const routeEntrySources = new Map()
|
|
120
|
+
const routeEntryPaths = new Map()
|
|
119
121
|
const rewrites = []
|
|
120
122
|
const emittedRoutes = new Set()
|
|
121
123
|
const emittedApplicationRoutes = new Set()
|
|
@@ -190,11 +192,30 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
190
192
|
navigationGroup.hasParams ||= result.hasParams
|
|
191
193
|
}
|
|
192
194
|
const usesDependencyRuntime = usesRouteDependencyRuntime({ plan: result.plan, navigable, hasBindings: result.hasBindings, hasLists: result.hasLists })
|
|
195
|
+
const routeRuntimeName = usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
|
|
193
196
|
const plan = { route: routePath, ...result.plan }
|
|
197
|
+
const entries = {}
|
|
198
|
+
let html = inlineQueryFormCarry(result.html, plan)
|
|
199
|
+
if (result.hasParams) {
|
|
200
|
+
const entry = retainRouteEntry(paramPath, output => printParamEntry(runtimeSchema, plan.params, plan.searchParams, plan.searchParamsWritable, output, join(outputDirectory, "assets"), base, routeRuntimeName, navigable), routeEntrySources, routeEntryPaths, outputDirectory)
|
|
201
|
+
entries.param = entry.path
|
|
202
|
+
html = html.replaceAll(assetPath(base, `assets/${paramPath}`), assetPath(base, `assets/${entry.path}`))
|
|
203
|
+
}
|
|
204
|
+
if (result.hasEffects) {
|
|
205
|
+
const entry = retainRouteEntry(effectPath, output => printEffectEntry(runtimeEffects(plan.effects, navigable), output, handlerModules, join(outputDirectory, "assets"), base, entries.param, routeRuntimeName, navigable), routeEntrySources, routeEntryPaths, outputDirectory)
|
|
206
|
+
entries.effect = entry.path
|
|
207
|
+
html = html.replaceAll(assetPath(base, `assets/${effectPath}`), assetPath(base, `assets/${entry.path}`))
|
|
208
|
+
}
|
|
209
|
+
if (plan.events.some(event => event.native)) {
|
|
210
|
+
const modules = [...new Set(plan.events.filter(event => event.native).map(event => event.native.module))]
|
|
211
|
+
const entry = retainRouteEntry(nativePath, () => printNativeEntrySource(modules, base), routeEntrySources, routeEntryPaths, outputDirectory)
|
|
212
|
+
entries.native = entry.path
|
|
213
|
+
html = html.replaceAll(assetPath(base, `assets/${nativePath}`), assetPath(base, `assets/${entry.path}`))
|
|
214
|
+
}
|
|
194
215
|
routeRecords.push(createRouteBuildRecord({
|
|
195
216
|
route: routePath,
|
|
196
217
|
output: route,
|
|
197
|
-
html
|
|
218
|
+
html,
|
|
198
219
|
plan,
|
|
199
220
|
handlerReferences: result.handlerReferences,
|
|
200
221
|
styles: styleUrls,
|
|
@@ -209,11 +230,7 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
209
230
|
hasParams: result.hasParams,
|
|
210
231
|
hasEffects: result.hasEffects
|
|
211
232
|
},
|
|
212
|
-
entries
|
|
213
|
-
...(result.hasParams ? { param: paramPath } : {}),
|
|
214
|
-
...(result.hasEffects ? { effect: effectPath } : {}),
|
|
215
|
-
...(result.plan.events.some(event => event.native) ? { native: nativePath } : {})
|
|
216
|
-
},
|
|
233
|
+
entries,
|
|
217
234
|
runtimeSchema
|
|
218
235
|
}))
|
|
219
236
|
}
|
|
@@ -293,10 +310,7 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
293
310
|
if (hasNativeHandlers) {
|
|
294
311
|
const generated = generateNativeRuntime(await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"), capabilityIR)
|
|
295
312
|
await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), generated.source, minify, generated.define)
|
|
296
|
-
for (const
|
|
297
|
-
path: record.entries.native,
|
|
298
|
-
modules: [...new Set(record.plan.events.filter(event => event.native).map(event => event.native.module))]
|
|
299
|
-
}, assetsDirectory, base, minify, routeEntryTransforms)
|
|
313
|
+
for (const [path, source] of routeEntrySources) if (path.startsWith("native/")) await writeRetainedRouteEntry(path, source, assetsDirectory, minify, routeEntryTransforms)
|
|
300
314
|
}
|
|
301
315
|
if (navigationGroups.length) {
|
|
302
316
|
const navigationSource = await readFile(new URL("./navigation-runtime.js", import.meta.url), "utf8")
|
|
@@ -309,16 +323,7 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
309
323
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
310
324
|
await writeJavaScript(output, handlerModule.code, minify)
|
|
311
325
|
}
|
|
312
|
-
for (const
|
|
313
|
-
const output = join(assetsDirectory, record.entries.param)
|
|
314
|
-
await mkdir(dirname(output), { recursive: true })
|
|
315
|
-
await writeRouteEntry(output, printParamEntry(record.runtimeSchema, record.plan.params, record.plan.searchParams, record.plan.searchParamsWritable, output, assetsDirectory, base, runtimeName(record.capabilities.usesDependencyRuntime), record.capabilities.navigable), minify, routeEntryTransforms)
|
|
316
|
-
}
|
|
317
|
-
for (const record of routeRecords) if (record.entries.effect) {
|
|
318
|
-
const output = join(assetsDirectory, record.entries.effect)
|
|
319
|
-
await mkdir(dirname(output), { recursive: true })
|
|
320
|
-
await writeRouteEntry(output, printEffectEntry(runtimeEffects(record.plan.effects, record.capabilities.navigable), output, emittedHandlerModules, assetsDirectory, base, record.entries.param, runtimeName(record.capabilities.usesDependencyRuntime), record.capabilities.navigable), minify, routeEntryTransforms)
|
|
321
|
-
}
|
|
326
|
+
for (const [path, source] of routeEntrySources) if (path.startsWith("params/") || path.startsWith("effects/")) await writeRetainedRouteEntry(path, source, assetsDirectory, minify, routeEntryTransforms)
|
|
322
327
|
const clientModules = await collectClientModules(emittedHandlerModules.flatMap(module => module.clientImports).map(file => resolve(root, file)), sourceFileSet)
|
|
323
328
|
for (const file of clientModules) {
|
|
324
329
|
const module = await compileClientModule(file, sourceFileSet, staticFiles, cssModules, base)
|
|
@@ -464,13 +469,41 @@ function preloadModules(html) {
|
|
|
464
469
|
return html.replace(scripts[0][0], `${links}${scripts[0][0]}`)
|
|
465
470
|
}
|
|
466
471
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
+
function inlineQueryFormCarry(html, plan) {
|
|
473
|
+
if (plan.searchParamsWritable || !plan.searchParams.length || plan.events.length || plan.effects.length || plan.conditions.length || plan.lists.length || plan.bindings.length !== plan.searchParams.length * 2) return html
|
|
474
|
+
const ids = new Map(plan.searchParams.map(param => [param.id, param.name]))
|
|
475
|
+
const pairs = new Map(plan.searchParams.map(param => [param.name, new Set()]))
|
|
476
|
+
for (const binding of plan.bindings) {
|
|
477
|
+
const signalIds = Object.values(binding.scopeStates ?? {})
|
|
478
|
+
const name = signalIds.length === 1 ? ids.get(signalIds[0]) : undefined
|
|
479
|
+
if (!name || !["value", "disabled"].includes(binding.target) || Object.keys(binding.states ?? {}).length || Object.keys(binding.scopeBindings ?? {}).length) return html
|
|
480
|
+
pairs.get(name).add(binding.target)
|
|
481
|
+
}
|
|
482
|
+
if ([...pairs].some(([name, targets]) => targets.size !== 2 || !html.includes(`type="hidden" name="${escapeAttribute(name)}"`))) return html
|
|
483
|
+
const script = `<script>(()=>{const q=new URLSearchParams(location.search);for(const e of document.querySelectorAll('input[type="hidden"][data-k-query-carry]')){if(q.has(e.name)){e.value=q.get(e.name)??"";e.disabled=false}else{e.value="";e.disabled=true}}})()</script>`
|
|
484
|
+
for (const name of pairs.keys()) html = html.replace(`type="hidden" name="${escapeAttribute(name)}"`, `type="hidden" data-k-query-carry name="${escapeAttribute(name)}"`)
|
|
485
|
+
return html.replace("</body>", `${script}</body>`)
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function printNativeEntrySource(modules, base) {
|
|
489
|
+
const imports = modules.map((module, index) => `import * as __kNativeModule${index} from ${JSON.stringify(module)}`).join("\n")
|
|
490
|
+
const registrations = modules.map((module, index) => `[${JSON.stringify(module)}, __kNativeModule${index}]`).join(",")
|
|
472
491
|
const runtime = assetPath(base, "assets/kudzu-native.js")
|
|
473
|
-
|
|
492
|
+
return `import { registerNativeModules } from ${JSON.stringify(runtime)}\n${imports}\nregisterNativeModules([${registrations}])`
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function retainRouteEntry(requestedPath, generate, sources, paths, outputDirectory) {
|
|
496
|
+
const source = generate(join(outputDirectory, "assets", requestedPath))
|
|
497
|
+
const path = paths.get(source) ?? requestedPath
|
|
498
|
+
paths.set(source, path)
|
|
499
|
+
sources.set(path, source)
|
|
500
|
+
return { path, source }
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function writeRetainedRouteEntry(path, source, assetsDirectory, minify, transforms) {
|
|
504
|
+
const output = join(assetsDirectory, path)
|
|
505
|
+
await mkdir(dirname(output), { recursive: true })
|
|
506
|
+
await writeRouteEntry(output, source, minify, transforms)
|
|
474
507
|
}
|
|
475
508
|
|
|
476
509
|
function runtimeEffects(effects, lifetimes = false) {
|
|
@@ -1013,7 +1013,21 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1013
1013
|
folded.parent = root.parent
|
|
1014
1014
|
return folded
|
|
1015
1015
|
}
|
|
1016
|
-
const
|
|
1016
|
+
const validateSetterCallbackProps = (call, component, callbackProps) => {
|
|
1017
|
+
if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, "Setter-callback components must use one destructured props parameter")
|
|
1018
|
+
for (const prop of callbackProps) {
|
|
1019
|
+
const element = component.parameters[0].name.elements.find(entry => !entry.dotDotDotToken && (entry.propertyName ?? entry.name).getText() === prop)
|
|
1020
|
+
if (!element || !ts.isIdentifier(element.name)) fail(call, `Setter-callback component must destructure callback prop ${JSON.stringify(prop)}`)
|
|
1021
|
+
const references = []
|
|
1022
|
+
const collectReferences = node => {
|
|
1023
|
+
if (ts.isIdentifier(node) && node.text === element.name.text && isReferenceIdentifier(node)) references.push(node)
|
|
1024
|
+
ts.forEachChild(node, collectReferences)
|
|
1025
|
+
}
|
|
1026
|
+
collectReferences(component.body)
|
|
1027
|
+
if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters, parentStateOwners, callbackDepth = 1) => {
|
|
1017
1031
|
root = foldSetterStaticConditions(root)
|
|
1018
1032
|
const replacements = new WeakMap()
|
|
1019
1033
|
let count = 0
|
|
@@ -1049,10 +1063,27 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1049
1063
|
for (const state of aggregate.ordinaryStates) setters.set(state.setter, state.state)
|
|
1050
1064
|
const stateOwners = new Map(parentStateOwners)
|
|
1051
1065
|
for (const state of aggregate.ordinaryStates) stateOwners.set(state.state, state.analysisReference)
|
|
1052
|
-
|
|
1066
|
+
const callbackProps = jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction))
|
|
1067
|
+
const callbackSubstitutions = new Map()
|
|
1068
|
+
if (callbackProps.length) {
|
|
1069
|
+
if (!callbackDepth) fail(node, "Setter callbacks cannot cross more than two component boundaries")
|
|
1070
|
+
const attributes = ts.isJsxElement(node) ? node.openingElement.attributes : node.attributes
|
|
1071
|
+
for (const prop of callbackProps) {
|
|
1072
|
+
const attribute = attributes.properties.find(entry => ts.isJsxAttribute(entry) && entry.name.text === prop)
|
|
1073
|
+
const value = attribute?.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
|
|
1074
|
+
if (!value || !ts.isIdentifier(value)) fail(attribute ?? node, "A second-boundary setter callback must be forwarded directly as one JSX event prop")
|
|
1075
|
+
const callback = functionsForNode(value).get(value.text)
|
|
1076
|
+
if (callback) callbackSubstitutions.set(value.text, callback)
|
|
1077
|
+
}
|
|
1078
|
+
validateSetterCallbackProps(node, component, callbackProps)
|
|
1079
|
+
}
|
|
1053
1080
|
const nested = specialize(node, component, "Nested setter-callback", true, true, new Set(setters.values()), { setters, stateOwners })
|
|
1081
|
+
if (callbackSubstitutions.size) {
|
|
1082
|
+
nested.root = substituteClone(nested.root, callbackSubstitutions, factory, context)
|
|
1083
|
+
for (const effect of nested.effects) effect.call = substituteClone(effect.call, callbackSubstitutions, factory, context)
|
|
1084
|
+
}
|
|
1054
1085
|
if (dynamic && (nested.hookDeclarations.length || nested.effects.length)) fail(node, "Hookful nested setter-callback components require an unconditional or statically truthy render path")
|
|
1055
|
-
nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters, stateOwners)
|
|
1086
|
+
nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters, stateOwners, callbackDepth - Boolean(callbackProps.length))
|
|
1056
1087
|
if (imported) synthesizeTree(nested.root = mergeSpecializedImports(nested.root, component.getSourceFile(), node, nested.effects))
|
|
1057
1088
|
aggregate.calculations.push(...nested.calculations)
|
|
1058
1089
|
aggregate.effects.push(...nested.effects)
|
|
@@ -1111,19 +1142,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1111
1142
|
}
|
|
1112
1143
|
const specializeSetterCallbacks = (call, component, callbackProps, imported) => {
|
|
1113
1144
|
if (componentSpecializations.has(call)) fail(call, "Setter callback props cannot be combined with another component specialization")
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const element = component.parameters[0].name.elements.find(entry => !entry.dotDotDotToken && (entry.propertyName ?? entry.name).getText() === prop)
|
|
1117
|
-
if (!element || !ts.isIdentifier(element.name)) fail(call, `Setter-callback component must destructure callback prop ${JSON.stringify(prop)}`)
|
|
1118
|
-
const references = []
|
|
1119
|
-
const collectReferences = node => {
|
|
1120
|
-
if (ts.isIdentifier(node) && node.text === element.name.text && isReferenceIdentifier(node)) references.push(node)
|
|
1121
|
-
ts.forEachChild(node, collectReferences)
|
|
1122
|
-
}
|
|
1123
|
-
collectReferences(component.body)
|
|
1124
|
-
if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
|
|
1125
|
-
}
|
|
1126
|
-
const specialization = specialize(call, component, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
|
|
1145
|
+
validateSetterCallbackProps(call, component, callbackProps)
|
|
1146
|
+
const specialization = specialize(call, component, "Setter-callback", true, true, new Set(settersForNode(call, settersByFunction).values()))
|
|
1127
1147
|
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
1128
1148
|
const substitutions = new Map()
|
|
1129
1149
|
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|