@kudzujs/core 0.8.17 → 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.
@@ -202,6 +202,13 @@ This queue orders the next investigations by general migration value. Start only
202
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
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
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
+
205
212
  ## Cross-Cutting Performance Gates
206
213
 
207
214
  Every migration feature must preserve:
package/PERFORMANCE.md CHANGED
@@ -1,5 +1,38 @@
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
+
3
36
  ## 0.8.17 Command ModuleIR
4
37
 
5
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.
package/README.md CHANGED
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
14
14
 
15
15
  > Experimental `0.8.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Latest release: 0.8.17 - Command ModuleIR.** Supported command handlers now cross a real JSON-safe ModuleIR boundary before existing source codegen, preserving byte-identical HTML, route plans, and browser assets. Read the [release notes](./RELEASES.md#0817---command-moduleir), open the [release page](https://kudzujs.cloud/releases/0.8.17), 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,38 @@
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
+
3
36
  ## 0.8.17 - Command ModuleIR
4
37
 
5
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.
@@ -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.17` command slice complete | Continue with `0.8.18` state/props/component analysis after the release boundary. |
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 completed `0.8.17` command ModuleIR 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,9 @@ This maps the completed `0.8.17` command ModuleIR boundary. File and function na
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. |
18
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. |
19
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. |
20
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. |
@@ -56,7 +57,7 @@ The browser consumes static HTML first. State seeds and descriptors in that HTML
56
57
  ## Current Coupling To Remove
57
58
 
58
59
  - `createKudzuTransformer()` combines discovery, validation, specialization, descriptor registration, and transformed-source emission.
59
- - 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.
60
61
  - `build()` destructures a broad capability manifest into many booleans and performs artifact-specific source surgery.
61
62
  - Runtime specialization relies on exact source-string and regular-expression replacements in `framework/build.mjs`.
62
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
- The `0.8.17` command vertical slice is complete. Goal A continues with the planned `0.8.18` state/props/component analysis result while preserving accepted React-shaped syntax, diagnostics, complete HTML, emitted capability behavior, and current public APIs.
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
 
@@ -77,7 +77,7 @@ Analysis produces plain data:
77
77
  }
78
78
  ```
79
79
 
80
- The `0.8.17` slice intentionally records only lexical signal identity and command handlers. Initial values, state scope, bindings, derived values, effects, and keyed blocks join ModuleIR in their later planned patches; `core.mjs` remains authoritative for those values today.
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
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` and `0.8.17` 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.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,6 +32,7 @@ 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.
@@ -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"
@@ -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) {
@@ -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,7 +900,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
894
900
  context,
895
901
  compileEventCommand,
896
902
  isPrimitiveLiteral: isPrimitiveDefaultLiteral,
897
- sourceName: source => relative(root, source.fileName).replaceAll(sep, "/"),
903
+ sourceName,
898
904
  rejectWorkerConstructions: expression => workerCompiler.rejectConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
899
905
  })
900
906
  const importBindings = clientImportBindings(sourceFile, file, sourceFiles)
@@ -923,6 +929,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
923
929
  } catch {}
924
930
  }
925
931
  const settersByFunction = new Map()
932
+ const stateOwnersByFunction = new Map()
926
933
  const localStateSettersByFunction = new Map()
927
934
  const reducersByFunction = new Map()
928
935
  const zustandStores = new Map()
@@ -951,6 +958,42 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
951
958
  const nestedLists = new WeakMap()
952
959
  const listEffectEntries = new WeakMap()
953
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
+ }
954
997
  let usesBehavior = false
955
998
  let usesBinding = false
956
999
  let usesConditional = false
@@ -996,6 +1039,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
996
1039
  if (!value || !ts.isObjectLiteralExpression(value)) throw sourceNodeError(provider, providerSource, "Context Provider value must be one direct object literal")
997
1040
  const owner = nearestFunction(provider)
998
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)}`
999
1043
 
1000
1044
  const states = new Map()
1001
1045
  const callbacks = new Map()
@@ -1033,7 +1077,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1033
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)}`)
1034
1078
  }
1035
1079
  }
1036
- 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 }
1037
1081
  }
