@kudzujs/core 0.8.18 → 0.8.19

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.
@@ -209,6 +209,13 @@ This queue orders the next investigations by general migration value. Start only
209
209
  - Specialized prop records retain default application and direct signal links; imported hook declarations keep source provenance while synthetic ownership invents no source range.
210
210
  - `core.mjs` remains authoritative for final route/layout state, ref, ID, conditional, and keyed allocation. Repeated, conditional, imported, setter-adapter, Context, and reducer fixtures preserve their existing ownership and deploy output.
211
211
 
212
+ ### Completed In 0.8.19
213
+
214
+ - Native and effect callback exports now finalize into JSON-safe HandlerIR with ordered signals, setters, captures, snapshot policy, imports, roles, source provenance, and generated module source.
215
+ - Reactive bindings, list expressions, and list condition evaluators finalize into BindingIR with explicit states, captures, parameters, imports, and deterministic export slots.
216
+ - Existing tagged collection expressions and selectors are canonical DerivedIR for rendered lists and derived effect dependencies; transformed source embeds the registered records rather than parallel analyzer values.
217
+ - AST, `Map`, and `Set` callback descriptors remain private only until source-local lowering completes. Handler codegen now performs no TypeScript traversal or state/capture/reducer/import discovery, while command-only routes retain zero handler ESM.
218
+
212
219
  ## Cross-Cutting Performance Gates
213
220
 
214
221
  Every migration feature must preserve:
package/PERFORMANCE.md CHANGED
@@ -1,5 +1,51 @@
1
1
  # Performance Records
2
2
 
3
+ ## 0.8.19 Handler, Binding, And Derived IR
4
+
5
+ Measured UTC 2026-08-08 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 `0.8.18` tag `3598be0` and the `0.8.19` compiler-only candidate used detached worktrees on the same temporary volume with identical installed dependencies.
6
+
7
+ The candidate compiler patch over `3598be0` had SHA-256 `7b2afc4c9a0d1963c8d3ccacfb1e95152136d77cc7afc250d17b1986ca329fb3`, produced by:
8
+
9
+ ```bash
10
+ git diff --binary 3598be0 -- framework/build.mjs framework/compiler/descriptor-session.mjs framework/compiler/handler-codegen.mjs framework/compiler/handler-lowering.mjs framework/compiler/ir/module-ir.mjs | shasum -a 256
11
+ ```
12
+
13
+ Both targets received one warm-up followed by 21 clean `worker-effects` production builds in round-robin alternating order. Cleanup remained outside timing. The distributions overlap; the candidate median was 0.32% lower and does not establish a material change.
14
+
15
+ | Target | Build median | Worker raw / gzip | Window raw / gzip |
16
+ |---|---:|---:|---:|
17
+ | 0.8.18 baseline | 253.4 ms | 907 B / 475 B | 12,148 B / 5,427 B |
18
+ | 0.8.19 candidate | 252.6 ms | 907 B / 475 B | 12,148 B / 5,427 B |
19
+
20
+ ```text
21
+ 0.8.18: [252.3,254.5,251.0,254.8,254.3,253.8,253.4,251.6,255.4,252.4,249.6,254.2,251.0,253.7,256.0,250.4,251.0,255.4,253.3,253.6,250.5]
22
+ 0.8.19: [252.3,251.7,252.9,251.9,251.5,253.0,252.3,251.3,253.0,252.1,254.3,258.4,249.8,251.6,256.2,253.7,253.5,252.6,252.9,252.5,255.9]
23
+ ```
24
+
25
+ Before release-content updates, the complete site `dist` was byte-identical. Representative fixture builds retained identical file lists and SHA-256 content; `.kudzu` comparisons replaced only each detached worktree's absolute root in existing source-location strings. The complete fixture lists were:
26
+
27
+ ```text
28
+ bindings: dist/assets/handlers/pages/index.js, dist/assets/kudzu-binding.js, dist/assets/kudzu-serialization.js, dist/assets/kudzu-style.js, dist/assets/kudzu.js, dist/index.html, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs
29
+ native: dist/assets/handlers/pages/index.js, dist/assets/handlers/pages/other/index.js, dist/assets/kudzu-native.js, dist/assets/kudzu-serialization.js, dist/assets/kudzu.js, dist/assets/native/index.js, dist/assets/native/other/index.js, dist/index.html, dist/other/index.html, .kudzu/helpers.mjs, .kudzu/kudzu-plan.json, .kudzu/math.mjs, .kudzu/pages/index.mjs, .kudzu/pages/other/index.mjs
30
+ reducer: dist/assets/handlers/pages/index.js, dist/assets/handlers/pages/lazy.js, dist/assets/kudzu-binding.js, dist/assets/kudzu-list.js, dist/assets/kudzu-native.js, dist/assets/kudzu-serialization.js, dist/assets/kudzu-style.js, dist/assets/kudzu.js, dist/assets/native/index.js, dist/index.html, dist/lazy/index.html, .kudzu/ImportedControls.mjs, .kudzu/ImportedInput.mjs, .kudzu/ImportedItem.mjs, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs, .kudzu/pages/lazy.mjs, .kudzu/todoReducer.mjs, .kudzu/todoSupport.mjs
31
+ context-actions: dist/assets/handlers/pages/index.js, dist/assets/kudzu-list.js, dist/assets/kudzu-native.js, dist/assets/kudzu-serialization.js, dist/assets/kudzu.js, dist/assets/native/index.js, dist/index.html, dist/static/index.html, .kudzu/kudzu-plan.json, .kudzu/notes.mjs, .kudzu/pages/index.mjs, .kudzu/pages/static.mjs, .kudzu/useNotes.mjs
32
+ zustand-migration: dist/assets/handlers/Shell.js, dist/assets/handlers/pages/cart.js, dist/assets/handlers/pages/index.js, dist/assets/kudzu-binding.js, dist/assets/kudzu-native.js, dist/assets/kudzu-navigation.js, dist/assets/kudzu-serialization.js, dist/assets/kudzu-style.js, dist/assets/kudzu.js, dist/assets/native/cart/index.js, dist/assets/native/index.js, dist/cart/index.html, dist/index.html, .kudzu/Shell.mjs, .kudzu/kudzu-plan.json, .kudzu/pages/cart.mjs, .kudzu/pages/index.mjs, .kudzu/store.mjs
33
+ event-package: dist/assets/handlers/pages/index.js, dist/assets/kudzu-native.js, dist/assets/kudzu-serialization.js, dist/assets/kudzu.js, dist/assets/native/index.js, dist/index.html, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs
34
+ list-expressions: dist/assets/handlers/pages/index.js, dist/assets/kudzu-list.js, dist/assets/kudzu.js, dist/index.html, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs
35
+ effect-dependencies: dist/assets/effects/index.js, dist/assets/handlers/pages/index.js, dist/assets/kudzu-collection-selector.js, dist/assets/kudzu-deps.js, dist/assets/kudzu-effect.js, dist/assets/kudzu-serialization.js, dist/assets/kudzu.js, dist/command/index.html, dist/index.html, .kudzu/kudzu-plan.json, .kudzu/pages/command.mjs, .kudzu/pages/index.mjs
36
+ landing-page-migration: dist/assets/assets/badge.png, dist/assets/assets/hero.svg, dist/assets/assets/landing.woff2, dist/assets/assets/module-mark.svg, dist/assets/assets/preview.webp, dist/assets/handlers/pages/index.js, dist/assets/kudzu-binding.js, dist/assets/kudzu-native.js, dist/assets/kudzu-serialization.js, dist/assets/kudzu-style.js, dist/assets/kudzu.js, dist/assets/native/index.js, dist/assets/styles/Hero.module.css, dist/assets/styles/landing.css, dist/index.html, dist/static/index.html, .kudzu/LandingSections.mjs, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs, .kudzu/pages/static.mjs
37
+ ```
38
+
39
+ This measurement covers compiler clean-build startup and artifact size, not browser interaction latency or cross-framework performance.
40
+
41
+ The recorded cleanup, warm-up, and alternating 21-run loop is reproducible with:
42
+
43
+ ```bash
44
+ BASELINE_ROOT="/private/var/folders/bt/3r_ntp5x65j81brs6_p93rl00000gn/T/opencode/kudzu-0819-baseline" CANDIDATE_ROOT="/var/folders/bt/3r_ntp5x65j81brs6_p93rl00000gn/T/opencode/kudzu-0.8.19-candidate" node --input-type=module -e 'import { spawnSync } from "node:child_process"; import { rmSync } from "node:fs"; import { performance } from "node:perf_hooks"; import { resolve } from "node:path"; const roots={baseline:process.env.BASELINE_ROOT,candidate:process.env.CANDIDATE_ROOT}; const runs={baseline:[],candidate:[]}; const build=name=>{const root=roots[name],fixture=resolve(root,"test/fixtures/worker-effects"); rmSync(resolve(fixture,"dist"),{recursive:true,force:true}); rmSync(resolve(fixture,".kudzu"),{recursive:true,force:true}); const start=performance.now(); const result=spawnSync(process.execPath,[resolve(root,"bin/kudzu.mjs"),"build"],{cwd:fixture,encoding:"utf8"}); if(result.status!==0) throw new Error(result.stderr||result.stdout); return Number((performance.now()-start).toFixed(1));}; build("baseline"); build("candidate"); for(let index=0;index<21;index++) for(const name of index%2?["candidate","baseline"]:["baseline","candidate"]) runs[name].push(build(name)); console.log(JSON.stringify(runs));'
45
+ ```
46
+
47
+ The complete `worker-effects` benchmark list was: `dist/assets/effects/dashboard/index.js`, `dist/assets/handlers/pages/dashboard.js`, `dist/assets/kudzu-effect.js`, `dist/assets/kudzu-navigation.js`, `dist/assets/kudzu.js`, `dist/assets/workers/telemetry.worker-BVG2SA55.js`, `dist/dashboard/index.html`, `dist/plain/index.html`, `dist/static/index.html`, `.kudzu/Shell.mjs`, `.kudzu/chart.mjs`, `.kudzu/kudzu-plan.json`, `.kudzu/pages/dashboard.mjs`, `.kudzu/pages/plain.mjs`, `.kudzu/pages/static.mjs`, `.kudzu/telemetry/downsample.mjs`, and `.kudzu/telemetry/ring.mjs`.
48
+
3
49
  ## 0.8.18 Explicit Component Ownership
