@kudzujs/core 0.8.16 → 0.8.18

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.
@@ -195,6 +195,20 @@ This queue orders the next investigations by general migration value. Start only
195
195
  - Component specialization, effect analysis, and keyed-list ownership remain in the main transformer until their AST-identity side tables can be replaced by explicit analysis results rather than moved behind large context objects.
196
196
  - The continuation packet under `docs/next-architecture` fixes the Goal A patch sequence, output/performance gates, and explicit decision boundaries for deferred optimization, state/resource research, routing, and React compatibility.
197
197
 
198
+ ### Completed In 0.8.17
199
+
200
+ - Supported command handlers cross the first JSON-safe ModuleIR slice: analysis emits plain command data, ModuleIR owns numeric signal/handler slots and lexical/source metadata, and focused codegen restores the existing `__kBehavior()` build ABI.
201
+ - Primitive SET, numeric ADD/subtract, functional setters, state logging, unary-plus, and negative-zero semantics remain specialized; non-finite values safely retain generic handler lowering.
202
+ - Same-named state in separate owners no longer aliases in IR, shared setter environments reuse signal identity, and synthetic specialized callbacks receive no fabricated source position.
203
+ - For unchanged source input, Counter HTML, route plans, command runtime, and generated artifacts remain byte-identical to 0.8.16; no browser capability or source syntax changed.
204
+
205
+ ### Completed In 0.8.18
206
+
207
+ - Every compiled non-Worker source retains an ordered JSON-safe component analysis result covering lexical state/setter pairs, destructured props, object refs, deterministic IDs, and supported component specializations.
208
+ - Command signals now resolve ownership per referenced state rather than per setter-map identity, preserving distinct parent, specialized-child, keyed-row, reducer, custom-hook, and Context Provider relationships.
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
+ - `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
+
198
212
  ## Cross-Cutting Performance Gates
199
213
 
200
214
  Every migration feature must preserve:
package/PERFORMANCE.md CHANGED
@@ -1,5 +1,56 @@
1
1
  # Performance Records
2
2
 
3
+ ## 0.8.18 Explicit Component Ownership
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.17` tag `d48f0cf` and the `0.8.18` compiler-only candidate used detached worktrees on the same temporary volume with identical installed dependencies.
6
+
7
+ 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 2.43% candidate median difference remains below the 5% architecture gate and does not establish a material regression.
8
+
9
+ | Target | Build median | Worker raw / gzip | Window raw / gzip |
10
+ |---|---:|---:|---:|
11
+ | 0.8.17 baseline | 770.9 ms | 907 B / 475 B | 12,148 B / 5,427 B |
12
+ | 0.8.18 candidate | 789.6 ms | 907 B / 475 B | 12,148 B / 5,427 B |
13
+
14
+ ```text
15
+ 0.8.17: [770.9,705.6,817.1,768.5,760.2,839.8,758.3,756.1,960.7,834.6,707.9,706.5,582.9,463.5,625.5,1004.2,846.9,821.3,837.5,876.9,810.8]
16
+ 0.8.18: [753.9,726.4,879.3,851.5,942.8,949.3,758.4,761.3,794.3,782.2,620.6,622.9,610.8,726.8,732.3,833.0,789.6,816.3,845.2,811.3,825.0]
17
+ ```
18
+
19
+ Before release-content updates, the complete `dist` and `.kudzu` trees were byte-identical. The Worker and window graphs remain byte-identical. This measurement covers compiler clean-build startup and artifact size, not browser interaction latency or cross-framework performance.
20
+
21
+ Representative fixture builds also retained identical file lists and SHA-256 content against `v0.8.17`. Deploy `dist` bytes matched exactly; `.kudzu` hashes matched after replacing only each detached worktree's absolute root in existing source-location strings. The complete compared lists were:
22
+
23
+ ```text
24
+ 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
25
+ conditionals: 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/index.html, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs
26
+ nested-component-lists: dist/assets/effects/index.js, dist/assets/handlers/pages/index.js, dist/assets/kudzu-effect.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, .kudzu/ImportedItem.mjs, .kudzu/ImportedShell.mjs, .kudzu/kudzu-plan.json, .kudzu/label.mjs, .kudzu/pages/index.mjs
27
+ effects: dist/api/items.json, dist/assets/effects/index.js, dist/assets/effects/oak/index.js, dist/assets/effects/only/index.js, dist/assets/handlers/pages/[slug].js, dist/assets/handlers/pages/index.js, dist/assets/handlers/pages/only.js, dist/assets/kudzu-binding.js, dist/assets/kudzu-effect.js, dist/assets/kudzu-list.js, dist/assets/kudzu-serialization.js, dist/assets/kudzu-style.js, dist/assets/kudzu.js, dist/index.html, dist/oak/index.html, dist/only/index.html, dist/static/index.html, .kudzu/kudzu-plan.json, .kudzu/pages/[slug].mjs, .kudzu/pages/index.mjs, .kudzu/pages/only.mjs, .kudzu/pages/static.mjs
28
+ worker-effects: 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, .kudzu/telemetry/ring.mjs
29
+ runtime-params: dist/assets/effects/orgs/[org]/items/[id]/index.js, dist/assets/handlers/pages/orgs/[org]/items/[id].js, dist/assets/kudzu-binding.js, dist/assets/kudzu-effect.js, dist/assets/kudzu-native.js, dist/assets/kudzu-serialization.js, dist/assets/kudzu-style.js, dist/assets/kudzu.js, dist/assets/native/orgs/[org]/items/[id]/index.js, dist/assets/params/orgs/[org]/items/[id]/index.js, dist/orgs/[org]/items/[id]/index.html, dist/orgs/acme/items/new/index.html, dist/rewrites.json, .kudzu/kudzu-plan.json, .kudzu/pages/orgs/[org]/items/[id].mjs, .kudzu/pages/orgs/acme/items/new.mjs
30
+ navigation: dist/[section]/[id]/index.html, dist/assets/effects/[section]/[id]/index.js, dist/assets/effects/broken/index.js, dist/assets/effects/cart/index.js, dist/assets/effects/chart/index.js, dist/assets/effects/items/[id]/index.js, dist/assets/effects/items/new/index.js, dist/assets/effects/product/index.js, dist/assets/handlers/Shell.js, dist/assets/handlers/chunks/chunk-VIS4MAAV.js, dist/assets/handlers/pages/cart.js, dist/assets/handlers/pages/chart.js, dist/assets/handlers/pages/items/[id].js, dist/assets/handlers/pages/product.js, dist/assets/kudzu-binding.js, dist/assets/kudzu-effect.js, dist/assets/kudzu-list.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/items/[id]/index.js, dist/assets/native/product/index.js, dist/assets/params/[section]/[id]/index.js, dist/assets/params/items/[id]/index.js, dist/broken/index.html, dist/browser-test.js, dist/cart/index.html, dist/chart/index.html, dist/items/[id]/index.html, dist/items/new/index.html, dist/outside/index.html, dist/product/index.html, .kudzu/Shell.mjs, .kudzu/chart.mjs, .kudzu/kudzu-plan.json, .kudzu/pages/[section]/[id].mjs, .kudzu/pages/broken.mjs, .kudzu/pages/cart.mjs, .kudzu/pages/chart.mjs, .kudzu/pages/items/[id].mjs, .kudzu/pages/items/new.mjs, .kudzu/pages/outside.mjs, .kudzu/pages/product.mjs
31
+ callback-ref-ownership: dist/assets/effects/index.js, dist/assets/handlers/pages/index.js, dist/assets/kudzu-binding.js, dist/assets/kudzu-effect.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, .kudzu/ImportedAgeInput.mjs, .kudzu/ImportedButton.mjs, .kudzu/ImportedSearch.mjs, .kudzu/ImportedTooltip.mjs, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs
32
+ 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
33
+ 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
34
+ ```
35
+
36
+ ## 0.8.17 Command ModuleIR
37
+
38
+ 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.16` tag `98a4ad9` and the `0.8.17` release candidate used detached worktrees on the same temporary volume with identical installed dependencies.
39
+
40
+ Both targets received two warm-ups followed by 21 clean `worker-effects` production builds in round-robin alternating order. Cleanup remained outside timing. The distributions overlap; the 1.76% candidate median difference remains below the 5% architecture gate and does not establish a material regression.
41
+
42
+ | Target | Build median | Worker raw / gzip | Window raw / gzip |
43
+ |---|---:|---:|---:|
44
+ | 0.8.16 baseline | 782.0 ms | 907 B / 475 B | 12,148 B / 5,427 B |
45
+ | 0.8.17 candidate | 795.8 ms | 907 B / 475 B | 12,148 B / 5,427 B |
46
+
47
+ ```text
48
+ 0.8.16: [775.9,787.9,846.8,702.3,782.0,712.5,665.1,702.9,835.0,743.2,780.6,931.7,894.1,1004.0,849.8,1031.1,829.7,768.8,718.5,842.9,763.0]
49
+ 0.8.17: [696.0,721.9,778.8,850.4,846.1,835.2,710.4,711.6,836.1,766.9,822.6,945.0,838.2,991.5,858.6,803.4,706.3,760.8,773.8,764.3,795.8]
50
+ ```
51
+
52
+ Before release-content updates, the unchanged source site, Counter build module, route plan, and command runtime were byte-identical. The final Worker and window graphs remain byte-identical. This measurement covers compiler clean-build startup and artifact size, not browser interaction latency or cross-framework performance.
53
+
3
54
  ## 0.8.16 Compiler Analysis Boundaries
4
55
 
5
56
  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.15` commit `8405d29` and the `0.8.16` release 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.16 - Compiler analysis boundaries.** Per-source descriptors, pure collection analysis, command specialization, Zustand migration, and route capability planning now have focused compiler ownership while preserving generated output and runtime behavior. Read the [release notes](./RELEASES.md#0816---compiler-analysis-boundaries), open the [release page](https://kudzujs.cloud/releases/0.8.16), or follow the [next architecture packet](./docs/next-architecture/README.md).
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).
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,70 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.18 - Explicit component ownership
4
+
5
+ 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
+ ### Changed in 0.8.18
8
+
9
+ - Every compiled non-Worker source retains component analysis alongside its transformed build module and optional handler module; Worker graphs keep their existing dedicated result.
10
+ - Lexical owners record ordered state and setter slots, destructured prop shape, object refs, deterministic IDs, and honest source ranges.
11
+ - Specialized calls record supplied/defaulted props, generated state/ref/ID ownership, and direct links to parent, reducer, custom-hook, or Context signals.
12
+ - Command IR resolves owner identity per state command instead of assigning one setter-map scope to an entire handler. Mixed parent/child and nested keyed commands remain distinct.
13
+ - Context consumer signals point to a stable Provider source owner; repeated imported and structural specializations retain independent compile-time ownership.
14
+ - AST `WeakMap` indexes remain private to immediate source rewriting. They do not cross the JSON-safe source result or become a runtime component model.
15
+
16
+ ### Goal A boundary
17
+
18
+ - `core.mjs` still executes transformed components at build time and allocates final route/layout state, ref, ID, conditional, and keyed ownership in the same order.
19
+ - Initial values, browser state IDs, complete HTML, and the serializable route plan remain authoritative runtime inputs; the component result does not duplicate them.
20
+ - HandlerIR, BindingIR, and DerivedIR are the planned `0.8.19` seam. KeyedBlockIR and EffectIR remain on their existing paths until `0.8.20` and `0.8.21`.
21
+ - No accepted syntax, public API, VDOM, hydration, component rerender, or retained browser component tree was added.
22
+
23
+ ### Validation
24
+
25
+ - The complete suite passes 167/167 tests, including JSON round-trip, per-signal owner, repeated/conditional/imported state, setter adapter, ref, ID, Context, reducer, and nested keyed ownership contracts.
26
+ - Before release-content updates, the complete `dist` and `.kudzu` trees were byte-identical to `v0.8.17`.
27
+ - Worker and window graphs remain byte-identical. A same-volume 21-run interleaved comparison measured a 2.43% candidate median difference, below the 5% architecture gate; raw arrays 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.18
34
+ ```
35
+
36
+ ## 0.8.17 - Command ModuleIR
37
+
38
+ Kudzu 0.8.17 lands the first real Goal A vertical slice: supported command handlers are recognized as plain JSON-safe data, registered in a sparse per-source ModuleIR, and lowered back through focused source codegen without changing deploy output.
39
+
40
+ ### Changed in 0.8.17
41
+
42
+ - Direct setter arithmetic, functional setter arithmetic, primitive setter values, and state logging now produce plain Command IR instead of generated TypeScript array nodes during analysis.
43
+ - ModuleIR assigns deterministic numeric signal and handler slots while retaining readable state names, lexical state-owner keys, and original source ranges when they exist.
44
+ - Command codegen consumes only ModuleIR signal and handler data to reconstruct the existing `__kBehavior()` call; it performs no source analysis.
45
+ - Signed literals preserve unary-plus and negative-zero source semantics through JSON-safe syntax metadata. Non-finite numeric forms stay on the existing generic handler path.
46
+ - Same-named states in independent component owners receive distinct IR slots, while nested handlers sharing one setter environment reuse the same signal identity.
47
+ - Synthetic specialized handlers omit invented source positions; imported/original handlers retain normalized repository-relative source provenance.
48
+
49
+ ### Goal A boundary
50
+
51
+ - The first active ModuleIR slice contains lexical signals and command handlers only. `core.mjs` remains authoritative for initial values, final route/layout state IDs, complete HTML, and the serializable route plan.
52
+ - State/props/ref/component specialization is the planned `0.8.18` seam. Bindings, derived values, keyed blocks, and effects remain on their existing paths until their own explicit results replace AST side tables.
53
+ - No VDOM, hydration, component rerender, runtime component tree, public API, or accepted syntax was added.
54
+
55
+ ### Validation
56
+
57
+ - The complete suite passes 166/166 tests, including JSON round-trip, numeric edge, lexical owner, command codegen, specialized-component, and existing generic-handler contracts.
58
+ - Before release-content updates, the unchanged source site `dist`, Counter build module, route plan, and command runtime were byte-identical to `v0.8.16`; the final release adds only its expected version, homepage, documentation, and release-page content.
59
+ - The tracked Worker and window graphs remain byte-identical. A same-volume 21-run comparison found no build regression; raw arrays and environment are recorded in `PERFORMANCE.md`.
60
+ - `create-kudzu` remains 0.1.101 because its unchanged template already accepts `@kudzujs/core@^0.8.15`.
61
+
62
+ ### Upgrade
63
+
64
+ ```bash
65
+ npm install @kudzujs/core@^0.8.17
66
+ ```
67
+
3
68
  ## 0.8.16 - Compiler analysis boundaries
4
69
 
5
70
  Kudzu 0.8.16 continues the behavior-preserving compiler decomposition: source-local descriptors, the pure collection language, command fast paths, reduced Zustand migration, and route capability projection now have explicit ownership outside build orchestration.
@@ -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 | Approved for execution | Continue with `0.8.17` after the `0.8.16` analysis-boundary release |
9
+ | A: compiler foundation | `0.8.18` component ownership result complete | Continue with `0.8.19` HandlerIR, BindingIR, and DerivedIR 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 `0.8.16` compiler-analysis 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.18` component ownership 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,8 +13,10 @@ This maps the `0.8.16` compiler-analysis boundary. File and function names are t
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()` | Still owns component specialization, hooks/state, effects, keyed-list ownership, imports, reactive JSX, and many AST-identity side tables. This is the largest remaining coupling. |
17
- | Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Registers deterministic native handler, effect handler, binding, list evaluator, and client-import descriptors into one source-local artifact. |
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. |
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. |
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. |
18
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. |
19
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. |
20
22
  | 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. |
@@ -55,7 +57,7 @@ The browser consumes static HTML first. State seeds and descriptors in that HTML
55
57
  ## Current Coupling To Remove
56
58
 
57
59
  - `createKudzuTransformer()` combines discovery, validation, specialization, descriptor registration, and transformed-source emission.
58
- - Component/effect/list relationships remain in `WeakMap`/`WeakSet` tables keyed by AST identity; they cannot be moved safely until replaced by explicit results.
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.
59
61
  - `build()` destructures a broad capability manifest into many booleans and performs artifact-specific source surgery.
60
62
  - Runtime specialization relies on exact source-string and regular-expression replacements in `framework/build.mjs`.
61
63
  - 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
- Approved for execution. Goal A is an architecture-only continuation: preserve accepted React-shaped syntax, diagnostics, complete HTML, emitted capability behavior, and current public APIs while making analysis and generation boundaries explicit.
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.
6
6
 
7
7
  ## Target Boundaries
8
8
 
@@ -68,7 +68,7 @@ Analysis produces plain data:
68
68
 
69
69
  ```js
