@kudzujs/core 0.8.19 → 0.8.20
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 +42 -0
- package/README.md +1 -1
- package/RELEASES.md +32 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/compiler-current-architecture.md +4 -4
- package/docs/next-architecture/goal-a-compiler-foundation.md +2 -2
- package/docs/next-architecture/versioning.md +1 -1
- package/framework/README.md +3 -3
- package/framework/build.mjs +122 -62
- package/framework/compiler/descriptor-session.mjs +24 -16
- package/framework/compiler/ir/module-ir.mjs +7 -1
- package/framework/core.d.ts +52 -1
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -216,6 +216,13 @@ This queue orders the next investigations by general migration value. Start only
|
|
|
216
216
|
- Existing tagged collection expressions and selectors are canonical DerivedIR for rendered lists and derived effect dependencies; transformed source embeds the registered records rather than parallel analyzer values.
|
|
217
217
|
- AST, `Map`, and `Set` callback descriptors remain private only until source-local lowering completes. Handler codegen now performs no TypeScript traversal or state/capture/reducer/import discovery, while command-only routes retain zero handler ESM.
|
|
218
218
|
|
|
219
|
+
### Completed In 0.8.20
|
|
220
|
+
|
|
221
|
+
- Every rendered keyed collection now finalizes into JSON-safe KeyedBlockIR with deterministic slots, explicit parent/child links, collection and selector ownership, key/index policy, source provenance, and complete component-specialization membership.
|
|
222
|
+
- Keyed row states and refs retain their declaration source and specialization owner. Command, native, effect, list-expression, list-conditional, and calculated-collection records link back to their owning keyed block.
|
|
223
|
+
- Previous transformer-wide keyed value, condition, event, nested-list, effect, and rendered-list AST side tables were removed. AST remains private only inside immediate source-local validation and lowering.
|
|
224
|
+
- `core.mjs` remains authoritative for final list IDs, key paths, route descriptors, complete HTML, DOM identity, and exact state/effect/ref release; accepted syntax and browser output are unchanged.
|
|
225
|
+
|
|
219
226
|
## Cross-Cutting Performance Gates
|
|
220
227
|
|
|
221
228
|
Every migration feature must preserve:
|
package/PERFORMANCE.md
CHANGED
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Performance Records
|
|
2
2
|
|
|
3
|
+
## 0.8.20 Explicit Keyed Ownership
|
|
4
|
+
|
|
5
|
+
Measured UTC 2026-08-09 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.19` commit `c516173` and the current `0.8.20` compiler candidate used the same local volume and identical installed dependencies.
|
|
6
|
+
|
|
7
|
+
The candidate implementation patch over `c516173` had SHA-256 `137f68090f6024374077f46cb61767f48dff27a193d33c9e92409b8dd7bb7d21`, produced by:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git diff --binary c516173 -- framework/build.mjs framework/compiler/descriptor-session.mjs framework/compiler/ir/module-ir.mjs framework/core.d.ts | shasum -a 256
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Both targets received one warm-up followed by 21 clean `keyed-row-hooks` production builds in round-robin alternating order. Cleanup remained outside timing. The distributions overlap; the candidate median was 0.52% lower and does not establish a material change.
|
|
14
|
+
|
|
15
|
+
| Target | Build median | List runtime raw / gzip |
|
|
16
|
+
|---|---:|---:|
|
|
17
|
+
| 0.8.19 baseline | 266.8 ms | 21,831 B / 6,930 B |
|
|
18
|
+
| 0.8.20 candidate | 265.4 ms | 21,831 B / 6,930 B |
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
0.8.19: [272.5,269.7,262.6,267.3,262.9,265.2,268.0,264.7,262.0,268.3,267.1,262.5,263.7,266.8,263.2,267.5,263.3,268.8,266.7,266.9,271.4]
|
|
22
|
+
0.8.20: [262.5,267.4,266.2,261.7,265.1,262.9,269.9,264.1,275.0,268.2,263.4,263.7,265.4,268.7,263.8,266.1,265.4,265.3,267.2,270.1,263.2]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
The recorded cleanup, warm-up, timing, median, and runtime-size measurement is reproducible with:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
BASELINE_ROOT="/var/folders/bt/3r_ntp5x65j81brs6_p93rl00000gn/T/opencode/kudzu-0820-baseline" CANDIDATE_ROOT="/Users/songchibong/Documents/GitHub/kudzu" node --input-type=module -e 'import { spawnSync } from "node:child_process"; import { rmSync,readFileSync } from "node:fs"; import { gzipSync } from "node:zlib"; import { performance } from "node:perf_hooks"; import { resolve } from "node:path"; const roots={baseline:process.env.BASELINE_ROOT,candidate:process.env.CANDIDATE_ROOT}; const runs={baseline:[],candidate:[]}; const fixture="keyed-row-hooks"; const build=name=>{const root=roots[name],cwd=resolve(root,"test/fixtures",fixture); rmSync(resolve(cwd,"dist"),{recursive:true,force:true}); rmSync(resolve(cwd,".kudzu"),{recursive:true,force:true}); const start=performance.now(); const result=spawnSync(process.execPath,[resolve(root,"bin/kudzu.mjs"),"build"],{cwd,encoding:"utf8"}); if(result.status) throw new Error(result.stderr||result.stdout); return Number((performance.now()-start).toFixed(1));}; build("baseline"); build("candidate"); for(let index=0;index<21;index++) for(const name of index%2?["candidate","baseline"]:["baseline","candidate"]) runs[name].push(build(name)); const median=values=>[...values].sort((a,b)=>a-b)[Math.floor(values.length/2)]; const sizes={}; for(const [name,root] of Object.entries(roots)){const file=readFileSync(resolve(root,"test/fixtures",fixture,"dist/assets/kudzu-list.js")); sizes[name]=[file.length,gzipSync(file).length];} console.log(JSON.stringify({runs,medians:{baseline:median(runs.baseline),candidate:median(runs.candidate)},sizes}));'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Before release-content updates, the complete site `dist` was byte-identical. Seven representative keyed fixtures retained identical file lists and SHA-256 content; `.kudzu` comparison replaced only each worktree's absolute root in existing source-location strings. The complete compared lists were:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
lists: dist/assets/handlers/pages/index.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, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs
|
|
35
|
+
nested-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/kudzu-plan.json, .kudzu/pages/index.mjs
|
|
36
|
+
keyed-row-hooks: dist/assets/effects/index.js, dist/assets/handlers/pages/index.js, dist/assets/kudzu-binding.js, dist/assets/kudzu-effect.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, .kudzu/HookRow.mjs, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs
|
|
37
|
+
svg-structures: dist/assets/handlers/pages/index.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, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs
|
|
38
|
+
calculated-collections: dist/assets/handlers/pages/index.js, dist/assets/handlers/pages/ordinary.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/assets/native/ordinary/index.js, dist/index.html, dist/ordinary/index.html, dist/static/index.html, .kudzu/calculate.mjs, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs, .kudzu/pages/ordinary.mjs, .kudzu/pages/static.mjs
|
|
39
|
+
rendered-collections: 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, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs, .kudzu/selectVisible.mjs
|
|
40
|
+
keyed-effects: dist/assets/effects/index.js, dist/assets/effects/item-only/index.js, dist/assets/effects/state-only/index.js, dist/assets/handlers/pages/index.js, dist/assets/handlers/pages/item-only.js, dist/assets/handlers/pages/state-only.js, dist/assets/kudzu-binding.js, dist/assets/kudzu-effect.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/item-only/index.html, dist/state-only/index.html, .kudzu/EffectRow.mjs, .kudzu/kudzu-plan.json, .kudzu/pages/index.mjs, .kudzu/pages/item-only.mjs, .kudzu/pages/state-only.mjs
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
This measurement covers compiler clean-build startup and generated list-runtime size, not browser reconciliation latency. Browser behavior remains covered by the existing insert/update/reorder/remove/nested/SVG/state/effect/ref integration checks.
|
|
44
|
+
|
|
3
45
|
## 0.8.19 Handler, Binding, And Derived IR
|
|
4
46
|
|
|
5
47
|
Measured UTC 2026-08-08 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 25.6.1, and npm 11.18.0. Baseline `0.8.18` tag `3598be0` and the `0.8.19` compiler-only candidate used detached worktrees on the same temporary volume with identical installed dependencies.
|
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.20 - Explicit keyed ownership.** Keyed collections now retain deterministic JSON-safe parent/child, selector, component-specialization, row state/ref, handler, and binding ownership while final DOM allocation remains unchanged. Read the [release notes](./RELEASES.md#0820---explicit-keyed-ownership), open the [release page](https://kudzujs.cloud/releases/0.8.20), 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,37 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.8.20 - Explicit keyed ownership
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.20 completes the next Goal A source-analysis boundary: keyed collection sites now finalize into deterministic JSON-safe ownership records before the existing build-time renderer allocates DOM identity and lifecycle state.
|
|
6
|
+
|
|
7
|
+
### Changed in 0.8.20
|
|
8
|
+
|
|
9
|
+
- KeyedBlockIR records parent/child slots, collection signal or calculated binding, key/index policy, owner field, selector reference and states, source provenance, and static status.
|
|
10
|
+
- Recursive same-file and imported row expansion records every component-specialization slot, including stateless intermediate components.
|
|
11
|
+
- Row states and refs retain their specialization owner and declaration source.
|
|
12
|
+
- Command, native, effect, list-expression, list-conditional, and calculated-collection HandlerIR/BindingIR records link to their owning keyed block.
|
|
13
|
+
- Transformer-wide keyed value, condition, event, nested-list, effect, rendered-list, and alias AST side tables were removed; temporary AST stays inside immediate source-local validation and lowering.
|
|
14
|
+
- The existing route list descriptor now has an explicit TypeScript shape without changing its serialized data.
|
|
15
|
+
|
|
16
|
+
### Goal A boundary
|
|
17
|
+
|
|
18
|
+
- `core.mjs` remains authoritative for final list IDs, row key paths, route descriptors, complete HTML, SVG context, state/ref/effect allocation, and exact release.
|
|
19
|
+
- Effect setup, cleanup, dependency, Worker, and lifetime ownership remains on the existing path until EffectIR in `0.8.21`.
|
|
20
|
+
- No accepted syntax, public API, browser capability, VDOM, hydration, component rerender, or retained browser component tree was added.
|
|
21
|
+
|
|
22
|
+
### Validation
|
|
23
|
+
|
|
24
|
+
- The complete suite passes 168/168 tests, including deterministic KeyedBlockIR JSON round-trip and calculated, nested, recursive-component, selector, state, ref, handler, binding, effect, and SVG ownership.
|
|
25
|
+
- Before release-content updates, the complete site `dist` and seven representative keyed fixture output trees matched `v0.8.19`; detached-worktree roots were normalized only in existing `.kudzu` source strings.
|
|
26
|
+
- The keyed list runtime remains byte-identical at 21,831 B raw / 6,930 B gzip. A same-volume 21-run interleaved comparison measured a 0.52% lower candidate median; raw arrays and provenance are recorded in `PERFORMANCE.md`.
|
|
27
|
+
- `create-kudzu` remains 0.1.101 because its unchanged template already accepts `@kudzujs/core@^0.8.15`.
|
|
28
|
+
|
|
29
|
+
### Upgrade
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @kudzujs/core@^0.8.20
|
|
33
|
+
```
|
|
34
|
+
|
|
3
35
|
## 0.8.19 - Handler, binding, and derived IR
|
|
4
36
|
|
|
5
37
|
Kudzu 0.8.19 completes the next Goal A source-analysis boundary: native callbacks, reactive bindings, list evaluators, imports, and pure derived expressions now finalize into JSON-safe ModuleIR before mechanical artifact codegen.
|
|
@@ -6,7 +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.20` KeyedBlockIR ownership complete | Continue with `0.8.21` EffectIR ownership after the release boundary. |
|
|
10
10
|
| B: optimization benchmarks | Deferred | Goal A complete and its output baseline recorded |
|
|
11
11
|
| C: state/resource model | Research only | Reduced fixtures expose a limitation |
|
|
12
12
|
| D: routing compatibility | Current behavior preserved | Revisit only with migration evidence and invariant review |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Current Compiler Architecture
|
|
2
2
|
|
|
3
|
-
This maps the completed `0.8.
|
|
3
|
+
This maps the completed `0.8.20` keyed 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,9 +13,9 @@ This maps the completed `0.8.19` handler, binding, and derived IR boundary. File
|
|
|
13
13
|
| Focused normalization passes | [`framework/compiler/`](../../framework/compiler/) | React, Router, browser signals, animation-frame refs, custom-hook timers, Zustand, and render control each validate and lower a narrow source shape. |
|
|
14
14
|
| Shared AST/scope helpers | [`framework/compiler/ast-helpers.mjs`](../../framework/compiler/ast-helpers.mjs) | Binding, scope, reference, effect-return, and source-location analysis. |
|
|
15
15
|
| Pure collection language | [`framework/compiler/collection-analysis.mjs`](../../framework/compiler/collection-analysis.mjs) | Analyzes collection roots/selectors and serializes the allowed pure expression language used by lists and derived dependencies. |
|
|
16
|
-
| Main semantic analysis | `framework/build.mjs`, `createKudzuTransformer()` | Produces transformed source plus explicit component, handler, binding, and
|
|
16
|
+
| Main semantic analysis | `framework/build.mjs`, `createKudzuTransformer()` | Produces transformed source plus explicit component, handler, binding, derived, and keyed ownership results while still owning effect-lifetime side tables. |
|
|
17
17
|
| Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Retains ordered JSON-safe owner and specialization records for state, setters, props, refs, IDs, direct signal links, and source provenance; AST identity remains private to its source-local session. |
|
|
18
|
-
| Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, imports, and client roots into ModuleIR. |
|
|
18
|
+
| Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, imports, and client roots into ModuleIR. |
|
|
19
19
|
| Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Supported command handlers specialize to JSON-safe ModuleIR, then emit the existing `__kBehavior` AST without changing route plans. |
|
|
20
20
|
| Build module generation | `framework/build.mjs`, `compile()` | Runs TypeScript with the Kudzu transformer, writes build-executable modules to `.kudzu`, rejects surviving React/Router runtime references, and generates handler source when descriptors exist. |
|
|
21
21
|
| Handler/evaluator lowering | [`framework/compiler/handler-lowering.mjs`](../../framework/compiler/handler-lowering.mjs) | Completes source-local callback/binding/list AST rewriting and diagnostics before the JSON-safe IR boundary. |
|
|
@@ -58,7 +58,7 @@ The browser consumes static HTML first. State seeds and descriptors in that HTML
|
|
|
58
58
|
## Current Coupling To Remove
|
|
59
59
|
|
|
60
60
|
- `createKudzuTransformer()` combines discovery, validation, specialization, descriptor registration, and transformed-source emission.
|
|
61
|
-
- Effect
|
|
61
|
+
- Effect ownership relationships and transient component rewrite indexes remain source-local AST indexes; handler, binding, derived, keyed, and component ownership now have explicit JSON-safe source results.
|
|
62
62
|
- `build()` destructures a broad capability manifest into many booleans and performs artifact-specific source surgery.
|
|
63
63
|
- Runtime specialization relies on exact source-string and regular-expression replacements in `framework/build.mjs`.
|
|
64
64
|
- Route facts, rendered plans, artifact requirements, and emitted-file decisions are represented at adjacent but not fully explicit boundaries.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
The `0.8.
|
|
5
|
+
The `0.8.20` KeyedBlockIR ownership result is complete. Goal A continues with the planned `0.8.21` EffectIR 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.20` source result adds deterministic KeyedBlockIR parent/child ownership, collection and selector references, complete specialization membership, row state/ref provenance, and keyed HandlerIR/BindingIR links beside the existing command, component, handler, binding, and derived results. Source-local keyed AST validation is consumed during lowering and does not cross this boundary. Initial values, final state IDs, and effect lifetime ownership join their explicit results in later planned patches; `core.mjs` remains authoritative for route allocation today.
|
|
81
81
|
|
|
82
82
|
Source codegen lowers that data through the existing build ABI:
|
|
83
83
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Planned Version Sequence
|
|
2
2
|
|
|
3
|
-
This is an execution sequence, not release history. `0.8.16` through `0.8.
|
|
3
|
+
This is an execution sequence, not release history. `0.8.16` through `0.8.20` 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
|
@@ -36,12 +36,12 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
|
|
|
36
36
|
- `compiler/animation-frame-pass.mjs`: effect-owned animation-frame ref validation and plain mutable effect-scope lowering.
|
|
37
37
|
- `compiler/browser-signal-passes.mjs`: static media-query external-store and navigator capability-condition normalization.
|
|
38
38
|
- `compiler/collection-analysis.mjs`: pure collection expression IR, selector pipeline, alias, and imported-transform analysis.
|
|
39
|
-
- `compiler/descriptor-session.mjs`: private source-local descriptor registration and deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, import, and client-root finalization.
|
|
39
|
+
- `compiler/descriptor-session.mjs`: private source-local descriptor registration and deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, import, and client-root finalization.
|
|
40
40
|
- `compiler/normalization-pipeline.mjs`: ordered source normalization with parent-pointer repair after every compiler pass.
|
|
41
41
|
- `compiler/react-migration-pass.mjs`: React import, memo, callback, `forwardRef`, and `useId` migration validation and lowering.
|
|
42
42
|
- `compiler/custom-hook-timer-pass.mjs`: private custom-hook timeout-ref validation and compiler-owned timer-state lowering.
|
|
43
43
|
- `compiler/effect-codegen.mjs`: route-specific ordinary, owned, and navigable effect entry generation.
|
|
44
|
-
- `compiler/optimize/command-specialization.mjs`, `compiler/ir/module-ir.mjs`, and `compiler/codegen/command-codegen.mjs`: JSON-safe command
|
|
44
|
+
- `compiler/optimize/command-specialization.mjs`, `compiler/ir/module-ir.mjs`, and `compiler/codegen/command-codegen.mjs`: JSON-safe command and keyed ownership registration, sparse per-source ModuleIR, and source-analysis-free existing behavior-call generation.
|
|
45
45
|
- `compiler/handler-lowering.mjs`: source-local native/effect callback, reducer, Context, Zustand, reactive-binding, and list-evaluator AST lowering before IR finalization.
|
|
46
46
|
- `compiler/handler-codegen.mjs`: AST-free ordered import rendering and finalized handler/binding export-source assembly.
|
|
47
47
|
- `compiler/render-control-pass.mjs`: render-function early-return and exhaustive adjacent-assignment normalization.
|
|
@@ -64,7 +64,7 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
|
|
|
64
64
|
- `dev-state.js`: dev-only, short-lived logical-state snapshot validation and restoration.
|
|
65
65
|
- `*.d.ts`: public TypeScript and JSX declarations.
|
|
66
66
|
|
|
67
|
-
Compiler ownership follows four explicit stages. `build.mjs` owns project discovery, route planning, and artifact emission. `compiler/normalization-pipeline.mjs` owns pass order and repairs parent pointers after every source transform; migration passes keep per-file state local and return additional metadata explicitly. The main transformer analyzes normalized source while `compiler/descriptor-session.mjs` owns one per-source semantic artifact containing handler, effect, binding,
|
|
67
|
+
Compiler ownership follows four explicit stages. `build.mjs` owns project discovery, route planning, and artifact emission. `compiler/normalization-pipeline.mjs` owns pass order and repairs parent pointers after every source transform; migration passes keep per-file state local and return additional metadata explicitly. The main transformer analyzes normalized source while `compiler/descriptor-session.mjs` owns one per-source semantic artifact containing handler, effect callback, binding, keyed block, and client-import descriptors. Keyed blocks link deterministic parent/child sites, component specializations, row states/refs, and generated handlers/bindings without replacing `core.mjs` final DOM ownership. `compiler/collection-analysis.mjs` owns the shared pure collection language used by React migration, reactive expressions, effects, and keyed lists. After build-time rendering, `compiler/route-capability-planner.mjs` projects serializable route plans into one capability manifest consumed by runtime specialization and artifact emission. Codegen modules turn descriptors into route-specific ESM without participating in source analysis. The versioned continuation plan lives in `docs/next-architecture`; it adds no runtime or accepted syntax by itself.
|
|
68
68
|
|
|
69
69
|
New syntax support belongs in an existing pass or a focused new pass only when a reduced migration fixture proves it. Passes must preserve source-located diagnostics, avoid module-global analysis state, and expose metadata through return values rather than AST-identity side channels. Build orchestration stays in `build.mjs`; feature-specific graph validation or code generation moves under `compiler/` when it has a stable input/output boundary.
|
|
70
70
|
|
package/framework/build.mjs
CHANGED
|
@@ -952,13 +952,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
952
952
|
const customHooks = new Map()
|
|
953
953
|
const jsxLocalDeclarations = new Map()
|
|
954
954
|
const jsxLocalsByFunction = new Map()
|
|
955
|
-
const listLocalDeclarations =
|
|
956
|
-
const listLocalUses =
|
|
957
|
-
const listValues = new WeakMap()
|
|
958
|
-
const listEventItems = new WeakMap()
|
|
959
|
-
const listConditions = new WeakMap()
|
|
960
|
-
const nestedLists = new WeakMap()
|
|
961
|
-
const listEffectEntries = new WeakMap()
|
|
955
|
+
const listLocalDeclarations = []
|
|
956
|
+
const listLocalUses = []
|
|
962
957
|
const componentEffectEntries = new WeakMap()
|
|
963
958
|
const analysisSource = node => {
|
|
964
959
|
const original = ts.getOriginalNode(node)
|
|
@@ -1340,8 +1335,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1340
1335
|
const position = sourceFile.getLineAndCharacterOfPosition(declaration.node.getStart(sourceFile))
|
|
1341
1336
|
if (uses.length > 1) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" must be rendered exactly once`)
|
|
1342
1337
|
if (references !== uses.length) throw new Error(`${sourceFile.fileName}:${position.line + 1}:${position.character + 1} Keyed list local "${name}" may only be used as a JSX child`)
|
|
1343
|
-
listLocalDeclarations.
|
|
1344
|
-
if (uses.length) listLocalUses.
|
|
1338
|
+
listLocalDeclarations.push(declaration.node)
|
|
1339
|
+
if (uses.length) listLocalUses.push({ node: uses[0], parts })
|
|
1345
1340
|
}
|
|
1346
1341
|
}
|
|
1347
1342
|
}
|
|
@@ -1484,6 +1479,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1484
1479
|
ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
|
|
1485
1480
|
})
|
|
1486
1481
|
for (const state of [...result.rowStates, ...result.ordinaryStates]) state.analysisOwner = `specialization:${result.analysis.slot}`
|
|
1482
|
+
for (const ref of [...result.rowRefs, ...result.ordinaryRefs]) ref.analysisOwner = `specialization:${result.analysis.slot}`
|
|
1487
1483
|
return result
|
|
1488
1484
|
}
|
|
1489
1485
|
const registerRowHooks = (call, specialization) => {
|
|
@@ -1847,20 +1843,20 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1847
1843
|
if (expanded === value || !referencedStateNames(expanded, setters).size) fail(value, "Calculated collection fields must directly depend on local state")
|
|
1848
1844
|
return expanded
|
|
1849
1845
|
}
|
|
1850
|
-
const parts = listLocalUses.
|
|
1846
|
+
const parts = listLocalUses.find(entry => entry.node === node)?.parts ?? keyedListParts(node.expression, setters, jsxLocalDeclarations.get(owner), fail, new Set(), importedCollections, factory, context, importedCollectionTransforms, calculatedCollection, staticCollection)
|
|
1851
1847
|
if (parts) {
|
|
1852
|
-
for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.
|
|
1848
|
+
for (const declaration of parts.aliasDeclarations ?? []) if (!listLocalDeclarations.includes(declaration)) listLocalDeclarations.push(declaration)
|
|
1853
1849
|
rawRenderedLists.push({ node, parts })
|
|
1854
1850
|
}
|
|
1855
1851
|
}
|
|
1856
1852
|
ts.forEachChild(node, collectRenderedLists)
|
|
1857
1853
|
}
|
|
1858
1854
|
collectRenderedLists(sourceFile)
|
|
1859
|
-
const collectionAliasUses =
|
|
1855
|
+
const collectionAliasUses = rawRenderedLists.flatMap(({ parts }) => parts.aliasUses ?? [])
|
|
1860
1856
|
const collectionAliasDeclarations = new Set(rawRenderedLists.flatMap(({ parts }) => parts.aliasDeclarations ?? []))
|
|
1861
1857
|
for (const declaration of collectionAliasDeclarations) {
|
|
1862
1858
|
const owner = nearestFunction(declaration)
|
|
1863
|
-
const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.
|
|
1859
|
+
const unsupported = identifierReferences(owner.body, declaration.name.text).find(reference => !collectionAliasUses.includes(reference))
|
|
1864
1860
|
if (unsupported) fail(unsupported, `Rendered collection alias "${declaration.name.text}" may only be used as a rendered collection source`)
|
|
1865
1861
|
}
|
|
1866
1862
|
const rejectUnsupportedRenderControl = node => {
|
|
@@ -1912,7 +1908,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1912
1908
|
let count = 0
|
|
1913
1909
|
const visit = (node, currentAggregate = aggregate) => {
|
|
1914
1910
|
if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) {
|
|
1915
|
-
const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [] }
|
|
1911
|
+
const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], specializations: [] }
|
|
1916
1912
|
for (const argument of node.arguments) visit(argument, nestedAggregate)
|
|
1917
1913
|
if (nestedAggregate.hookDeclarations.length || nestedAggregate.effects.length) nestedRowSpecializations.set(`${node.pos}:${node.end}`, nestedAggregate)
|
|
1918
1914
|
return
|
|
@@ -1939,6 +1935,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1939
1935
|
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), node, specialization.effects))
|
|
1940
1936
|
expandedRowSpecializations.set(specialization.root, specialization)
|
|
1941
1937
|
if (currentAggregate) {
|
|
1938
|
+
currentAggregate.specializations ??= []
|
|
1939
|
+
currentAggregate.specializations.push(specialization.analysis.slot, ...(specialization.specializations ?? []))
|
|
1942
1940
|
currentAggregate.effects.push(...specialization.effects)
|
|
1943
1941
|
currentAggregate.hookDeclarations.push(...specialization.hookDeclarations)
|
|
1944
1942
|
currentAggregate.rowStates.push(...specialization.rowStates)
|
|
@@ -1957,8 +1955,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1957
1955
|
expanded.parent = root.parent
|
|
1958
1956
|
return expanded
|
|
1959
1957
|
}
|
|
1960
|
-
const
|
|
1961
|
-
const prepareListCallback = (callback, root, specialization, item) => {
|
|
1958
|
+
const preparedRenderedLists = []
|
|
1959
|
+
const prepareListCallback = (callback, root, specialization, item, effectEntries) => {
|
|
1962
1960
|
const statements = [...specialization.hookDeclarations]
|
|
1963
1961
|
if (specialization.effects.length) {
|
|
1964
1962
|
usesListEffects = true
|
|
@@ -1966,7 +1964,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1966
1964
|
const call = factory.updateCallExpression(entry.call, factory.createIdentifier("__kListUseEffect"), entry.call.typeArguments, entry.call.arguments)
|
|
1967
1965
|
synthesizeTree(call)
|
|
1968
1966
|
const effectSource = entry.source.getSourceFile()
|
|
1969
|
-
|
|
1967
|
+
effectEntries.push({ node: call, item, source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
|
|
1970
1968
|
return factory.createExpressionStatement(call)
|
|
1971
1969
|
}))
|
|
1972
1970
|
}
|
|
@@ -1997,15 +1995,25 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1997
1995
|
ts.setParentRecursive(callback, false)
|
|
1998
1996
|
callback.parent = originalParts.callback.parent
|
|
1999
1997
|
}
|
|
2000
|
-
|
|
2001
|
-
|
|
1998
|
+
const effectEntries = []
|
|
1999
|
+
callback = prepareListCallback(callback, root, specialization, originalParts.item, effectEntries)
|
|
2000
|
+
const parts = {
|
|
2001
|
+
...originalParts,
|
|
2002
|
+
root,
|
|
2003
|
+
callback,
|
|
2004
|
+
effectEntries,
|
|
2005
|
+
specializations: [specialization.analysis?.slot, ...(specialization.specializations ?? [])].filter(slot => slot !== undefined),
|
|
2006
|
+
rowStates: [...specialization.rowStates, ...specialization.ordinaryStates],
|
|
2007
|
+
rowRefs: specialization.rowRefs,
|
|
2008
|
+
analysisStateOwners: new Map([...stateOwnersForNode(originalParts.root), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisOwner])])
|
|
2009
|
+
}
|
|
2002
2010
|
for (const calculation of specialization.calculations) {
|
|
2003
2011
|
ts.setParentRecursive(calculation, false)
|
|
2004
2012
|
calculation.parent = callback
|
|
2005
2013
|
validateListExpression(calculation, parts.item, originalParts.root, fail)
|
|
2006
2014
|
}
|
|
2007
|
-
validateKeyedList(parts, sourceFile,
|
|
2008
|
-
|
|
2015
|
+
const analysis = validateKeyedList(parts, sourceFile, settersForNode(originalParts.root, settersByFunction), specialization.rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
|
|
2016
|
+
preparedRenderedLists.push({ node, parts, analysis })
|
|
2009
2017
|
}
|
|
2010
2018
|
|
|
2011
2019
|
const compileRenderExpression = (expression, anchor) => {
|
|
@@ -2027,6 +2035,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2027
2035
|
}
|
|
2028
2036
|
|
|
2029
2037
|
let activeStateOwners
|
|
2038
|
+
let activeKeyedBlock
|
|
2030
2039
|
const visitWithStateOwners = (node, stateOwners) => {
|
|
2031
2040
|
const previous = activeStateOwners
|
|
2032
2041
|
activeStateOwners = new Map([...(previous ?? []), ...stateOwners])
|
|
@@ -2034,6 +2043,58 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2034
2043
|
activeStateOwners = previous
|
|
2035
2044
|
return result
|
|
2036
2045
|
}
|
|
2046
|
+
const keyedEntry = (entries, node) => entries.find(entry => entry.node === node)
|
|
2047
|
+
const compileKeyedBlock = (node, { parts: listParts, analysis }) => {
|
|
2048
|
+
usesBehavior = true
|
|
2049
|
+
usesList = true
|
|
2050
|
+
const blockSlot = moduleIR.keyedBlocks.length
|
|
2051
|
+
let listSource = listParts.state
|
|
2052
|
+
let collection = { kind: "signal", name: listParts.state?.text }
|
|
2053
|
+
if (listParts.calculation) {
|
|
2054
|
+
usesBinding = true
|
|
2055
|
+
listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings, keyedBlock: blockSlot })
|
|
2056
|
+
const exportName = ts.isCallExpression(listSource) && ts.isStringLiteral(listSource.arguments[2]) ? listSource.arguments[2].text : undefined
|
|
2057
|
+
collection = { kind: "binding", ...(exportName ? { exportName } : {}) }
|
|
2058
|
+
}
|
|
2059
|
+
const derived = listParts.selector?.length ? descriptors.registerDerived("selector", listParts.selector, listParts.selectorStates, node) : undefined
|
|
2060
|
+
const parent = activeKeyedBlock?.block
|
|
2061
|
+
const rowStates = (listParts.rowStates ?? []).map(state => ({ name: state.state, setter: state.setter, owner: state.analysisOwner, ...(analysisSource(state.source) ? { source: analysisSource(state.source) } : {}) }))
|
|
2062
|
+
const rowRefs = (listParts.rowRefs ?? []).map(ref => ({ name: ref.name, owner: ref.analysisOwner, ...(analysisSource(ref.source) ? { source: analysisSource(ref.source) } : {}) }))
|
|
2063
|
+
const specializations = [...new Set([...(listParts.specializations ?? []), ...rowStates.map(state => state.owner), ...rowRefs.map(ref => ref.owner)].filter(value => value !== undefined).map(value => typeof value === "string" ? Number(value.slice(value.lastIndexOf(":") + 1)) : value))]
|
|
2064
|
+
const block = descriptors.registerKeyedBlock({
|
|
2065
|
+
...(analysisSource(node) ? { source: analysisSource(node) } : {}),
|
|
2066
|
+
...(parent ? { parent: parent.slot } : {}),
|
|
2067
|
+
children: [],
|
|
2068
|
+
collection,
|
|
2069
|
+
key: listParts.keyField,
|
|
2070
|
+
...(listParts.ownerField ? { ownerField: listParts.ownerField } : {}),
|
|
2071
|
+
item: listParts.item,
|
|
2072
|
+
...(listParts.index ? { index: listParts.index } : {}),
|
|
2073
|
+
indexed: listParts.indexed,
|
|
2074
|
+
static: Boolean(listParts.static),
|
|
2075
|
+
...(derived ? { selector: derived.slot } : {}),
|
|
2076
|
+
selectorStates: [...(listParts.selectorStates ?? [])],
|
|
2077
|
+
specializations,
|
|
2078
|
+
rowStates,
|
|
2079
|
+
rowRefs
|
|
2080
|
+
})
|
|
2081
|
+
if (parent) parent.children.push(block.slot)
|
|
2082
|
+
const previous = activeKeyedBlock
|
|
2083
|
+
activeKeyedBlock = { analysis, block, parts: listParts }
|
|
2084
|
+
const callback = visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map())
|
|
2085
|
+
activeKeyedBlock = previous
|
|
2086
|
+
const arguments_ = [
|
|
2087
|
+
listSource,
|
|
2088
|
+
block.key === null ? factory.createNull() : factory.createStringLiteral(block.key),
|
|
2089
|
+
callback,
|
|
2090
|
+
factory.createStringLiteral(block.ownerField ?? ""),
|
|
2091
|
+
jsonExpression(derived?.selector ?? listParts.selector ?? [], factory),
|
|
2092
|
+
block.indexed ? factory.createTrue() : factory.createFalse()
|
|
2093
|
+
]
|
|
2094
|
+
if (block.selectorStates.length || block.static) arguments_.push(factory.createArrayLiteralExpression(block.selectorStates.map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
|
|
2095
|
+
if (block.static) arguments_.push(factory.createTrue())
|
|
2096
|
+
return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
|
|
2097
|
+
}
|
|
2037
2098
|
const visitor = node => {
|
|
2038
2099
|
if (ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && customHookPrivateFields.has(node)) {
|
|
2039
2100
|
const privateFields = customHookPrivateFields.get(node)
|
|
@@ -2077,7 +2138,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2077
2138
|
return factory.updateExportDeclaration(node, node.modifiers, node.isTypeOnly, node.exportClause, factory.createStringLiteral(relativeModulePath(compiledPath(file), compiledPath(target))), node.attributes)
|
|
2078
2139
|
}
|
|
2079
2140
|
|
|
2080
|
-
const listEffect = ts.isCallExpression(node) ?
|
|
2141
|
+
const listEffect = ts.isCallExpression(node) ? keyedEntry(activeKeyedBlock?.analysis.effects ?? [], node) : undefined
|
|
2081
2142
|
const componentEffect = ts.isCallExpression(node) ? componentEffectEntries.get(node) : undefined
|
|
2082
2143
|
const specializedEffect = listEffect ?? componentEffect
|
|
2083
2144
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && node.expression.text === "useEffect" || specializedEffect)) {
|
|
@@ -2195,6 +2256,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2195
2256
|
reducers: reducersForNode(node, reducersByFunction),
|
|
2196
2257
|
importBindings: specializedEffect?.imports ?? importBindings,
|
|
2197
2258
|
listItem: dependencyItem,
|
|
2259
|
+
keyedBlock: activeKeyedBlock?.block.slot,
|
|
2198
2260
|
deferValues: true,
|
|
2199
2261
|
snapshotNested: returns.cleanup,
|
|
2200
2262
|
liveStates: customHookTimerStates
|
|
@@ -2228,7 +2290,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2228
2290
|
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, initializer)
|
|
2229
2291
|
}
|
|
2230
2292
|
|
|
2231
|
-
if (ts.isVariableDeclaration(node) && listLocalDeclarations.
|
|
2293
|
+
if (ts.isVariableDeclaration(node) && listLocalDeclarations.includes(node)) {
|
|
2232
2294
|
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createIdentifier("undefined"))
|
|
2233
2295
|
}
|
|
2234
2296
|
|
|
@@ -2253,47 +2315,31 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2253
2315
|
if (compiled !== node.expression) return factory.updateReturnStatement(node, compiled)
|
|
2254
2316
|
}
|
|
2255
2317
|
|
|
2256
|
-
|
|
2257
|
-
|
|
2318
|
+
const listCondition = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.conditions ?? [], node.expression) : undefined
|
|
2319
|
+
if (listCondition) {
|
|
2320
|
+
const entry = listCondition.value
|
|
2258
2321
|
return factory.updateJsxExpression(node, descriptors.compileListConditional({
|
|
2259
2322
|
...entry,
|
|
2323
|
+
keyedBlock: activeKeyedBlock.block.slot,
|
|
2260
2324
|
truthy: ts.visitNode(entry.truthy, visitor),
|
|
2261
2325
|
falsy: ts.visitNode(entry.falsy, visitor)
|
|
2262
2326
|
}))
|
|
2263
2327
|
}
|
|
2264
2328
|
|
|
2265
|
-
|
|
2266
|
-
|
|
2329
|
+
const listValue = ts.isJsxExpression(node) && node.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.expression) : undefined
|
|
2330
|
+
if (listValue) {
|
|
2331
|
+
return factory.updateJsxExpression(node, descriptors.compileListValue(node.expression, { ...listValue.value, keyedBlock: activeKeyedBlock.block.slot }))
|
|
2267
2332
|
}
|
|
2268
2333
|
|
|
2269
|
-
|
|
2270
|
-
|
|
2334
|
+
const attributeListValue = ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression ? keyedEntry(activeKeyedBlock?.analysis.values ?? [], node.initializer.expression) : undefined
|
|
2335
|
+
if (attributeListValue) {
|
|
2336
|
+
return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, descriptors.compileListValue(node.initializer.expression, { ...attributeListValue.value, keyedBlock: activeKeyedBlock.block.slot })))
|
|
2271
2337
|
}
|
|
2272
2338
|
|
|
2273
2339
|
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
2274
|
-
const
|
|
2275
|
-
const
|
|
2276
|
-
if (
|
|
2277
|
-
usesBehavior = true
|
|
2278
|
-
usesList = true
|
|
2279
|
-
let listSource = listParts.state
|
|
2280
|
-
if (listParts.calculation) {
|
|
2281
|
-
usesBinding = true
|
|
2282
|
-
listSource = descriptors.compileReactiveBinding(listParts.calculation, { setters: settersForNode(node, settersByFunction), importBindings })
|
|
2283
|
-
}
|
|
2284
|
-
const selector = listParts.selector?.length ? descriptors.registerDerived("selector", listParts.selector, listParts.selectorStates, node).selector : listParts.selector ?? []
|
|
2285
|
-
const arguments_ = [
|
|
2286
|
-
listSource,
|
|
2287
|
-
listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
|
|
2288
|
-
visitWithStateOwners(listParts.callback, listParts.analysisStateOwners ?? new Map()),
|
|
2289
|
-
factory.createStringLiteral(listParts.ownerField ?? ""),
|
|
2290
|
-
jsonExpression(selector, factory),
|
|
2291
|
-
listParts.indexed ? factory.createTrue() : factory.createFalse()
|
|
2292
|
-
]
|
|
2293
|
-
if (listParts.selectorStates?.size || listParts.static) arguments_.push(factory.createArrayLiteralExpression([...(listParts.selectorStates ?? [])].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
|
|
2294
|
-
if (listParts.static) arguments_.push(factory.createTrue())
|
|
2295
|
-
return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
|
|
2296
|
-
}
|
|
2340
|
+
const renderedList = keyedEntry(preparedRenderedLists, node)
|
|
2341
|
+
const nestedList = keyedEntry(activeKeyedBlock?.analysis.nested ?? [], unwrapExpression(node.expression))
|
|
2342
|
+
if (renderedList || nestedList) return compileKeyedBlock(node, renderedList ?? nestedList)
|
|
2297
2343
|
const conditional = conditionalParts(node.expression)
|
|
2298
2344
|
if (conditional) {
|
|
2299
2345
|
const compiled = compileRenderExpression(node.expression, node)
|
|
@@ -2332,7 +2378,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2332
2378
|
setters,
|
|
2333
2379
|
reducers: reducersForNode(node, reducersByFunction),
|
|
2334
2380
|
functions: functionsForNode(node),
|
|
2335
|
-
listItem:
|
|
2381
|
+
listItem: activeKeyedBlock ? { item: activeKeyedBlock.parts.item, index: activeKeyedBlock.parts.index } : undefined,
|
|
2382
|
+
keyedBlock: activeKeyedBlock?.block.slot,
|
|
2336
2383
|
importBindings: new Map([...importBindings, ...packageBindings])
|
|
2337
2384
|
})
|
|
2338
2385
|
if (event) {
|
|
@@ -2567,10 +2614,11 @@ function insideJsxEventHandler(node, root) {
|
|
|
2567
2614
|
return false
|
|
2568
2615
|
}
|
|
2569
2616
|
|
|
2570
|
-
function validateKeyedList(parts, sourceFile,
|
|
2617
|
+
function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback) {
|
|
2571
2618
|
const fail = (node, message) => {
|
|
2572
2619
|
throw sourceNodeError(node, sourceFile, message)
|
|
2573
2620
|
}
|
|
2621
|
+
const analysis = { values: [], conditions: [], nested: [], effects: parts.effectEntries ?? [] }
|
|
2574
2622
|
const root = parts.root
|
|
2575
2623
|
const item = parts.item
|
|
2576
2624
|
const nestedDiagnostic = "Nested keyed list collections must be a direct property of the parent item"
|
|
@@ -2585,7 +2633,6 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2585
2633
|
if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, nestedDiagnostic)
|
|
2586
2634
|
if (ts.isJsxSpreadAttribute(node) && referencesIdentifier(node.expression, item)) fail(node, "Keyed list item spreads are not supported")
|
|
2587
2635
|
if (ts.isJsxAttribute(node) && /^on[A-Z]/.test(node.name.text)) {
|
|
2588
|
-
listEventItems.set(node, { item, index: parts.index })
|
|
2589
2636
|
return
|
|
2590
2637
|
}
|
|
2591
2638
|
if (ts.isJsxExpression(node) && node.expression) {
|
|
@@ -2610,16 +2657,28 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2610
2657
|
ts.setParentRecursive(callback, false)
|
|
2611
2658
|
callback.parent = nested.callback.parent
|
|
2612
2659
|
}
|
|
2613
|
-
|
|
2660
|
+
const effectEntries = []
|
|
2661
|
+
callback = prepareListCallback(callback, root, specialization ?? { hookDeclarations: [], effects: [] }, nested.item, effectEntries)
|
|
2614
2662
|
const specializedStates = [...(specialization?.rowStates ?? []), ...(specialization?.ordinaryStates ?? [])]
|
|
2615
|
-
const nestedParts = {
|
|
2663
|
+
const nestedParts = {
|
|
2664
|
+
...nested,
|
|
2665
|
+
root,
|
|
2666
|
+
callback,
|
|
2667
|
+
state: parts.state,
|
|
2668
|
+
nested: true,
|
|
2669
|
+
effectEntries,
|
|
2670
|
+
specializations: [specialization?.analysis?.slot, ...(specialization?.specializations ?? [])].filter(slot => slot !== undefined),
|
|
2671
|
+
rowStates: specializedStates,
|
|
2672
|
+
rowRefs: specialization?.rowRefs ?? [],
|
|
2673
|
+
analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisOwner])])
|
|
2674
|
+
}
|
|
2616
2675
|
for (const calculation of specialization?.calculations ?? []) {
|
|
2617
2676
|
ts.setParentRecursive(calculation, false)
|
|
2618
2677
|
calculation.parent = callback
|
|
2619
2678
|
validateListExpression(calculation, nested.item, nested.root, fail)
|
|
2620
2679
|
}
|
|
2621
|
-
|
|
2622
|
-
|
|
2680
|
+
const nestedAnalysis = validateKeyedList(nestedParts, sourceFile, setters, specialization?.rowStates ?? [], componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback)
|
|
2681
|
+
analysis.nested.push({ node: expression, parts: nestedParts, analysis: nestedAnalysis })
|
|
2623
2682
|
return
|
|
2624
2683
|
}
|
|
2625
2684
|
const condition = conditionalParts(expression)
|
|
@@ -2631,7 +2690,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2631
2690
|
}
|
|
2632
2691
|
if (!referencesIdentifier(condition.condition, item) && !(parts.index && referencesIdentifier(condition.condition, parts.index))) fail(node, "Keyed list item conditions must read the item or index")
|
|
2633
2692
|
validateListExpression(condition.condition, item, node, fail, parts.index)
|
|
2634
|
-
|
|
2693
|
+
analysis.conditions.push({ node: node.expression, value: { ...condition, item, index: parts.index } })
|
|
2635
2694
|
visit(condition.truthy)
|
|
2636
2695
|
visit(condition.falsy)
|
|
2637
2696
|
return
|
|
@@ -2642,7 +2701,7 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2642
2701
|
if (field && ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
|
|
2643
2702
|
if (isRootKey) return
|
|
2644
2703
|
if (field) {
|
|
2645
|
-
|
|
2704
|
+
analysis.values.push({ node: node.expression, value: { field } })
|
|
2646
2705
|
return
|
|
2647
2706
|
}
|
|
2648
2707
|
if (referencesIdentifier(expression, item) || parts.index && referencesIdentifier(expression, parts.index)) {
|
|
@@ -2651,13 +2710,14 @@ function validateKeyedList(parts, sourceFile, listValues, listEventItems, listCo
|
|
|
2651
2710
|
if (parts.nested && states.size) fail(node, "Nested keyed list item expressions cannot read parent state")
|
|
2652
2711
|
validateListExpression(expression, item, node, fail, parts.index, states)
|
|
2653
2712
|
if (ts.isJsxAttribute(node.parent) && ["ref", "dangerouslysetinnerhtml"].includes(node.parent.name.text.toLowerCase())) fail(node, `Keyed list item ${node.parent.name.text} is not supported`)
|
|
2654
|
-
|
|
2713
|
+
analysis.values.push({ node: node.expression, value: { item, index: parts.index, states } })
|
|
2655
2714
|
return
|
|
2656
2715
|
}
|
|
2657
2716
|
}
|
|
2658
2717
|
ts.forEachChild(node, visit)
|
|
2659
2718
|
}
|
|
2660
2719
|
visit(root)
|
|
2720
|
+
return analysis
|
|
2661
2721
|
}
|
|
2662
2722
|
|
|
2663
2723
|
function directConstObjectLiteral(expression, call) {
|
|
@@ -2,7 +2,7 @@ import ts from "typescript"
|
|
|
2
2
|
import { createComponentAnalysis } from "./analysis/component-analysis.mjs"
|
|
3
3
|
import { bindingNames, isFunctionLike, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, unwrapExpression } from "./ast-helpers.mjs"
|
|
4
4
|
import { generateCommandBehavior } from "./codegen/command-codegen.mjs"
|
|
5
|
-
import { createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerModuleHandler } from "./ir/module-ir.mjs"
|
|
5
|
+
import { createModuleIR, registerBinding, registerCommandHandler, registerDerived, registerKeyedBlock, registerModuleHandler } from "./ir/module-ir.mjs"
|
|
6
6
|
|
|
7
7
|
export function createSemanticArtifact(file) {
|
|
8
8
|
return { componentAnalysis: createComponentAnalysis(file), moduleIR: createModuleIR(file) }
|
|
@@ -16,9 +16,9 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
16
16
|
const listExpressions = []
|
|
17
17
|
const clientModules = new Set()
|
|
18
18
|
|
|
19
|
-
function compileListExpression(read, expression, item, index, states = new Set()) {
|
|
19
|
+
function compileListExpression(read, expression, item, index, states = new Set(), keyedBlock) {
|
|
20
20
|
const exportName = `listExpression${listExpressions.length}`
|
|
21
|
-
listExpressions.push({ exportName, expression, item, index, states, role: "list-expression" })
|
|
21
|
+
listExpressions.push({ exportName, expression, item, index, states, role: "list-expression", keyedBlock })
|
|
22
22
|
const arguments_ = [read, factory.createStringLiteral(handlerUrl), factory.createStringLiteral(exportName)]
|
|
23
23
|
if (states.size) arguments_.push(factory.createArrayLiteralExpression([...states].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
|
|
24
24
|
return factory.createCallExpression(factory.createIdentifier("__kListExpression"), undefined, arguments_)
|
|
@@ -26,7 +26,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
26
26
|
|
|
27
27
|
function compileListConditional(entry) {
|
|
28
28
|
const exportName = `listExpression${listExpressions.length}`
|
|
29
|
-
listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index, role: "list-conditional" })
|
|
29
|
+
listExpressions.push({ exportName, expression: entry.condition, item: entry.item, index: entry.index, role: "list-conditional", keyedBlock: entry.keyedBlock })
|
|
30
30
|
const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), entry.condition)
|
|
31
31
|
const thunk = branch => factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), branch)
|
|
32
32
|
return factory.createCallExpression(factory.createIdentifier("__kListConditional"), undefined, [
|
|
@@ -44,16 +44,16 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
44
44
|
const read = factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), initial)
|
|
45
45
|
return entry.field
|
|
46
46
|
? factory.createCallExpression(factory.createIdentifier("__kListField"), undefined, [read, factory.createStringLiteral(entry.field)])
|
|
47
|
-
: compileListExpression(read, expression, entry.item, entry.index, entry.states)
|
|
47
|
+
: compileListExpression(read, expression, entry.item, entry.index, entry.states, entry.keyedBlock)
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function compileReactiveBinding(expression, { setters, importBindings = new Map() }) {
|
|
50
|
+
function compileReactiveBinding(expression, { setters, importBindings = new Map(), keyedBlock }) {
|
|
51
51
|
const parts = conditionalParts(expression)
|
|
52
52
|
const state = parts && directStateIdentifier(parts.condition, setters)
|
|
53
53
|
if (state && isPrimitiveLiteral(parts.truthy) && isPrimitiveLiteral(parts.falsy)) {
|
|
54
54
|
return factory.createCallExpression(factory.createIdentifier("__kSelect"), undefined, [state, parts.truthy, parts.falsy])
|
|
55
55
|
}
|
|
56
|
-
return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, importBindings))
|
|
56
|
+
return factory.createCallExpression(factory.createIdentifier("__kBinding"), undefined, compileReactiveExpression(expression, setters, importBindings, keyedBlock))
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function compileConditional(kind, expression, truthy, falsy, setters) {
|
|
@@ -64,14 +64,14 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
64
64
|
return factory.createCallExpression(factory.createIdentifier("__kConditional"), undefined, [factory.createStringLiteral(kind), initial, thunk(truthy), thunk(falsy), ...descriptor])
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
function compileReactiveExpression(expression, setters, importBindings = new Map()) {
|
|
67
|
+
function compileReactiveExpression(expression, setters, importBindings = new Map(), keyedBlock) {
|
|
68
68
|
const usedStates = referencedStateNames(expression, setters)
|
|
69
69
|
const importedNames = referencedImportedBindings(expression, importBindings)
|
|
70
70
|
const imports = [...importedNames].map(name => importBindings.get(name))
|
|
71
71
|
registerClientImports(imports)
|
|
72
72
|
const captures = new Set([...captureNames(expression, expression, setters)].filter(name => !importedNames.has(name)))
|
|
73
73
|
const exportName = `binding${reactiveBindings.length}`
|
|
74
|
-
reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports, role: "binding" })
|
|
74
|
+
reactiveBindings.push({ exportName, expression, captures, states: usedStates, imports, role: "binding", keyedBlock })
|
|
75
75
|
const states = [...usedStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
|
|
76
76
|
const scope = [...captures].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))
|
|
77
77
|
const stateNames = new Set(usedStates)
|
|
@@ -91,13 +91,13 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
91
91
|
]
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
function compileEvent(expression, { owner = "module", stateOwners = new Map(), setters, reducers, functions, listItem, importBindings }) {
|
|
94
|
+
function compileEvent(expression, { owner = "module", stateOwners = new Map(), setters, reducers, functions, listItem, keyedBlock, importBindings }) {
|
|
95
95
|
if (ts.isIdentifier(expression)) expression = functions.get(expression.text)
|
|
96
96
|
if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
|
|
97
|
-
const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, stateOwners, owner)
|
|
97
|
+
const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock)
|
|
98
98
|
if (optimized) return optimized
|
|
99
99
|
rejectWorkerConstructions(expression)
|
|
100
|
-
const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", role: "native", listItem })
|
|
100
|
+
const descriptor = compileNativeCallback(expression, { setters, reducers, entries: nativeHandlers, importBindings, prefix: "handler", role: "native", listItem, keyedBlock })
|
|
101
101
|
return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
|
|
102
102
|
factory.createStringLiteral(handlerUrl), factory.createStringLiteral(descriptor.exportName), descriptor.states, descriptor.scope
|
|
103
103
|
])
|
|
@@ -107,7 +107,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
107
107
|
return compileNativeCallback(expression, { ...options, entries: effectHandlers, prefix: "effect", role: "effect" })
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
-
function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
|
|
110
|
+
function compileNativeCallback(expression, { setters, reducers, entries, importBindings, prefix, role, listItem, keyedBlock, deferValues = false, snapshotNested = false, liveStates = new Set() }) {
|
|
111
111
|
const allCaptures = nativeCaptureNames(expression, setters)
|
|
112
112
|
const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
|
|
113
113
|
const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
|
|
@@ -120,7 +120,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
120
120
|
if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction)) usedStates.add(state)
|
|
121
121
|
}
|
|
122
122
|
const exportName = `${prefix}${entries.length}`
|
|
123
|
-
entries.push({ exportName, expression, captures, deferValues, imports, listItem, liveStates, role, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested, usedStates })
|
|
123
|
+
entries.push({ exportName, expression, captures, deferValues, imports, listItem, keyedBlock, liveStates, role, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested, usedStates })
|
|
124
124
|
const value = name => deferValues
|
|
125
125
|
? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
|
|
126
126
|
: factory.createIdentifier(name)
|
|
@@ -136,13 +136,14 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
function compileOptimizedEvent(expression, setters, stateOwners, owner) {
|
|
139
|
+
function compileOptimizedEvent(expression, setters, stateOwners, owner, keyedBlock) {
|
|
140
140
|
const statements = ts.isBlock(expression.body) ? expression.body.statements : [factory.createExpressionStatement(expression.body)]
|
|
141
141
|
const commands = statements.map(statement => ts.isExpressionStatement(statement) ? compileEventCommand(statement.expression, setters) : undefined)
|
|
142
142
|
if (!commands.length || commands.some(command => !command)) return undefined
|
|
143
143
|
const original = ts.getOriginalNode(expression)
|
|
144
144
|
const source = original.pos >= 0 && original.end >= 0 ? { file: sourceName(original.getSourceFile()), start: original.getStart(), end: original.end } : undefined
|
|
145
145
|
const handler = registerCommandHandler(moduleIR, commands.map(command => ({ ...command, owner: stateOwners.get(command.state) ?? owner })), source, owner)
|
|
146
|
+
if (keyedBlock !== undefined) handler.keyedBlock = keyedBlock
|
|
146
147
|
return generateCommandBehavior(moduleIR, handler, factory)
|
|
147
148
|
}
|
|
148
149
|
|
|
@@ -155,6 +156,10 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
155
156
|
return registerDerived(moduleIR, { kind, [kind]: normalized, states: [...states], ...(source(node) ? { source: source(node) } : {}) })
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
function registerKeyedBlockResult(descriptor) {
|
|
160
|
+
return registerKeyedBlock(moduleIR, descriptor)
|
|
161
|
+
}
|
|
162
|
+
|
|
158
163
|
function finalize() {
|
|
159
164
|
const callbacks = [...nativeHandlers, ...effectHandlers]
|
|
160
165
|
for (const entry of callbacks) {
|
|
@@ -162,6 +167,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
162
167
|
const setters = Map.groupBy(entry.setters, ([, state]) => state)
|
|
163
168
|
registerModuleHandler(moduleIR, {
|
|
164
169
|
role: entry.role,
|
|
170
|
+
...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
|
|
165
171
|
exportName: entry.exportName,
|
|
166
172
|
async: Boolean(entry.expression.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)),
|
|
167
173
|
generator: Boolean(entry.expression.asteriskToken),
|
|
@@ -184,6 +190,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
184
190
|
}
|
|
185
191
|
for (const entry of reactiveBindings) registerBinding(moduleIR, {
|
|
186
192
|
role: entry.role,
|
|
193
|
+
...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
|
|
187
194
|
exportName: entry.exportName,
|
|
188
195
|
parameters: ["__k"],
|
|
189
196
|
states: [...entry.states],
|
|
@@ -194,6 +201,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
194
201
|
})
|
|
195
202
|
for (const entry of listExpressions) registerBinding(moduleIR, {
|
|
196
203
|
role: entry.role,
|
|
204
|
+
...(entry.keyedBlock !== undefined ? { keyedBlock: entry.keyedBlock } : {}),
|
|
197
205
|
exportName: entry.exportName,
|
|
198
206
|
parameters: [entry.item, entry.index ?? "__kIndex", "__k"],
|
|
199
207
|
states: [...(entry.states ?? [])],
|
|
@@ -215,7 +223,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
215
223
|
|
|
216
224
|
const importRecord = entry => ({ target: entry.target, kind: entry.kind, local: entry.local, ...(entry.imported ? { imported: entry.imported } : {}), package: Boolean(entry.package) })
|
|
217
225
|
|
|
218
|
-
return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult }
|
|
226
|
+
return { compileConditional, compileEffectCallback, compileEvent, compileListConditional, compileListValue, compileReactiveBinding, finalize, registerDerived: registerDerivedResult, registerKeyedBlock: registerKeyedBlockResult }
|
|
219
227
|
}
|
|
220
228
|
|
|
221
229
|
function directStateIdentifier(expression, setters) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function createModuleIR(file) {
|
|
2
|
-
return { version: 1, file, signals: [], handlers: [], bindings: [], derived: [], imports: [], clientModules: [] }
|
|
2
|
+
return { version: 1, file, signals: [], handlers: [], bindings: [], derived: [], keyedBlocks: [], imports: [], clientModules: [] }
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
export function registerCommandHandler(moduleIR, commands, source, scope = "module") {
|
|
@@ -39,3 +39,9 @@ export function registerDerived(moduleIR, descriptor) {
|
|
|
39
39
|
moduleIR.derived.push(derived)
|
|
40
40
|
return derived
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
export function registerKeyedBlock(moduleIR, descriptor) {
|
|
44
|
+
const block = { slot: moduleIR.keyedBlocks.length, ...descriptor }
|
|
45
|
+
moduleIR.keyedBlocks.push(block)
|
|
46
|
+
return block
|
|
47
|
+
}
|
package/framework/core.d.ts
CHANGED
|
@@ -80,6 +80,57 @@ export type MetadataContext<Props = Record<string, unknown>> = {
|
|
|
80
80
|
props: Props
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
export type ListSourceDescriptor =
|
|
84
|
+
| { state: string; truthy?: unknown; falsy?: unknown }
|
|
85
|
+
| {
|
|
86
|
+
module: string
|
|
87
|
+
handler: string
|
|
88
|
+
states: Record<string, string>
|
|
89
|
+
scope: Record<string, unknown>
|
|
90
|
+
scopeStates: Record<string, string>
|
|
91
|
+
scopeBindings: Record<string, unknown>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export type ChildListDescriptor = {
|
|
95
|
+
id: string
|
|
96
|
+
field: string
|
|
97
|
+
key: string | null
|
|
98
|
+
selector?: unknown[]
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export type ListDescriptor = {
|
|
102
|
+
id: string
|
|
103
|
+
state: string
|
|
104
|
+
key: string | null
|
|
105
|
+
keys: Array<string | number>
|
|
106
|
+
svg?: true
|
|
107
|
+
static?: true
|
|
108
|
+
source?: ListSourceDescriptor
|
|
109
|
+
ownerField?: string
|
|
110
|
+
selector?: unknown[]
|
|
111
|
+
selectorStates?: Record<string, string>
|
|
112
|
+
indexed?: true
|
|
113
|
+
reducer?: true
|
|
114
|
+
children?: ChildListDescriptor[]
|
|
115
|
+
mount?: true
|
|
116
|
+
nested?: true
|
|
117
|
+
effects?: true
|
|
118
|
+
conditions?: true
|
|
119
|
+
conditionHandlers?: true
|
|
120
|
+
textRanges?: true
|
|
121
|
+
attributes?: true
|
|
122
|
+
events?: true
|
|
123
|
+
expressions?: true
|
|
124
|
+
expressionAttributes?: true
|
|
125
|
+
expressionStates?: string[]
|
|
126
|
+
rowStates?: Array<{ id: string; initialValue: unknown }>
|
|
127
|
+
rowConditions?: string[]
|
|
128
|
+
rowRefs?: string[]
|
|
129
|
+
fastRelease?: true
|
|
130
|
+
seed?: Record<string, string | number | boolean | null>
|
|
131
|
+
valueSeed?: Record<string, string | number | boolean | null>
|
|
132
|
+
}
|
|
133
|
+
|
|
83
134
|
export function renderPage<Props = Record<string, never>>(
|
|
84
135
|
component: (props: Props) => unknown | Promise<unknown>,
|
|
85
136
|
metadata?: PageMetadata,
|
|
@@ -117,6 +168,6 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
117
168
|
scopeBindings?: Record<string, unknown>
|
|
118
169
|
}>
|
|
119
170
|
conditions: Array<Record<string, unknown>>
|
|
120
|
-
lists:
|
|
171
|
+
lists: ListDescriptor[]
|
|
121
172
|
}
|
|
122
173
|
}>
|