4
50
 
5
51
  Measured UTC 2026-08-08 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 `0.8.17` tag `d48f0cf` and the `0.8.18` compiler-only candidate used detached worktrees on the same temporary volume with identical installed dependencies.
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.18 - Explicit component ownership.** Source analysis now records JSON-safe state, setter, prop, ref, ID, and specialization ownership while final route state allocation remains unchanged. Read the [release notes](./RELEASES.md#0818---explicit-component-ownership), open the [release page](https://kudzujs.cloud/releases/0.8.18), or follow the [next architecture packet](./docs/next-architecture/README.md).
17
+ **Latest release: 0.8.19 - Handler, binding, and derived IR.** Native callbacks, reactive bindings, list evaluators, imports, and pure derived expressions now cross one JSON-safe ModuleIR boundary before source-only artifact codegen. Read the [release notes](./RELEASES.md#0819---handler-binding-and-derived-ir), open the [release page](https://kudzujs.cloud/releases/0.8.19), or follow the [next 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,38 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.19 - Handler, binding, and derived IR
4
+
5
+ Kudzu 0.8.19 completes the next Goal A source-analysis boundary: native callbacks, reactive bindings, list evaluators, imports, and pure derived expressions now finalize into JSON-safe ModuleIR before mechanical artifact codegen.
6
+
7
+ ### Changed in 0.8.19
8
+
9
+ - Native and effect callback exports retain explicit roles, signals, setters, captures, snapshot policy, imports, source ranges, and finalized export source in HandlerIR.
10
+ - Reactive bindings, list expressions, and list condition evaluators retain explicit states, captures, parameters, imports, and deterministic module-export slots in BindingIR.
11
+ - Rendered list selectors and derived effect dependencies use the existing tagged collection-expression language as canonical DerivedIR; transformed build source reads back the registered records.
12
+ - Reducer, Context, Zustand, package-import, nested snapshot, and scope semantics finish lowering while source AST and diagnostics are still available.
13
+ - `handler-codegen.mjs` imports no TypeScript or AST helpers. It renders the finalized ordered imports and concatenates generated HandlerIR/BindingIR export source.
14
+ - The previous semantic artifact arrays containing callback/expression AST, `Map`, and `Set` values were deleted. They remain only inside the source-local session until finalization and do not cross the IR boundary.
15
+
16
+ ### Goal A boundary
17
+
18
+ - Effect lifetime, cleanup, dependencies, Worker edges, and ownership remain on the existing path until EffectIR in `0.8.21`; only their generated callback exports join HandlerIR now.
19
+ - Keyed DOM ownership remains on the existing path until KeyedBlockIR in `0.8.20`; list evaluators and pure selectors are explicit without moving key-path ownership early.
20
+ - `core.mjs` remains authoritative for complete HTML, final route/layout state IDs, and the serializable route plan.
21
+ - No accepted syntax, public API, runtime capability, VDOM, hydration, component rerender, or retained browser component tree was added.
22
+
23
+ ### Validation
24
+
25
+ - The complete suite passes 167/167 tests, including ModuleIR JSON round-trip, command-only exclusion, native async handlers, bindings, package imports, reducer, Context, Zustand, list-derived, and effect-derived contracts.
26
+ - Before release-content updates, the complete site `dist` and nine representative fixture output trees matched `v0.8.18`; detached-worktree roots were normalized only in existing `.kudzu` diagnostic strings.
27
+ - Worker and window graphs remain byte-identical. A same-volume 21-run interleaved comparison measured a 0.32% lower candidate median; raw arrays, file lists, and environment are recorded in `PERFORMANCE.md`.
28
+ - `create-kudzu` remains 0.1.101 because its unchanged template already accepts `@kudzujs/core@^0.8.15`.
29
+
30
+ ### Upgrade
31
+
32
+ ```bash
33
+ npm install @kudzujs/core@^0.8.19
34
+ ```
35
+
3
36
  ## 0.8.18 - Explicit component ownership
4
37
 
5
38
  Kudzu 0.8.18 completes the next Goal A source-analysis seam: state, setters, props, refs, IDs, and supported component specializations now have one ordered JSON-safe ownership result without changing final route allocation or deploy behavior.
@@ -6,7 +6,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
6
6
 
7
7
  | Goal | Decision | Start condition |
8
8
  |---|---|---|
9
- | A: compiler foundation | `0.8.18` component ownership result complete | Continue with `0.8.19` HandlerIR, BindingIR, and DerivedIR after the release boundary. |
9
+ | A: compiler foundation | `0.8.19` handler/binding/derived IR complete | Continue with `0.8.20` KeyedBlockIR ownership after the release boundary. |
10
10
  | B: optimization benchmarks | Deferred | Goal A complete and its output baseline recorded |
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 |
@@ -1,6 +1,6 @@
1
1
  # Current Compiler Architecture
2
2
 
3
- This maps the completed `0.8.18` component ownership boundary. File and function names are the stable references; line numbers are intentionally omitted because Goal A moves code.
3
+ This maps the completed `0.8.19` handler, binding, and derived IR boundary. File and function names are the stable references; line numbers are intentionally omitted because Goal A moves code.
4
4
 
5
5
  ## Responsibility Map
6
6
 
@@ -13,12 +13,13 @@ This maps the completed `0.8.18` component ownership boundary. File and function
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. |
16
- | Main semantic analysis | `framework/build.mjs`, `createKudzuTransformer()` | Produces transformed source and the explicit component ownership result while still owning effects, keyed-list ownership, imports, reactive JSX, and their remaining AST-identity side tables. |
16
+ | Main semantic analysis | `framework/build.mjs`, `createKudzuTransformer()` | Produces transformed source plus explicit component, handler, binding, and derived results while still owning effect and keyed-list ownership side tables. |
17
17
  | Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Retains ordered JSON-safe owner and specialization records for state, setters, props, refs, IDs, direct signal links, and source provenance; AST identity remains private to its source-local session. |
18
- | Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Registers deterministic component analysis, native handler, effect handler, binding, list evaluator, and client-import results into one source-local artifact. |
18
+ | Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, imports, and client roots into ModuleIR. |
19
19
  | Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Supported command handlers specialize to JSON-safe ModuleIR, then emit the existing `__kBehavior` AST without changing route plans. |
20
20
  | Build module generation | `framework/build.mjs`, `compile()` | Runs TypeScript with the Kudzu transformer, writes build-executable modules to `.kudzu`, rejects surviving React/Router runtime references, and generates handler source when descriptors exist. |
21
- | Handler/evaluator codegen | [`framework/compiler/handler-codegen.mjs`](../../framework/compiler/handler-codegen.mjs) | Converts descriptor AST into browser ESM exports and rewrites state, setter, reducer, capture, and imported-helper reads. It does not discover features. |
21
+ | Handler/evaluator lowering | [`framework/compiler/handler-lowering.mjs`](../../framework/compiler/handler-lowering.mjs) | Completes source-local callback/binding/list AST rewriting and diagnostics before the JSON-safe IR boundary. |
22
+ | Handler module codegen | [`framework/compiler/handler-codegen.mjs`](../../framework/compiler/handler-codegen.mjs) | Renders finalized ordered imports and concatenates generated module-export source without TypeScript AST or semantic discovery. |
22
23
  | Worker graph | [`framework/compiler/worker-compiler.mjs`](../../framework/compiler/worker-compiler.mjs) | Validates the exact effect-owned Worker form, validates its relative graph, emits content-hashed ESM, and resolves placeholders only for rendered effects. |
23
24
  | Build-time JSX execution | [`framework/core.mjs`](../../framework/core.mjs), `renderPage()` | Executes compiled pages/layouts, allocates deterministic route/layout ownership IDs, emits complete HTML, and returns the serializable route plan and capability booleans. |
24
25
  | Route capability projection | [`framework/compiler/route-capability-planner.mjs`](../../framework/compiler/route-capability-planner.mjs), `planRouteCapabilities()` | Purely folds rendered route plans and route facts into aggregate runtime/artifact requirements. |
@@ -57,7 +58,7 @@ The browser consumes static HTML first. State seeds and descriptors in that HTML
57
58
  ## Current Coupling To Remove
58
59
 
59
60
  - `createKudzuTransformer()` combines discovery, validation, specialization, descriptor registration, and transformed-source emission.
60
- - Effect/list relationships and transient component rewrite indexes remain in `WeakMap`/`WeakSet` tables keyed by AST identity; component ownership itself now has an explicit JSON-safe source result.
61
+ - Effect/keyed ownership relationships and transient component rewrite indexes remain in `WeakMap`/`WeakSet` tables keyed by AST identity; handler, binding, derived, and component ownership now have explicit JSON-safe source results.
61
62
  - `build()` destructures a broad capability manifest into many booleans and performs artifact-specific source surgery.
62
63
  - Runtime specialization relies on exact source-string and regular-expression replacements in `framework/build.mjs`.
63
64
  - Route facts, rendered plans, artifact requirements, and emitted-file decisions are represented at adjacent but not fully explicit boundaries.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Status
4
4
 
5
- The `0.8.18` state/props/component ownership result is complete. Goal A continues with the planned `0.8.19` HandlerIR, BindingIR, and DerivedIR seam while preserving accepted React-shaped syntax, diagnostics, complete HTML, emitted capability behavior, and current public APIs.
5
+ The `0.8.19` HandlerIR, BindingIR, and DerivedIR seam is complete. Goal A continues with the planned `0.8.20` KeyedBlockIR ownership result while preserving accepted React-shaped syntax, diagnostics, complete HTML, emitted capability behavior, and current public APIs.
6
6
 
7
7
  ## Target Boundaries
8
8
 
@@ -77,7 +77,7 @@ Analysis produces plain data:
77
77
  }
78
78
  ```
79
79
 
80
- The `0.8.18` source result adds ordered JSON-safe component ownership beside ModuleIR: lexical state/setter pairs, props, refs, IDs, and supported specialization records carry numeric slots, direct signal links, and source provenance. Initial values, final state IDs, bindings, derived values, effects, and keyed blocks join their explicit results in later planned patches; `core.mjs` remains authoritative for route allocation today.
80
+ The `0.8.19` source result adds module-export HandlerIR, BindingIR, ordered imports/client roots, and canonical tagged DerivedIR beside command and component ownership. Source-local AST lowering completes before this boundary; artifact codegen consumes finalized records without semantic traversal. Initial values, final state IDs, effect ownership, and keyed blocks join their explicit results in later planned patches; `core.mjs` remains authoritative for route allocation today.
81
81
 
82
82
  Source codegen lowers that data through the existing build ABI:
83
83
 
@@ -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.18` are completed scopes represented by package/release records; later rows remain planned until their package version and release record exist.
3
+ This is an execution sequence, not release history. `0.8.16` through `0.8.19` are completed scopes represented by package/release records; later rows remain planned until their package version and release record exist.
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
 
@@ -36,13 +36,14 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
36
36
  - `compiler/animation-frame-pass.mjs`: effect-owned animation-frame ref validation and plain mutable effect-scope lowering.
37
37
  - `compiler/browser-signal-passes.mjs`: static media-query external-store and navigator capability-condition normalization.
38
38
  - `compiler/collection-analysis.mjs`: pure collection expression IR, selector pipeline, alias, and imported-transform analysis.
39
- - `compiler/descriptor-session.mjs`: per-source semantic artifact creation and deterministic command, handler, effect, binding, and list evaluator descriptor registration.
39
+ - `compiler/descriptor-session.mjs`: private source-local descriptor registration and deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, import, and client-root finalization.
40
40
  - `compiler/normalization-pipeline.mjs`: ordered source normalization with parent-pointer repair after every compiler pass.
41
41
  - `compiler/react-migration-pass.mjs`: React import, memo, callback, `forwardRef`, and `useId` migration validation and lowering.
42
42
  - `compiler/custom-hook-timer-pass.mjs`: private custom-hook timeout-ref validation and compiler-owned timer-state lowering.
43
43
  - `compiler/effect-codegen.mjs`: route-specific ordinary, owned, and navigable effect entry generation.
44
44
  - `compiler/optimize/command-specialization.mjs`, `compiler/ir/module-ir.mjs`, and `compiler/codegen/command-codegen.mjs`: JSON-safe command analysis, sparse per-source ModuleIR, and source-analysis-free existing behavior-call generation.
45
- - `compiler/handler-codegen.mjs`: route-specific native handler, reactive binding, keyed-list evaluator, and client-import module source generation.
45
+ - `compiler/handler-lowering.mjs`: source-local native/effect callback, reducer, Context, Zustand, reactive-binding, and list-evaluator AST lowering before IR finalization.
46
+ - `compiler/handler-codegen.mjs`: AST-free ordered import rendering and finalized handler/binding export-source assembly.
46
47
  - `compiler/render-control-pass.mjs`: render-function early-return and exhaustive adjacent-assignment normalization.
47
48
  - `compiler/router-pass.mjs`: React Router import validation and native Link, pathname parameter, search parameter, and imperative navigation lowering.
48
49
  - `compiler/route-capability-planner.mjs`: pure route-plan projection into runtime and artifact capability requirements.
@@ -13,6 +13,7 @@ import { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.
13
13
  import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./compiler/descriptor-session.mjs"
14
14
  import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
15
15
  import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
16
+ import { createHandlerLowering } from "./compiler/handler-lowering.mjs"
16
17
  import { createCommandSpecializer } from "./compiler/optimize/command-specialization.mjs"
17
18
  import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
18
19
  import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
@@ -704,18 +705,18 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
704
705
  await mkdir(resolve(output, ".."), { recursive: true })
705
706
  await writeFile(output, result.outputText)
706
707
 
707
- const { componentAnalysis, nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
708
- const sourceResult = { file: relative(root, file).replaceAll(sep, "/"), componentAnalysis }
709
- if (!nativeHandlers.length && !effectHandlers.length && !reactiveBindings.length && !listExpressions.length) return sourceResult
710
- const callbacks = [...nativeHandlers, ...effectHandlers]
711
- const moduleSource = printHandlerModule({ callbacks, reactiveBindings, listExpressions, handlerPath })
708
+ const { componentAnalysis, moduleIR } = semantic
709
+ const sourceResult = { file: relative(root, file).replaceAll(sep, "/"), componentAnalysis, moduleIR }
710
+ const moduleHandlers = moduleIR.handlers.filter(handler => handler.kind === "module-export")
711
+ if (!moduleHandlers.length && !moduleIR.bindings.length) return sourceResult
712
+ const moduleSource = printHandlerModule({ moduleIR, handlerPath })
712
713
  const moduleResult = ts.transpileModule(moduleSource, {
713
714
  compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.ESNext },
714
715
  reportDiagnostics: true
715
716
  })
716
717
  const moduleErrors = moduleResult.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
717
718
  if (moduleErrors.length) throw new Error(moduleErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
718
- sourceResult.handlerModule = { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: nativeHandlers.length > 0, hasEffects: effectHandlers.length > 0, clientImports: [...clientImports], hasPackageImports: [...callbacks, ...reactiveBindings].some(entry => entry.imports?.some(import_ => import_.package)) }
719
+ sourceResult.handlerModule = { path: handlerPath, code: moduleResult.outputText, hasNativeHandlers: moduleHandlers.some(handler => handler.role === "native"), hasEffects: moduleHandlers.some(handler => handler.role === "effect"), clientImports: moduleIR.clientModules, hasPackageImports: moduleIR.imports.some(entry => entry.package) }
719
720
  return sourceResult
720
721
  }
721
722
 
@@ -882,7 +883,7 @@ function normalizeCompilerSource(sourceFile, { base, context, file, importedColl
882
883
  }
883
884
 
884
885
  function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences }) {
885
- const { nativeHandlers, effectHandlers } = semantic
886
+ const { moduleIR } = semantic
886
887
  return context => sourceFile => {
887
888
  const hasLinkElements = /<link/i.test(sourceFile.text)
888
889
  const importedStaticCollections = importedSerializableCollections(sourceFile, file, sourceFiles, sourceIndex)
@@ -899,6 +900,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
899
900
  factory,
900
901
  context,
901
902
  compileEventCommand,
903
+ handlerLowering,
902
904
  isPrimitiveLiteral: isPrimitiveDefaultLiteral,
903
905
  sourceName,
904
906
  rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
@@ -2118,7 +2120,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2118
2120
  }
2119
2121
  const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
2120
2122
  if (invalidDependency) effectFail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
2121
- const dependencyExpressions = []
2123
+ const dependencyDerived = []
2122
2124
  const dependencyStates = new Map()
2123
2125
  const dependencySubstitutions = new Map()
2124
2126
  const subscriptionDependencies = []
@@ -2134,7 +2136,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2134
2136
  const usedStates = new Set()
2135
2137
  const expression = collectionExpression(initializer, { fail: effectFail, stateNames, selectorStates: usedStates })
2136
2138
  if (!usedStates.size) effectFail(dependency, `useEffect() derived dependency "${dependency.text}" must read direct primitive state`)
2137
- dependencyExpressions.push(expression)
2139
+ dependencyDerived.push({ expression, states: usedStates, source: initializer })
2138
2140
  for (const name of usedStates) {
2139
2141
  subscriptionDependencies.push(factory.createIdentifier(name))
2140
2142
  dependencyStates.set(name, factory.createIdentifier(name))
@@ -2143,12 +2145,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2143
2145
  hasDerivedDependency = true
2144
2146
  } else {
2145
2147
  subscriptionDependencies.push(dependency)
2146
- dependencyExpressions.push(["state", dependency.text])
2148
+ dependencyDerived.push({ expression: ["state", dependency.text], states: [dependency.text], source: dependency })
2147
2149
  dependencyStates.set(dependency.text, dependency)
2148
2150
  }
2149
2151
  }
2150
2152
  if (!hasDerivedDependency) {
2151
- dependencyExpressions.length = 0
2153
+ dependencyDerived.length = 0
2152
2154
  dependencyStates.clear()
2153
2155
  }
2154
2156
  if (!effectOwner) fail(node, "useEffect() cannot be used outside a Kudzu component")
@@ -2200,6 +2202,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2200
2202
  for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
2201
2203
  usesListItem ||= Boolean(itemDependencies.length && !listEffect)
2202
2204
  usesBehavior = true
2205
+ const derivedDependencies = hasDerivedDependency ? dependencyDerived.map(entry => descriptors.registerDerived("expression", entry.expression, entry.states, entry.source)) : []
2203
2206
  return factory.updateCallExpression(node, node.expression, node.typeArguments, [
2204
2207
  callback,
2205
2208
  factory.createArrayLiteralExpression(hasDerivedDependency ? subscriptionDependencies : ordinaryDependencies),
@@ -2210,7 +2213,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2210
2213
  factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
2211
2214
  returns.cleanup ? factory.createTrue() : factory.createFalse(),
2212
2215
  factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field))),
