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