@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.
- package/MIGRATION_ROADMAP.md +7 -0
- package/PERFORMANCE.md +33 -0
- package/README.md +1 -1
- package/RELEASES.md +33 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/compiler-current-architecture.md +5 -4
- package/docs/next-architecture/goal-a-compiler-foundation.md +2 -2
- package/docs/next-architecture/versioning.md +1 -1
- package/framework/README.md +1 -0
- package/framework/build.mjs +178 -37
- package/framework/compiler/analysis/component-analysis.mjs +49 -0
- package/framework/compiler/descriptor-session.mjs +6 -10
- package/framework/compiler/ir/module-ir.mjs +4 -4
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -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
|
+
**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.
|
|
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.
|
|
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()` |
|
|
17
|
-
|
|
|
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
|
-
-
|
|
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.
|
|
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.
|
|
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`
|
|
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
|
|
package/framework/README.md
CHANGED
|
@@ -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.
|
package/framework/build.mjs
CHANGED
|
@@ -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
|
|
76
|
+
const sourceResults = []
|
|
76
77
|
const workerReferences = []
|
|
77
78
|
for (const file of sourceFiles) {
|
|
78
79
|
if (file.endsWith(".worker.ts")) continue
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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))
|
|
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)
|
|
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)
|
|
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)
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
:
|
|
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 =
|
|
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))
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 = `${
|
|
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 =
|
|
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)
|