2213
- hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
2216
+ hasDerivedDependency ? jsonExpression(derivedDependencies.map(entry => entry.expression), factory) : factory.createArrayLiteralExpression(),
2214
2217
  factory.createArrayLiteralExpression([...dependencyStates].map(([name, state]) => factory.createArrayLiteralExpression([factory.createStringLiteral(name), state])))
2215
2218
  ])
2216
2219
  }
@@ -2278,12 +2281,13 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2278
2281
  usesBinding = true
2279
2282
  listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings })
2280
2283
  }
2284
+ const selector = listParts.selector?.length ? descriptors.registerDerived("selector", listParts.selector, listParts.selectorStates, node).selector : listParts.selector ?? []
2281
2285
  const arguments_ = [
2282
2286
  listSource,
2283
2287
  listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
2284
2288
  visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map()),
2285
2289
  factory.createStringLiteral(listParts.ownerField ?? ""),
2286
- jsonExpression(listParts.selector ?? [], factory),
2290
+ jsonExpression(selector, factory),
2287
2291
  listParts.indexed ? factory.createTrue() : factory.createFalse()
2288
2292
  ]
2289
2293
  if (listParts.selectorStates?.size || listParts.static) arguments_.push(factory.createArrayLiteralExpression([...(listParts.selectorStates ?? [])].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
@@ -2344,10 +2348,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2344
2348
  }
2345
2349
 
2346
2350
  const transformed = ts.visitNode(sourceFile, visitor)
2351
+ descriptors.finalize()
2347
2352
  if (!usesBehavior) return transformed
2348
2353
 
2349
2354
  const behaviorImports = [factory.createImportSpecifier(false, factory.createIdentifier("behavior"), factory.createIdentifier("__kBehavior"))]
2350
- if (nativeHandlers.length) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
2355
+ if (moduleIR.handlers.some(handler => handler.kind === "module-export" && handler.role === "native")) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("nativeBehavior"), factory.createIdentifier("__kNativeBehavior")))
2351
2356
  if (usesBinding) {
2352
2357
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("binding"), factory.createIdentifier("__kBinding")))
2353
2358
  behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("select"), factory.createIdentifier("__kSelect")))