1038
1082
 
1039
1083
  const resolveCustomHook = (binding, call) => {
@@ -1106,7 +1150,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1106
1150
  if (hook.context) {
1107
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`)
1108
1152
  if (!names.has(state) && !requiredContextStates.has(state)) continue
1109
- 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)
1110
1156
  if (requiredContextStates.has(state)) {
1111
1157
  const fields = customHookPrivateFields.get(node) ?? []
1112
1158
  for (const field of [state, setter]) {
@@ -1121,13 +1167,17 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1121
1167
  }
1122
1168
  if (hook.privateStates.has(state)) {
1123
1169
  setters.set(setter, state)
1170
+ registerState(owner, state, setter, "custom-hook", node)
1124
1171
  const fields = customHookPrivateFields.get(node) ?? []
1125
1172
  fields.push(state, setter)
1126
1173
  customHookPrivateFields.set(node, fields)
1127
1174
  continue
1128
1175
  }
1129
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`)
1130
- 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
+ }
1131
1181
  }
1132
1182
  settersByFunction.set(owner, setters)
1133
1183
  for (const name of names) {
@@ -1154,9 +1204,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1154
1204
  const owner = nearestFunction(node)
1155
1205
  if (!owner) throw sourceNodeError(node, sourceFile, "Zustand stores cannot be used outside a Kudzu component")
1156
1206
  const setters = settersByFunction.get(owner) ?? new Map()
1157
- 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
+ }
1158
1212
  else if (store.actions.has(selected)) {
1159
1213
  setters.set(node.name.text, node.name.text)
1214
+ registerState(owner, node.name.text, node.name.text, "store-action", node)
1160
1215
  const reducers = reducersByFunction.get(owner) ?? new Map()
1161
1216
  reducers.set(node.name.text, { state: node.name.text, store, action: selected })
1162
1217
  reducersByFunction.set(owner, reducers)
@@ -1183,6 +1238,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1183
1238
  if (!owner) throw sourceNodeError(node, sourceFile, "useReducer() cannot be used outside a Kudzu component")
1184
1239
  const setters = settersByFunction.get(owner) ?? new Map()
1185
1240
  setters.set(dispatchElement.name.text, stateElement.name.text)
1241
+ registerState(owner, stateElement.name.text, dispatchElement.name.text, "reducer", node)
1186
1242
  settersByFunction.set(owner, setters)
1187
1243
  const reducers = reducersByFunction.get(owner) ?? new Map()
1188
1244
  reducers.set(dispatchElement.name.text, { state: stateElement.name.text, reducer: reducer.text, import: reducerImport })
@@ -1195,6 +1251,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1195
1251
  if (owner) {
1196
1252
  const setters = settersByFunction.get(owner) ?? new Map()
1197
1253
  setters.set(setterElement.name.text, stateElement.name.text)
1254
+ registerState(owner, stateElement.name.text, setterElement.name.text, "state", node)
1198
1255
  settersByFunction.set(owner, setters)
1199
1256
  const localSetters = localStateSettersByFunction.get(owner) ?? new Set()
1200
1257
  localSetters.add(setterElement.name.text)
@@ -1205,11 +1262,28 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1205
1262
  }
1206
1263
  if (ts.isFunctionDeclaration(node) && node.name) {
1207
1264
  functions.set(node.name.text, node)
1208
- 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
+ }
1209
1269
  }
1210
1270
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
1211
1271
  functions.set(node.name.text, node.initializer)
1212
- 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
+ }
1213
1287
  }
1214
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)
1215
1289
  if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isLocalConst(node)) {
@@ -1378,6 +1452,38 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1378
1452
  const stateBackedComponentFunctions = new WeakSet()
1379
1453
  const stateBackedComponentRoots = []
1380
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
+ }
1381
1487
  const registerRowHooks = (call, specialization) => {
1382
1488
  if (!specialization.rowStates.length && !specialization.rowRefs.length) return
1383
1489
  let owner
@@ -1389,8 +1495,13 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1389
1495
  }
1390
1496
  if (!owner) owner = nearestFunction(call)