70
70
  {
71
- signals: [{ slot: 0, debugName: "count", initial: 0, scope: "local" }],
71
+ signals: [{ slot: 0, key: "state:0:count", debugName: "count" }],
72
72
  handlers: [{
73
73
  slot: 0,
74
74
  kind: "commands",
@@ -77,6 +77,8 @@ 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.
81
+
80
82
  Source codegen lowers that data through the existing build ABI:
81
83
 
82
84
  ```js
@@ -1,6 +1,6 @@
1
1
  # Planned Version Sequence
2
2
 
3
- This is an execution sequence, not release history. `0.8.16` is the completed analysis-boundary scope represented by the current 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.18` 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
 
@@ -32,15 +32,16 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
32
32
 
33
33
  - `build.mjs`: TSX compilation, static, `getStaticPaths`, and runtime-fallback routes, base paths, CSS collection, post-build hooks, behavior extraction, and static HTML output.
34
34
  - `compiler/ast-helpers.mjs`: shared TypeScript AST traversal, scope, binding, effect-return, and source-location helpers used by compiler passes.
35
+ - `compiler/analysis/component-analysis.mjs`: ordered JSON-safe state, setter, prop, ref, ID, and component-specialization ownership results.
35
36
  - `compiler/animation-frame-pass.mjs`: effect-owned animation-frame ref validation and plain mutable effect-scope lowering.
36
37
  - `compiler/browser-signal-passes.mjs`: static media-query external-store and navigator capability-condition normalization.
37
38
  - `compiler/collection-analysis.mjs`: pure collection expression IR, selector pipeline, alias, and imported-transform analysis.
38
- - `compiler/descriptor-session.mjs`: per-source semantic artifact creation and deterministic handler, effect, binding, and list evaluator descriptor registration.
39
+ - `compiler/descriptor-session.mjs`: per-source semantic artifact creation and deterministic command, handler, effect, binding, and list evaluator descriptor registration.
39
40
  - `compiler/normalization-pipeline.mjs`: ordered source normalization with parent-pointer repair after every compiler pass.
40
41
  - `compiler/react-migration-pass.mjs`: React import, memo, callback, `forwardRef`, and `useId` migration validation and lowering.
41
42
  - `compiler/custom-hook-timer-pass.mjs`: private custom-hook timeout-ref validation and compiler-owned timer-state lowering.
42
43
  - `compiler/effect-codegen.mjs`: route-specific ordinary, owned, and navigable effect entry generation.
43
- - `compiler/event-command-pass.mjs`: direct setter and state-log fast-path recognition into compact command descriptors.
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.
44
45
  - `compiler/handler-codegen.mjs`: route-specific native handler, reactive binding, keyed-list evaluator, and client-import module source generation.
45
46
  - `compiler/render-control-pass.mjs`: render-function early-return and exhaustive adjacent-assignment normalization.
46
47
  - `compiler/router-pass.mjs`: React Router import validation and native Link, pathname parameter, search parameter, and imperative navigation lowering.
@@ -4,6 +4,7 @@ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node
4
4
  import { pathToFileURL } from "node:url"
5
5
  import { build as bundle, transform } from "esbuild"
6
6
  import ts from "typescript"
7
+ import { createComponentAnalysisSession } from "./compiler/analysis/component-analysis.mjs"
7
8
  import { normalizeEffectAnimationFrameRefs } from "./compiler/animation-frame-pass.mjs"
8
9
  import { bindingNames, containsJsx, effectReturns, functionVarDeclaresName, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceLocation, sourceNodeError, statementDeclaresName, unwrapExpression } from "./compiler/ast-helpers.mjs"
9
10
  import { normalizeMediaQueryExternalStores, normalizeNavigatorCapabilityConditions } from "./compiler/browser-signal-passes.mjs"
@@ -11,8 +12,8 @@ import { analyzeCollectionPipeline, collectionExpression, collectionParameters,
11
12
  import { normalizeCustomHookTimerRefs } from "./compiler/custom-hook-timer-pass.mjs"
12
13
  import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCaptureNames, referencedReducerDispatches, referencedStateNames } from "./compiler/descriptor-session.mjs"
13
14
  import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
14
- import { createEventCommandCompiler } from "./compiler/event-command-pass.mjs"
15
15
  import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
16
+ import { createCommandSpecializer } from "./compiler/optimize/command-specialization.mjs"
16
17
  import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
17
18
  import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
18
19
  import { normalizeRenderControlFlow } from "./compiler/render-control-pass.mjs"
@@ -31,7 +32,7 @@ const pagesDirectory = join(sourceDirectory, "pages")
31
32
  const workDirectory = join(root, ".kudzu")
32
33
  const outputDirectory = join(root, "dist")
33
34
  const staticAssetExtensions = new Set([".avif", ".gif", ".ico", ".jpeg", ".jpg", ".otf", ".png", ".svg", ".ttf", ".webp", ".woff", ".woff2"])
34
- const compileEventCommand = createEventCommandCompiler({ isPrimitiveLiteral: isPrimitiveDefaultLiteral, synthesizeSerializableStateLiteral })
35
+ const compileEventCommand = createCommandSpecializer({ isPrimitiveLiteral: isPrimitiveDefaultLiteral })
35
36
  const { analyzeZustandStores, normalizeZustandMigrationSyntax } = createZustandPass({ isSerializableStateLiteral, nativeCaptureNames, sourceDirectory })
36
37
 
37
38
  export async function build({ quiet = false, minify = true } = {}) {
@@ -72,13 +73,13 @@ export async function build({ quiet = false, minify = true } = {}) {
72
73
  const importedAssets = new Set()
73
74
  const { cssModules, cssOutputs } = await prepareSourceStyles(cssFiles, staticFiles, importedAssets, base)
74
75
 
75
- const handlerModules = []
76
+ const sourceResults = []
76
77
  const workerReferences = []
77
78
  for (const file of sourceFiles) {
78
79
  if (file.endsWith(".worker.ts")) continue
79
- const handlerModule = await compile(file, sourceFileSet, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences)
80
- if (handlerModule) handlerModules.push(handlerModule)
80
+ sourceResults.push(await compile(file, sourceFileSet, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences))
81
81
  }
82
+ const handlerModules = sourceResults.flatMap(result => result.handlerModule ? [result.handlerModule] : [])
82
83
 
83
84
  const plans = []
84
85
  const routeCapabilities = new Map()
@@ -466,6 +467,7 @@ export async function build({ quiet = false, minify = true } = {}) {
466
467
  }
467
468
 
468
469
  if (!quiet) console.log(`Built ${plans.length} page(s), ${behaviorCount} interactive page(s) into dist/`)
470
+ return { sourceResults }
469
471
  }
470
472
 
471
473
  function preloadModules(html) {
@@ -677,7 +679,7 @@ function escapeAttribute(value) {
677
679
 
678
680
  async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAssets, cssModules, base, workerReferences) {
679
681
  const source = sourceIndex.get(file)
680
- const semantic = createSemanticArtifact()
682
+ const semantic = createSemanticArtifact(relative(root, file).replaceAll(sep, "/"))
681
683
  const handlerPath = `handlers/${relative(sourceDirectory, file).replaceAll(sep, "/").replace(/\.(?:ts|tsx)$/, ".js")}`
682
684
  const result = ts.transpileModule(source, {
683
685
  fileName: file,
@@ -702,8 +704,9 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
702
704
  await mkdir(resolve(output, ".."), { recursive: true })
703
705
  await writeFile(output, result.outputText)
704
706
 
705
- const { nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
706
- if (!nativeHandlers.length && !effectHandlers.length && !reactiveBindings.length && !listExpressions.length) return undefined
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
707
710
  const callbacks = [...nativeHandlers, ...effectHandlers]
708
711
  const moduleSource = printHandlerModule({ callbacks, reactiveBindings, listExpressions, handlerPath })
709
712
  const moduleResult = ts.transpileModule(moduleSource, {
@@ -712,7 +715,8 @@ async function compile(file, sourceFiles, sourceIndex, staticFiles, importedAsse
712
715
  })
713
716
  const moduleErrors = moduleResult.diagnostics?.filter(diagnostic => diagnostic.category === ts.DiagnosticCategory.Error) ?? []
714
717
  if (moduleErrors.length) throw new Error(moduleErrors.map(error => ts.flattenDiagnosticMessageText(error.messageText, "\n")).join("\n"))
715
- return { 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)) }
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
+ return sourceResult
716
720
  }
717
721
 
718
722
  function emittedPackageReference(source, file, packages) {
@@ -887,6 +891,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
887
891
  sourceFile = normalized.sourceFile
888
892
  const { customHookTimerStates } = normalized
889
893
  const factory = context.factory
894
+ const sourceName = source => relative(root, source.fileName).replaceAll(sep, "/")
895
+ const componentAnalysis = createComponentAnalysisSession(semantic.componentAnalysis)
890
896
  const descriptors = createDescriptorSession({
891
897
  semantic,
892
898
  handlerUrl,
@@ -894,6 +900,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
894
900
  context,
895
901
  compileEventCommand,
896
902
  isPrimitiveLiteral: isPrimitiveDefaultLiteral,
903
+ sourceName,
897
904
  rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
898
905
  })
899
906
  const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
@@ -922,6 +929,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
922
929
  } catch {}
923
930
  }
924
931
  const settersByFunction = new Map()
932
+ const stateOwnersByFunction = new Map()
925
933
  const localStateSettersByFunction = new Map()
926
934
  const reducersByFunction = new Map()
927
935
  const zustandStores = new Map()
@@ -950,6 +958,42 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
950
958
  const nestedLists = new WeakMap()
951
959
  const listEffectEntries = new WeakMap()
952
960
  const componentEffectEntries = new WeakMap()
961
+ const analysisSource = node => {
962
+ const original = ts.getOriginalNode(node)
963
+ return original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
964
+ }
965
+ const analyzedProps = owner => {
966
+ if (owner.parameters.length !== 1 || !ts.isObjectBindingPattern(owner.parameters[0].name)) return []
967
+ return owner.parameters[0].name.elements.map(element => ({
968
+ name: (element.propertyName ?? element.name).getText(),
969
+ local: element.name.getText(),
970
+ ...(element.dotDotDotToken ? { rest: true } : {}),
971
+ ...(element.initializer ? { hasDefault: true } : {})
972
+ }))
973
+ }
974
+ const ownerName = owner => owner.name?.text ?? (ts.isVariableDeclaration(owner.parent) && ts.isIdentifier(owner.parent.name) ? owner.parent.name.text : "anonymous")
975
+ const ensureOwner = (owner, kind = "component") => componentAnalysis.registerOwner(owner, { kind, name: ownerName(owner), props: analyzedProps(owner), source: analysisSource(owner) })
976
+ const registerState = (owner, state, setter, kind, node, externalOwner) => {
977
+ const ownerRecord = ensureOwner(owner)
978
+ const stateOwner = externalOwner ?? `owner:${ownerRecord.slot}`
979
+ const stateOwners = stateOwnersByFunction.get(owner) ?? new Map()
980
+ stateOwners.set(state, stateOwner)
981
+ stateOwnersByFunction.set(owner, stateOwners)
982
+ return componentAnalysis.registerState(owner, { name: state, setter, kind, ...(externalOwner ? { owner: externalOwner } : {}), source: analysisSource(node) })
983
+ }
984
+ const stateOwnersForNode = node => {
985
+ for (let current = node.parent; current; current = current.parent) {
986
+ if (isFunctionLike(current) && stateOwnersByFunction.has(current)) return stateOwnersByFunction.get(current)
987
+ }
988
+ return new Map()
989
+ }
990
+ const fallbackOwner = node => {
991
+ for (let current = node.parent; current; current = current.parent) {
992
+ const owner = isFunctionLike(current) ? componentAnalysis.owner(current) : undefined
993
+ if (owner) return `owner:${owner.slot}`
994
+ }
995
+ return "module"
996
+ }
953
997
  let usesBehavior = false
954
998
  let usesBinding = false
955
999
  let usesConditional = false
@@ -995,6 +1039,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
995
1039
  if (!value || !ts.isObjectLiteralExpression(value)) throw sourceNodeError(provider, providerSource, "Context Provider value must be one direct object literal")
996
1040
  const owner = nearestFunction(provider)
997
1041
  if (!owner) throw sourceNodeError(provider, providerSource, "Context Provider value must be returned by a component")
1042
+ const stateOwner = `external:${sourceName(providerSource)}:${owner.getStart(providerSource)}`
998
1043
 
999
1044
  const states = new Map()
1000
1045
  const callbacks = new Map()
@@ -1032,7 +1077,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1032
1077
  if (!setter || !fields.has(state) || !fields.has(setter)) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} requires exposed state and setter fields for ${JSON.stringify(state)}`)
1033
1078
  }
1034
1079
  }
1035
- return { callbacks: new Map([...callbacks].filter(([name]) => fields.has(name))), context: true, fields, privateStates: new Set(), states }
1080
+ return { callbacks: new Map([...callbacks].filter(([name]) => fields.has(name))), context: true, fields, privateStates: new Set(), stateOwner, states }
1036
1081
  }
1037
1082
 
1038
1083
  const resolveCustomHook = (binding, call) => {
@@ -1105,7 +1150,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1105
1150
  if (hook.context) {
1106
1151
  if (names.has(setter) && !names.has(state)) throw sourceNodeError(node.name, sourceFile, `Relative Context setter ${JSON.stringify(setter)} requires state ${JSON.stringify(state)} to be destructured`)
1107
1152
  if (!names.has(state) && !requiredContextStates.has(state)) continue
1108
- setters.set(names.has(setter) || requiredContextStates.has(state) ? setter : `__kContextState_${state}`, state)
1153
+ const localSetter = names.has(setter) || requiredContextStates.has(state) ? setter : `__kContextState_${state}`
1154
+ setters.set(localSetter, state)
1155
+ registerState(owner, state, localSetter, "context", node, hook.stateOwner)
1109
1156
  if (requiredContextStates.has(state)) {
1110
1157
  const fields = customHookPrivateFields.get(node) ?? []
1111
1158
  for (const field of [state, setter]) {
@@ -1120,13 +1167,17 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1120
1167
  }
1121
1168
  if (hook.privateStates.has(state)) {
1122
1169
  setters.set(setter, state)
1170
+ registerState(owner, state, setter, "custom-hook", node)
1123
1171
  const fields = customHookPrivateFields.get(node) ?? []
1124
1172
  fields.push(state, setter)
1125
1173
  customHookPrivateFields.set(node, fields)
1126
1174
  continue
1127
1175
  }
1128
1176
  if (names.has(setter) !== names.has(state)) throw sourceNodeError(node.name, sourceFile, `Relative custom hook state ${JSON.stringify(state)} and setter ${JSON.stringify(setter)} must be destructured together`)
1129
- if (names.has(setter)) setters.set(setter, state)
1177
+ if (names.has(setter)) {
1178
+ setters.set(setter, state)
1179
+ registerState(owner, state, setter, "custom-hook", node)
1180
+ }
1130
1181
  }
1131
1182
  settersByFunction.set(owner, setters)
1132
1183
  for (const name of names) {
@@ -1153,9 +1204,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1153
1204
  const owner = nearestFunction(node)
1154
1205
  if (!owner) throw sourceNodeError(node, sourceFile, "Zustand stores cannot be used outside a Kudzu component")
1155
1206
  const setters = settersByFunction.get(owner) ?? new Map()
1156
- if (selected === store.field) setters.set(`__kStoreState_${node.name.text}`, node.name.text)
1207
+ if (selected === store.field) {
1208
+ const setter = `__kStoreState_${node.name.text}`
1209
+ setters.set(setter, node.name.text)
1210
+ registerState(owner, node.name.text, setter, "store", node)
1211
+ }
1157
1212
  else if (store.actions.has(selected)) {
1158
1213
  setters.set(node.name.text, node.name.text)
1214
+ registerState(owner, node.name.text, node.name.text, "store-action", node)
1159
1215
  const reducers = reducersByFunction.get(owner) ?? new Map()
1160
1216
  reducers.set(node.name.text, { state: node.name.text, store, action: selected })
1161
1217
  reducersByFunction.set(owner, reducers)
@@ -1182,6 +1238,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1182
1238
  if (!owner) throw sourceNodeError(node, sourceFile, "useReducer() cannot be used outside a Kudzu component")
1183
1239
  const setters = settersByFunction.get(owner) ?? new Map()
1184
1240
  setters.set(dispatchElement.name.text, stateElement.name.text)
1241
+ registerState(owner, stateElement.name.text, dispatchElement.name.text, "reducer", node)
1185
1242
  settersByFunction.set(owner, setters)
1186
1243
  const reducers = reducersByFunction.get(owner) ?? new Map()
1187
1244
  reducers.set(dispatchElement.name.text, { state: stateElement.name.text, reducer: reducer.text, import: reducerImport })
@@ -1194,6 +1251,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1194
1251
  if (owner) {
1195
1252
  const setters = settersByFunction.get(owner) ?? new Map()
1196
1253
  setters.set(setterElement.name.text, stateElement.name.text)
1254
+ registerState(owner, stateElement.name.text, setterElement.name.text, "state", node)
1197
1255
  settersByFunction.set(owner, setters)
1198
1256
  const localSetters = localStateSettersByFunction.get(owner) ?? new Set()
1199
1257
  localSetters.add(setterElement.name.text)
@@ -1204,11 +1262,28 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1204
1262
  }
1205
1263
  if (ts.isFunctionDeclaration(node) && node.name) {
1206
1264
  functions.set(node.name.text, node)
1207
- if (node.parent === sourceFile) components.set(node.name.text, { function: node, declaration: node })
1265
+ if (node.parent === sourceFile) {
1266
+ components.set(node.name.text, { function: node, declaration: node })
1267
+ ensureOwner(node)
1268
+ }
1208
1269
  }
1209
1270
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
1210
1271
  functions.set(node.name.text, node.initializer)
1211
- if (node.parent?.parent?.parent === sourceFile) components.set(node.name.text, { function: node.initializer, declaration: node })
1272
+ if (node.parent?.parent?.parent === sourceFile) {
1273
+ components.set(node.name.text, { function: node.initializer, declaration: node })
1274
+ ensureOwner(node.initializer)
1275
+ }
1276
+ }
1277
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression)) {
1278
+ const owner = nearestFunction(node)
1279
+ if (owner && node.initializer.expression.text === "useRef" && node.initializer.arguments.length === 1 && node.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword) {
1280
+ ensureOwner(owner)
1281
+ componentAnalysis.registerRef(owner, { name: node.name.text, source: analysisSource(node) })
1282
+ }
1283
+ if (owner && node.initializer.expression.text === "useId" && node.initializer.arguments.length === 0) {
1284
+ ensureOwner(owner)
1285
+ componentAnalysis.registerId(owner, { name: node.name.text, source: analysisSource(node) })
1286
+ }
1212
1287
  }
1213
1288
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "createContext") contexts.add(node.name.text)
1214
1289
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isLocalConst(node)) {
@@ -1377,6 +1452,38 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1377
1452
  const stateBackedComponentFunctions = new WeakSet()
1378
1453
  const stateBackedComponentRoots = []
1379
1454
  let specializedImportIndex = 0
1455
+ const specialize = (call, component, label = "Keyed list", allowComponentRoot = false, ordinaryHooks = false, ordinaryStateNames = new Set(), ownership) => {
1456
+ const result = specializeComponentCall(call, component, sourceFile, factory, context, fail, label, allowComponentRoot, ordinaryHooks, ordinaryStateNames)
1457
+ const owner = nearestFunction(call)
1458
+ const setters = ownership?.setters ?? settersForNode(call, settersByFunction)
1459
+ const stateOwners = ownership?.stateOwners ?? stateOwnersForNode(call)
1460
+ const callbacks = functionsForNode(call)
1461
+ const propSignals = expression => {
1462
+ const signals = new Set()
1463
+ if (ts.isIdentifier(expression) && setters.has(expression.text)) signals.add(setters.get(expression.text))
1464
+ const callback = ts.isIdentifier(expression) ? callbacks.get(expression.text) : undefined
1465
+ for (const state of referencedStateNames((callback ?? expression).body ?? callback ?? expression, setters, callback ?? expression)) signals.add(state)
1466
+ return [...signals].map(name => ({ name, owner: stateOwners.get(name) ?? (owner ? `owner:${ensureOwner(owner).slot}` : "module") }))
1467
+ }
1468
+ result.analysis = componentAnalysis.registerSpecialization({
1469
+ kind: label,
1470
+ ...(owner ? { owner: ensureOwner(owner).slot } : {}),
1471
+ ...(analysisSource(call) ? { source: analysisSource(call) } : {}),
1472
+ props: result.props.map(prop => {
1473
+ const expression = result.propExpressions.get(prop.name)
1474
+ const signals = expression ? propSignals(expression) : []
1475
+ return { ...prop, ...(signals.length ? { signals } : {}) }
1476
+ }),
1477
+ states: [
1478
+ ...result.rowStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })),
1479
+ ...result.ordinaryStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
1480
+ ],
1481
+ refs: [...result.rowRefs.map(({ name, source }) => ({ name, kind: "row", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })), ...result.ordinaryRefs.map(({ name, source }) => ({ name, kind: "component", ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))],
1482
+ ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
1483
+ })
1484
+ for (const state of [...result.rowStates, ...result.ordinaryStates]) state.analysisOwner = `specialization:${result.analysis.slot}`
1485
+ return result
1486
+ }
1380
1487
  const registerRowHooks = (call, specialization) => {
1381
1488
  if (!specialization.rowStates.length && !specialization.rowRefs.length) return
1382
1489
  let owner
@@ -1388,8 +1495,13 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1388
1495
  }
1389
1496
  if (!owner) owner = nearestFunction(call)
1390
1497
  const setters = new Map(settersByFunction.get(owner))
1391
- for (const state of specialization.rowStates) setters.set(state.setter, state.state)
1498
+ const stateOwners = new Map(stateOwnersByFunction.get(owner))
1499
+ for (const state of specialization.rowStates) {
1500
+ setters.set(state.setter, state.state)
1501
+ stateOwners.set(state.state, state.analysisOwner)
1502
+ }
1392
1503
  settersByFunction.set(owner, setters)
1504
+ stateOwnersByFunction.set(owner, stateOwners)
1393
1505
  rowHookCalls.push(call)
1394
1506
  usesRowState ||= specialization.rowStates.length > 0
1395
1507
  usesRowRef ||= specialization.rowRefs.length > 0
@@ -1443,7 +1555,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1443
1555
  fail(nestedCalls[0], "Reducer callback props require a component imported from a relative TypeScript module")
1444
1556
  }
1445
1557
  for (const nestedCall of nestedCalls) {
1446
- const nested = specializeComponentCall(nestedCall, nestedComponent, sourceFile, factory, context, fail, "Reducer-callback")
1558
+ const nested = specialize(nestedCall, nestedComponent, "Reducer-callback")
1447
1559
  if (nested.effects.length) fail(nestedCall, "Reducer-callback components cannot declare effects")
1448
1560
  nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall, nested.effects)
1449
1561
  synthesizeTree(nested.root)
@@ -1482,7 +1594,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1482
1594
  folded.parent = root.parent
1483
1595
  return folded
1484
1596
  }
1485
- const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters) => {
1597
+ const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters, parentStateOwners) => {
1486
1598
  root = foldSetterStaticConditions(root)
1487
1599
  const replacements = new WeakMap()
1488
1600
  let count = 0
@@ -1516,10 +1628,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1516
1628
  }
1517
1629
  const setters = new Map(parentSetters)
1518
1630
  for (const state of aggregate.ordinaryStates) setters.set(state.setter, state.state)
1631
+ const stateOwners = new Map(parentStateOwners)
1632
+ for (const state of aggregate.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
1519
1633
  if (jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction)).length) fail(node, "Setter callbacks cannot cross a second component boundary")
1520
- const nested = specializeComponentCall(node, component, sourceFile, factory, context, fail, "Nested setter-callback", true, true, new Set(setters.values()))
1634
+ const nested = specialize(node, component, "Nested setter-callback", true, true, new Set(setters.values()), { setters, stateOwners })
1521
1635
  if (dynamic && (nested.hookDeclarations.length || nested.effects.length)) fail(node, "Hookful nested setter-callback components require an unconditional or statically truthy render path")
1522
- nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters)
1636
+ nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters, stateOwners)
1523
1637
  if (imported) synthesizeTree(nested.root = mergeSpecializedImports(nested.root, component.getSourceFile(), node, nested.effects))
1524
1638
  aggregate.calculations.push(...nested.calculations)
1525
1639
  aggregate.effects.push(...nested.effects)
@@ -1548,7 +1662,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1548
1662
  if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `State-backed list component ${name} may only be referenced as JSX`)
1549
1663
  if (stateBackedCalls.length !== calls.length) fail(component.declaration, `State-backed list component ${name} must receive its mapped prop from local state at every call`)
1550
1664
  for (const call of stateBackedCalls) {
1551
- const specialization = specializeComponentCall(call, component.function, sourceFile, factory, context, fail)
1665
+ const specialization = specialize(call, component.function)
1552
1666
  if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1553
1667
  componentSpecializations.set(call, specialization)
1554
1668
  stateBackedComponentRoots.push(specialization.root)
@@ -1570,7 +1684,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1570
1684
  }
1571
1685
  const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map()))
1572
1686
  for (const call of stateBackedCalls) {
1573
- const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail)
1687
+ const specialization = specialize(call, component)
1574
1688
  if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1575
1689
  componentSpecializations.set(call, specialization)
1576
1690
  stateBackedComponentRoots.push(specialization.root)
@@ -1590,7 +1704,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1590
1704
  collectReferences(component.body)
1591
1705
  if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
1592
1706
  }
1593
- const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
1707
+ const specialization = specialize(call, component, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
1594
1708
  if (specialization.hookDeclarations.length || specialization.effects.length) {
1595
1709
  const substitutions = new Map()
1596
1710
  const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
@@ -1604,7 +1718,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1604
1718
  for (const effect of specialization.effects) effect.call = substituteClone(effect.call, substitutions, factory, context)
1605
1719
  }
1606
1720
  }
1607
- specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction))
1721
+ specialization.root = expandSetterComponents(specialization.root, component.getSourceFile(), [component], specialization, settersForNode(call, settersByFunction), stateOwnersForNode(call))
1608
1722
  if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), call, specialization.effects))
1609
1723
  if (specialization.hookDeclarations.length || specialization.effects.length) {
1610
1724
  const owner = nearestFunction(call)
@@ -1633,6 +1747,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1633
1747
  const setters = new Map(settersForNode(call, settersByFunction))
1634
1748
  for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
1635
1749
  settersByFunction.set(helper, setters)
1750
+ const stateOwners = new Map(stateOwnersForNode(call))
1751
+ for (const state of specialization.ordinaryStates) stateOwners.set(state.state, state.analysisOwner)
1752
+ stateOwnersByFunction.set(helper, stateOwners)
1636
1753
  usesComponentState ||= specialization.ordinaryStates.length > 0
1637
1754
  usesComponentId ||= specialization.usesComponentId
1638
1755
  usesComponentRef ||= specialization.ordinaryRefs.length > 0
@@ -1672,7 +1789,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1672
1789
  if (dispatchCalls.length !== calls.length) fail(component.declaration, `Reducer-dispatch component ${name} must receive a direct local reducer dispatch at every call`)
1673
1790
  for (const call of dispatchCalls) {
1674
1791
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1675
- const specialization = specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Reducer-dispatch")
1792
+ const specialization = specialize(call, component.function, "Reducer-dispatch")
1676
1793
  registerRowHooks(call, specialization)
1677
1794
  specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
1678
1795
  componentSpecializations.set(call, specialization)
@@ -1695,7 +1812,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1695
1812
  const componentSource = component.getSourceFile()
1696
1813
  for (const call of dispatchCalls) {
1697
1814
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1698
- const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Reducer-dispatch")
1815
+ const specialization = specialize(call, component, "Reducer-dispatch")
1699
1816
  registerRowHooks(call, specialization)
1700
1817
  specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
1701
1818
  specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects)
@@ -1778,7 +1895,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1778
1895
  for (const call of calls) {
1779
1896
  const specialization = reducerComponentCalls.has(call)
1780
1897
  ? componentSpecializations.get(call)
1781
- : specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Keyed list", true)
1898
+ : specialize(call, component.function, "Keyed list", true)
1782
1899
  registerRowHooks(call, specialization)
1783
1900
  if (specialization.effects.length && !keyedComponentCalls.has(call)) fail(call, "Effectful keyed row components may only be used directly as keyed map rows")
1784
1901
  specialization.component = component.function
@@ -1814,7 +1931,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1814
1931
  const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
1815
1932
  fail(node, `Keyed list component cycle: ${chain}`)
1816
1933
  }
1817
- const specialization = specializeComponentCall(node, component, sourceFile, factory, context, fail, "Keyed list", true)
1934
+ const specialization = specialize(node, component, "Keyed list", true)
1818
1935
  registerRowHooks(node, specialization)
1819
1936
  specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
1820
1937
  if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
@@ -1859,7 +1976,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1859
1976
  }
1860
1977
  for (const { node, parts: originalParts } of rawRenderedLists) {
1861
1978
  if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
1862
- const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
1979
+ const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], ordinaryStates: [] }
1863
1980
  const componentSource = specialization.componentSource ?? sourceFile
1864
1981
  specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
1865
1982
  if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root, specialization.effects))
@@ -1879,7 +1996,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1879
1996
  callback.parent = originalParts.callback.parent
1880
1997
  }
1881
1998
  callback = prepareListCallback(callback, root, specialization, originalParts.item)
1882
- const parts = { ...originalParts, root, callback }
1999
+ const parts = { ...originalParts, root, callback, analysisStateOwners: new Map([...stateOwnersForNode(originalParts.root), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])]) }
1883
2000
  for (const calculation of specialization.calculations) {
1884
2001
  ts.setParentRecursive(calculation, false)
1885
2002
  calculation.parent = callback
@@ -1907,6 +2024,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1907
2024
  )
1908
2025
  }
1909
2026
 
2027
+ let activeStateOwners
2028
+ const visitWithStateOwners = (node, stateOwners) => {
2029
+ const previous = activeStateOwners
2030
+ activeStateOwners = new Map([...(previous ?? []), ...stateOwners])
2031
+ const result = ts.visitNode(node, visitor)
2032
+ activeStateOwners = previous
2033
+ return result
2034
+ }
1910
2035
  const visitor = node => {
1911
2036
  if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
1912
2037
  const privateFields = customHookPrivateFields.get(node)
@@ -1919,7 +2044,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1919
2044
  return ts.visitEachChild(factory.updateBlock(node, [...setterHookHelpers.get(node), ...node.statements]), visitor, context)
1920
2045
  }
1921
2046
  if (specializedDeclarations.has(node)) return node
1922
- if (componentSpecializations.has(node)) return ts.visitNode(componentSpecializations.get(node).root, visitor)
2047
+ if (componentSpecializations.has(node)) {
2048
+ const specialization = componentSpecializations.get(node)
2049
+ const stateOwners = new Map([...stateOwnersForNode(node), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])])
2050
+ return visitWithStateOwners(specialization.root, stateOwners)
2051
+ }
1923
2052
 
1924
2053
  if (hasLinkElements && (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) && isStylesheetLink(node)) {
1925
2054
  fail(node, "Stylesheets must be placed under src/ or declared in kudzu.config styles so Kudzu can emit them in <head>")
@@ -2152,7 +2281,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2152
2281
  const arguments_ = [
2153
2282
  listSource,
2154
2283
  listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
2155
- ts.visitNode(listParts.callback, visitor),
2284
+ visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map()),
2156
2285
  factory.createStringLiteral(listParts.ownerField ?? ""),
2157
2286
  jsonExpression(listParts.selector ?? [], factory),
2158
2287
  listParts.indexed ? factory.createTrue() : factory.createFalse()
@@ -2194,6 +2323,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2194
2323
  if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.text)) {
2195
2324
  const setters = settersForNode(node, settersByFunction)
2196
2325
  const event = descriptors.compileEvent(node.initializer.expression, {
2326
+ owner: fallbackOwner(node),
2327
+ stateOwners: activeStateOwners ?? stateOwnersForNode(node),
2197
2328
  setters,
2198
2329
  reducers: reducersForNode(node, reducersByFunction),
2199
2330
  functions: functionsForNode(node),
@@ -2475,7 +2606,8 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2475
2606
  callback.parent = nested.callback.parent
2476
2607
  }
2477
2608
  callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item)
2478
- const nestedParts = { ...nested, root, callback, state: parts.state, nested: true }
2609
+ const specializedStates = [...(specialization?.rowStates ?? []), ...(specialization?.ordinaryStates ?? [])]
2610
+ const nestedParts = { ...nested, root, callback, state: parts.state, nested: true, analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisOwner])]) }
2479
2611
  for (const calculation of specialization?.calculations ?? []) {
2480
2612
  ts.setParentRecursive(calculation, false)
2481
2613
  calculation.parent = callback
@@ -2703,6 +2835,13 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2703
2835
  }
2704
2836
  const restEntries = [...props].filter(([prop]) => !acceptedProps.has(prop))
2705
2837
  if (!rest) for (const [prop] of restEntries) fail(call, `Unknown ${label.toLowerCase()} component prop "${prop}"`)
2838
+ const propAnalysis = elements.map(element => ({
2839
+ name: (element.propertyName ?? element.name).getText(),
2840
+ local: element.name.getText(),
2841
+ provided: element.dotDotDotToken ? restEntries.length > 0 : props.has((element.propertyName ?? element.name).text),
2842
+ ...(element.dotDotDotToken ? { rest: true } : {}),
2843
+ ...(element.initializer ? { hasDefault: true, defaultApplied: !props.has((element.propertyName ?? element.name).text) } : {})
2844
+ }))
2706
2845
 
2707
2846
  let returned
2708
2847
  const calculations = []
@@ -2712,7 +2851,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2712
2851
  const rowRefs = []
2713
2852
  const ordinaryStates = []
2714
2853
  const ordinaryRefs = []
2715
- let usesComponentId = false
2854
+ const ordinaryIds = []
2716
2855
  if (!ts.isBlock(component.body)) {
2717
2856
  returned = component.body
2718
2857
  } else {
@@ -2747,8 +2886,8 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2747
2886
  synthesizeTree(initialValue)
2748
2887
  const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseState" : "__kRowUseState"), undefined, [initialValue])
2749
2888
  hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
2750
- if (ordinaryHooks) ordinaryStates.push({ state, setter })
2751
- else rowStates.push({ state, setter })
2889
+ if (ordinaryHooks) ordinaryStates.push({ state, setter, source: declaration })
2890
+ else rowStates.push({ state, setter, source: declaration })
2752
2891
  continue
2753
2892
  }
2754
2893
  if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef") {
@@ -2760,7 +2899,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2760
2899
  substitutions.set(declaration.name.text, factory.createIdentifier(name))
2761
2900
  const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseRef" : "__kRowUseRef"), declaration.initializer.typeArguments?.map(type => cloneAst(type, factory, context)), [factory.createNull()])
2762
2901
  hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2763
- refs.push({ name })
2902
+ refs.push({ name, source: declaration })
2764
2903
  continue
2765
2904
  }
2766
2905
  if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") {
@@ -2770,7 +2909,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2770
2909
  substitutions.set(declaration.name.text, factory.createIdentifier(name))
2771
2910
  const initializer = factory.createCallExpression(factory.createIdentifier("__kComponentUseId"), undefined, [])
2772
2911
  hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2773
- usesComponentId = true
2912
+ ordinaryIds.push({ name, source: declaration })
2774
2913
  continue
2775
2914
  }
2776
2915
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
@@ -2811,7 +2950,10 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2811
2950
  rowRefs,
2812
2951
  ordinaryStates,
2813
2952
  ordinaryRefs,
2814
- usesComponentId
2953
+ ordinaryIds,
2954
+ propExpressions: props,
2955
+ props: propAnalysis,
2956
+ usesComponentId: ordinaryIds.length > 0
2815
2957
  }
2816
2958
  }
2817
2959
 
@@ -0,0 +1,49 @@
1
+ export function createComponentAnalysis(file) {
2
+ return { version: 1, file, owners: [], specializations: [] }
3
+ }
4
+
5
+ export function createComponentAnalysisSession(analysis) {
6
+ const owners = new WeakMap()
7
+
8
+ function registerOwner(identity, descriptor = {}) {
9
+ let owner = owners.get(identity)
10
+ if (!owner) {
11
+ owner = { slot: analysis.owners.length, kind: descriptor.kind ?? "component", name: descriptor.name ?? "anonymous", props: descriptor.props ?? [], states: [], setters: [], refs: [], ids: [], ...(descriptor.source ? { source: descriptor.source } : {}) }
12
+ owners.set(identity, owner)
13
+ analysis.owners.push(owner)
14
+ }
15
+ return owner
16
+ }
17
+
18
+ function registerState(identity, descriptor) {
19
+ const owner = owners.get(identity)
20
+ if (!owner) throw new Error("Component owner must be registered before its state")
21
+ let state = owner.states.find(entry => entry.name === descriptor.name && entry.owner === descriptor.owner)
22
+ if (!state) {
23
+ state = { slot: owner.states.length, name: descriptor.name, kind: descriptor.kind, ...(descriptor.owner ? { owner: descriptor.owner } : {}), ...(descriptor.source ? { source: descriptor.source } : {}) }
24
+ owner.states.push(state)
25
+ }
26
+ if (descriptor.setter && !owner.setters.some(entry => entry.name === descriptor.setter)) owner.setters.push({ name: descriptor.setter, signal: state.slot, kind: descriptor.kind })
27
+ return state
28
+ }
29
+
30
+ function registerRef(identity, descriptor) {
31
+ const owner = owners.get(identity)
32
+ if (!owner) throw new Error("Component owner must be registered before its ref")
33
+ if (!owner.refs.some(entry => entry.name === descriptor.name)) owner.refs.push({ slot: owner.refs.length, ...descriptor })
34
+ }
35
+
36
+ function registerId(identity, descriptor) {
37
+ const owner = owners.get(identity)
38
+ if (!owner) throw new Error("Component owner must be registered before its ID")
39
+ if (!owner.ids.some(entry => entry.name === descriptor.name)) owner.ids.push({ slot: owner.ids.length, ...descriptor })
40
+ }
41
+
42
+ function registerSpecialization(descriptor) {
43
+ const specialization = { slot: analysis.specializations.length, ...descriptor }
44
+ analysis.specializations.push(specialization)
45
+ return specialization
46
+ }
47
+
48
+ return { owner: identity => owners.get(identity), registerId, registerOwner, registerRef, registerSpecialization, registerState }
49
+ }
@@ -0,0 +1,20 @@
1
+ import ts from "typescript"
2
+
3
+ export function generateCommandBehavior(moduleIR, handler, factory = ts.factory) {
4
+ const commands = handler.commands.map(command => factory.createArrayLiteralExpression([
5
+ factory.createStringLiteral(command.operation),
6
+ factory.createIdentifier(moduleIR.signals[command.signal].debugName),
7
+ literal(factory, command.value, command.syntax)
8
+ ]))
9
+ return factory.createCallExpression(factory.createIdentifier("__kBehavior"), undefined, [factory.createArrayLiteralExpression(commands)])
10
+ }
11
+
12
+ function literal(factory, value, syntax) {
13
+ if (value === null) return factory.createNull()
14
+ if (typeof value === "string") return factory.createStringLiteral(value)
15
+ if (typeof value === "boolean") return value ? factory.createTrue() : factory.createFalse()
16
+ const number = factory.createNumericLiteral(Math.abs(value))
17
+ if (syntax === "positive") return factory.createPrefixUnaryExpression(ts.SyntaxKind.PlusToken, number)
18
+ if (syntax === "negative") return factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, number)
19
+ return value < 0 ? factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, number) : number
20
+ }
@@ -1,13 +1,15 @@
1
1
  import ts from "typescript"
2
+ import { createComponentAnalysis } from "./analysis/component-analysis.mjs"
2
3
  import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
4
+ import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
5
+ import { createModuleIR, registerCommandHandler } from "./ir/module-ir.mjs"
3
6
 
4
- export function createSemanticArtifact() {
5
- return { nativeHandlers: [], effectHandlers: [], reactiveBindings: [], listExpressions: [], clientImports: new Set() }
7
+ export function createSemanticArtifact(file) {
8
+ return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file), nativeHandlers: [], effectHandlers: [], reactiveBindings: [], listExpressions: [], clientImports: new Set() }
6
9
  }
7
10
 
8
- export function createDescriptorSession({ semantic, handlerUrl, factory, context, compileEventCommand, isPrimitiveLiteral, rejectWorkerConstructions }) {
9
- const { nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
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
13
  function compileListExpression(read, expression, item, index, states = new Set()) {
12
14
  const exportName = `listExpression${listExpressions.length}`
13
15
  listExpressions.push({ exportName, expression, item, index, states })
@@ -83,10 +85,10 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
83
85
  ]
84
86
  }
85
87
 
86
- function compileEvent(expression, { setters, reducers, functions, listItem, importBindings }) {
88
+ function compileEvent(expression, { owner = "module", stateOwners = new Map(), setters, reducers, functions, listItem, importBindings }) {
87
89
  if (ts.isIdentifier(expression)) expression = functions.get(expression.text)
88
90
  if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
89
- const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters)
91
+ const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, stateOwners, owner)
90
92
  if (optimized) return optimized
91
93
  rejectWorkerConstructions(expression)
92
94
  const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", listItem })
@@ -128,11 +130,14 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
128
130
  }
129
131
  }
130
132
 
131
- function compileOptimizedEvent(expression, setters) {
133
+ function compileOptimizedEvent(expression, setters, stateOwners, owner) {
132
134
  const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
133
- const commands = statements.map(statement => ts.isExpressionStatement(statement) ? compileEventCommand(statement.expression, setters, factory) : undefined)
135
+ const commands = statements.map(statement => ts.isExpressionStatement(statement) ? compileEventCommand(statement.expression, setters) : undefined)
134
136
  if (!commands.length || commands.some(command => !command)) return undefined
135
- return factory.createCallExpression(factory.createIdentifier("__kBehavior"), undefined, [factory.createArrayLiteralExpression(commands)])
137
+ const original = ts.getOriginalNode(expression)
138
+ const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
139
+ const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, owner: stateOwners.get(command.state) ?? owner })), source, owner)
140
+ return generateCommandBehavior(moduleIR, handler, factory)
136
141
  }
137
142
 
138
143
  function registerClientImports(imports) {
@@ -0,0 +1,23 @@
1
+ export function createModuleIR(file) {
2
+ return { version: 1, file, signals: [], handlers: [] }
3
+ }
4
+
5
+ export function registerCommandHandler(moduleIR, commands, source, scope = "module") {
6
+ const slots = new Map(moduleIR.signals.map(signal => [signal.key, signal.slot]))
7
+ for (const { state, owner = scope } of commands) {
8
+ const key = `${owner}:${state}`
9
+ if (slots.has(key)) continue
10
+ const slot = moduleIR.signals.length
11
+ slots.set(key, slot)
12
+ moduleIR.signals.push({ slot, key, debugName: state })
13
+ }
14
+ const signal = command => slots.get(`${command.owner ?? scope}:${command.state}`)
15
+ const handler = {
16
+ slot: moduleIR.handlers.length,
17
+ kind: "commands",
18
+ commands: commands.map(({ operation, state, owner, value, syntax }) => ({ operation, signal: signal({ state, owner }), value, ...(syntax ? { syntax } : {}) })),
19
+ ...(source ? { source } : {})
20
+ }
21
+ moduleIR.handlers.push(handler)
22
+ return handler
23
+ }
@@ -0,0 +1,46 @@
1
+ import ts from "typescript"
2
+
3
+ export function createCommandSpecializer({ isPrimitiveLiteral }) {
4
+ return function specializeCommand(expression, setters) {
5
+ if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && ts.isIdentifier(expression.expression.expression) && expression.expression.expression.text === "console" && expression.expression.name.text === "log" && expression.arguments.length === 2 && ts.isStringLiteral(expression.arguments[0]) && ts.isIdentifier(expression.arguments[1]) && [...setters.values()].includes(expression.arguments[1].text)) {
6
+ return { operation: "log", state: expression.arguments[1].text, value: expression.arguments[0].text }
7
+ }
8
+
9
+ if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.arguments.length !== 1) return undefined
10
+ const state = setters.get(expression.expression.text)
11
+ if (!state) return undefined
12
+
13
+ const value = expression.arguments[0]
14
+ if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === state && ts.isNumericLiteral(value.right)) {
15
+ if (value.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
16
+ const operand = signedNumber(value.right, value.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
17
+ return operand ? { operation: "add", state, ...operand } : undefined
18
+ }
19
+ if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isBinaryExpression(value.body) && ts.isIdentifier(value.body.left) && value.body.left.text === value.parameters[0].name.text && ts.isNumericLiteral(value.body.right)) {
20
+ if (value.body.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.body.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
21
+ const operand = signedNumber(value.body.right, value.body.operatorToken.kind === ts.SyntaxKind.MinusToken ? "negative" : undefined)
22
+ return operand ? { operation: "add", state, ...operand } : undefined
23
+ }
24
+ if (isPrimitiveLiteral(value)) {
25
+ const literal = primitiveValue(value)
26
+ return literal ? { operation: "set", state, ...literal } : undefined
27
+ }
28
+ return undefined
29
+ }
30
+ }
31
+
32
+ function primitiveValue(node) {
33
+ if (ts.isStringLiteral(node)) return { value: node.text }
34
+ if (ts.isNumericLiteral(node)) return signedNumber(node)
35
+ if (node.kind === ts.SyntaxKind.TrueKeyword) return { value: true }
36
+ if (node.kind === ts.SyntaxKind.FalseKeyword) return { value: false }
37
+ if (node.kind === ts.SyntaxKind.NullKeyword) return { value: null }
38
+ return signedNumber(node.operand, node.operator === ts.SyntaxKind.MinusToken ? "negative" : "positive")
39
+ }
40
+
41
+ function signedNumber(node, syntax) {
42
+ const number = Number(node.text)
43
+ if (!Number.isFinite(number)) return undefined
44
+ if (syntax === "negative") return number === 0 ? { value: 0, syntax } : { value: -number }
45
+ return syntax ? { value: number, syntax } : { value: number }
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.16",
3
+ "version": "0.8.18",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,35 +0,0 @@
1
- import ts from "typescript"
2
-
3
- export function createEventCommandCompiler({ isPrimitiveLiteral, synthesizeSerializableStateLiteral }) {
4
- return function compileEventCommand(expression, setters, factory) {
5
- if (ts.isCallExpression(expression) && ts.isPropertyAccessExpression(expression.expression) && ts.isIdentifier(expression.expression.expression) && expression.expression.expression.text === "console" && expression.expression.name.text === "log" && expression.arguments.length === 2 && ts.isStringLiteral(expression.arguments[0]) && ts.isIdentifier(expression.arguments[1]) && [...setters.values()].includes(expression.arguments[1].text)) {
6
- return command(factory, "log", expression.arguments[1], factory.createStringLiteral(expression.arguments[0].text))
7
- }
8
-
9
- if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression) || expression.arguments.length !== 1) return undefined
10
- const stateName = setters.get(expression.expression.text)
11
- if (!stateName) return undefined
12
-
13
- const state = factory.createIdentifier(stateName)
14
- const value = expression.arguments[0]
15
- if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === stateName && ts.isNumericLiteral(value.right)) {
16
- if (value.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
17
- return command(factory, "add", state, numericExpression(factory, Number(value.right.text), value.operatorToken.kind === ts.SyntaxKind.MinusToken))
18
- }
19
- if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isBinaryExpression(value.body) && ts.isIdentifier(value.body.left) && value.body.left.text === value.parameters[0].name.text && ts.isNumericLiteral(value.body.right)) {
20
- if (value.body.operatorToken.kind !== ts.SyntaxKind.PlusToken && value.body.operatorToken.kind !== ts.SyntaxKind.MinusToken) return undefined
21
- return command(factory, "add", state, numericExpression(factory, Number(value.body.right.text), value.body.operatorToken.kind === ts.SyntaxKind.MinusToken))
22
- }
23
- if (isPrimitiveLiteral(value)) return command(factory, "set", state, synthesizeSerializableStateLiteral(value, factory))
24
- return undefined
25
- }
26
- }
27
-
28
- function command(factory, operation, state, value) {
29
- return factory.createArrayLiteralExpression([factory.createStringLiteral(operation), state, value])
30
- }
31
-
32
- function numericExpression(factory, value, negative) {
33
- const literal = factory.createNumericLiteral(value)
34
- return negative ? factory.createPrefixUnaryExpression(ts.SyntaxKind.MinusToken, literal) : literal
35
- }