@kudzujs/core 0.8.37 → 0.8.39
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 +50 -0
- package/README.md +1 -1
- package/RELEASES.md +64 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/compiler-current-architecture.md +12 -9
- package/docs/next-architecture/large-application-ai-native-roadmap.md +7 -1
- package/docs/next-architecture/versioning.md +3 -1
- package/framework/README.md +1 -1
- package/framework/build.mjs +48 -43
- package/framework/compiler/route-build-record.mjs +92 -0
- package/framework/compiler/route-capability-planner.mjs +25 -23
- package/framework/compiler/route-ir.mjs +194 -0
- package/framework/core.d.ts +2 -1
- package/framework/core.mjs +29 -12
- 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.39` 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,56 @@
|
|
|
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
|
+
## P0.12 Deep RouteIR And CapabilityIR Validation
|
|
6
|
+
|
|
7
|
+
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:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
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
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
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.
|
|
14
|
+
|
|
15
|
+
| Target | Clean build median | Range | Peak RSS median | Deploy bytes |
|
|
16
|
+
|---|---:|---:|---:|---:|
|
|
17
|
+
| `v0.8.38` | 2,356.477 ms | 2,159.934-2,389.644 ms | 352.7 MiB | 3,840,694 B |
|
|
18
|
+
| P0.12 candidate | 2,352.382 ms | 2,193.576-2,527.911 ms | 356.8 MiB | 3,840,694 B |
|
|
19
|
+
|
|
20
|
+
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.
|
|
21
|
+
|
|
22
|
+
```text
|
|
23
|
+
v0.8.38: [2159.934,2223.405,2313.086,2378.192,2356.477,2380.726,2389.644]
|
|
24
|
+
candidate: [2193.576,2223.644,2317.685,2410.142,2352.382,2394.981,2527.911]
|
|
25
|
+
paired candidate-baseline: [33.642,0.239,4.599,31.950,-4.095,14.255,138.267]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
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.
|
|
29
|
+
|
|
30
|
+
## P0.11 Structural Route Artifact Graph
|
|
31
|
+
|
|
32
|
+
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:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
sha256sum framework/build.mjs framework/compiler/route-build-record.mjs framework/compiler/route-capability-planner.mjs framework/core.mjs framework/core.d.ts test/compiler-passes.test.mjs | sha256sum
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The repository's 152-page build used one warm-up and seven alternating clean fresh-process samples. Every sample produced the same 173-file deploy graph, 3,835,970 raw bytes, 1,979,875 aggregate gzip bytes, and SHA-256 `c1f95f25d43589b61d1abe7ecf256e5b5e9dddd5647fbcf6368c08cc5ae3ee20`. Baseline and candidate `kudzu-plan.json` files are byte-identical.
|
|
39
|
+
|
|
40
|
+
| Target | Clean build median | Range | Peak RSS median | Deploy bytes |
|
|
41
|
+
|---|---:|---:|---:|---:|
|
|
42
|
+
| `v0.8.37` | 1,820.186 ms | 1,769.592-1,910.670 ms | 351.6 MiB | 3,835,970 B |
|
|
43
|
+
| P0.11 candidate | 1,804.568 ms | 1,756.849-1,843.941 ms | 351.4 MiB | 3,835,970 B |
|
|
44
|
+
|
|
45
|
+
The candidate's unpaired median is 0.86% lower. Round-paired candidate-minus-baseline differences had a -20.933 ms median, with the candidate faster in five of seven pairs and the baseline faster in two. Timing ranges overlap, so no material improvement is claimed; peak-RSS medians differ by 0.3 MiB.
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
v0.8.37: [1786.548,1834.792,1910.670,1853.493,1820.186,1769.592,1778.201]
|
|
49
|
+
candidate: [1756.849,1804.568,1843.941,1832.560,1811.390,1798.756,1782.132]
|
|
50
|
+
paired candidate-baseline: [-29.698,-30.225,-66.729,-20.933,-8.796,29.164,3.930]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
RouteBuildRecord now owns each rendered route's RouteIR, capabilities, entry paths, styles, and exact handler/effect edges. Handler, Worker, package-client, and chunk closure starts from those structural edges instead of serialized HTML/plan searches or formatted effect keys. This changes build-scratch orchestration only; browser runtime source, RouteIR v1, CapabilityIR v1, emitted files, and deploy bytes remain unchanged.
|
|
54
|
+
|
|
5
55
|
## P0.10 Structural ModuleIR References
|
|
6
56
|
|
|
7
57
|
Measured UTC 2026-08-11 on an Apple M4 macOS arm64 host with 10 logical CPUs, 16 GiB RAM, Node 24.14.0, and npm 11.9.0. The baseline was clean tag `v0.8.36` at `268cd9023c9f47a912601f298963a9ffe9c00da2`. The compiler and focused-check patch had SHA-256 `f684c79027b290bc8c7d549667ef0d7f21a9d0057b5d6291fcce18b91947eff2`, 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.39 - Fail-closed route contracts.** RouteIR, RouteBuildRecord, and CapabilityIR now reject invalid state, command, effect, binding, condition, list, ownership, and capability references before browser code generation. Read the [release notes](./RELEASES.md#0839---fail-closed-route-contracts), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.39), 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,69 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.8.39 - Fail-closed route contracts
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.39 completes P0.12 by deeply validating the concrete RouteIR, RouteBuildRecord, and CapabilityIR contracts before artifact selection and browser code generation.
|
|
6
|
+
|
|
7
|
+
### Changed in 0.8.39
|
|
8
|
+
|
|
9
|
+
- RouteIR v1 rejects duplicate state/parameter IDs, missing command targets, unsupported command operations, malformed captures, and invalid effect dependencies.
|
|
10
|
+
- Direct and evaluator bindings validate recursively, including nested scope bindings and every referenced state.
|
|
11
|
+
- Conditions validate identity, kinds, reactive sources, and owned states.
|
|
12
|
+
- Keyed lists validate state/key identity, row ownership templates, child reciprocity, duplicate parents, and ownership cycles.
|
|
13
|
+
- RouteBuildRecord verifies behavior, binding, list, effect, native, and parameter capability reciprocity before artifact planning.
|
|
14
|
+
- CapabilityIR validates standalone event/count/flag implications and exact projection equality from its validated route records before codegen.
|
|
15
|
+
- Strict JSON-safe checks reject cyclic, lossy, nonfinite, BigInt, sparse, accessor, symbol, and non-plain contract data with a deterministic field path.
|
|
16
|
+
|
|
17
|
+
### Performance
|
|
18
|
+
|
|
19
|
+
- 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.
|
|
20
|
+
- Peak RSS medians were 352.7 MiB and 356.8 MiB. Repeated validation of the same immutable contracts is eliminated by identity caching.
|
|
21
|
+
- 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`.
|
|
22
|
+
|
|
23
|
+
### Validation
|
|
24
|
+
|
|
25
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 203 tests and 154 generated pages.
|
|
26
|
+
- Focused checks cover duplicate/missing identity, malformed commands, effects, captures, bindings, conditions, lists, ownership, capabilities, versions, and JSON safety.
|
|
27
|
+
- Existing static, command, binding, keyed, effect, Worker, package-import, navigation, reducer, and migration fixtures retain browser behavior and zero-unused-runtime checks.
|
|
28
|
+
- The next migration-backed investigation is property-level derived dependencies over ordinary object state.
|
|
29
|
+
|
|
30
|
+
### Upgrade
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install @kudzujs/core@^0.8.39
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## 0.8.38 - Structural route artifacts
|
|
37
|
+
|
|
38
|
+
Kudzu 0.8.38 completes P0.11 by replacing serialized route-output searches and parallel route facts with one validated RouteBuildRecord artifact graph.
|
|
39
|
+
|
|
40
|
+
### Changed in 0.8.38
|
|
41
|
+
|
|
42
|
+
- Each emitted route records its RouteIR, capability facts, route-entry paths, styles, and exact handler/effect references.
|
|
43
|
+
- Final event, effect, binding, conditional, and keyed-list descriptors retain handler references only when they are rendered.
|
|
44
|
+
- Handler modules resolve by exact module URL, and Worker references resolve by structural module/handler pairs instead of formatted keys.
|
|
45
|
+
- CSS output, package-client compilation, and bundle entry/chunk closure derive from retained route edges.
|
|
46
|
+
- CapabilityIR now folds RouteBuildRecord values directly; serialized HTML/plan `includes()` searches and parallel route capability/entry arrays are removed.
|
|
47
|
+
|
|
48
|
+
### Performance
|
|
49
|
+
|
|
50
|
+
- Seven alternating clean 152-page builds measured `v0.8.37` and candidate medians of 1,820.186 ms and 1,804.568 ms. The -20.933 ms paired median and overlapping ranges establish no material speedup or regression.
|
|
51
|
+
- Peak RSS medians were 351.6 MiB and 351.4 MiB.
|
|
52
|
+
- Both targets emit the same 173 files, 3,835,970 raw bytes, 1,979,875 aggregate gzip bytes, deploy digest, and byte-identical `kudzu-plan.json`.
|
|
53
|
+
|
|
54
|
+
### Validation
|
|
55
|
+
|
|
56
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 200 tests and 153 generated pages.
|
|
57
|
+
- Focused checks reject unsupported versions, malformed and duplicate references, missing handler modules, inconsistent route entries, and invalid JSON round trips.
|
|
58
|
+
- Existing static, command, binding, keyed, effect, Worker, package-import, navigation, and migration fixtures retain their browser behavior and zero-unused-runtime checks.
|
|
59
|
+
- P0.12 deep RouteIR and CapabilityIR validation is next.
|
|
60
|
+
|
|
61
|
+
### Upgrade
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install @kudzujs/core@^0.8.38
|
|
65
|
+
```
|
|
66
|
+
|
|
3
67
|
## 0.8.37 - Structural ModuleIR references
|
|
4
68
|
|
|
5
69
|
Kudzu 0.8.37 completes P0.10 by replacing mixed state names, export strings, formatted owner keys, and partial slots with one validated structural reference 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.39` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.12 deep RouteIR and CapabilityIR validation is complete; the next migration-backed investigation is property-level derived dependencies over ordinary object state. 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.39` 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
|
|
|
@@ -9,7 +9,7 @@ This maps the current `0.8.37` architecture, built on the completed `0.8.23` Goa
|
|
|
9
9
|
| CLI entry | [`bin/kudzu.mjs`](../../bin/kudzu.mjs) | Dispatches build and development commands. |
|
|
10
10
|
| Project session | [`framework/compiler/project-session.mjs`](../../framework/compiler/project-session.mjs), `createProjectSession()` | Owns one absolute root, standard project paths, source records, bound graph operations, and Worker compiler for a build. Omitted roots resolve from call-time CWD. |
|
|
11
11
|
| Parsed module cache and symbols | [`framework/compiler/project-session.mjs`](../../framework/compiler/project-session.mjs) | Parses each unchanged source module once per ProjectSession and records source-local declaration/import/re-export sites. Stable ModuleSymbol records resolve direct, aliased, barrel, and `export *` exports with cycle and ambiguity checks; repeated resolutions are cached against their source dependencies, and normalization consumers locate the resolved SiteId in a fresh clone with independent parent links. |
|
|
12
|
-
| Build orchestration | [`framework/build.mjs`](../../framework/build.mjs), `build()` | Coordinates config, discovery, source compilation,
|
|
12
|
+
| Build orchestration | [`framework/build.mjs`](../../framework/build.mjs), `build()` | Coordinates config, discovery, source compilation, RouteBuildRecord collection, CapabilityIR projection, generator invocation, artifact emission, and `afterBuild`. |
|
|
13
13
|
| Reachability/import resolution | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `reachableSourceFiles()`; [`framework/compiler/source-graph.mjs`](../../framework/compiler/source-graph.mjs), `ordinaryRuntimeDependencies()`, `resolveSourceImport()` | Starts from page entries, follows relative runtime imports/re-exports and validated Worker references, excludes unreachable migration source, and fails unresolved ordinary edges or dynamic imports at the importer source location before code generation. |
|
|
14
14
|
| Ordered normalization | [`framework/compiler/normalization-pipeline.mjs`](../../framework/compiler/normalization-pipeline.mjs), `applyNormalizationPasses()`; [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `normalizeCompilerSource()` | Applies migration/resource passes in order and repairs TypeScript parent pointers after every structural change. Imported source uses the same pipeline. |
|
|
15
15
|
| 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. |
|
|
@@ -19,6 +19,8 @@ This maps the current `0.8.37` architecture, built on the completed `0.8.23` Goa
|
|
|
19
19
|
| Main semantic analysis | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `createKudzuTransformer()` | Produces transformed source plus explicit component, handler, binding, derived, keyed, and effect ownership results. |
|
|
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
|
+
| 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. |
|
|
22
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. |
|
|
23
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. |
|
|
24
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,11 +28,11 @@ This maps the current `0.8.37` architecture, built on the completed `0.8.23` Goa
|
|
|
26
28
|
| Effect analysis | [`framework/compiler/effect-analysis.mjs`](../../framework/compiler/effect-analysis.mjs) | Classifies ordered signal, derived, and keyed-item dependencies and validates cleanup-owned browser resources before EffectIR registration. |
|
|
27
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. |
|
|
28
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. |
|
|
29
|
-
| 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. |
|
|
30
|
-
| Route capability projection | [`framework/compiler/route-capability-planner.mjs`](../../framework/compiler/route-capability-planner.mjs), `planRouteCapabilities()` | Validates RouteIR v1
|
|
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. |
|
|
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. |
|
|
31
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. |
|
|
32
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. |
|
|
33
|
-
| Artifact emission | `framework/build.mjs` | Selects
|
|
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. |
|
|
34
36
|
| Browser capabilities | [`framework/*.js`](../../framework/) | Small optional modules for commands, bindings, lists, effects, native handlers, serialization, parameters, and navigation; native contexts invalidate writes and refs at DOM ownership release, with no component runtime. |
|
|
35
37
|
| Opt-in navigation | [`framework/navigation-runtime.js`](../../framework/navigation-runtime.js) plus `framework/build.mjs` navigation configuration/emission | Fetches and validates complete same-origin documents, replaces only the marked route range, manages route/layout disposal, history, focus, finite prefetch retention, and native fallback. |
|
|
36
38
|
| Development serving | [`framework/dev-server.mjs`](../../framework/dev-server.mjs) and [`framework/dev-state.js`](../../framework/dev-state.js) | Rebuild/watch/SSE and response-only short-lived state restoration; failed rebuilds show the existing error overlay while preserving the previous on-disk output. |
|
|
@@ -50,10 +52,11 @@ src/pages entries + config
|
|
|
50
52
|
-> build orchestration writes .kudzu executable and handler modules
|
|
51
53
|
-> import page modules and execute renderPage()
|
|
52
54
|
-> complete HTML
|
|
53
|
-
-> RouteIR v1
|
|
54
|
-
|
|
55
|
+
-> RouteIR v1 and exact handler references
|
|
56
|
+
-> validated RouteBuildRecord per emitted route
|
|
57
|
+
-> capability facts, route entries, styles, handler/effect artifact edges
|
|
55
58
|
-> remove unrendered handlers/effects/Workers
|
|
56
|
-
-> planRouteCapabilities(
|
|
59
|
+
-> planRouteCapabilities(RouteBuildRecord[])
|
|
57
60
|
-> CapabilityIR v1
|
|
58
61
|
-> specialize and emit only selected runtime/capability ESM
|
|
59
62
|
-> reuse exact generated route-entry transforms within this build
|
|
@@ -69,7 +72,7 @@ The browser consumes static HTML first. State seeds and descriptors in that HTML
|
|
|
69
72
|
|
|
70
73
|
- `createKudzuTransformer()` remains one large source-local analysis unit combining validation, specialization, descriptor registration, and transformed-source emission.
|
|
71
74
|
- Transient component rewrite indexes remain source-local AST indexes; finalized handler, binding, derived, keyed, effect, signal, import, and component ownership edges use explicit JSON-safe slots or stable symbols.
|
|
72
|
-
- `build()` still owns
|
|
75
|
+
- `build()` still owns filesystem writes after structural route artifact selection and generator results are produced.
|
|
73
76
|
- Runtime generators intentionally specialize readable authored sources through exact anchors; every required anchor fails closed, but a future generator format may remove this transitional dependency.
|
|
74
77
|
- Source reachability and source compilation remain in one session-bound compiler factory because both consume the same normalization and import graph contracts. ModuleSymbol resolution is stable across canonical and cloned trees; unsupported export syntax remains intentionally narrow.
|
|
75
78
|
- Imported declarations resolve by ModuleSymbol and source-local SiteId. Specialized and compiler-synthesized trees still use conservative name/scope fallback where the source-local binding index does not own the complete AST.
|
|
@@ -139,6 +139,8 @@ This is an incremental evolution of the current repository:
|
|
|
139
139
|
- [x] P0.8 Stable ModuleSymbol and SiteId is complete in `0.8.35`. ProjectSession records source-local declaration, import, and re-export sites and resolves stable ModuleSymbol records through default/named exports, aliases, barrel chains, `export *`, ambiguity, and cycles. Cross-module compiler consumers locate resolved declarations by SiteId in their private normalized clones, while component calls, hooks, keyed blocks, effects, and ownership records expose deterministic source-local SiteIds. Repeated sessions and compilations preserve IDs, and symbol-only barrel traversal avoids cloning intermediate modules without changing browser artifacts or source syntax.
|
|
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
|
+
- [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.
|
|
142
144
|
|
|
143
145
|
### P0: Semantic Correctness And Compiler Foundation
|
|
144
146
|
|
|
@@ -361,12 +363,16 @@ function increment(value) { setCount(value + 1) }; increment(count)
|
|
|
361
363
|
|
|
362
364
|
**Done condition:** handler, effect, Worker, CSS, package, and chunk reachability derives from explicit route edges; serialized `includes()` searches and parallel route-fact maps are removed.
|
|
363
365
|
|
|
366
|
+
**Completed in `0.8.38`:** `renderPage()` retains exact handler references only when final event, effect, binding, conditional, or keyed-list descriptors are emitted. RouteBuildRecord validates those references with RouteIR, capability facts, route-entry paths, and styles before `build.mjs` projects runtime entries and artifact closure. Handler modules resolve by exact URL, Worker references resolve by structural module/handler pairs, and package-client/chunk roots derive from retained modules. The clean 152-page baseline and candidate builds produce byte-identical `kudzu-plan.json`, 173 deploy files, raw/gzip bytes, and deploy digest with no material build-time regression.
|
|
367
|
+
|
|
364
368
|
### PR 12: Deep RouteIR And CapabilityIR Validation
|
|
365
369
|
|
|
366
370
|
**Objective:** fail before codegen for invalid concrete route references and capability projections.
|
|
367
371
|
|
|
368
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.
|
|
369
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
|
+
|
|
370
376
|
## Large Application Capability Order
|
|
371
377
|
|
|
372
378
|
After the relevant P0 foundations, investigate capabilities in this order:
|
|
@@ -474,4 +480,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
474
480
|
|
|
475
481
|
## Immediate Decision
|
|
476
482
|
|
|
477
|
-
PR 1 through PR
|
|
483
|
+
PR 1 through PR 12 are complete. The next migration-backed investigation is **property-level derived dependencies over ordinary object state**. 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.39` 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
|
|
|
@@ -28,6 +28,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
28
28
|
| `0.8.35` | Resolve cross-module declarations through stable ModuleSymbol and source-local SiteId records. | Default/named exports, aliases, barrel chains, `export *`, cycles, ambiguity, repeated compilation, source invalidation, private clones, and measured build behavior remain deterministic. |
|
|
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
|
+
| `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. |
|
|
31
33
|
|
|
32
34
|
## Sequence Rules
|
|
33
35
|
|
package/framework/README.md
CHANGED
|
@@ -68,7 +68,7 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
|
|
|
68
68
|
- `dev-state.js`: dev-only, short-lived logical-state snapshot validation and restoration.
|
|
69
69
|
- `*.d.ts`: public TypeScript and JSX declarations.
|
|
70
70
|
|
|
71
|
-
Compiler ownership follows explicit stages. `build.mjs` coordinates project discovery, source compilation, RouteIR rendering, CapabilityIR planning, generator calls, and artifact emission. `compiler/normalization-pipeline.mjs` owns pass order and parent repair. The main transformer analyzes normalized source while `compiler/descriptor-session.mjs` owns one per-source semantic artifact containing HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, EffectIR, and client imports. `core.mjs` emits complete HTML and
|
|
71
|
+
Compiler ownership follows explicit stages. `build.mjs` coordinates project discovery, source compilation, RouteIR rendering, RouteBuildRecord collection, CapabilityIR planning, generator calls, and artifact emission. `compiler/normalization-pipeline.mjs` owns pass order and parent repair. The main transformer analyzes normalized source while `compiler/descriptor-session.mjs` owns one per-source semantic artifact containing HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, EffectIR, and client imports. `core.mjs` emits complete HTML, RouteIR v1, and exact handler references: route-local state `slot` is an internal array reference, `id` remains the browser/DOM identity, and `name` remains readable development metadata. `compiler/route-build-record.mjs` validates structural artifact edges, and `compiler/route-capability-planner.mjs` projects CapabilityIR v1 from those records. Focused codegen modules consume IR/descriptors and return route-specific source without source analysis or filesystem ownership. The completed architecture record lives in `docs/next-architecture`; it adds no runtime or accepted syntax by itself.
|
|
72
72
|
|
|
73
73
|
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.
|
|
74
74
|
|
package/framework/build.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
|
|
|
7
7
|
import { generateListRuntime } from "./compiler/list-runtime-codegen.mjs"
|
|
8
8
|
import { assetPath, browserPath, relativeModulePath, withBase } from "./compiler/path-helpers.mjs"
|
|
9
9
|
import { createProjectSession } from "./compiler/project-session.mjs"
|
|
10
|
+
import { createRouteBuildRecord, planRouteArtifacts } from "./compiler/route-build-record.mjs"
|
|
10
11
|
import { createSourceCompiler } from "./compiler/source-compiler.mjs"
|
|
11
12
|
import { createParamCodegen } from "./compiler/param-codegen.mjs"
|
|
12
13
|
import { planRouteCapabilities, usesRouteDependencyRuntime } from "./compiler/route-capability-planner.mjs"
|
|
@@ -113,18 +114,12 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
113
114
|
return effect.workers.map(worker => ({ ...worker, module: assetPath(base, `assets/${result.handlerModule.path}`), handler: handler.exportName }))
|
|
114
115
|
}))
|
|
115
116
|
|
|
116
|
-
const
|
|
117
|
-
const routeCapabilities = new Map()
|
|
118
|
-
const pageEntries = []
|
|
119
|
-
const effectEntries = []
|
|
120
|
-
const nativeEntries = []
|
|
121
|
-
const paramEntries = []
|
|
117
|
+
const routeRecords = []
|
|
122
118
|
const routeEntryTransforms = new Map()
|
|
123
119
|
const rewrites = []
|
|
124
120
|
const emittedRoutes = new Set()
|
|
125
121
|
const emittedApplicationRoutes = new Set()
|
|
126
122
|
const emittedNavigationRecords = []
|
|
127
|
-
const renderedHandlerUrls = new Set()
|
|
128
123
|
const styleUrls = [...new Set([
|
|
129
124
|
...cssFiles.map(file => assetPath(base, `assets/${relative(sourceDirectory, file).replaceAll(sep, "/")}`)),
|
|
130
125
|
...configuredStyles.urls
|
|
@@ -190,30 +185,37 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
190
185
|
runtimeParams: runtimeSchema?.params,
|
|
191
186
|
...(navigable ? { navigationAsset: navigationGroup.assetPath, applicationId: navigationGroup.applicationId, layoutId: navigationGroup.layoutId, routeId: applicationRoute } : {})
|
|
192
187
|
}, props, module.layout)
|
|
193
|
-
const renderedOutput = `${JSON.stringify(result.plan)}\n${result.html}`
|
|
194
|
-
for (const url of result.handlerModules) if (renderedOutput.includes(JSON.stringify(url))) renderedHandlerUrls.add(url)
|
|
195
188
|
if (navigationGroup) {
|
|
196
189
|
navigationGroup.hasEffects ||= result.hasEffects
|
|
197
190
|
navigationGroup.hasParams ||= result.hasParams
|
|
198
191
|
}
|
|
199
192
|
const usesDependencyRuntime = usesRouteDependencyRuntime({ plan: result.plan, navigable, hasBindings: result.hasBindings, hasLists: result.hasLists })
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
193
|
+
const plan = { route: routePath, ...result.plan }
|
|
194
|
+
routeRecords.push(createRouteBuildRecord({
|
|
195
|
+
route: routePath,
|
|
196
|
+
output: route,
|
|
197
|
+
html: result.html,
|
|
198
|
+
plan,
|
|
199
|
+
handlerReferences: result.handlerReferences,
|
|
200
|
+
styles: styleUrls,
|
|
201
|
+
capabilities: {
|
|
202
|
+
navigable,
|
|
203
|
+
usesDependencyRuntime,
|
|
204
|
+
hasBehaviors: result.hasBehaviors,
|
|
205
|
+
hasBindings: result.hasBindings,
|
|
206
|
+
hasLists: result.hasLists,
|
|
207
|
+
hasListStyles: result.hasListStyles,
|
|
208
|
+
hasStateSeed: result.hasStateSeed,
|
|
209
|
+
hasParams: result.hasParams,
|
|
210
|
+
hasEffects: result.hasEffects
|
|
211
|
+
},
|
|
212
|
+
entries: {
|
|
213
|
+
...(result.hasParams ? { param: paramPath } : {}),
|
|
214
|
+
...(result.hasEffects ? { effect: effectPath } : {}),
|
|
215
|
+
...(result.plan.events.some(event => event.native) ? { native: nativePath } : {})
|
|
216
|
+
},
|
|
217
|
+
runtimeSchema
|
|
218
|
+
}))
|
|
217
219
|
}
|
|
218
220
|
}
|
|
219
221
|
|
|
@@ -227,9 +229,8 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
227
229
|
|
|
228
230
|
const assetsDirectory = join(outputDirectory, "assets")
|
|
229
231
|
await mkdir(assetsDirectory, { recursive: true })
|
|
230
|
-
const emittedHandlerModules = handlerModules
|
|
231
|
-
const
|
|
232
|
-
const renderedWorkerReferences = workerReferences.filter(reference => renderedEffects.has(`${reference.module}:${reference.handler}`))
|
|
232
|
+
const { handlerModules: emittedHandlerModules, workerReferences: renderedWorkerReferences, styles: renderedStyles } = planRouteArtifacts(routeRecords, handlerModules, workerReferences, module => assetPath(base, `assets/${module.path}`))
|
|
233
|
+
const renderedStyleUrls = new Set(renderedStyles)
|
|
233
234
|
if (renderedWorkerReferences.length && await exists(join(publicDirectory, "assets", "workers"))) throw new Error("public/assets/workers collides with Kudzu's generated Worker asset namespace")
|
|
234
235
|
const workerAssets = await project.workerCompiler.emit(renderedWorkerReferences, sourceFileSet, assetsDirectory, base, minify)
|
|
235
236
|
for (const module of emittedHandlerModules) {
|
|
@@ -240,7 +241,8 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
240
241
|
}
|
|
241
242
|
if (module.code.includes("/__kudzu_worker_")) throw new Error(`Worker URL placeholder survived in ${module.path}`)
|
|
242
243
|
}
|
|
243
|
-
const
|
|
244
|
+
const plans = routeRecords.map(record => record.plan)
|
|
245
|
+
const capabilityIR = planRouteCapabilities(routeRecords, { navigationRouteCount: navigationRoutes.length })
|
|
244
246
|
const {
|
|
245
247
|
routes: { behaviors: behaviorCount, regularBehaviors: regularBehaviorCount, dependencyStateSeeds: dependencyStateSeedCount },
|
|
246
248
|
events: { command: commandEvents, hasNativeHandlers },
|
|
@@ -251,11 +253,11 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
251
253
|
runtime: { shared: hasSharedRuntime, dependency: hasDependencyRuntime }
|
|
252
254
|
} = capabilityIR
|
|
253
255
|
const runtimeName = usesDependencyRuntime => usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
|
|
254
|
-
for (let offset = 0; offset <
|
|
255
|
-
await Promise.all(
|
|
256
|
-
const routeDirectory = join(outputDirectory,
|
|
256
|
+
for (let offset = 0; offset < routeRecords.length; offset += 64) {
|
|
257
|
+
await Promise.all(routeRecords.slice(offset, offset + 64).map(async record => {
|
|
258
|
+
const routeDirectory = join(outputDirectory, record.output)
|
|
257
259
|
await mkdir(routeDirectory, { recursive: true })
|
|
258
|
-
const html = preloadModules(
|
|
260
|
+
const html = preloadModules(record.html.replace(runtimePlaceholder, escapeAttribute(assetPath(base, `assets/${runtimeName(record.capabilities.usesDependencyRuntime)}`))))
|
|
259
261
|
await writeFile(join(routeDirectory, "index.html"), html)
|
|
260
262
|
}))
|
|
261
263
|
}
|
|
@@ -291,7 +293,10 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
291
293
|
if (hasNativeHandlers) {
|
|
292
294
|
const generated = generateNativeRuntime(await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"), capabilityIR)
|
|
293
295
|
await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), generated.source, minify, generated.define)
|
|
294
|
-
for (const
|
|
296
|
+
for (const record of routeRecords) if (record.entries.native) await printNativeEntry({
|
|
297
|
+
path: record.entries.native,
|
|
298
|
+
modules: [...new Set(record.plan.events.filter(event => event.native).map(event => event.native.module))]
|
|
299
|
+
}, assetsDirectory, base, minify, routeEntryTransforms)
|
|
295
300
|
}
|
|
296
301
|
if (navigationGroups.length) {
|
|
297
302
|
const navigationSource = await readFile(new URL("./navigation-runtime.js", import.meta.url), "utf8")
|
|
@@ -304,15 +309,15 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
304
309
|
await mkdir(resolve(output, ".."), { recursive: true })
|
|
305
310
|
await writeJavaScript(output, handlerModule.code, minify)
|
|
306
311
|
}
|
|
307
|
-
for (const
|
|
308
|
-
const output = join(assetsDirectory,
|
|
312
|
+
for (const record of routeRecords) if (record.entries.param) {
|
|
313
|
+
const output = join(assetsDirectory, record.entries.param)
|
|
309
314
|
await mkdir(dirname(output), { recursive: true })
|
|
310
|
-
await writeRouteEntry(output, printParamEntry(
|
|
315
|
+
await writeRouteEntry(output, printParamEntry(record.runtimeSchema, record.plan.params, record.plan.searchParams, record.plan.searchParamsWritable, output, assetsDirectory, base, runtimeName(record.capabilities.usesDependencyRuntime), record.capabilities.navigable), minify, routeEntryTransforms)
|
|
311
316
|
}
|
|
312
|
-
for (const
|
|
313
|
-
const output = join(assetsDirectory,
|
|
317
|
+
for (const record of routeRecords) if (record.entries.effect) {
|
|
318
|
+
const output = join(assetsDirectory, record.entries.effect)
|
|
314
319
|
await mkdir(dirname(output), { recursive: true })
|
|
315
|
-
await writeRouteEntry(output, printEffectEntry(
|
|
320
|
+
await writeRouteEntry(output, printEffectEntry(runtimeEffects(record.plan.effects, record.capabilities.navigable), output, emittedHandlerModules, assetsDirectory, base, record.entries.param, runtimeName(record.capabilities.usesDependencyRuntime), record.capabilities.navigable), minify, routeEntryTransforms)
|
|
316
321
|
}
|
|
317
322
|
const clientModules = await collectClientModules(emittedHandlerModules.flatMap(module => module.clientImports).map(file => resolve(root, file)), sourceFileSet)
|
|
318
323
|
for (const file of clientModules) {
|
|
@@ -342,7 +347,7 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
342
347
|
}
|
|
343
348
|
const sortedRewrites = rewrites.sort((left, right) => runtimeSpecificity(right) - runtimeSpecificity(left) || left.pattern.localeCompare(right.pattern))
|
|
344
349
|
await writeFile(join(workDirectory, "kudzu-plan.json"), JSON.stringify({ routes: plans, rewrites: sortedRewrites }, null, 2))
|
|
345
|
-
for (const file of cssFiles) {
|
|
350
|
+
for (const file of cssFiles.filter(file => renderedStyleUrls.has(assetPath(base, `assets/${relative(sourceDirectory, file).replaceAll(sep, "/")}`)))) {
|
|
346
351
|
const output = join(assetsDirectory, relative(sourceDirectory, file))
|
|
347
352
|
await mkdir(dirname(output), { recursive: true })
|
|
348
353
|
await writeFile(output, cssOutputs.get(file))
|
|
@@ -353,7 +358,7 @@ async function buildInto(project, outputDirectory, { minify }) {
|
|
|
353
358
|
await mkdir(dirname(output), { recursive: true })
|
|
354
359
|
await writeFile(output, await readFile(file))
|
|
355
360
|
}
|
|
356
|
-
for (const style of configuredStyles.sources) {
|
|
361
|
+
for (const style of configuredStyles.sources.filter(style => renderedStyleUrls.has(withBase(base, style.output)))) {
|
|
357
362
|
let css = await readFile(style.source, "utf8")
|
|
358
363
|
if (style.transform) {
|
|
359
364
|
const result = await style.transform(css, { source: style.source, output: style.output })
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { assertJsonSafe, assertRouteIR } from "./route-ir.mjs"
|
|
2
|
+
|
|
3
|
+
const validated = new WeakSet()
|
|
4
|
+
|
|
5
|
+
export function createRouteBuildRecord(input) {
|
|
6
|
+
const record = {
|
|
7
|
+
version: input.version ?? 1,
|
|
8
|
+
route: input.route,
|
|
9
|
+
output: input.output,
|
|
10
|
+
html: input.html,
|
|
11
|
+
plan: input.plan,
|
|
12
|
+
capabilities: input.capabilities,
|
|
13
|
+
artifacts: {
|
|
14
|
+
handlers: input.handlerReferences ?? [],
|
|
15
|
+
effects: (input.plan?.effects ?? []).map(({ module, handler }) => ({ module, handler })),
|
|
16
|
+
styles: [...new Set(input.styles ?? [])]
|
|
17
|
+
},
|
|
18
|
+
entries: input.entries ?? {},
|
|
19
|
+
...(input.runtimeSchema ? { runtimeSchema: input.runtimeSchema } : {})
|
|
20
|
+
}
|
|
21
|
+
return assertRouteBuildRecord(record)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function assertRouteBuildRecord(record) {
|
|
25
|
+
if (validated.has(record)) return record
|
|
26
|
+
if (record?.version !== 1) throw new Error(`Unsupported RouteBuildRecord version: ${JSON.stringify(record?.version)}`)
|
|
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 })
|
|
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)}`)
|
|
30
|
+
const capabilityNames = ["navigable", "usesDependencyRuntime", "hasBehaviors", "hasBindings", "hasLists", "hasListStyles", "hasStateSeed", "hasParams", "hasEffects"]
|
|
31
|
+
if (!isRecord(record.capabilities) || !capabilityNames.every(name => typeof record.capabilities[name] === "boolean")) throw new Error("Invalid RouteBuildRecord v1 capabilities")
|
|
32
|
+
if (!isRecord(record.artifacts) || !Array.isArray(record.artifacts.handlers) || !Array.isArray(record.artifacts.effects) || !Array.isArray(record.artifacts.styles) || !isRecord(record.entries)) throw new Error("Invalid RouteBuildRecord v1 artifacts")
|
|
33
|
+
const handlers = new Set()
|
|
34
|
+
for (const reference of record.artifacts.handlers) {
|
|
35
|
+
assertHandlerReference(reference)
|
|
36
|
+
const key = referenceKey(reference)
|
|
37
|
+
if (handlers.has(key)) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has duplicate handler reference ${key}`)
|
|
38
|
+
handlers.add(key)
|
|
39
|
+
}
|
|
40
|
+
for (const effect of record.artifacts.effects) {
|
|
41
|
+
assertHandlerReference(effect)
|
|
42
|
+
if (!handlers.has(referenceKey(effect))) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} effect references an unretained handler ${referenceKey(effect)}`)
|
|
43
|
+
}
|
|
44
|
+
for (const event of record.plan.events ?? []) if (event.native && !handlers.has(referenceKey(event.native))) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} event references an unretained handler ${referenceKey(event.native)}`)
|
|
45
|
+
for (const descriptor of [...(record.plan.bindings ?? []), ...(record.plan.conditions ?? []), ...(record.plan.lists ?? []).map(list => list.source).filter(Boolean)]) {
|
|
46
|
+
if (descriptor.module && !handlers.has(referenceKey(descriptor))) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} descriptor references an unretained handler ${referenceKey(descriptor)}`)
|
|
47
|
+
}
|
|
48
|
+
if (record.artifacts.styles.some(style => typeof style !== "string")) throw new Error("RouteBuildRecord styles must be strings")
|
|
49
|
+
for (const [kind, path] of Object.entries(record.entries)) if (!["effect", "native", "param"].includes(kind) || typeof path !== "string") throw new Error(`Invalid RouteBuildRecord entry ${JSON.stringify(kind)}`)
|
|
50
|
+
if (record.runtimeSchema !== undefined && !isRecord(record.runtimeSchema)) throw new Error("Invalid RouteBuildRecord runtime schema")
|
|
51
|
+
const hasNative = (record.plan.events ?? []).some(event => event.native)
|
|
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`)
|
|
53
|
+
if (Boolean(record.entries.native) !== hasNative) throw new Error(`RouteBuildRecord ${JSON.stringify(record.route)} has inconsistent native artifacts`)
|
|
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)
|
|
60
|
+
return record
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function planRouteArtifacts(records, handlerModules, workerReferences, moduleUrl) {
|
|
64
|
+
for (const record of records) assertRouteBuildRecord(record)
|
|
65
|
+
const modules = new Map()
|
|
66
|
+
for (const module of handlerModules) {
|
|
67
|
+
const url = moduleUrl(module)
|
|
68
|
+
if (modules.has(url)) throw new Error(`Duplicate compiled handler module: ${url}`)
|
|
69
|
+
modules.set(url, module)
|
|
70
|
+
}
|
|
71
|
+
const retainedUrls = new Set(records.flatMap(record => record.artifacts.handlers.map(reference => reference.module)))
|
|
72
|
+
for (const url of retainedUrls) if (!modules.has(url)) throw new Error(`Handler module was not compiled: ${url}`)
|
|
73
|
+
const effects = new Map()
|
|
74
|
+
for (const reference of records.flatMap(record => record.artifacts.effects)) {
|
|
75
|
+
const handlers = effects.get(reference.module) ?? new Set()
|
|
76
|
+
handlers.add(reference.handler)
|
|
77
|
+
effects.set(reference.module, handlers)
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
handlerModules: handlerModules.filter(module => retainedUrls.has(moduleUrl(module))),
|
|
81
|
+
workerReferences: workerReferences.filter(reference => effects.get(reference.module)?.has(reference.handler)),
|
|
82
|
+
styles: [...new Set(records.flatMap(record => record.artifacts.styles))]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const isRecord = value => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
87
|
+
|
|
88
|
+
function assertHandlerReference(reference) {
|
|
89
|
+
if (!isRecord(reference) || typeof reference.module !== "string" || !reference.module || typeof reference.handler !== "string" || !reference.handler) throw new Error("RouteBuildRecord handler references require module and handler strings")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const referenceKey = reference => `${JSON.stringify(reference.module)}#${JSON.stringify(reference.handler)}`
|
|
@@ -1,11 +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(
|
|
8
|
-
for (const
|
|
10
|
+
export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}, validate = true) {
|
|
11
|
+
for (const record of records) assertRouteBuildRecord(record)
|
|
12
|
+
const plans = records.map(record => record.plan)
|
|
13
|
+
for (const plan of plans) assertRouteIR(plan, { concrete: true })
|
|
9
14
|
const commandEvents = new Set()
|
|
10
15
|
const nativeEvents = new Set()
|
|
11
16
|
const bindings = { count: 0, text: false, svgConditions: false }
|
|
@@ -39,7 +44,7 @@ export function planRouteCapabilities(plans, { routes = new Map(), navigationRou
|
|
|
39
44
|
|
|
40
45
|
for (let index = 0; index < plans.length; index++) {
|
|
41
46
|
const plan = plans[index]
|
|
42
|
-
const route =
|
|
47
|
+
const route = records[index].capabilities
|
|
43
48
|
for (const event of plan.events) {
|
|
44
49
|
if (event.commands) commandEvents.add(event.event)
|
|
45
50
|
if (event.native) nativeEvents.add(event.event)
|
|
@@ -77,7 +82,7 @@ export function planRouteCapabilities(plans, { routes = new Map(), navigationRou
|
|
|
77
82
|
}
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
const routeEntries =
|
|
85
|
+
const routeEntries = records.map(record => record.capabilities)
|
|
81
86
|
const routeCounts = {
|
|
82
87
|
behaviors: routeEntries.filter(route => route.hasBehaviors).length,
|
|
83
88
|
regularBehaviors: routeEntries.filter(route => route.hasBehaviors && !route.usesDependencyRuntime).length,
|
|
@@ -91,7 +96,7 @@ export function planRouteCapabilities(plans, { routes = new Map(), navigationRou
|
|
|
91
96
|
lists.asyncParts = lists.expressions || lists.expressionAttributes || lists.conditions
|
|
92
97
|
lists.mounts ||= lists.conditions || lists.nested
|
|
93
98
|
|
|
94
|
-
|
|
99
|
+
const capabilityIR = {
|
|
95
100
|
version: 1,
|
|
96
101
|
routes: routeCounts,
|
|
97
102
|
events: { command: [...commandEvents].sort(), native: [...nativeEvents].sort(), hasNativeHandlers: nativeEvents.size > 0 },
|
|
@@ -104,19 +109,10 @@ export function planRouteCapabilities(plans, { routes = new Map(), navigationRou
|
|
|
104
109
|
dependency: routeEntries.some(route => route.usesDependencyRuntime)
|
|
105
110
|
}
|
|
106
111
|
}
|
|
112
|
+
return validate ? assertCapabilityIR(capabilityIR, records, { navigationRouteCount }) : capabilityIR
|
|
107
113
|
}
|
|
108
114
|
|
|
109
|
-
function
|
|
110
|
-
if (plan?.version !== 1) throw new Error(`Unsupported RouteIR version: ${JSON.stringify(plan?.version)}`)
|
|
111
|
-
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")
|
|
112
|
-
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")
|
|
113
|
-
if ([...plan.params, ...plan.searchParams].some(param => !isRecord(param) || typeof param.name !== "string" || typeof param.id !== "string")) throw new Error("Invalid RouteIR v1 parameter")
|
|
114
|
-
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")
|
|
115
|
-
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")
|
|
116
|
-
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")
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function assertCapabilityIR(capabilityIR) {
|
|
115
|
+
export function assertCapabilityIR(capabilityIR, records, options = {}) {
|
|
120
116
|
if (capabilityIR?.version !== 1) throw new Error(`Unsupported CapabilityIR version: ${JSON.stringify(capabilityIR?.version)}`)
|
|
121
117
|
const sections = ["routes", "events", "bindings", "lists", "effects", "captures", "runtime"]
|
|
122
118
|
if (!sections.every(name => isRecord(capabilityIR[name]))) throw new Error("Invalid CapabilityIR v1 structure")
|
|
@@ -126,17 +122,23 @@ export function assertCapabilityIR(capabilityIR) {
|
|
|
126
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"]
|
|
127
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")
|
|
128
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
|
|
129
138
|
}
|
|
130
139
|
|
|
131
140
|
const isRecord = value => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
132
141
|
const isCount = value => Number.isSafeInteger(value) && value >= 0
|
|
133
|
-
const isRouteList = list => isRecord(list)
|
|
134
|
-
&& typeof list.id === "string"
|
|
135
|
-
&& typeof list.state === "string"
|
|
136
|
-
&& (typeof list.key === "string" || list.key === null)
|
|
137
|
-
&& Array.isArray(list.keys)
|
|
138
|
-
&& ["svg", "static", "indexed", "reducer", "mount", "nested", "effects", "conditions", "conditionHandlers", "textRanges", "attributes", "events", "expressions", "expressionAttributes", "fastRelease"].every(name => list[name] === undefined || list[name] === true)
|
|
139
|
-
&& ["selector", "children", "expressionStates", "rowStates", "rowConditions", "rowRefs"].every(name => list[name] === undefined || Array.isArray(list[name]))
|
|
140
142
|
|
|
141
143
|
function hasCaptureType(value, type) {
|
|
142
144
|
if (!value || typeof value !== "object") return false
|
|
@@ -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 }>
|
|
@@ -185,6 +186,6 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
185
186
|
hasLists: boolean
|
|
186
187
|
hasListStyles: boolean
|
|
187
188
|
hasStateSeed: boolean
|
|
188
|
-
|
|
189
|
+
handlerReferences: Array<{ module: string; handler: string }>
|
|
189
190
|
plan: RouteIR
|
|
190
191
|
}>
|
package/framework/core.mjs
CHANGED
|
@@ -207,8 +207,10 @@ export function useEffect(callback, dependencies, module, handler, states, scope
|
|
|
207
207
|
owner = nextRenderId("e")
|
|
208
208
|
owners.push(owner)
|
|
209
209
|
}
|
|
210
|
-
if (!renderContext.listDepth || list)
|
|
211
|
-
|
|
210
|
+
if (!renderContext.listDepth || list) {
|
|
211
|
+
renderContext.effects.push({ module, handler, states, scope, source, renderScope: renderContext.renderScope, ...(dependencyIds.length ? { dependencies: dependencyIds } : {}), ...(dependencyExpressions.length ? { dependencyExpressions, dependencyStates: dependencyStateIds } : {}), ...(itemDependencies.length ? { itemDependencies, listState: renderContext.listRoot.state } : {}), ...(cleanup ? { cleanup: true } : {}), ...(owner ? { owner } : {}), ...(list ? { list: true } : {}) })
|
|
212
|
+
retainHandlerReference(module, handler)
|
|
213
|
+
}
|
|
212
214
|
renderContext.hasBehaviors = true
|
|
213
215
|
renderContext.hasEffects = true
|
|
214
216
|
}
|
|
@@ -254,7 +256,6 @@ export function behavior(commands) {
|
|
|
254
256
|
}
|
|
255
257
|
|
|
256
258
|
export function nativeBehavior(module, handler, states, scope) {
|
|
257
|
-
renderContext?.handlerModules.add(module)
|
|
258
259
|
return {
|
|
259
260
|
[nativeBehaviorMarker]: true,
|
|
260
261
|
module,
|
|
@@ -325,7 +326,6 @@ export function listField(read, field) {
|
|
|
325
326
|
}
|
|
326
327
|
|
|
327
328
|
export function listExpression(read, module, handler, states = []) {
|
|
328
|
-
renderContext?.handlerModules.add(module)
|
|
329
329
|
const stateMap = Object.fromEntries(states.map(([name, state]) => {
|
|
330
330
|
if (!state?.[signalMarker] || !validEffectDependency(state.value)) throw new Error(`Derived keyed list item expression state ${JSON.stringify(name)} must be primitive Kudzu state`)
|
|
331
331
|
return [name, state.id]
|
|
@@ -346,7 +346,6 @@ export function listIndex() {
|
|
|
346
346
|
}
|
|
347
347
|
|
|
348
348
|
export function listConditional(kind, read, truthy, falsy, module, handler) {
|
|
349
|
-
renderContext?.handlerModules.add(module)
|
|
350
349
|
return { [listConditionalMarker]: true, kind, value: renderContext?.listTemplate ? undefined : read(), truthy, falsy, module, handler }
|
|
351
350
|
}
|
|
352
351
|
|
|
@@ -380,7 +379,6 @@ function assertListValue(value, seen) {
|
|
|
380
379
|
}
|
|
381
380
|
|
|
382
381
|
function reactiveDescriptor(module, handler, states, scope) {
|
|
383
|
-
renderContext?.handlerModules.add(module)
|
|
384
382
|
const scopeStates = {}
|
|
385
383
|
const serializedScope = {}
|
|
386
384
|
const scopeBindings = {}
|
|
@@ -402,6 +400,16 @@ function reactiveDescriptor(module, handler, states, scope) {
|
|
|
402
400
|
}
|
|
403
401
|
}
|
|
404
402
|
|
|
403
|
+
function retainHandlerReference(module, handler) {
|
|
404
|
+
const reference = { module, handler }
|
|
405
|
+
renderContext?.handlerReferences.set(JSON.stringify([module, handler]), reference)
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function retainDescriptorHandlers(descriptor) {
|
|
409
|
+
if (descriptor?.module && descriptor.handler) retainHandlerReference(descriptor.module, descriptor.handler)
|
|
410
|
+
for (const nested of Object.values(descriptor?.scopeBindings ?? {})) retainDescriptorHandlers(nested)
|
|
411
|
+
}
|
|
412
|
+
|
|
405
413
|
export function bindingValue(value) {
|
|
406
414
|
return value?.[signalMarker] || value?.[bindingMarker] ? value.value : value
|
|
407
415
|
}
|
|
@@ -449,7 +457,7 @@ function serializeCapture(name, value, seen) {
|
|
|
449
457
|
}
|
|
450
458
|
|
|
451
459
|
export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
452
|
-
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, nextId: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], stores: new Map(), states: {}, textStates: new Set(), conditionStates: new Set(), conditionOwnedStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [],
|
|
460
|
+
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, nextId: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], stores: new Map(), states: {}, textStates: new Set(), conditionStates: new Set(), conditionOwnedStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerReferences: new Map(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, searchParams: new Map(), searchParamEntries: [], searchParamsWritable: false, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
453
461
|
|
|
454
462
|
try {
|
|
455
463
|
const page = { [routeScopeMarker]: true, component, props }
|
|
@@ -525,7 +533,7 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
|
525
533
|
hasLists: renderContext.hasLists,
|
|
526
534
|
hasListStyles: renderContext.hasListStyles,
|
|
527
535
|
hasStateSeed: initialState.length > 0,
|
|
528
|
-
|
|
536
|
+
handlerReferences: [...renderContext.handlerReferences.values()],
|
|
529
537
|
plan: {
|
|
530
538
|
version: 1,
|
|
531
539
|
states: Object.entries(renderContext.states).map(([id, state], slot) => ({ slot, id, ...state })),
|
|
@@ -621,7 +629,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
621
629
|
}
|
|
622
630
|
}
|
|
623
631
|
if (node?.[conditionalMarker]) {
|
|
624
|
-
const descriptor = bindingDescriptor(node)
|
|
632
|
+
const descriptor = node.state ? { state: node.state } : bindingDescriptor(node)
|
|
625
633
|
const stateIds = reactiveStateIds(descriptor)
|
|
626
634
|
if (!stateIds.size) return renderNode(node.value ? node.truthy() : node.falsy(), namespace, selectValue)
|
|
627
635
|
if (namespace === "math") throw new Error("Reactive conditional DOM is not supported inside math")
|
|
@@ -648,6 +656,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
648
656
|
const metadata = { id, kind: node.kind, initial: node.value, ...descriptor, ...(namespace === "svg" ? { svg: true } : {}), ...(owned ? { owned } : {}), ...(mount ? { mount: true } : {}) }
|
|
649
657
|
for (const stateId of stateIds) renderContext.conditionStates.add(stateId)
|
|
650
658
|
renderContext.conditions.push(metadata)
|
|
659
|
+
retainDescriptorHandlers(metadata)
|
|
651
660
|
renderContext.hasBehaviors = true
|
|
652
661
|
renderContext.hasBindings = true
|
|
653
662
|
const encoded = escapeJsonAttribute(metadata)
|
|
@@ -667,7 +676,9 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
667
676
|
}
|
|
668
677
|
if (node?.[listExpressionMarker]) {
|
|
669
678
|
const descriptor = { module: node.module, handler: node.handler, ...(Object.keys(node.states).length ? { states: node.states } : {}) }
|
|
670
|
-
const
|
|
679
|
+
const retained = renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch
|
|
680
|
+
if (retained) retainDescriptorHandlers(descriptor)
|
|
681
|
+
const marker = retained ? ` data-k-list-expression='${escapeJsonAttribute(descriptor)}'` : ""
|
|
671
682
|
return `<template${marker}></template>${escapeHtml(node.value ?? "")}<template data-k-list-expression-end></template>`
|
|
672
683
|
}
|
|
673
684
|
if (node?.[bindingMarker]) {
|
|
@@ -675,6 +686,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
675
686
|
const reactive = reactiveStateIds(descriptor).size > 0
|
|
676
687
|
if (!reactive) return renderNode(node.value, namespace, selectValue)
|
|
677
688
|
renderContext.bindings.push({ target: "text", ...descriptor })
|
|
689
|
+
retainDescriptorHandlers(descriptor)
|
|
678
690
|
if (renderContext.conditionDepth || renderContext.listDepth) for (const stateId of reactiveStateIds(descriptor)) renderContext.conditionStates.add(stateId)
|
|
679
691
|
renderContext.hasBehaviors = true
|
|
680
692
|
renderContext.hasBindings = true
|
|
@@ -685,6 +697,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
685
697
|
if (node?.[listConditionalMarker]) {
|
|
686
698
|
if (namespace === "svg") throw new Error("Keyed row conditions are not supported inside svg")
|
|
687
699
|
const descriptor = { kind: node.kind, module: node.module, handler: node.handler }
|
|
700
|
+
retainDescriptorHandlers(descriptor)
|
|
688
701
|
const owner = renderContext.listRoot ?? renderContext.listRowRoot
|
|
689
702
|
if (owner) owner.conditions = true
|
|
690
703
|
const previousBranch = renderContext.listConditionalBranch
|
|
@@ -800,6 +813,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
800
813
|
const native = template
|
|
801
814
|
attributes += ` data-k-native-${event}='${escapeJsonAttribute(native)}'`
|
|
802
815
|
renderContext.events.push({ event, native })
|
|
816
|
+
retainDescriptorHandlers(native)
|
|
803
817
|
if (renderContext.listDepth && Object.values(template.scope).some(entry => entry?.type === "list-item" || entry?.type === "list-index")) listEvents.push([event, template])
|
|
804
818
|
renderContext.hasNativeBehaviors = true
|
|
805
819
|
} else {
|
|
@@ -817,9 +831,10 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
817
831
|
if (name === "style") renderContext.hasListStyles = true
|
|
818
832
|
continue
|
|
819
833
|
}
|
|
820
|
-
|
|
834
|
+
if (value?.[listExpressionMarker]) {
|
|
821
835
|
attributes += renderAttribute(name, value.value)
|
|
822
|
-
|
|
836
|
+
listExpressionAttributes.push([name, value.module, value.handler, ...(Object.keys(value.states).length ? [value.states] : [])])
|
|
837
|
+
if (renderContext.listTemplate || renderContext.listInitialMarkers || renderContext.listConditionalBranch) retainHandlerReference(value.module, value.handler)
|
|
823
838
|
if (name === "style") renderContext.hasListStyles = true
|
|
824
839
|
continue
|
|
825
840
|
}
|
|
@@ -837,6 +852,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
837
852
|
if (propertyTarget) attributes += ` data-k-bind-${name}='${escapeJsonAttribute(descriptor)}'`
|
|
838
853
|
else attributeBindings.push({ target: name, ...descriptor })
|
|
839
854
|
renderContext.bindings.push({ target: name, ...descriptor })
|
|
855
|
+
retainDescriptorHandlers(descriptor)
|
|
840
856
|
if (renderContext.conditionDepth || renderContext.listDepth) for (const stateId of reactiveStateIds(descriptor)) renderContext.conditionStates.add(stateId)
|
|
841
857
|
renderContext.hasBehaviors = true
|
|
842
858
|
renderContext.hasBindings = true
|
|
@@ -944,6 +960,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
944
960
|
}
|
|
945
961
|
if (!node.ownerField || ownerTemplate && !rowList.planned) {
|
|
946
962
|
renderContext.lists.push(descriptor)
|
|
963
|
+
retainDescriptorHandlers(descriptor.source)
|
|
947
964
|
if (rowList) rowList.planned = true
|
|
948
965
|
}
|
|
949
966
|
renderContext.hasBehaviors = true
|