1391
1497
  const setters = new Map(settersByFunction.get(owner))
1392
- 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
+ }
1393
1503
  settersByFunction.set(owner, setters)
1504
+ stateOwnersByFunction.set(owner, stateOwners)
1394
1505
  rowHookCalls.push(call)
1395
1506
  usesRowState ||= specialization.rowStates.length > 0
1396
1507
  usesRowRef ||= specialization.rowRefs.length > 0
@@ -1444,7 +1555,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1444
1555
  fail(nestedCalls[0], "Reducer callback props require a component imported from a relative TypeScript module")
1445
1556
  }
1446
1557
  for (const nestedCall of nestedCalls) {
1447
- const nested = specializeComponentCall(nestedCall, nestedComponent, sourceFile, factory, context, fail, "Reducer-callback")
1558
+ const nested = specialize(nestedCall, nestedComponent, "Reducer-callback")
1448
1559
  if (nested.effects.length) fail(nestedCall, "Reducer-callback components cannot declare effects")
1449
1560
  nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall, nested.effects)
1450
1561
  synthesizeTree(nested.root)
@@ -1483,7 +1594,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1483
1594
  folded.parent = root.parent
1484
1595
  return folded
1485
1596
  }
1486
- const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters) => {
1597
+ const expandSetterComponents = (root, componentSource, trail, aggregate, parentSetters, parentStateOwners) => {
1487
1598
  root = foldSetterStaticConditions(root)
1488
1599
  const replacements = new WeakMap()
1489
1600
  let count = 0
@@ -1517,10 +1628,12 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1517
1628
  }
1518
1629
  const setters = new Map(parentSetters)
1519
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)
1520
1633
  if (jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction)).length) fail(node, "Setter callbacks cannot cross a second component boundary")
1521
- 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 })
1522
1635
  if (dynamic && (nested.hookDeclarations.length || nested.effects.length)) fail(node, "Hookful nested setter-callback components require an unconditional or statically truthy render path")
1523
- nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters)
1636
+ nested.root = expandSetterComponents(nested.root, component.getSourceFile(), [...trail, component], nested, setters, stateOwners)
1524
1637
  if (imported) synthesizeTree(nested.root = mergeSpecializedImports(nested.root, component.getSourceFile(), node, nested.effects))
1525
1638
  aggregate.calculations.push(...nested.calculations)
1526
1639
  aggregate.effects.push(...nested.effects)
@@ -1549,7 +1662,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1549
1662
  if (identifierReferenceCount(sourceFile, name) !== calls.length) fail(component.declaration, `State-backed list component ${name} may only be referenced as JSX`)