@@ -3885,9 +3890,8 @@ const workerCompiler = createWorkerCompiler({
3885
3890
  })
3886
3891
 
3887
3892
  const printEffectEntry = createEffectCodegen({ assetPath, inlineJson, relativeModulePath })
3893
+ const handlerLowering = createHandlerLowering({ cloneAst, synthesizeTree })
3888
3894
  const printHandlerModule = createHandlerCodegen({
3889
- cloneAst,
3890
- synthesizeTree,
3891
3895
  resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
3892
3896
  })
3893
3897
  const { normalizeReactMigrationSyntax, validateUseIdSyntax } = createReactMigrationPass({ cloneAst, jsxTagName })
@@ -2,17 +2,23 @@ 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, registerCommandHandler } from "./ir/module-ir.mjs"
5
+ import { createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerModuleHandler } from "./ir/module-ir.mjs"
6
6
 
7
7
  export function createSemanticArtifact(file) {
8
- return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file), nativeHandlers: [], effectHandlers: [], reactiveBindings: [], listExpressions: [], clientImports: new Set() }
8
+ return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file) }
9
9
  }
10
10
 
11
- export function createDescriptorSession({ semantic, handlerUrl, factory, context, compileEventCommand, isPrimitiveLiteral, rejectWorkerConstructions, sourceName = source => source.fileName }) {
12
- const { moduleIR, nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
11
+ export function createDescriptorSession({ semantic, handlerUrl, factory, context, compileEventCommand, handlerLowering, isPrimitiveLiteral, rejectWorkerConstructions, sourceName = source => source.fileName }) {
12
+ const { moduleIR } = semantic
13
+ const nativeHandlers = []
14
+ const effectHandlers = []
15
+ const reactiveBindings = []
16
+ const listExpressions = []
17
+ const clientModules = new Set()
18
+
13
19
  function compileListExpression(read, expression, item, index, states = new Set()) {
14
20
  const exportName = `listExpression${listExpressions.length}`
15
- listExpressions.push({ exportName, expression, item, index, states })
21
+ listExpressions.push({ exportName, expression, item, index, states, role: "list-expression" })
16
22
  const arguments_ = [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)]
17
23
  if (states.size) arguments_.push(factory.createArrayLiteralExpression([...states].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
18
24
  return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, arguments_)
@@ -20,7 +26,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
20
26
 
21
27
  function compileListConditional(entry) {
22
28
  const exportName = `listExpression${listExpressions.length}`
23
- listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index })
29
+ listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index, role: "list-conditional" })
24
30
  const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
25
31
  const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
26
32
  return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
@@ -65,7 +71,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
65
71
  registerClientImports(imports)
66
72
  const captures = new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
67
73
  const exportName = `binding${reactiveBindings.length}`
68
- reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports })
74
+ reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports, role: "binding" })
69
75
  const states = [...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
70
76
  const scope = [...captures].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
71
77
  const stateNames = new Set(usedStates)
@@ -91,17 +97,17 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
91
97
  const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, stateOwners, owner)
