@kudzujs/core 0.8.38 → 0.8.40
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 +1 -1
- package/PERFORMANCE.md +69 -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 +4 -3
- package/docs/next-architecture/large-application-ai-native-roadmap.md +7 -4
- package/docs/next-architecture/versioning.md +3 -1
- package/framework/compiler/effect-analysis.mjs +25 -9
- package/framework/compiler/route-build-record.mjs +11 -0
- package/framework/compiler/route-capability-planner.mjs +21 -22
- package/framework/compiler/route-ir.mjs +194 -0
- package/framework/core.d.ts +1 -0
- package/framework/core.mjs +4 -3
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This document is the source of truth for Kudzu's product direction, architecture invariants, and future development order. Read it before extending React-shaped syntax or browser capabilities.
|
|
4
4
|
|
|
5
|
-
The executable post-`0.8.
|
|
5
|
+
The executable post-`0.8.40` compiler and large-application sequence is maintained in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md). Follow its PR dependencies for implementation work; this document remains authoritative when selecting or accepting a migration capability.
|
|
6
6
|
|
|
7
7
|
[`GOAL_A.md`](./GOAL_A.md) and [`GOAL_B.md`](./GOAL_B.md) are completed capability-validation records. Their commerce and realtime dashboard fixtures prove general lifecycle, navigation, async-workflow, and Worker capabilities; they are not separate product verticals or future priority lists.
|
|
8
8
|
|
package/PERFORMANCE.md
CHANGED
|
@@ -2,6 +2,75 @@
|
|
|
2
2
|
|
|
3
3
|
Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm run benchmark:native`, and `npm run benchmark:module-cache` are maintained in this repository; `npm run benchmark:commerce` is a maintained paired runner over the public external storefront; older excluded-workspace sections are historical provenance only and are not current framework rankings.
|
|
4
4
|
|
|
5
|
+
## P1 Property-Level Object-State Effect Dependencies
|
|
6
|
+
|
|
7
|
+
Measured UTC 2026-08-12 on the Intel Core i5-9500 Linux x64 host with 6 physical cores, Node 24.14.0, npm 11.9.0, and Chrome 142.0.7444.175. The baseline was clean tag `v0.8.39` at `090b42124d596260bdd6a0814c014e5b906dc0eb`. The focused implementation and migration checks had SHA-256 `c097c10e786ff5d09537b52384c1bf3d32cb46998ef62125c23dda3355b62277`, produced by:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
sha256sum framework/compiler/effect-analysis.mjs framework/core.mjs test/compiler-passes.test.mjs test/fixtures/zustand-migration/src/Shell.tsx test/framework.test.mjs | sha256sum
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The complete 154-page repository build used one warm-up and 21 alternating clean fresh-process samples. Both targets emit 175 files and byte-identical `kudzu-plan.json` (`be842101f08aab2d8b3af5daa3ed0539a6a08ea8c3cba15cd25f3a36d53ef61f`). The only deploy difference is the updated public explanation in `docs/index.html`: candidate output adds 98 raw bytes and 31 aggregate gzip bytes. Compiler, effect runtime, route JavaScript, and all other artifacts are byte-identical.
|
|
14
|
+
|
|
15
|
+
| Target | Clean build median | Range | Peak RSS median | Deploy raw / gzip bytes |
|
|
16
|
+
|---|---:|---:|---:|---:|
|
|
17
|
+
| `v0.8.39` | 3,753.276 ms | 3,097.211-5,294.316 ms | 356.9 MiB | 3,845,393 / 1,983,262 B |
|
|
18
|
+
| Property dependency candidate | 3,883.841 ms | 3,533.936-4,919.378 ms | 363.7 MiB | 3,845,491 / 1,983,293 B |
|
|
19
|
+
|
|
20
|
+
The candidate unpaired median is 3.48% higher and the round-paired candidate-minus-baseline median is +103.031 ms. Peak RSS is 1.92% higher. Timing ranges overlap substantially and both changes remain below the 5% material-regression threshold, so no material build or memory regression is established.
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
v0.8.39: [5294.316,4885.317,4462.086,3696.012,3105.264,3491.623,3852.100,3414.218,3753.276,3854.507,4311.174,3833.095,3097.211,3669.963,3567.084,3399.614,3626.351,3687.408,3883.556,4104.665,3880.847]
|
|
24
|
+
candidate: [4919.378,4548.065,4236.325,3752.842,3788.156,3738.609,3883.841,4110.706,4055.150,4116.317,3710.973,4016.578,3533.936,3625.603,3670.115,3840.795,4045.689,3988.434,3619.226,3944.068,3760.976]
|
|
25
|
+
paired candidate-baseline: [-374.938,-337.252,-225.761,56.830,682.892,246.986,31.741,696.488,301.874,261.810,-600.201,183.483,436.725,-44.360,103.031,441.181,419.338,301.026,-264.330,-160.597,-119.871]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
The tracked runtime matrix then ran one warm-up, seven interleaved clean builds, and seven rotating fresh Chrome profiles for matched Kudzu, React/Vite, Vue/Vite, and Svelte/Vite workloads. Every correctness, accessibility, identity, effect-cleanup, and browser-error gate passed. This matrix uses a primitive effect dependency, so it is broad regression evidence rather than a property-dependency-specific framework claim.
|
|
29
|
+
|
|
30
|
+
| Runtime matrix target | Build median | JS raw / gzip B | Total raw / gzip B | Initial DOM | Effect update + cleanup |
|
|
31
|
+
|---|---:|---:|---:|---:|---:|
|
|
32
|
+
| Kudzu `0.8.39` candidate | 862.432 ms | 33,575 / 12,928 | 212,963 / 49,936 | 363.4 ms | 1.9 ms |
|
|
33
|
+
| React 19.2.8 + Vite 8.1.5 | 498.572 ms | 193,685 / 60,043 | 193,967 / 60,262 | 355.7 ms | 4.4 ms |
|
|
34
|
+
| Vue 3.5.40 + Vite 8.1.5 | 655.787 ms | 64,023 / 24,772 | 64,304 / 24,993 | 283.0 ms | 1.9 ms |
|
|
35
|
+
| Svelte 5.56.7 + Vite 8.1.5 | 907.584 ms | 40,726 / 15,659 | 41,007 / 15,878 | 307.9 ms | 2.5 ms |
|
|
36
|
+
|
|
37
|
+
The tracked six-route commerce sources also completed seven rotating clean builds for Kudzu, React SSR + Vite hydration, Next.js static export, Nuxt generation, and SvelteKit adapter-static. These targets have matched initial content and behavior contracts but materially different architectures.
|
|
38
|
+
|
|
39
|
+
| Commerce target | Build median | Files | HTML raw / gzip B | JS raw / gzip B | Total raw / gzip B |
|
|
40
|
+
|---|---:|---:|---:|---:|---:|
|
|
41
|
+
| Kudzu | 1,062.059 ms | 17 | 17,123 / 5,376 | 18,428 / 8,261 | 37,434 / 14,689 |
|
|
42
|
+
| React 19.2.8 SSR + Vite hydration | 1,020.288 ms | 10 | 9,304 / 4,265 | 198,261 / 61,464 | 209,270 / 66,741 |
|
|
43
|
+
| Next.js 16.2.11 static export | 7,939.352 ms | 74 | 72,890 / 19,860 | 643,484 / 191,844 | 814,186 / 247,612 |
|
|
44
|
+
| Nuxt 4.5.0 generation | 8,566.437 ms | 26 | 17,557 / 7,857 | 191,758 / 70,925 | 215,851 / 82,075 |
|
|
45
|
+
| SvelteKit 2.70.1 static export | 5,778.467 ms | 19 | 15,832 / 6,520 | 85,095 / 33,482 | 102,659 / 41,061 |
|
|
46
|
+
|
|
47
|
+
Kudzu commerce output is 4.54x-16.86x smaller in aggregate gzip than the compared hydrated/client-navigation outputs. React's clean build median is 3.9% lower than Kudzu's on this small fixture; Kudzu builds 5.4x-8.1x faster than the three full static-export frameworks. The full commerce browser command was attempted twice, but both runs timed out in Kudzu's existing in-flight rejection-navigation wait before cross-target browser sampling; no commerce browser timing result is claimed. The complete runtime matrix browser run above passed. Property-path effects reuse existing DerivedIR, source subscriptions, expression evaluation, and `Object.is`; no runtime module or field-signal mechanism was added.
|
|
48
|
+
|
|
49
|
+
## P0.12 Deep RouteIR And CapabilityIR Validation
|
|
50
|
+
|
|
51
|
+
Measured UTC 2026-08-12 on the Intel Core i5-9500 Linux x64 host with 6 physical cores, Node 24.14.0, and npm 11.9.0. The baseline was clean tag `v0.8.38` at `918f721d369fed486c5ff04a7423f79563e1c42e`. The focused implementation files had SHA-256 `f8f58432f264d5df12cadfba07986712cecf148be300b6071c03adaf42fd6594`, produced by:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
sha256sum framework/compiler/route-ir.mjs framework/compiler/route-build-record.mjs framework/compiler/route-capability-planner.mjs framework/core.mjs test/compiler-passes.test.mjs | sha256sum
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The repository's 153-page build used one warm-up and seven alternating clean fresh-process samples. Every sample produced the same 174-file deploy graph, 3,840,694 raw bytes, 1,981,560 aggregate gzip bytes, and SHA-256 `6c37ee550a217f4cf78494662e1e1ab4533582f031a2422b07eec74ed50eae74`. Baseline and candidate `kudzu-plan.json` files are byte-identical.
|
|
58
|
+
|
|
59
|
+
| Target | Clean build median | Range | Peak RSS median | Deploy bytes |
|
|
60
|
+
|---|---:|---:|---:|---:|
|
|
61
|
+
| `v0.8.38` | 2,356.477 ms | 2,159.934-2,389.644 ms | 352.7 MiB | 3,840,694 B |
|
|
62
|
+
| P0.12 candidate | 2,352.382 ms | 2,193.576-2,527.911 ms | 356.8 MiB | 3,840,694 B |
|
|
63
|
+
|
|
64
|
+
The candidate's unpaired median is 0.17% lower. Round-paired candidate-minus-baseline differences had a +14.255 ms median, with the candidate faster in one of seven pairs and the baseline faster in six. Timing ranges overlap and remain below the 5% material-regression threshold; candidate peak-RSS median is 4.1 MiB higher.
|
|
65
|
+
|
|
66
|
+
```text
|
|
67
|
+
v0.8.38: [2159.934,2223.405,2313.086,2378.192,2356.477,2380.726,2389.644]
|
|
68
|
+
candidate: [2193.576,2223.644,2317.685,2410.142,2352.382,2394.981,2527.911]
|
|
69
|
+
paired candidate-baseline: [33.642,0.239,4.599,31.950,-4.095,14.255,138.267]
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
RouteIR v1 now validates concrete state and parameter IDs, commands, native/effect captures, dependencies, reactive descriptors, conditions, keyed-list identity and ownership, marker fields, and strict JSON safety. RouteBuildRecord validates those references before artifact selection, while CapabilityIR validates standalone implications and exact projection from its route records before codegen. Repeated checks of the same immutable in-memory contracts are cached by identity. No runtime source, accepted TSX, deploy artifact, or browser behavior changes.
|
|
73
|
+
|
|
5
74
|
## P0.11 Structural Route Artifact Graph
|
|
6
75
|
|
|
7
76
|
Measured UTC 2026-08-11 on the Intel Core i5-9500 Linux x64 host with 6 physical cores, Node 24.14.0, and npm 11.9.0. The baseline was clean tag `v0.8.37` at `8b4e8850c40f2856216e43f23ad02ada8434eb37`. The focused implementation files had SHA-256 `1a93b7fff5d80f271c588f89486a47dc262427195a6a768fe392be949eb99b5e`, produced by:
|
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.40 - Property-level effect dependencies.** Effects can now depend on direct property paths and immutable primitive locals over ordinary object state while reusing existing DerivedIR and `Object.is` comparison. Read the [release notes](./RELEASES.md#0840---property-level-effect-dependencies), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.40), or follow the [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.40 - Property-level effect dependencies
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.40 starts the large-application foundation sequence by allowing ordinary object state to expose property-level effect dependencies without artificial primitive-state decomposition.
|
|
6
|
+
|
|
7
|
+
### Changed in 0.8.40
|
|
8
|
+
|
|
9
|
+
- Direct property paths over ordinary object state compile to existing tagged DerivedIR.
|
|
10
|
+
- Top-level immutable primitive locals derived from object properties use the same source subscription and expression evaluator.
|
|
11
|
+
- Source object commits schedule dependency comparison, while `Object.is` prevents cleanup and rerun when the selected property value is unchanged.
|
|
12
|
+
- Whole-object, dynamic-property, object-valued, and mixed whole-object/property dependencies remain fail-closed.
|
|
13
|
+
- The Zustand migration fixture proves property updates, equality across enhanced navigation, persistent layout ownership, and removal.
|
|
14
|
+
- No field signal, proxy, component rerender, RouteIR field, or browser runtime module was added.
|
|
15
|
+
|
|
16
|
+
### Performance
|
|
17
|
+
|
|
18
|
+
- Twenty-one alternating clean builds measured `v0.8.39` and candidate medians of 3,753.276 ms and 3,883.841 ms, with a +103.031 ms paired median.
|
|
19
|
+
- Peak RSS medians were 356.9 MiB and 363.7 MiB. Build and memory deltas remain below the 5% material-regression threshold.
|
|
20
|
+
- Both targets emit 175 files and byte-identical `kudzu-plan.json`; only updated docs add 98 raw and 31 aggregate gzip bytes. Runtime and route JavaScript are unchanged.
|
|
21
|
+
- Seven-build/fresh-profile React, Vue, and Svelte runtime comparisons pass. The matched commerce build matrix records Kudzu output at 14,689 aggregate gzip bytes versus 66,741 for React hydration, 247,612 for Next, 82,075 for Nuxt, and 41,061 for SvelteKit.
|
|
22
|
+
|
|
23
|
+
### Validation
|
|
24
|
+
|
|
25
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 205 tests and 155 generated pages.
|
|
26
|
+
- Focused checks cover direct object-property classification, canonical DerivedIR, whole-object/property ambiguity, RouteIR projection, browser equality behavior, and layout lifetime.
|
|
27
|
+
- The next migration-backed investigation is multi-boundary component/prop/callback/ref/context dataflow.
|
|
28
|
+
|
|
29
|
+
### Upgrade
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @kudzujs/core@^0.8.40
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 0.8.39 - Fail-closed route contracts
|
|
36
|
+
|
|
37
|
+
Kudzu 0.8.39 completes P0.12 by deeply validating the concrete RouteIR, RouteBuildRecord, and CapabilityIR contracts before artifact selection and browser code generation.
|
|
38
|
+
|
|
39
|
+
### Changed in 0.8.39
|
|
40
|
+
|
|
41
|
+
- RouteIR v1 rejects duplicate state/parameter IDs, missing command targets, unsupported command operations, malformed captures, and invalid effect dependencies.
|
|
42
|
+
- Direct and evaluator bindings validate recursively, including nested scope bindings and every referenced state.
|
|
43
|
+
- Conditions validate identity, kinds, reactive sources, and owned states.
|
|
44
|
+
- Keyed lists validate state/key identity, row ownership templates, child reciprocity, duplicate parents, and ownership cycles.
|
|
45
|
+
- RouteBuildRecord verifies behavior, binding, list, effect, native, and parameter capability reciprocity before artifact planning.
|
|
46
|
+
- CapabilityIR validates standalone event/count/flag implications and exact projection equality from its validated route records before codegen.
|
|
47
|
+
- Strict JSON-safe checks reject cyclic, lossy, nonfinite, BigInt, sparse, accessor, symbol, and non-plain contract data with a deterministic field path.
|
|
48
|
+
|
|
49
|
+
### Performance
|
|
50
|
+
|
|
51
|
+
- Seven alternating clean 153-page builds measured `v0.8.38` and candidate medians of 2,356.477 ms and 2,352.382 ms. The +14.255 ms paired median and overlapping ranges establish no material speedup or regression.
|
|
52
|
+
- Peak RSS medians were 352.7 MiB and 356.8 MiB. Repeated validation of the same immutable contracts is eliminated by identity caching.
|
|
53
|
+
- Both targets emit the same 174 files, 3,840,694 raw bytes, 1,981,560 aggregate gzip bytes, deploy digest, and byte-identical `kudzu-plan.json`.
|
|
54
|
+
|
|
55
|
+
### Validation
|
|
56
|
+
|
|
57
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 203 tests and 154 generated pages.
|
|
58
|
+
- Focused checks cover duplicate/missing identity, malformed commands, effects, captures, bindings, conditions, lists, ownership, capabilities, versions, and JSON safety.
|
|
59
|
+
- Existing static, command, binding, keyed, effect, Worker, package-import, navigation, reducer, and migration fixtures retain browser behavior and zero-unused-runtime checks.
|
|
60
|
+
- The next migration-backed investigation is property-level derived dependencies over ordinary object state.
|
|
61
|
+
|
|
62
|
+
### Upgrade
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm install @kudzujs/core@^0.8.39
|
|
66
|
+
```
|
|
67
|
+
|
|
3
68
|
## 0.8.38 - Structural route artifacts
|
|
4
69
|
|
|
5
70
|
Kudzu 0.8.38 completes P0.11 by replacing serialized route-output searches and parallel route facts with one validated RouteBuildRecord artifact graph.
|
|
@@ -11,7 +11,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
|
|
|
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 |
|
|
13
13
|
|
|
14
|
-
The active post-`0.8.
|
|
14
|
+
The active post-`0.8.40` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.12 deep RouteIR and CapabilityIR validation and the first P1 property-level object-state dependency slice are complete; the next migration-backed investigation is multi-boundary component/prop/callback/ref/context dataflow. The plan orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
|
|
15
15
|
|
|
16
16
|
## Required Invariants
|
|
17
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Current Compiler Architecture
|
|
2
2
|
|
|
3
|
-
This maps the current `0.8.
|
|
3
|
+
This maps the current `0.8.40` architecture, built on the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
|
|
4
4
|
|
|
5
5
|
## Responsibility Map
|
|
6
6
|
|
|
@@ -20,15 +20,16 @@ This maps the current `0.8.38` architecture, built on the completed `0.8.23` Goa
|
|
|
20
20
|
| Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Produces ComponentAnalysis v2 with ordered JSON-safe owner and specialization slots for state, setters, props, refs, IDs, direct SignalIR links, structural OwnerRefs, source-local SiteIds, and source provenance; AST identity remains private to its source-local session. |
|
|
21
21
|
| Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes ModuleIR v2 with deterministic SymbolRef, SignalIR, HandlerIR, BindingIR, DerivedIR, EffectIR, KeyedBlockIR, and ImportIR slots. One fail-closed boundary validates every source-local and component ownership edge before build-module generation. |
|
|
22
22
|
| Route artifact graph | [`framework/compiler/route-build-record.mjs`](../../framework/compiler/route-build-record.mjs), `createRouteBuildRecord()`, `planRouteArtifacts()` | Validates each rendered route's RouteIR, capabilities, entry paths, styles, and exact handler/effect references. Handler modules, Workers, package-client modules, and bundle entry/chunk closure derive from structural edges without serialized output searches or formatted composite keys. |
|
|
23
|
+
| Route contract validation | [`framework/compiler/route-ir.mjs`](../../framework/compiler/route-ir.mjs), `assertRouteIR()` | Fails before artifact selection for invalid state/parameter identity, commands, native/effect captures and dependencies, reactive descriptors, conditions, keyed-list identity/ownership, marker fields, or JSON safety. Immutable in-memory contracts validate once by identity. |
|
|
23
24
|
| 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) | Direct commands use the existing fast path; proven immutable state aliases and one-call local helpers specialize to the same JSON-safe command ModuleIR. Recursion, escape, mutation, and dynamic helper dispatch fail explicitly, while unrelated handlers retain native ESM. Codegen emits the existing `__kBehavior` AST and command ABI. |
|
|
24
25
|
| Source compilation | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `compileSource()` | Runs TypeScript with the Kudzu transformer, rejects surviving React/Router references, and returns a JSON-safe project-relative build module, component analysis, ModuleIR, optional handler module, and imported assets without filesystem writes. |
|
|
25
26
|
| 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. |
|
|
26
27
|
| Handler module codegen | [`framework/compiler/handler-codegen.mjs`](../../framework/compiler/handler-codegen.mjs) | Renders finalized ordered imports and concatenates generated module-export source without TypeScript AST or semantic discovery. |
|
|
27
|
-
| Effect analysis | [`framework/compiler/effect-analysis.mjs`](../../framework/compiler/effect-analysis.mjs) | Classifies ordered signal,
|
|
28
|
+
| Effect analysis | [`framework/compiler/effect-analysis.mjs`](../../framework/compiler/effect-analysis.mjs) | Classifies ordered signal, ordinary object-property DerivedIR, and keyed-item dependencies and validates cleanup-owned browser resources before EffectIR registration. Object sources keep one signal subscription while the existing tagged evaluator and `Object.is` compare the selected primitive value. |
|
|
28
29
|
| Worker graph | [`framework/compiler/worker-compiler.mjs`](../../framework/compiler/worker-compiler.mjs) | Returns functional Worker rewrite results and JSON-safe EffectIR edges, validates relative graphs, emits content-hashed ESM, and resolves placeholders only for rendered effects. |
|
|
29
30
|
| Shared path conversion | [`framework/compiler/path-helpers.mjs`](../../framework/compiler/path-helpers.mjs) | Converts project-relative module, browser, asset, and base paths for build and development serving. |
|
|
30
31
|
| Build-time JSX execution | [`framework/core.mjs`](../../framework/core.mjs), `renderPage()` | Executes compiled pages/layouts, allocates deterministic route/layout ownership IDs, emits complete HTML, and returns RouteIR v1 plus capability facts and exact retained handler references. |
|
|
31
|
-
| Route capability projection | [`framework/compiler/route-capability-planner.mjs`](../../framework/compiler/route-capability-planner.mjs), `planRouteCapabilities()` | Validates RouteBuildRecord and RouteIR v1,
|
|
32
|
+
| Route capability projection | [`framework/compiler/route-capability-planner.mjs`](../../framework/compiler/route-capability-planner.mjs), `planRouteCapabilities()` | Validates RouteBuildRecord and RouteIR v1, purely folds their plans and capability facts into CapabilityIR v1, then checks standalone implications and exact projection equality before codegen. |
|
|
32
33
|
| Effect entry generation | [`framework/compiler/effect-codegen.mjs`](../../framework/compiler/effect-codegen.mjs) | Generates ordinary, dependency, owned, and navigable effect entries from rendered descriptors. |
|
|
33
34
|
| Runtime generation | [`framework/compiler/runtime-codegen.mjs`](../../framework/compiler/runtime-codegen.mjs), [`framework/compiler/list-runtime-codegen.mjs`](../../framework/compiler/list-runtime-codegen.mjs), [`framework/compiler/param-codegen.mjs`](../../framework/compiler/param-codegen.mjs) | Consumes versioned contracts, specializes authored capability sources with fail-closed anchors, and returns source/define results without filesystem ownership. |
|
|
34
35
|
| Artifact emission | `framework/build.mjs` | Selects route artifacts from RouteBuildRecord edges and shared runtimes from CapabilityIR, writes route HTML in bounded batches, writes and bundles the complete generation in a project-local staging sibling, copies public subtrees without replacing generated paths, runs `afterBuild`, then promotes with rollback so failed builds preserve the prior `dist`. Byte-identical native, parameter, and effect route entries reuse one exact-source transform result within the current build only. |
|
|
@@ -140,6 +140,7 @@ This is an incremental evolution of the current repository:
|
|
|
140
140
|
- [x] P0.9 Semantic State Operations is complete in `0.8.36`. Direct setters, one immutable state-value alias, one synchronous zero-argument arrow helper, and one synchronous one-parameter function helper lower to identical existing command HandlerIR. Binding identity proves state/setter/helper/parameter ownership; recursion, escape, mutation, and dynamic helper dispatch fail at authored source locations. Existing direct command specialization remains first, unrelated safe handlers retain native ESM, and no command ABI, runtime, JavaScript VM, or general expression IR is added.
|
|
141
141
|
- [x] P0.10 ModuleIR Reference Unification is complete in `0.8.37`. ModuleIR and ComponentAnalysis v2 assign deterministic slots to symbols, signals, handlers, bindings, derived values, effects, keyed blocks, imports, owners, specializations, states, refs, and IDs. State, capture, import, effect, collection, parent/child, specialization, and row edges use structural slots or ModuleSymbol records while readable names remain codegen/debug metadata. A fail-closed pre-codegen validator rejects malformed slots, unsupported versions, duplicate exports, broken reciprocity, and ownership cycles; focused JSON round-trip checks and all 198 tests pass without changing browser runtime behavior.
|
|
142
142
|
- [x] P0.11 RouteBuildRecord and Artifact Graph is complete in `0.8.38`. Each rendered route records RouteIR, capability facts, route-entry paths, styles, and exact handler/effect references. Build orchestration derives Handler ESM, Worker, package-client, and chunk closure from those edges; serialized HTML/plan `includes()` searches, formatted effect keys, and parallel route-fact/entry arrays are removed. Focused malformed-reference and JSON round-trip checks plus the standard suite preserve the exact 173-file deploy digest and bytes.
|
|
143
|
+
- [x] P0.12 Deep RouteIR and CapabilityIR Validation is complete in `0.8.39`. RouteIR v1 validates state/parameter identity, commands, captures, effects, bindings, conditions, keyed-list ownership, and JSON safety before artifact selection. RouteBuildRecord checks concrete capability reciprocity, and CapabilityIR validates standalone implications plus exact projection from route records before codegen. Identity caching avoids repeated validation of immutable contracts; all 203 tests and byte-identical deploy checks pass without runtime or accepted-source changes.
|
|
143
144
|
|
|
144
145
|
### P0: Semantic Correctness And Compiler Foundation
|
|
145
146
|
|
|
@@ -165,7 +166,7 @@ P0 creates the semantic base required by every later large-application capabilit
|
|
|
165
166
|
| Objective | Required result |
|
|
166
167
|
|---|---|
|
|
167
168
|
| Cross-module component and prop dataflow | Deep composition, callbacks, setters, refs, context, and children retain semantic links without repeated AST inlining |
|
|
168
|
-
| Property-level derived dependencies | Object state
|
|
169
|
+
| Property-level derived dependencies | Object state exposes direct property-path effect dependencies through existing DerivedIR without splitting application state into artificial primitives; multi-boundary propagation remains ordered next |
|
|
169
170
|
| Package-neutral shared state and actions | Existing Zustand support lowers through generic signal/action records; Redux-shaped research has a reusable target |
|
|
170
171
|
| Resource ownership | WebSocket, SSE, subscriptions, timers, observers, Workers, and imperative SDKs can share acquire/owner/dependency/cleanup semantics |
|
|
171
172
|
| Browser-only package modules | Native JS packages may be bundled only into effect/handler/resource chunks without build-time execution |
|
|
@@ -370,12 +371,14 @@ function increment(value) { setCount(value + 1) }; increment(count)
|
|
|
370
371
|
|
|
371
372
|
**Done condition:** duplicate/missing state IDs, invalid event/effect dependencies, broken list ownership, invalid binding descriptors, and artifact mismatches have focused tests and deterministic diagnostics.
|
|
372
373
|
|
|
374
|
+
**Completed in `0.8.39`:** A focused RouteIR validator now indexes state and parameter identities, validates command vocabulary and targets, recursively checks native/effect captures and binding descriptors, proves condition and keyed-list identity/ownership, and rejects non-JSON-safe contracts. RouteBuildRecord invokes that boundary before artifact selection and verifies behavior/binding/list/entry reciprocity. CapabilityIR validates event/count/flag implications and recomputes its exact expected projection from validated records before codegen. The clean 153-page baseline and candidate builds retain byte-identical plans, 174 deploy files, raw/gzip bytes, and deploy digest with no material build-time regression.
|
|
375
|
+
|
|
373
376
|
## Large Application Capability Order
|
|
374
377
|
|
|
375
378
|
After the relevant P0 foundations, investigate capabilities in this order:
|
|
376
379
|
|
|
377
|
-
1. Property-level derived dependencies over ordinary object state.
|
|
378
|
-
2. Multi-boundary component/prop/callback/ref/context dataflow.
|
|
380
|
+
1. Property-level derived dependencies over ordinary object state. **Completed in `0.8.40`:** direct property paths and top-level immutable primitive locals over object state reuse tagged DerivedIR, subscribe to the source signal, and compare selected values with `Object.is`; whole-object and dynamic dependencies remain rejected.
|
|
381
|
+
2. Multi-boundary component/prop/callback/ref/context dataflow. **Next.**
|
|
379
382
|
3. Package-neutral shared state/actions and migration of current Zustand internals.
|
|
380
383
|
4. Browser-only package imports in owned effect/resource modules.
|
|
381
384
|
5. ResourceIR from at least two independent WebSocket/SSE/SDK fixtures with the same semantics.
|
|
@@ -477,4 +480,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
477
480
|
|
|
478
481
|
## Immediate Decision
|
|
479
482
|
|
|
480
|
-
PR 1 through PR
|
|
483
|
+
PR 1 through PR 12 and the `0.8.40` P1 property-dependency slice are complete. The next migration-backed investigation is **multi-boundary component/prop/callback/ref/context dataflow**. Do not skip directly to a store, resource, router, virtualization, or ecosystem package feature.
|
|
@@ -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.40` are completed scopes represented by package/release records.
|
|
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
|
|
|
@@ -29,6 +29,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
29
29
|
| `0.8.36` | Lower equivalent direct, aliased, and local-helper state updates through existing command HandlerIR. | The four required forms emit identical commands and no native handler module; recursion, escape, mutation, and dynamic dispatch fail at authored source locations. |
|
|
30
30
|
| `0.8.37` | Unify ModuleIR and ComponentAnalysis references through deterministic source-local slots and stable symbols. | Every handler, binding, derived, effect, keyed, specialization, state, ref, capture, and import edge validates before build-module generation. |
|
|
31
31
|
| `0.8.38` | Replace serialized route artifact discovery with validated RouteBuildRecord edges. | Handler, effect, Worker, CSS, package-client, and chunk retention is structural while deploy files, bytes, plans, and browser behavior remain unchanged. |
|
|
32
|
+
| `0.8.39` | Deeply validate concrete RouteIR, RouteBuildRecord, and CapabilityIR contracts. | Invalid state, command, effect, binding, condition, list, ownership, capability, and JSON references fail before codegen without changing deploy output. |
|
|
33
|
+
| `0.8.40` | Add property-level derived effect dependencies over ordinary object state. | Direct property paths and immutable primitive locals reuse existing DerivedIR and `Object.is`; whole-object/dynamic dependencies fail, runtime JavaScript is unchanged, and measured build/RSS changes remain below 5%. |
|
|
32
34
|
|
|
33
35
|
## Sequence Rules
|
|
34
36
|
|
|
@@ -20,9 +20,6 @@ export function analyzeEffectDependencies({ dependencies, node, listEffect, keye
|
|
|
20
20
|
ordinaryDependencies.push(dependency)
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
|
-
const invalidDependency = ordinaryDependencies.find(dependency => !ts.isIdentifier(dependency))
|
|
24
|
-
if (invalidDependency) fail(invalidDependency, "useEffect() dependencies must be direct state or runtime parameter identifiers")
|
|
25
|
-
|
|
26
23
|
const entries = []
|
|
27
24
|
const dependencyStates = new Map()
|
|
28
25
|
const substitutions = new Map()
|
|
@@ -30,28 +27,36 @@ export function analyzeEffectDependencies({ dependencies, node, listEffect, keye
|
|
|
30
27
|
let hasDerived = false
|
|
31
28
|
const stateNames = new Set(setters.values())
|
|
32
29
|
for (const dependency of ordinaryDependencies) {
|
|
33
|
-
const
|
|
30
|
+
const direct = ts.isIdentifier(dependency)
|
|
31
|
+
if (!direct && !statePropertyDependency(dependency, stateNames)) fail(dependency, "useEffect() dependencies must be direct state or runtime parameter identifiers or property reads")
|
|
32
|
+
const declarations = direct ? localDeclarations?.get(dependency.text) : undefined
|
|
34
33
|
const initializer = declarations?.length === 1 ? declarations[0].initializer : undefined
|
|
35
34
|
const indexedInitializer = initializer && bindingIndex?.hasNode(initializer) ? bindingIndex : undefined
|
|
36
35
|
const directAlias = initializer && ts.isIdentifier(unwrapExpression(initializer)) && stateNames.has(unwrapExpression(initializer).text) && (!indexedInitializer || ["capture", "unresolved"].includes(indexedInitializer.resolveReference(unwrapExpression(initializer), initializer)?.kind))
|
|
37
|
-
const
|
|
36
|
+
const expressionSource = initializer && !directAlias ? initializer : direct ? undefined : dependency
|
|
37
|
+
let derivedStates = expressionSource ? referencedStateNames(expressionSource, setters, expressionSource, bindingIndex) : new Set()
|
|
38
|
+
if (!derivedStates.size && expressionSource) derivedStates = referencedStateNames(expressionSource, setters, expressionSource)
|
|
38
39
|
if (derivedStates.size) {
|
|
39
40
|
const usedStates = new Set()
|
|
40
|
-
const expression = collectionExpression(
|
|
41
|
-
if (!usedStates.size) fail(dependency, `useEffect() derived dependency
|
|
42
|
-
entries.push({ kind: "derived", name: dependency.text, expression, states: usedStates, source:
|
|
41
|
+
const expression = collectionExpression(expressionSource, { fail, stateNames, selectorStates: usedStates })
|
|
42
|
+
if (!usedStates.size) fail(dependency, `useEffect() derived dependency must read direct state`)
|
|
43
|
+
entries.push({ kind: "derived", name: direct ? dependency.text : dependency.getText(), expression, states: usedStates, source: expressionSource })
|
|
43
44
|
for (const name of usedStates) {
|
|
44
45
|
subscriptions.push(factory.createIdentifier(name))
|
|
45
46
|
dependencyStates.set(name, factory.createIdentifier(name))
|
|
46
47
|
}
|
|
47
|
-
substitutions.set(dependency.text, initializer)
|
|
48
|
+
if (initializer) substitutions.set(dependency.text, initializer)
|
|
48
49
|
hasDerived = true
|
|
49
50
|
} else {
|
|
51
|
+
if (!direct) fail(dependency, "useEffect() dependencies must be direct state or runtime parameter identifiers or property reads")
|
|
50
52
|
subscriptions.push(dependency)
|
|
51
53
|
entries.push({ kind: "signal", name: dependency.text })
|
|
52
54
|
dependencyStates.set(dependency.text, dependency)
|
|
53
55
|
}
|
|
54
56
|
}
|
|
57
|
+
const derivedSourceNames = new Set(entries.filter(entry => entry.kind === "derived").flatMap(entry => [...entry.states]))
|
|
58
|
+
const ambiguous = entries.find(entry => entry.kind === "signal" && derivedSourceNames.has(entry.name))
|
|
59
|
+
if (ambiguous) fail(ordinaryDependencies[entries.indexOf(ambiguous)], `useEffect() cannot mix whole-object and property dependencies for state ${JSON.stringify(ambiguous.name)}`)
|
|
55
60
|
if (!hasDerived) dependencyStates.clear()
|
|
56
61
|
return { dependencyItem, itemDependencies, ordinaryDependencies, entries, dependencyStates, substitutions, subscriptions, hasDerived }
|
|
57
62
|
}
|
|
@@ -91,3 +96,14 @@ function directProperty(expression, objectName) {
|
|
|
91
96
|
const value = unwrapExpression(expression)
|
|
92
97
|
return ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && value.expression.text === objectName ? value.name.text : undefined
|
|
93
98
|
}
|
|
99
|
+
|
|
100
|
+
function statePropertyDependency(expression, stateNames) {
|
|
101
|
+
let value = unwrapExpression(expression)
|
|
102
|
+
let property = false
|
|
103
|
+
while (ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value)) {
|
|
104
|
+
if (ts.isElementAccessExpression(value) && !ts.isStringLiteral(value.argumentExpression) && !ts.isNumericLiteral(value.argumentExpression)) return false
|
|
105
|
+
property = true
|
|
106
|
+
value = unwrapExpression(value.expression)
|
|
107
|
+
}
|
|
108
|
+
return property && ts.isIdentifier(value) && stateNames.has(value.text)
|
|
109
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { assertJsonSafe, assertRouteIR } from "./route-ir.mjs"
|
|
2
|
+
|
|
3
|
+
const validated = new WeakSet()
|
|
4
|
+
|
|
1
5
|
export function createRouteBuildRecord(input) {
|
|
2
6
|
const record = {
|
|
3
7
|
version: input.version ?? 1,
|
|
@@ -18,8 +22,10 @@ export function createRouteBuildRecord(input) {
|
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export function assertRouteBuildRecord(record) {
|
|
25
|
+
if (validated.has(record)) return record
|
|
21
26
|
if (record?.version !== 1) throw new Error(`Unsupported RouteBuildRecord version: ${JSON.stringify(record?.version)}`)
|
|
22
27
|
if (typeof record.route !== "string" || typeof record.output !== "string" || typeof record.html !== "string" || !isRecord(record.plan)) throw new Error("Invalid RouteBuildRecord v1 structure")
|
|
28
|
+
assertRouteIR(record.plan, { concrete: true })
|
|
23
29
|
if (record.plan.route !== record.route) throw new Error(`RouteBuildRecord route ${JSON.stringify(record.route)} does not match RouteIR route ${JSON.stringify(record.plan.route)}`)
|
|
24
30
|
const capabilityNames = ["navigable", "usesDependencyRuntime", "hasBehaviors", "hasBindings", "hasLists", "hasListStyles", "hasStateSeed", "hasParams", "hasEffects"]
|
|
25
31
|
if (!isRecord(record.capabilities) || !capabilityNames.every(name => typeof record.capabilities[name] === "boolean")) throw new Error("Invalid RouteBuildRecord v1 capabilities")
|
|
@@ -46,6 +52,11 @@ export function assertRouteBuildRecord(record) {
|
|
|
46
52
|
if (Boolean(record.entries.effect) !== record.capabilities.hasEffects || record.capabilities.hasEffects !== Boolean(record.plan.effects?.length)) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has inconsistent effect artifacts`)
|
|
47
53
|
if (Boolean(record.entries.native) !== hasNative) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has inconsistent native artifacts`)
|
|
48
54
|
if (Boolean(record.entries.param) !== record.capabilities.hasParams) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has inconsistent parameter artifacts`)
|
|
55
|
+
if (record.capabilities.hasBindings !== Boolean(record.plan.bindings.length || record.plan.conditions.length || record.plan.lists.some(list => list.source))) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has inconsistent binding capability`)
|
|
56
|
+
if (record.capabilities.hasLists !== Boolean(record.plan.lists.length)) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has inconsistent list capability`)
|
|
57
|
+
if (record.capabilities.hasBehaviors !== Boolean(record.plan.events.length || record.plan.effects.length || record.plan.bindings.length || record.plan.conditions.length || record.plan.lists.length || record.capabilities.hasParams)) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has inconsistent behavior capability`)
|
|
58
|
+
assertJsonSafe(record, "RouteBuildRecord")
|
|
59
|
+
validated.add(record)
|
|
49
60
|
return record
|
|
50
61
|
}
|
|
51
62
|
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
+
import { assertRouteBuildRecord } from "./route-build-record.mjs"
|
|
2
|
+
import { assertJsonSafe, assertRouteIR } from "./route-ir.mjs"
|
|
3
|
+
|
|
1
4
|
export function usesRouteDependencyRuntime({ plan, navigable, hasBindings, hasLists }) {
|
|
2
5
|
assertRouteIR(plan)
|
|
3
6
|
const hasDependencies = plan.effects.some(effect => effect.dependencies?.length)
|
|
4
7
|
return !navigable && hasDependencies && !plan.effects.some(effect => effect.owner) && !hasBindings && !hasLists && !plan.events.some(event => event.native)
|
|
5
8
|
}
|
|
6
9
|
|
|
7
|
-
export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}) {
|
|
10
|
+
export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}, validate = true) {
|
|
8
11
|
for (const record of records) assertRouteBuildRecord(record)
|
|
9
12
|
const plans = records.map(record => record.plan)
|
|
10
|
-
for (const plan of plans) assertRouteIR(plan)
|
|
13
|
+
for (const plan of plans) assertRouteIR(plan, { concrete: true })
|
|
11
14
|
const commandEvents = new Set()
|
|
12
15
|
const nativeEvents = new Set()
|
|
13
16
|
const bindings = { count: 0, text: false, svgConditions: false }
|
|
@@ -93,7 +96,7 @@ export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}
|
|
|
93
96
|
lists.asyncParts = lists.expressions || lists.expressionAttributes || lists.conditions
|
|
94
97
|
lists.mounts ||= lists.conditions || lists.nested
|
|
95
98
|
|
|
96
|
-
|
|
99
|
+
const capabilityIR = {
|
|
97
100
|
version: 1,
|
|
98
101
|
routes: routeCounts,
|
|
99
102
|
events: { command: [...commandEvents].sort(), native: [...nativeEvents].sort(), hasNativeHandlers: nativeEvents.size > 0 },
|
|
@@ -106,19 +109,10 @@ export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}
|
|
|
106
109
|
dependency: routeEntries.some(route => route.usesDependencyRuntime)
|
|
107
110
|
}
|
|
108
111
|
}
|
|
112
|
+
return validate ? assertCapabilityIR(capabilityIR, records, { navigationRouteCount }) : capabilityIR
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
function
|
|
112
|
-
if (plan?.version !== 1) throw new Error(`Unsupported RouteIR version: ${JSON.stringify(plan?.version)}`)
|
|
113
|
-
if (!["states", "params", "searchParams", "events", "effects", "bindings", "conditions", "lists"].every(name => Array.isArray(plan[name])) || typeof plan.searchParamsWritable !== "boolean") throw new Error("Invalid RouteIR v1 structure")
|
|
114
|
-
if (plan.states.some((state, slot) => state?.slot !== slot || typeof state.id !== "string" || typeof state.name !== "string" || !Object.hasOwn(state, "initialValue") || state.lifetime !== undefined && !["layout", "route"].includes(state.lifetime) || state.internal !== undefined && state.internal !== true)) throw new Error("Invalid RouteIR v1 state")
|
|
115
|
-
if ([...plan.params, ...plan.searchParams].some(param => !isRecord(param) || typeof param.name !== "string" || typeof param.id !== "string")) throw new Error("Invalid RouteIR v1 parameter")
|
|
116
|
-
if (plan.events.some(event => !isRecord(event) || typeof event.event !== "string" || event.commands !== undefined && (!Array.isArray(event.commands) || event.commands.some(command => !Array.isArray(command))) || event.native !== undefined && (!isRecord(event.native) || typeof event.native.module !== "string" || typeof event.native.handler !== "string" || !isRecord(event.native.states) || !isRecord(event.native.scope)))) throw new Error("Invalid RouteIR v1 event")
|
|
117
|
-
if (plan.effects.some(effect => !isRecord(effect) || typeof effect.module !== "string" || typeof effect.handler !== "string" || !isRecord(effect.states) || !isRecord(effect.scope))) throw new Error("Invalid RouteIR v1 effect")
|
|
118
|
-
if (plan.bindings.some(binding => !isRecord(binding) || typeof binding.target !== "string") || plan.conditions.some(condition => !isRecord(condition) || condition.svg !== undefined && typeof condition.svg !== "boolean") || plan.lists.some(list => !isRouteList(list))) throw new Error("Invalid RouteIR v1 binding, condition, or list")
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function assertCapabilityIR(capabilityIR) {
|
|
115
|
+
export function assertCapabilityIR(capabilityIR, records, options = {}) {
|
|
122
116
|
if (capabilityIR?.version !== 1) throw new Error(`Unsupported CapabilityIR version: ${JSON.stringify(capabilityIR?.version)}`)
|
|
123
117
|
const sections = ["routes", "events", "bindings", "lists", "effects", "captures", "runtime"]
|
|
124
118
|
if (!sections.every(name => isRecord(capabilityIR[name]))) throw new Error("Invalid CapabilityIR v1 structure")
|
|
@@ -128,17 +122,23 @@ export function assertCapabilityIR(capabilityIR) {
|
|
|
128
122
|
const listFlags = ["conditions", "svg", "deepConditions", "textRanges", "attributes", "events", "expressions", "expressionAttributes", "seeds", "effects", "rowHooks", "rowRefs", "complexRowState", "nested", "selectors", "calculated", "static", "indexes", "stableFastPaths", "generalRowHooks", "asyncParts", "mounts"]
|
|
129
123
|
if (!isCount(capabilityIR.lists.count) || !isCount(capabilityIR.lists.styleCount) || !listFlags.every(name => typeof capabilityIR.lists[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 lists")
|
|
130
124
|
if (!["any", "derivedDependencies", "itemDependencies", "captures", "navigable", "navigableOwners"].every(name => typeof capabilityIR.effects[name] === "boolean") || !["nestedState", "setter"].every(name => typeof capabilityIR.captures[name] === "boolean") || !["shared", "dependency"].every(name => typeof capabilityIR.runtime[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 effect, capture, or runtime flags")
|
|
125
|
+
if (capabilityIR.events.hasNativeHandlers !== Boolean(capabilityIR.events.native.length)) throw new Error("CapabilityIR native handler flag does not match native events")
|
|
126
|
+
for (const events of [capabilityIR.events.command, capabilityIR.events.native]) if (new Set(events).size !== events.length || events.some(event => !event)) throw new Error("CapabilityIR events must be unique nonempty names")
|
|
127
|
+
if (capabilityIR.lists.styleCount > capabilityIR.lists.count) throw new Error("CapabilityIR list style count exceeds list count")
|
|
128
|
+
if (!capabilityIR.lists.count && (capabilityIR.lists.styleCount || listFlags.some(name => capabilityIR.lists[name]))) throw new Error("CapabilityIR list features require at least one list")
|
|
129
|
+
if (capabilityIR.lists.rowRefs && !capabilityIR.lists.rowHooks || capabilityIR.lists.generalRowHooks && !capabilityIR.lists.rowHooks) throw new Error("CapabilityIR row ownership requires row hooks")
|
|
130
|
+
if (capabilityIR.effects.navigableOwners && (!capabilityIR.effects.navigable || !capabilityIR.effects.any)) throw new Error("CapabilityIR navigable effect owners require navigable effects")
|
|
131
|
+
if ((capabilityIR.bindings.count || capabilityIR.lists.count || capabilityIR.events.native.length || options.navigationRouteCount) && !capabilityIR.runtime.shared) throw new Error("CapabilityIR shared runtime flag is inconsistent")
|
|
132
|
+
assertJsonSafe(capabilityIR, "CapabilityIR")
|
|
133
|
+
if (records) {
|
|
134
|
+
const expected = planRouteCapabilities(records, options, false)
|
|
135
|
+
if (JSON.stringify(capabilityIR) !== JSON.stringify(expected)) throw new Error("CapabilityIR does not match RouteBuildRecord projection")
|
|
136
|
+
}
|
|
137
|
+
return capabilityIR
|
|
131
138
|
}
|
|
132
139
|
|
|
133
140
|
const isRecord = value => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
134
141
|
const isCount = value => Number.isSafeInteger(value) && value >= 0
|
|
135
|
-
const isRouteList = list => isRecord(list)
|
|
136
|
-
&& typeof list.id === "string"
|
|
137
|
-
&& typeof list.state === "string"
|
|
138
|
-
&& (typeof list.key === "string" || list.key === null)
|
|
139
|
-
&& Array.isArray(list.keys)
|
|
140
|
-
&& ["svg", "static", "indexed", "reducer", "mount", "nested", "effects", "conditions", "conditionHandlers", "textRanges", "attributes", "events", "expressions", "expressionAttributes", "fastRelease"].every(name => list[name] === undefined || list[name] === true)
|
|
141
|
-
&& ["selector", "children", "expressionStates", "rowStates", "rowConditions", "rowRefs"].every(name => list[name] === undefined || Array.isArray(list[name]))
|
|
142
142
|
|
|
143
143
|
function hasCaptureType(value, type) {
|
|
144
144
|
if (!value || typeof value !== "object") return false
|
|
@@ -153,4 +153,3 @@ function hasNestedCaptureState(value, insideCapture = false) {
|
|
|
153
153
|
if (value.type === "object") return value.value.some(([, entry]) => hasNestedCaptureState(entry, true))
|
|
154
154
|
return (Array.isArray(value) ? value : Object.values(value)).some(entry => hasNestedCaptureState(entry, false))
|
|
155
155
|
}
|
|
156
|
-
import { assertRouteBuildRecord } from "./route-build-record.mjs"
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
const markerFields = new Set(["cleanup", "list", "svg", "mount", "static", "indexed", "reducer", "nested", "effects", "conditions", "conditionHandlers", "textRanges", "attributes", "events", "expressions", "expressionAttributes", "fastRelease"])
|
|
2
|
+
const validated = new WeakSet()
|
|
3
|
+
|
|
4
|
+
export function assertRouteIR(plan, { concrete = false } = {}) {
|
|
5
|
+
if (validated.has(plan) && (!concrete || typeof plan.route === "string")) return plan
|
|
6
|
+
if (plan?.version !== 1) throw new Error(`Unsupported RouteIR version: ${JSON.stringify(plan?.version)}`)
|
|
7
|
+
if (concrete && typeof plan.route !== "string" || !["states", "params", "searchParams", "events", "effects", "bindings", "conditions", "lists"].every(name => Array.isArray(plan[name])) || typeof plan.searchParamsWritable !== "boolean") throw new Error("Invalid RouteIR v1 structure")
|
|
8
|
+
const stateIds = new Set()
|
|
9
|
+
for (const [slot, state] of plan.states.entries()) {
|
|
10
|
+
if (!isRecord(state) || state.slot !== slot || !nonempty(state.id) || !nonempty(state.name) || !Object.hasOwn(state, "initialValue") || state.lifetime !== undefined && !["layout", "route"].includes(state.lifetime) || state.internal !== undefined && state.internal !== true) throw new Error(`Invalid RouteIR v1 state at slot ${slot}`)
|
|
11
|
+
if (stateIds.has(state.id)) throw new Error(`RouteIR has duplicate state ID ${JSON.stringify(state.id)}`)
|
|
12
|
+
stateIds.add(state.id)
|
|
13
|
+
}
|
|
14
|
+
const ids = new Set(stateIds)
|
|
15
|
+
for (const [kind, parameters] of [["parameter", plan.params], ["search parameter", plan.searchParams]]) for (const parameter of parameters) {
|
|
16
|
+
if (!isRecord(parameter) || !nonempty(parameter.name) || !nonempty(parameter.id)) throw new Error(`Invalid RouteIR v1 ${kind}`)
|
|
17
|
+
if (ids.has(parameter.id)) throw new Error(`RouteIR has duplicate state or parameter ID ${JSON.stringify(parameter.id)}`)
|
|
18
|
+
ids.add(parameter.id)
|
|
19
|
+
}
|
|
20
|
+
const listIds = new Set()
|
|
21
|
+
const parentByChild = new Map()
|
|
22
|
+
for (const [index, list] of plan.lists.entries()) assertList(list, index, ids, stateIds, listIds, parentByChild)
|
|
23
|
+
for (const list of plan.lists) {
|
|
24
|
+
if (list.ownerField && !parentByChild.has(list.id)) throw new Error(`RouteIR nested list ${JSON.stringify(list.id)} has no parent`)
|
|
25
|
+
for (const child of list.children ?? []) {
|
|
26
|
+
if (!listIds.has(child.id)) throw new Error(`RouteIR list ${JSON.stringify(list.id)} references missing child ${JSON.stringify(child.id)}`)
|
|
27
|
+
const target = plan.lists.find(entry => entry.id === child.id)
|
|
28
|
+
if (target.ownerField !== child.field) throw new Error(`RouteIR list ${JSON.stringify(child.id)} does not reciprocally reference parent field ${JSON.stringify(child.field)}`)
|
|
29
|
+
if (target.key !== child.key) throw new Error(`RouteIR list ${JSON.stringify(child.id)} does not match parent key metadata`)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const listsById = new Map(plan.lists.map(list => [list.id, list]))
|
|
33
|
+
const visiting = new Set()
|
|
34
|
+
const visited = new Set()
|
|
35
|
+
const visitList = list => {
|
|
36
|
+
if (visiting.has(list.id)) throw new Error(`RouteIR nested list ownership cycle at ${JSON.stringify(list.id)}`)
|
|
37
|
+
if (visited.has(list.id)) return
|
|
38
|
+
visiting.add(list.id)
|
|
39
|
+
for (const child of list.children ?? []) visitList(listsById.get(child.id))
|
|
40
|
+
visiting.delete(list.id)
|
|
41
|
+
visited.add(list.id)
|
|
42
|
+
}
|
|
43
|
+
for (const list of plan.lists) visitList(list)
|
|
44
|
+
for (const [index, event] of plan.events.entries()) assertEvent(event, index, ids)
|
|
45
|
+
for (const [index, effect] of plan.effects.entries()) assertEffect(effect, index, ids, plan.lists)
|
|
46
|
+
for (const [index, binding] of plan.bindings.entries()) assertReactiveDescriptor(binding, `RouteIR binding ${index}`, ids)
|
|
47
|
+
const conditionIds = new Set()
|
|
48
|
+
for (const [index, condition] of plan.conditions.entries()) {
|
|
49
|
+
if (!isRecord(condition) || !nonempty(condition.id) || !["and", "ternary"].includes(condition.kind) || !Object.hasOwn(condition, "initial")) throw new Error(`RouteIR condition has invalid kind ${JSON.stringify(condition?.kind)}`)
|
|
50
|
+
if (conditionIds.has(condition.id)) throw new Error(`RouteIR has duplicate condition ID ${JSON.stringify(condition.id)}`)
|
|
51
|
+
conditionIds.add(condition.id)
|
|
52
|
+
assertReactiveDescriptor(condition, `RouteIR condition ${index}`, ids)
|
|
53
|
+
for (const branch of Object.values(condition.owned ?? {})) for (const entry of branch) {
|
|
54
|
+
if (!Array.isArray(entry) || entry.length !== 2 || !ids.has(entry[0])) throw new Error(`RouteIR condition ${index} owns missing state ${JSON.stringify(entry?.[0])}`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
assertJsonSafe(plan, `RouteIR${plan.route === undefined ? "" : ` ${JSON.stringify(plan.route)}`}`)
|
|
58
|
+
validated.add(plan)
|
|
59
|
+
return plan
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function assertEvent(event, index, ids) {
|
|
63
|
+
if (!isRecord(event) || !nonempty(event.event) || event.commands === undefined && event.native === undefined) throw new Error(`Invalid RouteIR v1 event at index ${index}`)
|
|
64
|
+
for (const command of event.commands ?? []) {
|
|
65
|
+
if (!Array.isArray(command) || command.length !== 3 || !["set", "add", "log"].includes(command[0])) throw new Error(`RouteIR event ${index} command has unsupported operation ${JSON.stringify(command?.[0])}`)
|
|
66
|
+
if (!ids.has(command[1]) && !rowTemplate(command[1])) throw new Error(`RouteIR event ${index} command references missing state ${JSON.stringify(command[1])}`)
|
|
67
|
+
if (command[0] === "add" && (typeof command[2] !== "number" || !Number.isFinite(command[2]))) throw new Error(`RouteIR event ${index} add command requires a finite number`)
|
|
68
|
+
}
|
|
69
|
+
if (event.native !== undefined) assertHandlerDescriptor(event.native, `RouteIR event ${index} native handler`, ids)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function assertEffect(effect, index, ids, lists) {
|
|
73
|
+
const label = `RouteIR effect ${index}`
|
|
74
|
+
assertHandlerDescriptor(effect, label, ids)
|
|
75
|
+
for (const dependency of effect.dependencies ?? []) if (!ids.has(dependency) && !rowTemplate(dependency)) throw new Error(`${label} dependency references missing state ${JSON.stringify(dependency)}`)
|
|
76
|
+
if (new Set(effect.dependencies ?? []).size !== (effect.dependencies ?? []).length) throw new Error(`${label} has duplicate dependencies`)
|
|
77
|
+
for (const state of Object.values(effect.dependencyStates ?? {})) if (!ids.has(state) && !rowTemplate(state)) throw new Error(`${label} derived dependency references missing state ${JSON.stringify(state)}`)
|
|
78
|
+
if (effect.dependencyExpressions !== undefined && !Array.isArray(effect.dependencyExpressions) || effect.itemDependencies !== undefined && (!Array.isArray(effect.itemDependencies) || effect.itemDependencies.some(field => !nonempty(field)))) throw new Error(`${label} has invalid dependencies`)
|
|
79
|
+
if (effect.itemDependencies?.length) {
|
|
80
|
+
if (!nonempty(effect.listState) || !lists.some(list => list.state === effect.listState) || !effect.owner) throw new Error(`${label} item dependencies require a matching owned list`)
|
|
81
|
+
}
|
|
82
|
+
if (effect.list && !effect.owner) throw new Error(`${label} list ownership requires an owner`)
|
|
83
|
+
if (effect.lifetime !== undefined && !["layout", "route"].includes(effect.lifetime)) throw new Error(`${label} has invalid lifetime`)
|
|
84
|
+
for (const field of ["cleanup", "list"]) if (effect[field] !== undefined && effect[field] !== true) throw new Error(`${label} ${field} must be true when present`)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function assertReactiveDescriptor(descriptor, label, ids, seen = new Set()) {
|
|
88
|
+
if (!isRecord(descriptor)) throw new Error(`${label} must be a descriptor`)
|
|
89
|
+
if (seen.has(descriptor)) throw new Error(`${label} contains a descriptor cycle`)
|
|
90
|
+
const direct = Object.hasOwn(descriptor, "state")
|
|
91
|
+
const evaluated = Object.hasOwn(descriptor, "module") || Object.hasOwn(descriptor, "handler")
|
|
92
|
+
if (direct === evaluated) throw new Error(`${label} must use exactly one direct state or evaluator`)
|
|
93
|
+
if (direct) {
|
|
94
|
+
if (!ids.has(descriptor.state) && !rowTemplate(descriptor.state)) throw new Error(`${label} references missing state ${JSON.stringify(descriptor.state)}`)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
assertHandlerDescriptor(descriptor, label, ids)
|
|
98
|
+
if (!isRecord(descriptor.scopeStates) || !isRecord(descriptor.scopeBindings)) throw new Error(`${label} evaluator requires scopeStates and scopeBindings records`)
|
|
99
|
+
for (const state of Object.values(descriptor.scopeStates)) if (!ids.has(state) && !rowTemplate(state)) throw new Error(`${label} references missing state ${JSON.stringify(state)}`)
|
|
100
|
+
seen.add(descriptor)
|
|
101
|
+
for (const nested of Object.values(descriptor.scopeBindings)) assertReactiveDescriptor(nested, `${label} nested binding`, ids, seen)
|
|
102
|
+
seen.delete(descriptor)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function assertHandlerDescriptor(descriptor, label, ids) {
|
|
106
|
+
if (!isRecord(descriptor) || !nonempty(descriptor.module) || !nonempty(descriptor.handler) || !isRecord(descriptor.states) || !isRecord(descriptor.scope)) throw new Error(`${label} must contain module, handler, states, and scope`)
|
|
107
|
+
for (const state of Object.values(descriptor.states)) if (!ids.has(state) && !rowTemplate(state)) throw new Error(`${label} references missing state ${JSON.stringify(state)}`)
|
|
108
|
+
for (const capture of Object.values(descriptor.scope)) assertCapture(capture, label, ids)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assertCapture(value, label, ids, seen = new Set()) {
|
|
112
|
+
if (value === null || typeof value === "string" || typeof value === "boolean" || typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0)) return
|
|
113
|
+
if (!isRecord(value) || seen.has(value)) throw new Error(`${label} has invalid capture`)
|
|
114
|
+
seen.add(value)
|
|
115
|
+
if (["undefined", "list-item", "list-index"].includes(value.type)) return seen.delete(value)
|
|
116
|
+
if (value.type === "number") {
|
|
117
|
+
if (!["NaN", "Infinity", "-Infinity", "-0"].includes(value.value)) throw new Error(`${label} capture number is invalid`)
|
|
118
|
+
} else if (["state", "setter"].includes(value.type)) {
|
|
119
|
+
if (!ids.has(value.id) && !rowTemplate(value.id)) throw new Error(`${label} capture ${value.type} references missing state ${JSON.stringify(value.id)}`)
|
|
120
|
+
} else if (value.type === "ref") {
|
|
121
|
+
if (!nonempty(value.id)) throw new Error(`${label} capture ref requires an ID`)
|
|
122
|
+
} else if (value.type === "array") {
|
|
123
|
+
if (!Array.isArray(value.value)) throw new Error(`${label} capture array requires an array value`)
|
|
124
|
+
for (const entry of value.value) assertCapture(entry, label, ids, seen)
|
|
125
|
+
} else if (value.type === "object") {
|
|
126
|
+
if (!Array.isArray(value.value)) throw new Error(`${label} capture object requires entries`)
|
|
127
|
+
const keys = new Set()
|
|
128
|
+
for (const entry of value.value) {
|
|
129
|
+
if (!Array.isArray(entry) || entry.length !== 2 || typeof entry[0] !== "string" || keys.has(entry[0])) throw new Error(`${label} capture object has invalid entries`)
|
|
130
|
+
keys.add(entry[0])
|
|
131
|
+
assertCapture(entry[1], label, ids, seen)
|
|
132
|
+
}
|
|
133
|
+
} else throw new Error(`${label} capture has unsupported type ${JSON.stringify(value.type)}`)
|
|
134
|
+
seen.delete(value)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function assertList(list, index, ids, stateIds, listIds, parentByChild) {
|
|
138
|
+
const label = `RouteIR list ${index}`
|
|
139
|
+
if (!isRecord(list) || !nonempty(list.id) || !nonempty(list.state) || typeof list.key !== "string" && list.key !== null || !Array.isArray(list.keys)) throw new Error(`${label} is invalid`)
|
|
140
|
+
if (listIds.has(list.id)) throw new Error(`RouteIR has duplicate list ID ${JSON.stringify(list.id)}`)
|
|
141
|
+
listIds.add(list.id)
|
|
142
|
+
if (!stateIds.has(list.state)) throw new Error(`${label} references missing state ${JSON.stringify(list.state)}`)
|
|
143
|
+
const keys = new Set()
|
|
144
|
+
for (const key of list.keys) {
|
|
145
|
+
if (typeof key !== "string" && !(typeof key === "number" && Number.isFinite(key))) throw new Error(`${label} has invalid key`)
|
|
146
|
+
const token = `${typeof key}:${key}`
|
|
147
|
+
if (keys.has(token)) throw new Error(`${label} has duplicate key ${JSON.stringify(key)}`)
|
|
148
|
+
keys.add(token)
|
|
149
|
+
}
|
|
150
|
+
for (const field of markerFields) if (list[field] !== undefined && list[field] !== true) throw new Error(`${label} ${field} must be true when present`)
|
|
151
|
+
for (const state of Object.values(list.selectorStates ?? {})) if (!ids.has(state)) throw new Error(`${label} selector references missing state ${JSON.stringify(state)}`)
|
|
152
|
+
for (const state of list.expressionStates ?? []) if (!ids.has(state) && !rowTemplate(state)) throw new Error(`${label} expression references missing state ${JSON.stringify(state)}`)
|
|
153
|
+
if (list.source) assertReactiveDescriptor(list.source, `${label} source`, ids)
|
|
154
|
+
for (const child of list.children ?? []) {
|
|
155
|
+
if (!isRecord(child) || !nonempty(child.id) || !nonempty(child.field) || typeof child.key !== "string" && child.key !== null || parentByChild.has(child.id)) throw new Error(`${label} has invalid or duplicate child ${JSON.stringify(child?.id)}`)
|
|
156
|
+
parentByChild.set(child.id, list.id)
|
|
157
|
+
}
|
|
158
|
+
for (const row of list.rowStates ?? []) if (!isRecord(row) || !rowTemplate(row.id) || !Object.hasOwn(row, "initialValue")) throw new Error(`${label} has invalid row state`)
|
|
159
|
+
for (const row of [...(list.rowRefs ?? []), ...(list.rowConditions ?? [])]) if (!rowTemplate(row)) throw new Error(`${label} has invalid row ownership ID`)
|
|
160
|
+
if (list.seed && list.valueSeed || list.seed !== undefined && !validSeed(list.seed) || list.valueSeed !== undefined && !validSeed(list.valueSeed)) throw new Error(`${label} has invalid seed`)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function validSeed(seed) {
|
|
164
|
+
return isRecord(seed) && Object.values(seed).every(value => ["string", "number", "boolean", "null"].includes(value))
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function assertJsonSafe(value, label = "Value") {
|
|
168
|
+
const invalid = invalidJsonPath(value, new Set(), "$")
|
|
169
|
+
if (invalid) throw new Error(`${label} is not JSON-safe at ${invalid}`)
|
|
170
|
+
return value
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function invalidJsonPath(value, seen, path) {
|
|
174
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return undefined
|
|
175
|
+
if (typeof value === "number") return Number.isFinite(value) && !Object.is(value, -0) ? undefined : path
|
|
176
|
+
if (!value || typeof value !== "object" || seen.has(value) || Object.getOwnPropertySymbols(value).length) return path
|
|
177
|
+
const prototype = Object.getPrototypeOf(value)
|
|
178
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) return path
|
|
179
|
+
const descriptors = Object.getOwnPropertyDescriptors(value)
|
|
180
|
+
if (Array.isArray(value) && (Object.keys(descriptors).some(key => key !== "length" && !/^(0|[1-9]\d*)$/.test(key)) || Object.keys(value).length !== value.length)) return path
|
|
181
|
+
seen.add(value)
|
|
182
|
+
for (const [key, descriptor] of Object.entries(descriptors)) {
|
|
183
|
+
if (Array.isArray(value) && key === "length") continue
|
|
184
|
+
if (!descriptor.enumerable || !("value" in descriptor)) return `${path}.${key}`
|
|
185
|
+
const invalid = invalidJsonPath(descriptor.value, seen, `${path}.${key}`)
|
|
186
|
+
if (invalid) return invalid
|
|
187
|
+
}
|
|
188
|
+
seen.delete(value)
|
|
189
|
+
return undefined
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const isRecord = value => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
193
|
+
const nonempty = value => typeof value === "string" && value.length > 0
|
|
194
|
+
const rowTemplate = value => nonempty(value) && value.includes("$k")
|
package/framework/core.d.ts
CHANGED
|
@@ -133,6 +133,7 @@ export type ListDescriptor = {
|
|
|
133
133
|
|
|
134
134
|
export interface RouteIR {
|
|
135
135
|
version: 1
|
|
136
|
+
route?: string
|
|
136
137
|
states: Array<{ slot: number; id: string; name: string; initialValue: unknown; lifetime?: "layout" | "route"; internal?: true }>
|
|
137
138
|
params: Array<{ name: string; id: string }>
|
|
138
139
|
searchParams: Array<{ name: string; id: string }>
|
package/framework/core.mjs
CHANGED
|
@@ -174,12 +174,13 @@ export function useEffect(callback, dependencies, module, handler, states, scope
|
|
|
174
174
|
if (!renderContext) throw new Error("useEffect() can only run while rendering a Kudzu component")
|
|
175
175
|
if (typeof callback !== "function" || !Array.isArray(dependencies) || !module || !handler) throw new Error("useEffect() must be compiled with a literal dependency array")
|
|
176
176
|
if (itemDependencies.length && !renderContext.listDepth) throw new Error(`${source} useEffect() item-property dependencies are only supported in direct keyed row components`)
|
|
177
|
+
const derivedSources = new Set(dependencyStates.map(([, dependency]) => dependency))
|
|
177
178
|
const dependencyIds = [...new Set(dependencies.map(dependency => {
|
|
178
|
-
if (!dependency?.[signalMarker] || !validEffectDependency(dependency.value)) throw new Error(`${source} useEffect() dependencies must be primitive Kudzu state or runtime parameter identifiers`)
|
|
179
|
+
if (!dependency?.[signalMarker] || !derivedSources.has(dependency) && !validEffectDependency(dependency.value)) throw new Error(`${source} useEffect() dependencies must be primitive Kudzu state or runtime parameter identifiers`)
|
|
179
180
|
return dependency.id
|
|
180
181
|
}))]
|
|
181
182
|
const dependencyStateIds = Object.fromEntries(dependencyStates.map(([name, dependency]) => {
|
|
182
|
-
if (!dependency?.[signalMarker]
|
|
183
|
+
if (!dependency?.[signalMarker]) throw new Error(`${source} useEffect() derived dependency state ${JSON.stringify(name)} must be Kudzu state`)
|
|
183
184
|
return [name, dependency.id]
|
|
184
185
|
}))
|
|
185
186
|
let owner
|
|
@@ -629,7 +630,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
629
630
|
}
|
|
630
631
|
}
|
|
631
632
|
if (node?.[conditionalMarker]) {
|
|
632
|
-
const descriptor = bindingDescriptor(node)
|
|
633
|
+
const descriptor = node.state ? { state: node.state } : bindingDescriptor(node)
|
|
633
634
|
const stateIds = reactiveStateIds(descriptor)
|
|
634
635
|
if (!stateIds.size) return renderNode(node.value ? node.truthy() : node.falsy(), namespace, selectValue)
|
|
635
636
|
if (namespace === "math") throw new Error("Reactive conditional DOM is not supported inside math")
|