1550
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`)
1551
1664
  for (const call of stateBackedCalls) {
1552
- const specialization = specializeComponentCall(call, component.function, sourceFile, factory, context, fail)
1665
+ const specialization = specialize(call, component.function)
1553
1666
  if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1554
1667
  componentSpecializations.set(call, specialization)
1555
1668
  stateBackedComponentRoots.push(specialization.root)
@@ -1571,7 +1684,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1571
1684
  }
1572
1685
  const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component, settersByFunction.get(nearestFunction(call)) ?? new Map()))
1573
1686
  for (const call of stateBackedCalls) {
1574
- const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail)
1687
+ const specialization = specialize(call, component)
1575
1688
  if (specialization.effects.length) fail(call, "State-backed list components cannot declare effects")
1576
1689
  componentSpecializations.set(call, specialization)
1577
1690
  stateBackedComponentRoots.push(specialization.root)
@@ -1591,7 +1704,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1591
1704
  collectReferences(component.body)
1592
1705
  if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
1593
1706
  }
1594
- 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()))
1595
1708
  if (specialization.hookDeclarations.length || specialization.effects.length) {
1596
1709
  const substitutions = new Map()
1597
1710
  const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
@@ -1605,7 +1718,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1605
1718
  for (const effect of specialization.effects) effect.call = substituteClone(effect.call, substitutions, factory, context)
1606
1719
  }
1607
1720
  }
1608
- 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))
1609
1722
  if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), call, specialization.effects))
1610
1723
  if (specialization.hookDeclarations.length || specialization.effects.length) {
1611
1724
  const owner = nearestFunction(call)
@@ -1634,6 +1747,9 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1634
1747
  const setters = new Map(settersForNode(call, settersByFunction))
1635
1748
  for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
1636
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)
1637
1753
  usesComponentState ||= specialization.ordinaryStates.length > 0
1638
1754
  usesComponentId ||= specialization.usesComponentId
1639
1755
  usesComponentRef ||= specialization.ordinaryRefs.length > 0
@@ -1673,7 +1789,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1673
1789
  if (dispatchCalls.length !== calls.length) fail(component.declaration, `Reducer-dispatch component ${name} must receive a direct local reducer dispatch at every call`)
1674
1790
  for (const call of dispatchCalls) {
1675
1791
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1676
- const specialization = specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Reducer-dispatch")
1792
+ const specialization = specialize(call, component.function, "Reducer-dispatch")
1677
1793
  registerRowHooks(call, specialization)
1678
1794
  specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
1679
1795
  componentSpecializations.set(call, specialization)
@@ -1696,7 +1812,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1696
1812
  const componentSource = component.getSourceFile()
1697
1813
  for (const call of dispatchCalls) {
1698
1814
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1699
- const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Reducer-dispatch")
1815
+ const specialization = specialize(call, component, "Reducer-dispatch")
1700
1816
  registerRowHooks(call, specialization)
1701
1817
  specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
1702
1818
  specialization.root = mergeSpecializedImports(specialization.root, componentSource, call, specialization.effects)
@@ -1779,7 +1895,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1779
1895
  for (const call of calls) {
1780
1896
  const specialization = reducerComponentCalls.has(call)
1781
1897
  ? componentSpecializations.get(call)
1782
- : specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Keyed list", true)
1898
+ : specialize(call, component.function, "Keyed list", true)
1783
1899
  registerRowHooks(call, specialization)
1784
1900
  if (specialization.effects.length && !keyedComponentCalls.has(call)) fail(call, "Effectful keyed row components may only be used directly as keyed map rows")
1785
1901
  specialization.component = component.function
@@ -1815,7 +1931,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1815
1931
  const chain = [...trail, component].map(entry => entry.name?.text || "anonymous").join(" -> ")
1816
1932
  fail(node, `Keyed list component cycle: ${chain}`)
1817
1933
  }
1818
- const specialization = specializeComponentCall(node, component, sourceFile, factory, context, fail, "Keyed list", true)
1934
+ const specialization = specialize(node, component, "Keyed list", true)
1819
1935
  registerRowHooks(node, specialization)
1820
1936
  specialization.root = expandKeyedComponents(specialization.root, component.getSourceFile(), [...trail, component], specialization)
1821
1937
  if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
@@ -1860,7 +1976,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1860
1976
  }
1861
1977
  for (const { node, parts: originalParts } of rawRenderedLists) {
1862
1978
  if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
1863
- 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: [] }
1864
1980
  const componentSource = specialization.componentSource ?? sourceFile
1865
1981
  specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
1866
1982
  if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root, specialization.effects))
@@ -1880,7 +1996,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1880
1996
  callback.parent = originalParts.callback.parent
1881
1997
  }
1882
1998
  callback = prepareListCallback(callback, root, specialization, originalParts.item)
1883
- 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])]) }
1884
2000
  for (const calculation of specialization.calculations) {
1885
2001
  ts.setParentRecursive(calculation, false)
1886
2002
  calculation.parent = callback
@@ -1908,6 +2024,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1908
2024
  )
1909
2025
  }
1910
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
+ }
1911
2035
  const visitor = node => {
1912
2036
  if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
1913
2037
  const privateFields = customHookPrivateFields.get(node)
@@ -1920,7 +2044,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
1920
2044
  return ts.visitEachChild(factory.updateBlock(node, [...setterHookHelpers.get(node), ...node.statements]), visitor, context)
1921
2045
  }
1922
2046
  if (specializedDeclarations.has(node)) return node
1923
- 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
+ }
1924
2052
 
1925
2053
  if (hasLinkElements && (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) && isStylesheetLink(node)) {
1926
2054
  fail(node, "Stylesheets must be placed under src/ or declared in kudzu.config styles so Kudzu can emit them in <head>")
@@ -2153,7 +2281,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2153
2281
  const arguments_ = [
2154
2282
  listSource,
2155
2283
  listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
2156
- ts.visitNode(listParts.callback, visitor),
2284
+ visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map()),
2157
2285
  factory.createStringLiteral(listParts.ownerField ?? ""),
2158
2286
  jsonExpression(listParts.selector ?? [], factory),
2159
2287
  listParts.indexed ? factory.createTrue() : factory.createFalse()
@@ -2195,6 +2323,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
2195
2323
  if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.text)) {
2196
2324
  const setters = settersForNode(node, settersByFunction)
2197
2325
  const event = descriptors.compileEvent(node.initializer.expression, {
2326
+ owner: fallbackOwner(node),
2327
+ stateOwners: activeStateOwners ?? stateOwnersForNode(node),
2198
2328
  setters,
2199
2329
  reducers: reducersForNode(node, reducersByFunction),
2200
2330
  functions: functionsForNode(node),
@@ -2476,7 +2606,8 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
2476
2606
  callback.parent = nested.callback.parent
2477
2607
  }
2478
2608
  callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item)
2479
- 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])]) }
2480
2611
  for (const calculation of specialization?.calculations ?? []) {
2481
2612
  ts.setParentRecursive(calculation, false)
2482
2613
  calculation.parent = callback
@@ -2704,6 +2835,13 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2704
2835
  }
2705
2836
  const restEntries = [...props].filter(([prop]) => !acceptedProps.has(prop))
2706
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
+ }))
2707
2845
 
2708
2846
  let returned
2709
2847
  const calculations = []
@@ -2713,7 +2851,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2713
2851
  const rowRefs = []
2714
2852
  const ordinaryStates = []
2715
2853
  const ordinaryRefs = []
2716
- let usesComponentId = false
2854
+ const ordinaryIds = []
2717
2855
  if (!ts.isBlock(component.body)) {
2718
2856
  returned = component.body
2719
2857
  } else {
@@ -2748,8 +2886,8 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2748
2886
  synthesizeTree(initialValue)
2749
2887
  const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseState" : "__kRowUseState"), undefined, [initialValue])
2750
2888
  hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
2751
- if (ordinaryHooks) ordinaryStates.push({ state, setter })
2752
- else rowStates.push({ state, setter })
2889
+ if (ordinaryHooks) ordinaryStates.push({ state, setter, source: declaration })
2890
+ else rowStates.push({ state, setter, source: declaration })
2753
2891
  continue
2754
2892
  }
2755
2893
  if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef") {
@@ -2761,7 +2899,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2761
2899
  substitutions.set(declaration.name.text, factory.createIdentifier(name))
2762
2900
  const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseRef" : "__kRowUseRef"), declaration.initializer.typeArguments?.map(type => cloneAst(type, factory, context)), [factory.createNull()])
2763
2901
  hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2764
- refs.push({ name })
2902
+ refs.push({ name, source: declaration })
2765
2903
  continue
2766
2904
  }
2767
2905
  if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") {
@@ -2771,7 +2909,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2771
2909
  substitutions.set(declaration.name.text, factory.createIdentifier(name))
2772
2910
  const initializer = factory.createCallExpression(factory.createIdentifier("__kComponentUseId"), undefined, [])
2773
2911
  hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
2774
- usesComponentId = true
2912
+ ordinaryIds.push({ name, source: declaration })
2775
2913
  continue
2776
2914
  }
2777
2915
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
@@ -2812,7 +2950,10 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2812
2950
  rowRefs,
2813
2951
  ordinaryStates,
2814
2952
  ordinaryRefs,
2815
- usesComponentId
2953
+ ordinaryIds,
2954
+ propExpressions: props,
2955
+ props: propAnalysis,
2956
+ usesComponentId: ordinaryIds.length > 0
2816
2957
  }
2817
2958
  }
2818
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
+ }
@@ -1,17 +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"
3
4
  import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
4
5
  import { createModuleIR, registerCommandHandler } from "./ir/module-ir.mjs"
5
6
 
6
7
  export function createSemanticArtifact(file) {
7
- return { moduleIR: createModuleIR(file), nativeHandlers: [], effectHandlers: [], reactiveBindings: [], listExpressions: [], clientImports: new Set() }
8
+ return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file), nativeHandlers: [], effectHandlers: [], reactiveBindings: [], listExpressions: [], clientImports: new Set() }
8
9
  }
9
10
 
10
11
  export function createDescriptorSession({ semantic, handlerUrl, factory, context, compileEventCommand, isPrimitiveLiteral, rejectWorkerConstructions, sourceName = source => source.fileName }) {
11
12
  const { moduleIR, nativeHandlers, effectHandlers, reactiveBindings, listExpressions, clientImports } = semantic
12
- const stateScopes = new WeakMap()
13
- let nextStateScope = 0
14
-
15
13
  function compileListExpression(read, expression, item, index, states = new Set()) {
16
14
  const exportName = `listExpression${listExpressions.length}`
17
15
  listExpressions.push({ exportName, expression, item, index, states })
@@ -87,10 +85,10 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
87
85
  ]
88
86
  }
89
87
 
90
- function compileEvent(expression, { setters, reducers, functions, listItem, importBindings }) {
88
+ function compileEvent(expression, { owner = "module", stateOwners = new Map(), setters, reducers, functions, listItem, importBindings }) {
91
89
  if (ts.isIdentifier(expression)) expression = functions.get(expression.text)
92
90
  if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
93
- 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)
94
92
  if (optimized) return optimized
95
93
  rejectWorkerConstructions(expression)
96
94
  const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", listItem })
@@ -132,15 +130,13 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
132
130
  }
133
131
  }
134
132
 
135
- function compileOptimizedEvent(expression, setters) {
133
+ function compileOptimizedEvent(expression, setters, stateOwners, owner) {
136
134
  const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
137
135
  const commands = statements.map(statement => ts.isExpressionStatement(statement) ? compileEventCommand(statement.expression, setters) : undefined)
138
136
  if (!commands.length || commands.some(command => !command)) return undefined
139
137
  const original = ts.getOriginalNode(expression)
140
138
  const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
141
- if (!stateScopes.has(setters)) stateScopes.set(setters, `state:${nextStateScope++}`)
142
- const scope = stateScopes.get(setters)
143
- const handler = registerCommandHandler(moduleIR, commands, source, scope)
139
+ const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, owner: stateOwners.get(command.state) ?? owner })), source, owner)
144
140
  return generateCommandBehavior(moduleIR, handler, factory)
145
141
  }
146
142
 
@@ -4,18 +4,18 @@ export function createModuleIR(file) {
4
4
 
5
5
  export function registerCommandHandler(moduleIR, commands, source, scope = "module") {
6
6
  const slots = new Map(moduleIR.signals.map(signal => [signal.key, signal.slot]))
7
- for (const { state } of commands) {
8
- const key = `${scope}:${state}`
7
+ for (const { state, owner = scope } of commands) {
8
+ const key = `${owner}:${state}`
9
9
  if (slots.has(key)) continue
10
10
  const slot = moduleIR.signals.length
11
11
  slots.set(key, slot)
12
12
  moduleIR.signals.push({ slot, key, debugName: state })
13
13
  }
14
- const signal = state => slots.get(`${scope}:${state}`) ?? slots.get(state)
14
+ const signal = command => slots.get(`${command.owner ?? scope}:${command.state}`)
15
15
  const handler = {
16
16
  slot: moduleIR.handlers.length,
17
17
  kind: "commands",
18
- commands: commands.map(({ operation, state, value, syntax }) => ({ operation, signal: signal(state), value, ...(syntax ? { syntax } : {}) })),
18
+ commands: commands.map(({ operation, state, owner, value, syntax }) => ({ operation, signal: signal({ state, owner }), value, ...(syntax ? { syntax } : {}) })),
19
19
  ...(source ? { source } : {})
20
20
  }
21
21
  moduleIR.handlers.push(handler)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.17",
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",