92
98
  if (optimized) return optimized
93
99
  rejectWorkerConstructions(expression)
94
- const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", listItem })
100
+ const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", role: "native", listItem })
95
101
  return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
96
102
  factory.createStringLiteral(handlerUrl), factory.createStringLiteral(descriptor.exportName), descriptor.states, descriptor.scope
97
103
  ])
98
104
  }
99
105
 
100
106
  function compileEffectCallback(expression, options) {
101
- return compileNativeCallback(expression, { ...options, entries: effectHandlers, prefix: "effect" })
107
+ return compileNativeCallback(expression, { ...options, entries: effectHandlers, prefix: "effect", role: "effect" })
102
108
  }
103
109
 
104
- function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, listItem, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
110
+ function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
105
111
  const allCaptures = nativeCaptureNames(expression, setters)
106
112
  const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
107
113
  const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
@@ -114,7 +120,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
114
120
  if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction)) usedStates.add(state)
115
121
  }
116
122
  const exportName = `${prefix}${entries.length}`
117
- entries.push({ exportName, expression, captures, imports, liveStates, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested })
123
+ entries.push({ exportName, expression, captures, deferValues, imports, listItem, liveStates, role, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested, usedStates })
118
124
  const value = name => deferValues
119
125
  ? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
120
126
  : factory.createIdentifier(name)
@@ -141,10 +147,75 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
141
147
  }
142
148
 
143
149
  function registerClientImports(imports) {
144
- for (const entry of imports) if (!entry.package) clientImports.add(entry.target)
150
+ for (const entry of imports) if (!entry.package) clientModules.add(entry.target)
151
+ }
152
+
153
+ function registerDerivedResult(kind, value, states = [], node) {
154
+ const normalized = JSON.parse(JSON.stringify(value))
155
+ return registerDerived(moduleIR, { kind, [kind]: normalized, states: [...states], ...(source(node) ? { source: source(node) } : {}) })
145
156
  }
146
157
 
147
- return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding }
158
+ function finalize() {
159
+ const callbacks = [...nativeHandlers, ...effectHandlers]
160
+ for (const entry of callbacks) {
161
+ const lowered = handlerLowering.lowerNativeHandler(entry)
162
+ const setters = Map.groupBy(entry.setters, ([, state]) => state)
163
+ registerModuleHandler(moduleIR, {
164
+ role: entry.role,
165
+ exportName: entry.exportName,
166
+ async: Boolean(entry.expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)),
167
+ generator: Boolean(entry.expression.asteriskToken),
168
+ signals: [...entry.usedStates].map(name => ({
169
+ name,
170
+ setters: (setters.get(name) ?? []).map(([setter]) => setter),
171
+ value: entry.deferValues ? "deferred" : "direct",
172
+ snapshot: lowered.stateSnapshots.includes(name)
173
+ })),
174
+ captures: [...entry.captures].map(name => ({
175
+ name,
176
+ source: name === (typeof entry.listItem === "string" ? entry.listItem : entry.listItem?.item) ? "list-item" : name === entry.listItem?.index ? "list-index" : "scope",
177
+ value: entry.deferValues ? "deferred" : "direct",
178
+ snapshot: lowered.captureSnapshots.includes(name)
179
+ })),
180
+ imports: entry.imports.map(importRecord),
181
+ code: lowered.code,
182
+ ...(source(entry.expression) ? { source: source(entry.expression) } : {})
183
+ })
184
+ }
185
+ for (const entry of reactiveBindings) registerBinding(moduleIR, {
186
+ role: entry.role,
187
+ exportName: entry.exportName,
188
+ parameters: ["__k"],
189
+ states: [...entry.states],
190
+ captures: [...entry.captures].map(name => ({ name, source: "scope" })),
191
+ imports: entry.imports.map(importRecord),
192
+ code: handlerLowering.lowerReactiveBinding(entry),
193
+ ...(source(entry.expression) ? { source: source(entry.expression) } : {})
194
+ })
195
+ for (const entry of listExpressions) registerBinding(moduleIR, {
196
+ role: entry.role,
197
+ exportName: entry.exportName,
198
+ parameters: [entry.item, entry.index ?? "__kIndex", "__k"],
199
+ states: [...(entry.states ?? [])],
200
+ captures: [],
201
+ imports: [],
202
+ code: handlerLowering.lowerListExpression(entry),
203
+ ...(source(entry.expression) ? { source: source(entry.expression) } : {})
204
+ })
205
+ const imports = [...callbacks, ...reactiveBindings].flatMap(entry => entry.imports ?? []).map(importRecord)
206
+ moduleIR.imports = [...new Map(imports.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry])).values()]
207
+ moduleIR.clientModules = [...clientModules]
208
+ }
209
+
210
+ function source(node) {
211
+ if (!node) return undefined
212
+ const original = ts.getOriginalNode(node)
213
+ return original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
214
+ }
215
+
216
+ const importRecord = entry => ({ target: entry.target, kind: entry.kind, local: entry.local, ...(entry.imported ? { imported: entry.imported } : {}), package: Boolean(entry.package) })
217
+
218
+ return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult }
148
219
  }
149
220
 
150
221
  function directStateIdentifier(expression, setters) {
@@ -1,19 +1,14 @@
1
- import ts from "typescript"
2
- import { isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, sourceNodeError } from "./ast-helpers.mjs"
3
-
4
- export function createHandlerCodegen({ cloneAst, synthesizeTree, resolveClientImport }) {
5
- return function printHandlerModule({ callbacks, reactiveBindings, listExpressions, handlerPath }) {
1
+ export function createHandlerCodegen({ resolveClientImport }) {
2
+ return function printHandlerModule({ moduleIR, handlerPath }) {
6
3
  return [
7
- printClientImports([...callbacks, ...reactiveBindings].flatMap(handler => handler.imports ?? []), handlerPath),
8
- ...callbacks.map(handler => printNativeHandler(handler)),
9
- ...reactiveBindings.map(entry => printReactiveBinding(entry)),
10
- ...listExpressions.map(entry => printListExpression(entry))
4
+ printClientImports(moduleIR.imports, handlerPath),
5
+ ...moduleIR.handlers.filter(handler => handler.kind === "module-export").map(handler => handler.code),
6
+ ...moduleIR.bindings.map(binding => binding.code)
11
7
  ].join("\n")
12
8
  }
13
9
 
14
10
  function printClientImports(entries, handlerPath) {
15
- const unique = new Map(entries.map(entry => [`${entry.target}:${entry.kind}:${entry.imported ?? ""}:${entry.local}`, entry]))
16
- const groups = Map.groupBy(unique.values(), entry => entry.target)
11
+ const groups = Map.groupBy(entries, entry => entry.target)
17
12
  const imports = []
18
13
  for (const [target, group] of groups) {
19
14
  const specifier = resolveClientImport(group[0], handlerPath)
@@ -25,272 +20,4 @@ export function createHandlerCodegen({ cloneAst, synthesizeTree, resolveClientIm
25
20
  }
26
21
  return imports.join("\n")
27
22
  }
28
-
29
- function printNativeHandler({ exportName, expression, captures, setters, reducers = new Map(), snapshotNested, liveStates = new Set() }) {
30
- const factory = ts.factory
31
- const stateNames = new Set(setters.values())
32
- const snapshotNames = snapshotNested ? nestedStateNames(expression, setters, liveStates) : new Set()
33
- const snapshots = new Map([...snapshotNames].map(name => [name, factory.createUniqueName("__kEffectState")]))
34
- const captureSnapshotNames = snapshotNested ? nestedCaptureNames(expression, captures) : new Set()
35
- const captureSnapshots = new Map([...captureSnapshotNames].map(name => [name, factory.createUniqueName("__kEffectCapture")]))
36
- const transformer = context => root => {
37
- const visitor = node => {
38
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
39
- const reducer = reducers.get(node.expression.text)
40
- if (reducer.contextAction) {
41
- const action = synthesizeTree(cloneAst(reducer.contextAction, factory, context))
42
- const call = factory.createCallExpression(action, undefined, node.arguments)
43
- ts.setParentRecursive(call, false)
44
- return ts.visitNode(call, visitor)
45
- }
46
- if (reducer.store) return zustandActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)))
47
- if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
48
- return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
49
- }
50
- if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
51
- if (reducers.get(node.name.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
52
- if (reducers.get(node.name.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
53
- return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
54
- }
55
- if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
56
- if (reducers.get(node.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
57
- if (reducers.get(node.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
58
- return reducerReference(factory, reducers.get(node.text))
59
- }
60
- if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
61
- return factory.createCallExpression(
62
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"),
63
- undefined,
64
- [factory.createStringLiteral(setters.get(node.expression.text)), ...node.arguments.map(argument => ts.visitNode(argument, visitor))]
65
- )
66
- }
67
- if (ts.isShorthandPropertyAssignment(node) && setters.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
68
- return factory.createPropertyAssignment(node.name, setterReference(factory, setters.get(node.name.text)))
69
- }
70
- if (ts.isIdentifier(node) && setters.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
71
- return setterReference(factory, setters.get(node.text))
72
- }
73
- if (ts.isShorthandPropertyAssignment(node) && stateNames.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
74
- if (snapshots.has(node.name.text) && insideNestedFunction(node, expression)) return factory.createPropertyAssignment(node.name, snapshots.get(node.name.text))
75
- return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
76
- }
77
- if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
78
- if (snapshots.has(node.text) && insideNestedFunction(node, expression)) return snapshots.get(node.text)
79
- return factory.createCallExpression(
80
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
81
- undefined,
82
- [factory.createStringLiteral(node.text)]
83
- )
84
- }
85
- if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
86
- if (captureSnapshots.has(node.name.text) && insideNestedFunction(node, expression)) return factory.createPropertyAssignment(node.name, captureSnapshots.get(node.name.text))
87
- return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
88
- }
89
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
90
- if (captureSnapshots.has(node.text) && insideNestedFunction(node, expression)) return captureSnapshots.get(node.text)
91
- return scopeRead(factory, node.text)
92
- }
93
- return ts.visitEachChild(node, visitor, context)
94
- }
95
- return ts.visitNode(root, visitor)
96
- }
97
- const transformed = ts.transform(expression.body, [transformer])
98
- try {
99
- let body = ts.isBlock(expression.body)
100
- ? transformed.transformed[0]
101
- : factory.createBlock([factory.createReturnStatement(transformed.transformed[0])], true)
102
- const snapshotDeclarations = [
103
- ...[...snapshots].map(([name, identifier]) => factory.createVariableDeclaration(identifier, undefined, undefined, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(name)]))),
104
- ...[...captureSnapshots].map(([name, identifier]) => factory.createVariableDeclaration(identifier, undefined, undefined, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "scope"), undefined, [factory.createStringLiteral(name)])))
105
- ]
106
- if (snapshotDeclarations.length) body = factory.updateBlock(body, [
107
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList(snapshotDeclarations, ts.NodeFlags.Const)),
108
- ...body.statements
109
- ])
110
- const modifiers = [factory.createModifier(ts.SyntaxKind.ExportKeyword)]
111
- if (expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) modifiers.push(factory.createModifier(ts.SyntaxKind.AsyncKeyword))
112
- const declaration = factory.createFunctionDeclaration(
113
- modifiers,
114
- expression.asteriskToken,
115
- exportName,
116
- undefined,
117
- [factory.createParameterDeclaration(undefined, undefined, "__k"), ...expression.parameters],
118
- undefined,
119
- body
120
- )
121
- return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
122
- } finally {
123
- transformed.dispose()
124
- }
125
- }
126
-
127
- function nestedCaptureNames(expression, captures) {
128
- const names = new Set()
129
- const visit = node => {
130
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
131
- ts.forEachChild(node, visit)
132
- }
133
- visit(expression.body)
134
- return names
135
- }
136
-
137
- function nestedStateNames(expression, setters, liveStates = new Set()) {
138
- const states = new Set(setters.values())
139
- const names = new Set()
140
- const visit = node => {
141
- if (ts.isIdentifier(node) && states.has(node.text) && !liveStates.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
142
- ts.forEachChild(node, visit)
143
- }
144
- visit(expression.body)
145
- return names
146
- }
147
-
148
- function insideNestedFunction(node, root) {
149
- for (let current = node.parent; current && current !== root; current = current.parent) {
150
- if (isFunctionLike(current)) return true
151
- }
152
- return false
153
- }
154
-
155
- function setterReference(factory, stateName) {
156
- return factory.createArrowFunction(
157
- undefined,
158
- undefined,
159
- [factory.createParameterDeclaration(undefined, undefined, "value")],
160
- undefined,
161
- factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
162
- factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(stateName), factory.createIdentifier("value")])
163
- )
164
- }
165
-
166
- function reducerReference(factory, reducer) {
167
- const action = factory.createUniqueName("__kAction")
168
- return factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, action)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), reducerDispatch(factory, reducer, action))
169
- }
170
-
171
- function reducerDispatch(factory, reducer, action) {
172
- if (reducer.store) return zustandActionDispatch(factory, reducer, [action])
173
- const previous = factory.createUniqueName("__kPrevious")
174
- const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createCallExpression(factory.createIdentifier(reducer.reducer), undefined, [previous, action]))
175
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
176
- }
177
-
178
- function zustandActionDispatch(factory, reducer, args) {
179
- const previous = factory.createUniqueName("__kPrevious")
180
- const current = factory.createUniqueName("__kStore")
181
- const updateValue = factory.createUniqueName("__kUpdate")
182
- const partial = factory.createUniqueName("__kPartial")
183
- const action = factory.createUniqueName("__kAction")
184
- const set = factory.createIdentifier(reducer.store.setName)
185
- const merge = factory.createExpressionStatement(factory.createBinaryExpression(current, factory.createToken(ts.SyntaxKind.EqualsToken), factory.createObjectLiteralExpression([
186
- factory.createSpreadAssignment(current),
187
- factory.createSpreadAssignment(partial)
188
- ])))
189
- const setBody = factory.createBlock([
190
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(partial, undefined, undefined, factory.createConditionalExpression(
191
- factory.createBinaryExpression(factory.createTypeOfExpression(updateValue), factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), factory.createStringLiteral("function")),
192
- undefined,
193
- factory.createCallExpression(updateValue, undefined, [current]),
194
- undefined,
195
- updateValue
196
- ))], ts.NodeFlags.Const)),
197
- merge
198
- ], true)
199
- const body = factory.createBlock([
200
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(current, undefined, undefined, factory.createObjectLiteralExpression([factory.createPropertyAssignment(reducer.store.field, previous)]))], ts.NodeFlags.Let)),
201
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(set, undefined, undefined, factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, updateValue)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), setBody))], ts.NodeFlags.Const)),
202
- factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(action, undefined, undefined, synthesizeTree(reducer.store.actions.get(reducer.action)))], ts.NodeFlags.Const)),
203
- factory.createExpressionStatement(factory.createCallExpression(action, undefined, args)),
204
- factory.createReturnStatement(factory.createPropertyAccessExpression(current, reducer.store.field))
205
- ], true)
206
- const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), body)
207
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
208
- }
209
-
210
- function printReactiveBinding({ exportName, expression, captures, states }) {
211
- const factory = ts.factory
212
- const transformer = context => root => {
213
- const visitor = node => {
214
- if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
215
- return factory.createPropertyAssignment(
216
- node.name,
217
- factory.createCallExpression(
218
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
219
- undefined,
220
- [factory.createStringLiteral(node.name.text)]
221
- )
222
- )
223
- }
224
- if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
225
- return factory.createCallExpression(
226
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
227
- undefined,
228
- [factory.createStringLiteral(node.text)]
229
- )
230
- }
231
- if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text)) {
232
- return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
233
- }
234
- if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node)) {
235
- return scopeRead(factory, node.text)
236
- }
237
- return ts.visitEachChild(node, visitor, context)
238
- }
239
- return ts.visitNode(root, visitor)
240
- }
241
- const transformed = ts.transform(expression, [transformer])
242
- try {
243
- const declaration = factory.createFunctionDeclaration(
244
- [factory.createModifier(ts.SyntaxKind.ExportKeyword)],
245
- undefined,
246
- exportName,
247
- undefined,
248
- [factory.createParameterDeclaration(undefined, undefined, "__k")],
249
- undefined,
250
- factory.createBlock([factory.createReturnStatement(transformed.transformed[0])], true)
251
- )
252
- return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
253
- } finally {
254
- transformed.dispose()
255
- }
256
- }
257
-
258
- function printListExpression({ exportName, expression, item, index, states = new Set() }) {
259
- const factory = ts.factory
260
- const transformer = context => root => {
261
- const visitor = node => {
262
- if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
263
- return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
264
- }
265
- if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
266
- return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.text)])
267
- }
268
- return ts.visitEachChild(node, visitor, context)
269
- }
270
- return ts.visitNode(root, visitor)
271
- }
272
- const transformed = ts.transform(expression, [transformer])
273
- const declaration = ts.factory.createFunctionDeclaration(
274
- [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
275
- undefined,
276
- exportName,
277
- undefined,
278
- [ts.factory.createParameterDeclaration(undefined, undefined, item), ts.factory.createParameterDeclaration(undefined, undefined, index ?? "__kIndex"), ts.factory.createParameterDeclaration(undefined, undefined, "__k")],
279
- undefined,
280
- ts.factory.createBlock([ts.factory.createReturnStatement(transformed.transformed[0])], true)
281
- )
282
- try {
283
- return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
284
- } finally {
285
- transformed.dispose()
286
- }
287
- }
288
-
289
- function scopeRead(factory, name) {
290
- return factory.createCallExpression(
291
- factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "scope"),
292
- undefined,
293
- [factory.createStringLiteral(name)]
294
- )
295
- }
296
23
  }
@@ -0,0 +1,278 @@
1
+ import ts from "typescript"
2
+ import { isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, sourceNodeError } from "./ast-helpers.mjs"
3
+
4
+ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
5
+ return { lowerListExpression: printListExpression, lowerNativeHandler: printNativeHandler, lowerReactiveBinding: printReactiveBinding }
6
+
7
+ function printNativeHandler({ exportName, expression, captures, setters, reducers = new Map(), snapshotNested, liveStates = new Set() }) {
8
+ const factory = ts.factory
9
+ const stateNames = new Set(setters.values())
10
+ const snapshotNames = snapshotNested ? nestedStateNames(expression, setters, liveStates) : new Set()
11
+ const snapshots = new Map([...snapshotNames].map(name => [name, factory.createUniqueName("__kEffectState")]))
12
+ const captureSnapshotNames = snapshotNested ? nestedCaptureNames(expression, captures) : new Set()
13
+ const captureSnapshots = new Map([...captureSnapshotNames].map(name => [name, factory.createUniqueName("__kEffectCapture")]))
14
+ const transformer = context => root => {
15
+ const visitor = node => {
16
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
17
+ const reducer = reducers.get(node.expression.text)
18
+ if (reducer.contextAction) {
19
+ const action = synthesizeTree(cloneAst(reducer.contextAction, factory, context))
20
+ const call = factory.createCallExpression(action, undefined, node.arguments)
21
+ ts.setParentRecursive(call, false)
22
+ return ts.visitNode(call, visitor)
23
+ }
24
+ if (reducer.store) return zustandActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)))
25
+ if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
26
+ return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
27
+ }
28
+ if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
29
+ if (reducers.get(node.name.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
30
+ if (reducers.get(node.name.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
31
+ return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
32
+ }
33
+ if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
34
+ if (reducers.get(node.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
35
+ if (reducers.get(node.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
36
+ return reducerReference(factory, reducers.get(node.text))
37
+ }
38
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
39
+ return factory.createCallExpression(
40
+ factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"),
41
+ undefined,
42
+ [factory.createStringLiteral(setters.get(node.expression.text)), ...node.arguments.map(argument => ts.visitNode(argument, visitor))]
43
+ )
44
+ }
45
+ if (ts.isShorthandPropertyAssignment(node) && setters.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
46
+ return factory.createPropertyAssignment(node.name, setterReference(factory, setters.get(node.name.text)))
47
+ }
48
+ if (ts.isIdentifier(node) && setters.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
49
+ return setterReference(factory, setters.get(node.text))
50
+ }
51
+ if (ts.isShorthandPropertyAssignment(node) && stateNames.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
52
+ if (snapshots.has(node.name.text) && insideNestedFunction(node, expression)) return factory.createPropertyAssignment(node.name, snapshots.get(node.name.text))
53
+ return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
54
+ }
55
+ if (ts.isIdentifier(node) && stateNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
56
+ if (snapshots.has(node.text) && insideNestedFunction(node, expression)) return snapshots.get(node.text)
57
+ return factory.createCallExpression(
58
+ factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
59
+ undefined,
60
+ [factory.createStringLiteral(node.text)]
61
+ )
62
+ }
63
+ if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
64
+ if (captureSnapshots.has(node.name.text) && insideNestedFunction(node, expression)) return factory.createPropertyAssignment(node.name, captureSnapshots.get(node.name.text))
65
+ return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
66
+ }
67
+ if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
68
+ if (captureSnapshots.has(node.text) && insideNestedFunction(node, expression)) return captureSnapshots.get(node.text)
69
+ return scopeRead(factory, node.text)
70
+ }
71
+ return ts.visitEachChild(node, visitor, context)
72
+ }
73
+ return ts.visitNode(root, visitor)
74
+ }
75
+ const transformed = ts.transform(expression.body, [transformer])
76
+ try {
77
+ let body = ts.isBlock(expression.body)
78
+ ? transformed.transformed[0]
79
+ : factory.createBlock([factory.createReturnStatement(transformed.transformed[0])], true)
80
+ const snapshotDeclarations = [
81
+ ...[...snapshots].map(([name, identifier]) => factory.createVariableDeclaration(identifier, undefined, undefined, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(name)]))),
82
+ ...[...captureSnapshots].map(([name, identifier]) => factory.createVariableDeclaration(identifier, undefined, undefined, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "scope"), undefined, [factory.createStringLiteral(name)])))
83
+ ]
84
+ if (snapshotDeclarations.length) body = factory.updateBlock(body, [
85
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList(snapshotDeclarations, ts.NodeFlags.Const)),
86
+ ...body.statements
87
+ ])
88
+ const modifiers = [factory.createModifier(ts.SyntaxKind.ExportKeyword)]
89
+ if (expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) modifiers.push(factory.createModifier(ts.SyntaxKind.AsyncKeyword))
90
+ const declaration = factory.createFunctionDeclaration(
91
+ modifiers,
92
+ expression.asteriskToken,
93
+ exportName,
94
+ undefined,
95
+ [factory.createParameterDeclaration(undefined, undefined, "__k"), ...expression.parameters],
96
+ undefined,
97
+ body
98
+ )
99
+ return {
100
+ code: ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile()),
101
+ stateSnapshots: [...snapshotNames],
102
+ captureSnapshots: [...captureSnapshotNames]
103
+ }
104
+ } finally {
105
+ transformed.dispose()
106
+ }
107
+ }
108
+
109
+ function nestedCaptureNames(expression, captures) {
110
+ const names = new Set()
111
+ const visit = node => {
112
+ if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
113
+ ts.forEachChild(node, visit)
114
+ }
115
+ visit(expression.body)
116
+ return names
117
+ }
118
+
119
+ function nestedStateNames(expression, setters, liveStates = new Set()) {
120
+ const states = new Set(setters.values())
121
+ const names = new Set()
122
+ const visit = node => {
123
+ if (ts.isIdentifier(node) && states.has(node.text) && !liveStates.has(node.text) && isReferenceIdentifier(node) && insideNestedFunction(node, expression) && !isShadowedIdentifier(node, expression)) names.add(node.text)
124
+ ts.forEachChild(node, visit)
125
+ }
126
+ visit(expression.body)
127
+ return names
128
+ }
129
+
130
+ function insideNestedFunction(node, root) {
131
+ for (let current = node.parent; current && current !== root; current = current.parent) {
132
+ if (isFunctionLike(current)) return true
133
+ }
134
+ return false
135
+ }
136
+
137
+ function setterReference(factory, stateName) {
138
+ return factory.createArrowFunction(
139
+ undefined,
140
+ undefined,
141
+ [factory.createParameterDeclaration(undefined, undefined, "value")],
142
+ undefined,
143
+ factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
144
+ factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(stateName), factory.createIdentifier("value")])
145
+ )
146
+ }
147
+
148
+ function reducerReference(factory, reducer) {
149
+ const action = factory.createUniqueName("__kAction")
150
+ return factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, action)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), reducerDispatch(factory, reducer, action))
151
+ }
152
+
153
+ function reducerDispatch(factory, reducer, action) {
154
+ if (reducer.store) return zustandActionDispatch(factory, reducer, [action])
155
+ const previous = factory.createUniqueName("__kPrevious")
156
+ const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createCallExpression(factory.createIdentifier(reducer.reducer), undefined, [previous, action]))
157
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
158
+ }
159
+
160
+ function zustandActionDispatch(factory, reducer, args) {
161
+ const previous = factory.createUniqueName("__kPrevious")
162
+ const current = factory.createUniqueName("__kStore")
163
+ const updateValue = factory.createUniqueName("__kUpdate")
164
+ const partial = factory.createUniqueName("__kPartial")
165
+ const action = factory.createUniqueName("__kAction")
166
+ const set = factory.createIdentifier(reducer.store.setName)
167
+ const merge = factory.createExpressionStatement(factory.createBinaryExpression(current, factory.createToken(ts.SyntaxKind.EqualsToken), factory.createObjectLiteralExpression([
168
+ factory.createSpreadAssignment(current),
169
+ factory.createSpreadAssignment(partial)
170
+ ])))
171
+ const setBody = factory.createBlock([
172
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(partial, undefined, undefined, factory.createConditionalExpression(
173
+ factory.createBinaryExpression(factory.createTypeOfExpression(updateValue), factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), factory.createStringLiteral("function")),
174
+ undefined,
175
+ factory.createCallExpression(updateValue, undefined, [current]),
176
+ undefined,
177
+ updateValue
178
+ ))], ts.NodeFlags.Const)),
179
+ merge
180
+ ], true)
181
+ const body = factory.createBlock([
182
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(current, undefined, undefined, factory.createObjectLiteralExpression([factory.createPropertyAssignment(reducer.store.field, previous)]))], ts.NodeFlags.Let)),
183
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(set, undefined, undefined, factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, updateValue)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), setBody))], ts.NodeFlags.Const)),
184
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(action, undefined, undefined, synthesizeTree(reducer.store.actions.get(reducer.action)))], ts.NodeFlags.Const)),
185
+ factory.createExpressionStatement(factory.createCallExpression(action, undefined, args)),
186
+ factory.createReturnStatement(factory.createPropertyAccessExpression(current, reducer.store.field))
187
+ ], true)
188
+ const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), body)
189
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
190
+ }
191
+
192
+ function printReactiveBinding({ exportName, expression, captures, states }) {
193
+ const factory = ts.factory
194
+ const transformer = context => root => {
195
+ const visitor = node => {
196
+ if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
197
+ return factory.createPropertyAssignment(
198
+ node.name,
199
+ factory.createCallExpression(
200
+ factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
201
+ undefined,
202
+ [factory.createStringLiteral(node.name.text)]
203
+ )
204
+ )
205
+ }
206
+ if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
207
+ return factory.createCallExpression(
208
+ factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"),
209
+ undefined,
210
+ [factory.createStringLiteral(node.text)]
211
+ )
212
+ }
213
+ if (ts.isShorthandPropertyAssignment(node) && captures.has(node.name.text)) {
214
+ return factory.createPropertyAssignment(node.name, scopeRead(factory, node.name.text))
215
+ }
216
+ if (ts.isIdentifier(node) && captures.has(node.text) && isReferenceIdentifier(node)) {
217
+ return scopeRead(factory, node.text)
218
+ }
219
+ return ts.visitEachChild(node, visitor, context)
220
+ }
221
+ return ts.visitNode(root, visitor)
222
+ }
223
+ const transformed = ts.transform(expression, [transformer])
224
+ try {
225
+ const declaration = factory.createFunctionDeclaration(
226
+ [factory.createModifier(ts.SyntaxKind.ExportKeyword)],
227
+ undefined,
228
+ exportName,
229
+ undefined,
230
+ [factory.createParameterDeclaration(undefined, undefined, "__k")],
231
+ undefined,
232
+ factory.createBlock([factory.createReturnStatement(transformed.transformed[0])], true)
233
+ )
234
+ return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
235
+ } finally {
236
+ transformed.dispose()
237
+ }
238
+ }
239
+
240
+ function printListExpression({ exportName, expression, item, index, states = new Set() }) {
241
+ const factory = ts.factory
242
+ const transformer = context => root => {
243
+ const visitor = node => {
244
+ if (ts.isShorthandPropertyAssignment(node) && states.has(node.name.text)) {
245
+ return factory.createPropertyAssignment(node.name, factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.name.text)]))
246
+ }
247
+ if (ts.isIdentifier(node) && states.has(node.text) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression)) {
248
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "get"), undefined, [factory.createStringLiteral(node.text)])
249
+ }
250
+ return ts.visitEachChild(node, visitor, context)
251
+ }
252
+ return ts.visitNode(root, visitor)
253
+ }
254
+ const transformed = ts.transform(expression, [transformer])
255
+ const declaration = ts.factory.createFunctionDeclaration(
256
+ [ts.factory.createModifier(ts.SyntaxKind.ExportKeyword)],
257
+ undefined,
258
+ exportName,
259
+ undefined,
260
+ [ts.factory.createParameterDeclaration(undefined, undefined, item), ts.factory.createParameterDeclaration(undefined, undefined, index ?? "__kIndex"), ts.factory.createParameterDeclaration(undefined, undefined, "__k")],
261
+ undefined,
262
+ ts.factory.createBlock([ts.factory.createReturnStatement(transformed.transformed[0])], true)
263
+ )
264
+ try {
265
+ return ts.createPrinter().printNode(ts.EmitHint.Unspecified, declaration, expression.getSourceFile())
266
+ } finally {
267
+ transformed.dispose()
268
+ }
269
+ }
270
+
271
+ function scopeRead(factory, name) {
272
+ return factory.createCallExpression(
273
+ factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "scope"),
274
+ undefined,
275
+ [factory.createStringLiteral(name)]
276
+ )
277
+ }
278
+ }
@@ -1,5 +1,5 @@
1
1
  export function createModuleIR(file) {
2
- return { version: 1, file, signals: [], handlers: [] }
2
+ return { version: 1, file, signals: [], handlers: [], bindings: [], derived: [], imports: [], clientModules: [] }
3
3
  }
4
4
 
5
5
  export function registerCommandHandler(moduleIR, commands, source, scope = "module") {
@@ -21,3 +21,21 @@ export function registerCommandHandler(moduleIR, commands, source, scope = "modu
21
21
  moduleIR.handlers.push(handler)
22
22
  return handler
23
23
  }
24
+
25
+ export function registerModuleHandler(moduleIR, descriptor) {
26
+ const handler = { slot: moduleIR.handlers.length, kind: "module-export", ...descriptor }
27
+ moduleIR.handlers.push(handler)
28
+ return handler
29
+ }
30
+
31
+ export function registerBinding(moduleIR, descriptor) {
32
+ const binding = { slot: moduleIR.bindings.length, kind: "module-export", ...descriptor }
33
+ moduleIR.bindings.push(binding)
34
+ return binding
35
+ }
36
+
37
+ export function registerDerived(moduleIR, descriptor) {
38
+ const derived = { slot: moduleIR.derived.length, ...descriptor }
39
+ moduleIR.derived.push(derived)
40
+ return derived
41
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.18",
3
+ "version": "0.8.19",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",