@kudzujs/core 0.8.21 → 0.8.22
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 +13 -0
- package/PERFORMANCE.md +24 -0
- package/README.md +1 -1
- package/RELEASES.md +36 -0
- package/docs/next-architecture/README.md +2 -2
- package/docs/next-architecture/compiler-current-architecture.md +14 -13
- package/docs/next-architecture/goal-a-compiler-foundation.md +12 -15
- package/docs/next-architecture/versioning.md +2 -2
- package/framework/README.md +5 -2
- package/framework/build.mjs +26 -293
- package/framework/compiler/list-runtime-codegen.mjs +95 -0
- package/framework/compiler/param-codegen.mjs +72 -0
- package/framework/compiler/route-capability-planner.mjs +35 -0
- package/framework/compiler/runtime-codegen.mjs +146 -0
- package/framework/core.d.ts +41 -38
- package/framework/core.mjs +2 -1
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -223,6 +223,19 @@ This queue orders the next investigations by general migration value. Start only
|
|
|
223
223
|
- Previous transformer-wide keyed value, condition, event, nested-list, effect, and rendered-list AST side tables were removed. AST remains private only inside immediate source-local validation and lowering.
|
|
224
224
|
- `core.mjs` remains authoritative for final list IDs, key paths, route descriptors, complete HTML, DOM identity, and exact state/effect/ref release; accepted syntax and browser output are unchanged.
|
|
225
225
|
|
|
226
|
+
### Completed In 0.8.21
|
|
227
|
+
|
|
228
|
+
- Every supported effect finalizes into JSON-safe EffectIR with setup HandlerIR, cleanup, ordered signal/DerivedIR dependencies, component/keyed ownership, source provenance, and Worker edges.
|
|
229
|
+
- Effect dependency/resource analysis and Worker rewriting return explicit results; transformer-wide effect AST side tables and build-wide mutable Worker references were removed.
|
|
230
|
+
- Existing route/layout/conditional/keyed lifetime allocation, stale-write invalidation, cleanup order, accepted syntax, and browser output remain unchanged.
|
|
231
|
+
|
|
232
|
+
### Completed In 0.8.22
|
|
233
|
+
|
|
234
|
+
- The existing rendered route plan is RouteIR v1, with route-local numeric state slots beside unchanged browser IDs and readable state names; it is not duplicated in ModuleIR.
|
|
235
|
+
- The existing pure capability projection is CapabilityIR v1 and every runtime/list codegen consumer rejects unsupported versions.
|
|
236
|
+
- List, parameter, core, effect, binding, native, and navigation source generation moved out of `build.mjs` behind focused fail-closed generators; `build.mjs` decreased by 267 lines and coordinates stage outputs.
|
|
237
|
+
- Complete-site and representative deploy artifacts remain byte-identical to `v0.8.21`; only additive RouteIR version/slot metadata changes build plans.
|
|
238
|
+
|
|
226
239
|
## Cross-Cutting Performance Gates
|
|
227
240
|
|
|
228
241
|
Every migration feature must preserve:
|
package/PERFORMANCE.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Performance Records
|
|
2
2
|
|
|
3
|
+
## 0.8.22 Versioned Compiler Foundation
|
|
4
|
+
|
|
5
|
+
Measured UTC 2026-08-09 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 25.6.1, and npm 11.18.0. Baseline tag `v0.8.21` at `ff38092` and the current `0.8.22` compiler candidate used the same local volume and identical installed dependencies.
|
|
6
|
+
|
|
7
|
+
The candidate implementation patch over `v0.8.21` had SHA-256 `e1d89c97dd8dfb60d41ae9c14ac64dfbb50466e024f80076f430ae9e725fb28f`, produced by:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
git diff --binary v0.8.21 -- framework/build.mjs framework/compiler/list-runtime-codegen.mjs framework/compiler/param-codegen.mjs framework/compiler/route-capability-planner.mjs framework/compiler/runtime-codegen.mjs framework/core.mjs framework/core.d.ts | shasum -a 256
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Both targets received one warm-up followed by seven clean `worker-effects` production builds in alternating round-robin order. Cleanup remained outside timing. Both medians were exactly 250.1 ms; the distributions overlap and establish no material change.
|
|
14
|
+
|
|
15
|
+
| Target | Build median | Worker raw / gzip | Window raw / gzip |
|
|
16
|
+
|---|---:|---:|---:|
|
|
17
|
+
| 0.8.21 baseline | 250.1 ms | 907 B / 475 B | 12,148 B / 5,427 B |
|
|
18
|
+
| 0.8.22 candidate | 250.1 ms | 907 B / 475 B | 12,148 B / 5,427 B |
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
0.8.21: [249.7,252.0,250.1,251.2,250.0,254.8,249.1]
|
|
22
|
+
0.8.22: [250.1,247.3,255.1,249.3,253.1,250.3,248.7]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Before release-content updates, the complete 135-page site and the `bindings`, `keyed-row-hooks`, `effect-dependencies`, `worker-effects`, `runtime-params`, and `navigation` deploy trees had identical file lists and bytes. Their build plans were equivalent after removing the intentional additive RouteIR `version` and state `slot` fields. Every measured Worker/window artifact name, raw byte count, and gzip byte count was identical. `build.mjs` decreased from 3,999 to 3,732 lines; this source-organization metric is not a runtime performance claim.
|
|
26
|
+
|
|
3
27
|
## 0.8.21 Explicit Effect Ownership
|
|
4
28
|
|
|
5
29
|
Measured UTC 2026-08-09 on Apple M3, 8 logical CPUs, 8 GiB RAM, macOS 26.5.2 / Darwin 25.5.0, Node 25.6.1, and npm 11.18.0. Baseline tag `v0.8.20` at `7fb6e37` and the current `0.8.21` compiler candidate used the same local volume and identical installed dependencies.
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
14
14
|
|
|
15
15
|
> Experimental `0.8.x`: the compiler API and supported TSX surface may change.
|
|
16
16
|
|
|
17
|
-
**Latest release: 0.8.
|
|
17
|
+
**Latest release: 0.8.22 - Versioned compiler foundation.** Existing rendered route plans and capability manifests are now RouteIR v1 and CapabilityIR v1, with route-local state slots, stable runtime IDs, and readable names. Runtime generators consume those contracts while `build.mjs` coordinates their emission. Read the [release notes](./RELEASES.md#0822---versioned-compiler-foundation), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.22), 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,41 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.8.22 - Versioned compiler foundation
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.22 completes Goal A: source-local ModuleIR seams accompany the existing rendered route plan as RouteIR v1, while CapabilityIR v1 selects shared runtime families and branches for focused artifact generators.
|
|
6
|
+
|
|
7
|
+
### Changed in 0.8.22
|
|
8
|
+
|
|
9
|
+
- The existing `renderPage().plan` is RouteIR v1 rather than a duplicated route representation.
|
|
10
|
+
- Route states add a deterministic route-local numeric `slot`; existing string `id` values remain authoritative browser/DOM identities and `name` remains readable development metadata.
|
|
11
|
+
- The existing pure capability manifest is CapabilityIR v1. Route and capability consumers reject unsupported versions.
|
|
12
|
+
- List, parameter, core, effect, binding, native, and navigation runtime generation moved from `build.mjs` into focused generator modules with narrow inputs and no filesystem ownership.
|
|
13
|
+
- Required authored-runtime source anchors fail closed instead of silently retaining stale branches.
|
|
14
|
+
- `build.mjs` decreased from 3,999 to 3,732 lines and now coordinates generator results and artifact emission rather than containing those feature semantics.
|
|
15
|
+
|
|
16
|
+
### Goal A boundary
|
|
17
|
+
|
|
18
|
+
- ModuleIR slots identify source-local compiler records; RouteIR state slots identify positions in one rendered plan; browser IDs continue to own DOM/runtime behavior. The namespaces are deliberately independent.
|
|
19
|
+
- `core.mjs` remains authoritative for complete HTML, route/layout IDs, conditions, keyed paths, effect lifetimes, and exact cleanup.
|
|
20
|
+
- CapabilityIR selects shared runtime families and branches; RouteIR and ModuleIR references retain route entries, handlers, and Workers. Static pages remain complete documents with zero JavaScript.
|
|
21
|
+
- Goal B optimization remains deferred; `0.8.22` is its recorded baseline if explicitly started later.
|
|
22
|
+
- No accepted syntax, public API behavior, runtime capability, VDOM, hydration, component rerender, or retained browser component tree was added.
|
|
23
|
+
|
|
24
|
+
### Validation
|
|
25
|
+
|
|
26
|
+
- The complete suite passes 170/170 tests, including RouteIR JSON round-trip and slots, CapabilityIR JSON round-trip/version rejection, fail-closed generator contracts, list/parameter/navigation generation, lifecycle cleanup, and development restoration by readable state name.
|
|
27
|
+
- `npm run check`, focused Node 22 compiler tests, and a Node 22 complete-site build pass.
|
|
28
|
+
- The complete site plus six representative binding, list, effect, Worker, runtime-parameter, and navigation deploy trees are byte-identical to `v0.8.21`; normalized RouteIR meaning is identical in all seven comparisons.
|
|
29
|
+
- Worker and window graph files remain byte-identical. Seven interleaved clean builds measured equal 250.1 ms medians; raw arrays and provenance are recorded in `PERFORMANCE.md`.
|
|
30
|
+
- Release-blocking architecture audits drove full IR schema validation, accurate reachability ownership, and fail-closed source contracts before publication.
|
|
31
|
+
- `create-kudzu` remains 0.1.101 because its unchanged template already accepts `@kudzujs/core@^0.8.15`.
|
|
32
|
+
|
|
33
|
+
### Upgrade
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npm install @kudzujs/core@^0.8.22
|
|
37
|
+
```
|
|
38
|
+
|
|
3
39
|
## 0.8.21 - Explicit effect ownership
|
|
4
40
|
|
|
5
41
|
Kudzu 0.8.21 completes the next Goal A source-analysis seam: every supported effect now registers deterministic JSON-safe setup, cleanup, dependency, ownership, source, and Worker-edge data before existing build-time rendering allocates concrete lifecycle IDs.
|
|
@@ -6,8 +6,8 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
|
|
|
6
6
|
|
|
7
7
|
| Goal | Decision | Start condition |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| A: compiler foundation | `0.8.
|
|
10
|
-
| B: optimization benchmarks | Deferred |
|
|
9
|
+
| A: compiler foundation | Complete in `0.8.22` | RouteIR v1, CapabilityIR v1, source IR seams, generator boundaries, and output baseline recorded. |
|
|
10
|
+
| B: optimization benchmarks | Deferred | Start only by explicit decision from the recorded `0.8.22` baseline. |
|
|
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
|
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
# Current Compiler Architecture
|
|
2
2
|
|
|
3
|
-
This maps the
|
|
3
|
+
This maps the completed `0.8.22` 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
|
|
|
7
7
|
| Responsibility | Current owner | Current contract |
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| CLI entry | [`bin/kudzu.mjs`](../../bin/kudzu.mjs) | Dispatches build and development commands. |
|
|
10
|
-
| Build orchestration | [`framework/build.mjs`](../../framework/build.mjs), `build()` |
|
|
10
|
+
| Build orchestration | [`framework/build.mjs`](../../framework/build.mjs), `build()` | Coordinates config, discovery, source compilation, RouteIR rendering, CapabilityIR projection, generator invocation, artifact emission, and `afterBuild`. |
|
|
11
11
|
| Reachability/import resolution | `framework/build.mjs`, `reachableSourceFiles()`, `resolveSourceImport()` | Starts from page entries, follows relative runtime imports/re-exports and validated Worker references, and excludes unreachable migration source. |
|
|
12
12
|
| Ordered normalization | [`framework/compiler/normalization-pipeline.mjs`](../../framework/compiler/normalization-pipeline.mjs), `applyNormalizationPasses()`; `framework/build.mjs`, `normalizeCompilerSource()` | Applies migration/resource passes in order and repairs TypeScript parent pointers after every pass. Imported source uses the same pipeline. |
|
|
13
13
|
| Focused normalization passes | [`framework/compiler/`](../../framework/compiler/) | React, Router, browser signals, animation-frame refs, custom-hook timers, Zustand, and render control each validate and lower a narrow source shape. |
|
|
@@ -22,10 +22,11 @@ This maps the EffectIR boundary prepared for `0.8.21`. File and function names a
|
|
|
22
22
|
| Handler module codegen | [`framework/compiler/handler-codegen.mjs`](../../framework/compiler/handler-codegen.mjs) | Renders finalized ordered imports and concatenates generated module-export source without TypeScript AST or semantic discovery. |
|
|
23
23
|
| 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. |
|
|
24
24
|
| 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. |
|
|
25
|
-
| 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
|
|
26
|
-
| Route capability projection | [`framework/compiler/route-capability-planner.mjs`](../../framework/compiler/route-capability-planner.mjs), `planRouteCapabilities()` |
|
|
25
|
+
| 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. |
|
|
26
|
+
| Route capability projection | [`framework/compiler/route-capability-planner.mjs`](../../framework/compiler/route-capability-planner.mjs), `planRouteCapabilities()` | Validates RouteIR v1 and purely folds rendered plans and route facts into CapabilityIR v1. |
|
|
27
27
|
| Effect entry generation | [`framework/compiler/effect-codegen.mjs`](../../framework/compiler/effect-codegen.mjs) | Generates ordinary, dependency, owned, and navigable effect entries from rendered descriptors. |
|
|
28
|
-
| Runtime
|
|
28
|
+
| 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. |
|
|
29
|
+
| Artifact emission | `framework/build.mjs` | Selects required files from CapabilityIR and writes or bundles generated sources with esbuild. |
|
|
29
30
|
| Browser capabilities | [`framework/*.js`](../../framework/) | Small optional modules for commands, bindings, lists, effects, native handlers, serialization, parameters, and navigation; no component runtime. |
|
|
30
31
|
| 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. |
|
|
31
32
|
| 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; never changes production `dist/`. |
|
|
@@ -44,11 +45,11 @@ src/pages entries + config
|
|
|
44
45
|
-> generated handler/effect/binding/list-evaluator modules
|
|
45
46
|
-> import page modules and execute renderPage()
|
|
46
47
|
-> complete HTML
|
|
47
|
-
->
|
|
48
|
+
-> RouteIR v1
|
|
48
49
|
-> referenced handler URLs and route facts
|
|
49
50
|
-> remove unrendered handlers/effects/Workers
|
|
50
|
-
|
|
51
|
-
->
|
|
51
|
+
-> planRouteCapabilities(RouteIR records, route facts)
|
|
52
|
+
-> CapabilityIR v1
|
|
52
53
|
-> specialize and emit only selected runtime/capability ESM
|
|
53
54
|
-> write route index.html, CSS/assets, Worker graphs, rewrites, and .kudzu/kudzu-plan.json
|
|
54
55
|
-> optional afterBuild()
|
|
@@ -56,15 +57,15 @@ src/pages entries + config
|
|
|
56
57
|
|
|
57
58
|
The browser consumes static HTML first. State seeds and descriptors in that HTML connect only to emitted command, binding, list, native-handler, effect, parameter, or navigation modules. Browser execution patches owned DOM directly; it does not invoke component functions or reconstruct a component tree.
|
|
58
59
|
|
|
59
|
-
##
|
|
60
|
+
## Residual Coupling
|
|
60
61
|
|
|
61
62
|
- `createKudzuTransformer()` combines discovery, validation, specialization, descriptor registration, and transformed-source emission.
|
|
62
63
|
- Transient component rewrite indexes remain source-local AST indexes; handler, binding, derived, keyed, effect, and component ownership now have explicit JSON-safe source results.
|
|
63
|
-
- `build()`
|
|
64
|
-
- Runtime
|
|
65
|
-
-
|
|
64
|
+
- `build()` still owns explicit artifact selection and filesystem writes after generator results are produced.
|
|
65
|
+
- 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.
|
|
66
|
+
- The main transformer remains the largest source-analysis unit even though its durable outputs are explicit JSON-safe records.
|
|
66
67
|
|
|
67
|
-
Goal A
|
|
68
|
+
These are future simplification opportunities, not incomplete Goal A contracts. Goal A changed no source support, browser output semantics, or browser architecture.
|
|
68
69
|
|
|
69
70
|
## Continuation Checklist
|
|
70
71
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Goal A is complete in `0.8.22`. ModuleIR source seams, RouteIR v1, CapabilityIR v1, focused runtime generators, and the architecture/output baseline preserve accepted React-shaped syntax, diagnostics, complete HTML, emitted capability behavior, and current public APIs.
|
|
6
6
|
|
|
7
7
|
## Target Boundaries
|
|
8
8
|
|
|
@@ -77,7 +77,7 @@ Analysis produces plain data:
|
|
|
77
77
|
}
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
The
|
|
80
|
+
The completed source result contains deterministic command, component, HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, and EffectIR records. Source-local AST analysis is consumed during lowering and does not cross this boundary. Final route/layout/conditional/keyed IDs and lifetime allocation remain authoritative in `core.mjs`; no component tree or duplicate route plan exists.
|
|
81
81
|
|
|
82
82
|
Source codegen lowers that data through the existing build ABI:
|
|
83
83
|
|
|
@@ -93,11 +93,11 @@ One source-local result owns transformed source plus the sparse ModuleIR, genera
|
|
|
93
93
|
|
|
94
94
|
### Route Result
|
|
95
95
|
|
|
96
|
-
`renderPage()` remains the build-time authority for complete HTML and ownership IDs. Its
|
|
96
|
+
`renderPage()` remains the build-time authority for complete HTML and ownership IDs. Its existing plan is RouteIR v1, the behavioral source for states, events, effects, bindings, conditions, lists, parameters, and search parameters. Each state has a route-local numeric `slot`, its unchanged browser/DOM `id`, and readable `name` metadata used by development restoration. RouteIR slots are not ModuleIR signal slots or persistent browser identities. Route-level facts such as navigability and dependency-runtime selection accompany rather than mutate the plan.
|
|
97
97
|
|
|
98
98
|
### Artifact Plan
|
|
99
99
|
|
|
100
|
-
|
|
100
|
+
CapabilityIR v1 determines shared runtime families and optional branches. Rendered RouteIR descriptors and ModuleIR references independently retain route entries, handler modules, and Worker reachability. Runtime, list, parameter, effect, and handler generators consume those explicit results; they do not rediscover TSX semantics or inspect application AST.
|
|
101
101
|
|
|
102
102
|
## Generator Strategy
|
|
103
103
|
|
|
@@ -122,17 +122,14 @@ No generator may analyze TSX, invent runtime component abstractions, or broaden
|
|
|
122
122
|
- Preserve current runtime files until their corresponding generator has artifact parity.
|
|
123
123
|
- Treat deterministic byte changes as review items even when tests pass.
|
|
124
124
|
|
|
125
|
-
## Acceptance
|
|
126
|
-
|
|
127
|
-
-
|
|
128
|
-
- Source normalization, semantic analysis,
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
-
|
|
132
|
-
-
|
|
133
|
-
- Unreachable handlers, effects, package helpers, and Workers remain absent.
|
|
134
|
-
- Diagnostics retain source file and location.
|
|
135
|
-
- [`performance-gates.md`](./performance-gates.md) passes for every patch.
|
|
125
|
+
## Acceptance Record
|
|
126
|
+
|
|
127
|
+
- `build.mjs` decreased from 3,999 to 3,732 lines and coordinates focused generators rather than containing list, parameter, core, effect, binding, native, or navigation source generation.
|
|
128
|
+
- Source normalization, semantic analysis, RouteIR rendering, CapabilityIR projection, generation, and writing have explicit ownership without a broad context object.
|
|
129
|
+
- RouteIR and CapabilityIR reject unsupported versions; required runtime source anchors fail closed.
|
|
130
|
+
- Static routes retain complete HTML and zero JavaScript; deploy output across the complete site and representative binding, list, effect, Worker, parameter, and navigation fixtures is byte-identical to `v0.8.21`.
|
|
131
|
+
- Interactive and navigation routes preserve capability selection, ownership, stale-write, and cleanup behavior; unreachable handlers, effects, package helpers, and Workers remain absent.
|
|
132
|
+
- Diagnostics retain source file and location, and [`performance-gates.md`](./performance-gates.md) records no build or artifact-size regression.
|
|
136
133
|
|
|
137
134
|
## Non-Goals
|
|
138
135
|
|
|
@@ -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.22` 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
|
|
|
@@ -16,7 +16,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
16
16
|
|
|
17
17
|
## Sequence Rules
|
|
18
18
|
|
|
19
|
-
-
|
|
19
|
+
- Goal A is complete. Goal B optimization remains deferred until explicitly started from the recorded `0.8.22` baseline.
|
|
20
20
|
- Keep Goal C research out of these patches and publish no store/resource API.
|
|
21
21
|
- Keep Goal D behavior unchanged: complete HTML, native default, opt-in groups, no islands.
|
|
22
22
|
- A planned patch may ship under a different actual version only if this table and release-facing documentation are updated before release.
|
package/framework/README.md
CHANGED
|
@@ -39,6 +39,7 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
|
|
|
39
39
|
- `compiler/descriptor-session.mjs`: private source-local descriptor registration and deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, EffectIR, import, and client-root finalization.
|
|
40
40
|
- `compiler/effect-analysis.mjs`: ordered effect dependency classification and cleanup-owned browser resource validation before EffectIR registration.
|
|
41
41
|
- `compiler/normalization-pipeline.mjs`: ordered source normalization with parent-pointer repair after every compiler pass.
|
|
42
|
+
- `compiler/param-codegen.mjs`: runtime pathname and search-parameter entry generation from rendered route descriptors.
|
|
42
43
|
- `compiler/react-migration-pass.mjs`: React import, memo, callback, `forwardRef`, and `useId` migration validation and lowering.
|
|
43
44
|
- `compiler/custom-hook-timer-pass.mjs`: private custom-hook timeout-ref validation and compiler-owned timer-state lowering.
|
|
44
45
|
- `compiler/effect-codegen.mjs`: route-specific ordinary, owned, and navigable effect entry generation.
|
|
@@ -47,7 +48,9 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
|
|
|
47
48
|
- `compiler/handler-codegen.mjs`: AST-free ordered import rendering and finalized handler/binding export-source assembly.
|
|
48
49
|
- `compiler/render-control-pass.mjs`: render-function early-return and exhaustive adjacent-assignment normalization.
|
|
49
50
|
- `compiler/router-pass.mjs`: React Router import validation and native Link, pathname parameter, search parameter, and imperative navigation lowering.
|
|
50
|
-
- `compiler/route-capability-planner.mjs`: pure
|
|
51
|
+
- `compiler/route-capability-planner.mjs`: RouteIR v1 validation and pure CapabilityIR v1 projection into runtime and artifact requirements.
|
|
52
|
+
- `compiler/runtime-codegen.mjs`: fail-closed core, effect, binding, native, and navigation runtime generation from CapabilityIR.
|
|
53
|
+
- `compiler/list-runtime-codegen.mjs`: fail-closed keyed-list runtime generation from CapabilityIR list/effect sections.
|
|
51
54
|
- `compiler/worker-compiler.mjs`: relative TypeScript Worker candidate validation, effect rewriting, graph validation, and content-hashed ESM emission.
|
|
52
55
|
- `compiler/zustand-pass.mjs`: reduced Zustand store analysis, diagnostics, and migration normalization.
|
|
53
56
|
- `dev-server.mjs`: development HTTP server, source watching, rebuild serialization, SSE reloads, runtime-route fallback serving, and response-only state restoration injection.
|
|
@@ -65,7 +68,7 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
|
|
|
65
68
|
- `dev-state.js`: dev-only, short-lived logical-state snapshot validation and restoration.
|
|
66
69
|
- `*.d.ts`: public TypeScript and JSX declarations.
|
|
67
70
|
|
|
68
|
-
Compiler ownership follows
|
|
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 RouteIR v1: route-local state `slot` is an internal array reference, `id` remains the browser/DOM identity, and `name` remains readable development metadata. `compiler/route-capability-planner.mjs` validates those plans and projects CapabilityIR v1. 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.
|
|
69
72
|
|
|
70
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.
|
|
71
74
|
|
package/framework/build.mjs
CHANGED
|
@@ -15,18 +15,22 @@ import { analyzeEffectDependencies, validateEffectOwnedBrowserResources } from "
|
|
|
15
15
|
import { createEffectCodegen } from "./compiler/effect-codegen.mjs"
|
|
16
16
|
import { createHandlerCodegen } from "./compiler/handler-codegen.mjs"
|
|
17
17
|
import { createHandlerLowering } from "./compiler/handler-lowering.mjs"
|
|
18
|
+
import { generateListRuntime } from "./compiler/list-runtime-codegen.mjs"
|
|
18
19
|
import { createCommandSpecializer } from "./compiler/optimize/command-specialization.mjs"
|
|
19
20
|
import { applyNormalizationPasses } from "./compiler/normalization-pipeline.mjs"
|
|
21
|
+
import { createParamCodegen } from "./compiler/param-codegen.mjs"
|
|
20
22
|
import { createReactMigrationPass, reactMemoExpression } from "./compiler/react-migration-pass.mjs"
|
|
21
23
|
import { normalizeRenderControlFlow } from "./compiler/render-control-pass.mjs"
|
|
22
24
|
import { createRouterPass } from "./compiler/router-pass.mjs"
|
|
23
25
|
import { planRouteCapabilities, usesRouteDependencyRuntime } from "./compiler/route-capability-planner.mjs"
|
|
26
|
+
import { generateBindingRuntime, generateCoreRuntime, generateEffectRuntime, generateNativeRuntime, generateNavigationRuntime, specializeRuntime } from "./compiler/runtime-codegen.mjs"
|
|
24
27
|
import { createWorkerCompiler } from "./compiler/worker-compiler.mjs"
|
|
25
28
|
import { createZustandPass } from "./compiler/zustand-pass.mjs"
|
|
26
29
|
import { renderPage } from "./core.mjs"
|
|
27
30
|
import { parseDevHost, parseDevPort, startDevServer } from "./dev-server.mjs"
|
|
28
31
|
|
|
29
32
|
export { parseDevHost, parseDevPort }
|
|
33
|
+
export { specializeRuntime }
|
|
30
34
|
|
|
31
35
|
const root = process.cwd()
|
|
32
36
|
const sourceDirectory = join(root, "src")
|
|
@@ -213,41 +217,16 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
213
217
|
}
|
|
214
218
|
if (module.code.includes("/__kudzu_worker_")) throw new Error(`Worker URL placeholder survived in ${module.path}`)
|
|
215
219
|
}
|
|
216
|
-
const
|
|
220
|
+
const capabilityIR = planRouteCapabilities(plans, { routes: routeCapabilities, navigationRouteCount: navigationRoutes.length })
|
|
217
221
|
const {
|
|
218
|
-
routes: { behaviors: behaviorCount, regularBehaviors: regularBehaviorCount,
|
|
219
|
-
events: { command: commandEvents,
|
|
220
|
-
bindings: { count: bindingCount
|
|
221
|
-
lists
|
|
222
|
-
|
|
223
|
-
styleCount: listStyleCount,
|
|
224
|
-
conditions: hasListConditions,
|
|
225
|
-
svg: hasSvgLists,
|
|
226
|
-
deepConditions: hasDeepListConditions,
|
|
227
|
-
textRanges: hasListTextRanges,
|
|
228
|
-
attributes: hasListAttributes,
|
|
229
|
-
events: hasListEvents,
|
|
230
|
-
expressions: hasListExpressions,
|
|
231
|
-
expressionAttributes: hasListExpressionAttributes,
|
|
232
|
-
seeds: hasListSeeds,
|
|
233
|
-
effects: hasListEffects,
|
|
234
|
-
rowHooks: hasListRowHooks,
|
|
235
|
-
rowRefs: hasListRowRefs,
|
|
236
|
-
complexRowState: hasComplexListRowState,
|
|
237
|
-
nested: hasNestedLists,
|
|
238
|
-
selectors: hasCollectionSelectors,
|
|
239
|
-
calculated: hasCalculatedCollections,
|
|
240
|
-
static: hasStaticCollections,
|
|
241
|
-
indexes: hasListIndexes,
|
|
242
|
-
stableFastPaths: hasListStableFastPaths,
|
|
243
|
-
generalRowHooks: hasGeneralListRowHooks,
|
|
244
|
-
asyncParts: hasListAsyncParts,
|
|
245
|
-
mounts: hasListMounts
|
|
246
|
-
},
|
|
247
|
-
effects: { any: hasEffects, derivedDependencies: hasDerivedEffectDependencies, itemDependencies: hasItemDependencies, captures: hasEffectCaptures, navigable: hasNavigableEffects, navigableOwners: hasNavigableOwners },
|
|
222
|
+
routes: { behaviors: behaviorCount, regularBehaviors: regularBehaviorCount, dependencyStateSeeds: dependencyStateSeedCount },
|
|
223
|
+
events: { command: commandEvents, hasNativeHandlers },
|
|
224
|
+
bindings: { count: bindingCount },
|
|
225
|
+
lists,
|
|
226
|
+
effects: { any: hasEffects, derivedDependencies: hasDerivedEffectDependencies, captures: hasEffectCaptures },
|
|
248
227
|
captures: { nestedState: hasNestedStateCaptures, setter: hasSetterCaptures },
|
|
249
228
|
runtime: { shared: hasSharedRuntime, dependency: hasDependencyRuntime }
|
|
250
|
-
} =
|
|
229
|
+
} = capabilityIR
|
|
251
230
|
const runtimeName = usesDependencyRuntime => usesDependencyRuntime ? "kudzu-deps.js" : "kudzu.js"
|
|
252
231
|
for (const entry of pageEntries) {
|
|
253
232
|
const routeDirectory = join(outputDirectory, entry.route)
|
|
@@ -257,12 +236,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
257
236
|
}
|
|
258
237
|
if (navigationRoutes.length || behaviorCount && (hasSharedRuntime || regularBehaviorCount)) {
|
|
259
238
|
const runtimeFile = hasSharedRuntime ? "./shared-runtime.js" : "./runtime.js"
|
|
260
|
-
|
|
261
|
-
if (!hasItemDependencies) runtime = runtime.replace(/\/\* list-item-hooks \*\/[\s\S]*?\/\* list-item-hooks-end \*\/\n/, "")
|
|
262
|
-
if (hasNavigableEffects) runtime = runtime.replace("export function registerCommitter(commit) {\n committers.push(commit)\n}", "export function registerCommitter(commit) {\n committers.push(commit)\n return () => {\n const index = committers.indexOf(commit)\n if (index !== -1) committers.splice(index, 1)\n }\n}")
|
|
263
|
-
if (hasNavigableOwners) runtime = runtime
|
|
264
|
-
.replace("export function registerMountHook(mount) {\n mountHooks.push(mount)\n}", "export function registerMountHook(mount) {\n mountHooks.push(mount)\n return () => {\n const index = mountHooks.indexOf(mount)\n if (index !== -1) mountHooks.splice(index, 1)\n }\n}")
|
|
265
|
-
.replace("export function registerUnmountHook(unmount) {\n unmountHooks.push(unmount)\n}", "export function registerUnmountHook(unmount) {\n unmountHooks.push(unmount)\n return () => {\n const index = unmountHooks.indexOf(unmount)\n if (index !== -1) unmountHooks.splice(index, 1)\n }\n}")
|
|
239
|
+
const runtime = generateCoreRuntime(await readFile(new URL(runtimeFile, import.meta.url), "utf8"), capabilityIR)
|
|
266
240
|
await writeJavaScript(join(assetsDirectory, "kudzu.js"), runtime, minify)
|
|
267
241
|
}
|
|
268
242
|
if (hasDependencyRuntime) {
|
|
@@ -274,129 +248,30 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
274
248
|
"globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
|
|
275
249
|
})
|
|
276
250
|
if (hasEffects) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
await writeBundledJavaScript(join(assetsDirectory, "kudzu-effect.js"), effectRuntime, minify, {
|
|
280
|
-
"globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures),
|
|
281
|
-
"globalThis.__KUDZU_EFFECT_CAPTURES__": String(hasEffectCaptures)
|
|
282
|
-
})
|
|
251
|
+
const generated = generateEffectRuntime(await readFile(new URL("./effect-runtime.js", import.meta.url), "utf8"), capabilityIR)
|
|
252
|
+
await writeBundledJavaScript(join(assetsDirectory, "kudzu-effect.js"), generated.source, minify, generated.define)
|
|
283
253
|
}
|
|
284
|
-
if (bindingCount ||
|
|
254
|
+
if (bindingCount || lists.styleCount) await writeJavaScript(join(assetsDirectory, "kudzu-style.js"), await readFile(new URL("./style.js", import.meta.url), "utf8"), minify)
|
|
285
255
|
if (bindingCount) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
.replace('"./serialization.js"', '"./kudzu-serialization.js"')
|
|
289
|
-
.replace('"./style.js"', '"./kudzu-style.js"')
|
|
290
|
-
if (navigationRoutes.length) bindingRuntime = specializeNavigationTextDescriptors(bindingRuntime)
|
|
291
|
-
await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), bindingRuntime, minify, {
|
|
292
|
-
"globalThis.__KUDZU_TEXT_BINDINGS__": String(hasTextBindings),
|
|
293
|
-
"globalThis.__KUDZU_SVG_CONDITIONS__": String(hasSvgConditions),
|
|
294
|
-
"globalThis.__KUDZU_CAPTURE_STATE__": String(hasNestedStateCaptures)
|
|
295
|
-
})
|
|
256
|
+
const generated = generateBindingRuntime(await readFile(new URL("./binding-runtime.js", import.meta.url), "utf8"), capabilityIR, navigationRoutes.length > 0)
|
|
257
|
+
await writeBundledJavaScript(join(assetsDirectory, "kudzu-binding.js"), generated.source, minify, generated.define)
|
|
296
258
|
}
|
|
297
259
|
if (hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
|
|
298
|
-
if (
|
|
299
|
-
if (
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
? listRuntime.replace('"./binding-runtime.js"', '"./kudzu-binding.js"')
|
|
304
|
-
: listRuntime.replace(/^const loadListEvaluator[^\n]+\n/m, "")
|
|
305
|
-
listRuntime = hasCollectionSelectors
|
|
306
|
-
? listRuntime.replace('"./collection-selector.js"', '"./kudzu-collection-selector.js"')
|
|
307
|
-
: listRuntime.replace(/^import \{ selectCollection \}[^\n]+\n/m, "")
|
|
308
|
-
if (!hasListIndexes) listRuntime = listRuntime
|
|
309
|
-
.replace("for (const [index, item] of items.entries()) {", "for (const item of items) {")
|
|
310
|
-
.replace("const key = list.descriptor.key === null ? index : item?.[list.descriptor.key]", "const key = item?.[list.descriptor.key]")
|
|
311
|
-
.replace("entries.push({ item, index, key, token, value:", "entries.push({ item, key, token, value:")
|
|
312
|
-
.replace("for (const { item, index, key, token, value } of entries) {", "for (const { item, key, token, value } of entries) {")
|
|
313
|
-
.replaceAll("fillListItem(node, item, list.descriptor.nested, index)", "fillListItem(node, item, list.descriptor.nested)")
|
|
314
|
-
.replace("fillListItem(node, item, list.descriptor.nested, index, mapListItemParts", "fillListItem(node, item, list.descriptor.nested, 0, mapListItemParts")
|
|
315
|
-
.replace("function addListRoot(list, { item, index = list.roots.size, key, token, value })", "function addListRoot(list, { item, key, token, value })")
|
|
316
|
-
.replace("fillListParts(root, listItemParts(root), listItems.get(owner), 0, __KUDZU_LIST_INDEXES__ ? listIndexes.get(owner) ?? 0 : 0)", "fillListParts(root, listItemParts(root), listItems.get(owner), 0)")
|
|
317
|
-
.replace("function fillListItem(root, item, nested = false, index = 0)", "function fillListItem(root, item, nested = false)")
|
|
318
|
-
.replace("fillListParts(root, parts, item, revision, index, previous)", "fillListParts(root, parts, item, revision, previous)")
|
|
319
|
-
.replace("function fillListParts(root, parts, item, revision, index = 0, previous)", "function fillListParts(root, parts, item, revision, previous)")
|
|
320
|
-
.replace("fillListExpressions(root, parts, item, revision, index)", "fillListExpressions(root, parts, item, revision)")
|
|
321
|
-
.replaceAll('value?.type === "list-item" ? serializeItem(item) : value?.type === "list-index" ? index : value', 'value?.type === "list-item" ? serializeItem(item) : value')
|
|
322
|
-
.replaceAll("evaluate(descriptor, item, index)", "evaluate(descriptor, item)")
|
|
323
|
-
.replaceAll("evaluate({ module, handler }, item, index)", "evaluate({ module, handler }, item)")
|
|
324
|
-
.replace("updateListCondition(marker, descriptor.kind, value, item, index)", "updateListCondition(marker, descriptor.kind, value, item)")
|
|
325
|
-
.replace("function updateListCondition(marker, kind, value, item, index)", "function updateListCondition(marker, kind, value, item)")
|
|
326
|
-
.replace("fillListParts(marker, listItemParts(fragment), item, revision, index)", "fillListParts(marker, listItemParts(fragment), item, revision)")
|
|
327
|
-
.replace("function evaluate(descriptor, item, index)", "function evaluate(descriptor, item)")
|
|
328
|
-
.replace("exports[descriptor.handler](item, index)", "exports[descriptor.handler](item)")
|
|
329
|
-
.replace("exports[descriptor.handler](item, index, {", "exports[descriptor.handler](item, undefined, {")
|
|
330
|
-
if (!hasCollectionSelectors) listRuntime = listRuntime.replaceAll(" && !list.descriptor.selector", "")
|
|
331
|
-
if (!hasListIndexes) listRuntime = listRuntime
|
|
332
|
-
.replaceAll(" && !list.descriptor.indexed", "")
|
|
333
|
-
.replaceAll(" && list.descriptor.key !== null", "")
|
|
334
|
-
.replaceAll("list.descriptor.key !== null && !list.descriptor.indexed && ", "")
|
|
335
|
-
.replace("list.descriptor.key !== null && !list.descriptor.indexed && !list.descriptor.selector && list.values.size", "list.values.size")
|
|
336
|
-
.replace("(referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value) || list.descriptor.indexed || list.descriptor.key === null", "referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value")
|
|
337
|
-
if (hasListRowHooks && !hasGeneralListRowHooks) listRuntime = listRuntime
|
|
338
|
-
.replace(/\/\* general-row-hooks \*\/[\s\S]*?\/\* general-row-hooks-end \*\/\n/, "")
|
|
339
|
-
.replaceAll("initializeGeneralRowHooks", "initializeRowStates")
|
|
340
|
-
.replace("if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index], roots[index], nested?.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])")
|
|
341
|
-
.replaceAll("if (__KUDZU_LIST_ROW_HOOKS__) initializeRowStates(list.descriptor, key, node, list.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)")
|
|
342
|
-
.replace("for (const node of registration.list.roots.values()) deleteRowStates(registration.list.descriptor, ownershipPaths.get(node))", "for (const token of registration.list.roots.keys()) deleteFlatRowStates(registration.list.descriptor, token)")
|
|
343
|
-
.replaceAll("deleteRowStates(list.descriptor, ownershipPaths.get(node))", "deleteFlatRowStates(list.descriptor, token)")
|
|
344
|
-
.replace(" if (__KUDZU_LIST_ROW_HOOKS__) replaceRowIds(root, rowReplacements.get(root))\n", "")
|
|
345
|
-
.replace(" if (!replacements) return\n", "")
|
|
346
|
-
if (!hasItemDependencies) listRuntime = listRuntime.replace(", notifyListItem", "")
|
|
347
|
-
if (!hasListStableFastPaths) listRuntime = listRuntime.replace(/\/\* stable-list-fast-path \*\/[\s\S]*?\/\* stable-list-fast-path-end \*\/\n/, "")
|
|
348
|
-
const stylePatch = ` if (target === "style") {
|
|
349
|
-
const style = serializeStyle(value)
|
|
350
|
-
if (style) node.setAttribute("style", style)
|
|
351
|
-
else node.removeAttribute("style")
|
|
352
|
-
return
|
|
353
|
-
}`
|
|
354
|
-
listRuntime = listRuntime.replace(" /* list-style */", listStyleCount ? stylePatch : "")
|
|
355
|
-
if (listStyleCount) listRuntime = `import { serializeStyle } from "./kudzu-style.js"\n${listRuntime}`
|
|
356
|
-
await writeBundledJavaScript(join(assetsDirectory, "kudzu-list.js"), listRuntime, minify, {
|
|
357
|
-
__KUDZU_LIST_CONDITIONS__: String(hasListConditions),
|
|
358
|
-
__KUDZU_DEEP_LIST_CONDITIONS__: String(hasDeepListConditions),
|
|
359
|
-
__KUDZU_LIST_TEXT_RANGES__: String(hasListTextRanges),
|
|
360
|
-
__KUDZU_LIST_ATTRIBUTES__: String(hasListAttributes),
|
|
361
|
-
__KUDZU_LIST_EVENTS__: String(hasListEvents),
|
|
362
|
-
__KUDZU_LIST_EXPRESSIONS__: String(hasListExpressions),
|
|
363
|
-
__KUDZU_LIST_EXPRESSION_ATTRIBUTES__: String(hasListExpressionAttributes),
|
|
364
|
-
__KUDZU_LIST_SEEDS__: String(hasListSeeds),
|
|
365
|
-
__KUDZU_LIST_EFFECTS__: String(hasListEffects),
|
|
366
|
-
__KUDZU_LIST_ASYNC_PARTS__: String(hasListAsyncParts),
|
|
367
|
-
__KUDZU_LIST_MOUNTS__: String(hasListMounts),
|
|
368
|
-
__KUDZU_LIST_ITEM_HOOKS__: String(hasItemDependencies),
|
|
369
|
-
__KUDZU_LIST_ROW_HOOKS__: String(hasListRowHooks),
|
|
370
|
-
__KUDZU_LIST_ROW_REFS__: String(hasListRowRefs),
|
|
371
|
-
__KUDZU_COMPLEX_LIST_ROW_STATE__: String(hasComplexListRowState),
|
|
372
|
-
__KUDZU_NESTED_LISTS__: String(hasNestedLists),
|
|
373
|
-
__KUDZU_COLLECTION_SELECTORS__: String(hasCollectionSelectors),
|
|
374
|
-
__KUDZU_STATIC_COLLECTIONS__: String(hasStaticCollections),
|
|
375
|
-
__KUDZU_LIST_INDEXES__: String(hasListIndexes),
|
|
376
|
-
__KUDZU_LIST_STABLE_FAST_PATHS__: String(hasListStableFastPaths),
|
|
377
|
-
__KUDZU_SVG_LISTS__: String(hasSvgLists)
|
|
378
|
-
})
|
|
379
|
-
if (hasCollectionSelectors && !hasDerivedEffectDependencies) await rm(join(assetsDirectory, "kudzu-collection-selector.js"))
|
|
260
|
+
if (lists.count) {
|
|
261
|
+
if (lists.selectors && !hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
|
|
262
|
+
const generated = generateListRuntime(await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"), capabilityIR)
|
|
263
|
+
await writeBundledJavaScript(join(assetsDirectory, "kudzu-list.js"), generated.source, minify, generated.define)
|
|
264
|
+
if (lists.selectors && !hasDerivedEffectDependencies) await rm(join(assetsDirectory, "kudzu-collection-selector.js"))
|
|
380
265
|
}
|
|
381
266
|
if (hasNativeHandlers) {
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
.replace('"./serialization.js"', '"./kudzu-serialization.js"')
|
|
385
|
-
await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), specializeEvents(nativeRuntime, nativeEvents), minify, {
|
|
386
|
-
"globalThis.__KUDZU_CAPTURE_SETTER__": String(hasSetterCaptures)
|
|
387
|
-
})
|
|
267
|
+
const generated = generateNativeRuntime(await readFile(new URL("./native-runtime.js", import.meta.url), "utf8"), capabilityIR)
|
|
268
|
+
await writeJavaScript(join(assetsDirectory, "kudzu-native.js"), generated.source, minify, generated.define)
|
|
388
269
|
for (const entry of nativeEntries) await printNativeEntry(entry, assetsDirectory, base, minify)
|
|
389
270
|
}
|
|
390
271
|
if (navigationGroups.length) {
|
|
391
272
|
const navigationSource = await readFile(new URL("./navigation-runtime.js", import.meta.url), "utf8")
|
|
392
273
|
for (const group of navigationGroups) {
|
|
393
|
-
|
|
394
|
-
.replace("__KUDZU_NAVIGATION_ROUTES__", inlineJson(group.records))
|
|
395
|
-
.replace("__KUDZU_APPLICATION_ID__", JSON.stringify(group.applicationId))
|
|
396
|
-
.replace("__KUDZU_LAYOUT_ID__", JSON.stringify(group.layoutId))
|
|
397
|
-
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
398
|
-
navigationRuntime = specializeNavigationPatterns(navigationRuntime, group.records.some(record => record.segments))
|
|
399
|
-
await writeJavaScript(join(assetsDirectory, group.assetName), specializeNavigationEffects(navigationRuntime, group.hasEffects || group.hasParams), minify)
|
|
274
|
+
await writeJavaScript(join(assetsDirectory, group.assetName), generateNavigationRuntime(navigationSource, group), minify)
|
|
400
275
|
}
|
|
401
276
|
}
|
|
402
277
|
for (const handlerModule of emittedHandlerModules) {
|
|
@@ -483,64 +358,6 @@ function preloadModules(html) {
|
|
|
483
358
|
return html.replace(scripts[0][0], `${links}${scripts[0][0]}`)
|
|
484
359
|
}
|
|
485
360
|
|
|
486
|
-
function specializeEvents(source, events) {
|
|
487
|
-
return source.replace(/const eventNames = \[[^\n]+\]/, `const eventNames = ${JSON.stringify(events)}`)
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
function specializeNavigationEffects(source, enabled) {
|
|
491
|
-
if (enabled) return source
|
|
492
|
-
return source
|
|
493
|
-
.replace("const noDispose = async () => {}\nlet routeDispose = noDispose\nlet layoutDispose = noDispose\nconst ready = mountInitial()\n", "")
|
|
494
|
-
.replace(`addEventListener("pagehide", event => {
|
|
495
|
-
if (event.persisted) return
|
|
496
|
-
++revision
|
|
497
|
-
request?.abort()
|
|
498
|
-
void (async () => {
|
|
499
|
-
await routeDispose()
|
|
500
|
-
await layoutDispose()
|
|
501
|
-
})()
|
|
502
|
-
})
|
|
503
|
-
`, "")
|
|
504
|
-
.replace(`
|
|
505
|
-
async function mountInitial() {
|
|
506
|
-
try {
|
|
507
|
-
const record = matchRoute(location.pathname)
|
|
508
|
-
if (!record) throw new Error("Initial navigation route does not match")
|
|
509
|
-
const capabilities = await loadCapabilities(validate(document, record))
|
|
510
|
-
capabilities.params?.(location.pathname, location.search)
|
|
511
|
-
layoutDispose = await capabilities.effects?.mountLayoutEffects?.() ?? noDispose
|
|
512
|
-
routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose
|
|
513
|
-
} catch (error) {
|
|
514
|
-
console.error(error)
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
`, "")
|
|
518
|
-
.replace(" await ready\n", "")
|
|
519
|
-
.replace(" const { incoming, parsed, capabilities } = documentResult\n", " const { incoming, parsed } = documentResult\n")
|
|
520
|
-
.replace(" await routeDispose()\n if (current !== revision) return\n", "")
|
|
521
|
-
.replace(" commit(incoming, parsed.nodes, capabilities.params, url.pathname, url.search)\n", " commit(incoming, parsed.nodes)\n")
|
|
522
|
-
.replace(" routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose\n", "")
|
|
523
|
-
.replace(" return { incoming, parsed, capabilities: await loadCapabilities(parsed), record }\n", " await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))\n return { incoming, parsed, record }\n")
|
|
524
|
-
.replace(`
|
|
525
|
-
async function loadCapabilities(parsed) {
|
|
526
|
-
const modules = await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))
|
|
527
|
-
const params = modules.filter(module => typeof module.initializeParams === "function")
|
|
528
|
-
const effects = modules.filter(module => typeof module.mountRouteEffects === "function")
|
|
529
|
-
if (params.length > 1 || effects.length > 1) throw new Error("Navigation document has duplicate route capabilities")
|
|
530
|
-
return { params: params[0]?.initializeParams, effects: effects[0] }
|
|
531
|
-
}
|
|
532
|
-
`, "")
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
function specializeNavigationPatterns(source, enabled) {
|
|
536
|
-
if (enabled) return source
|
|
537
|
-
return source.replace(/function matchRoute\(pathname\) \{[\s\S]+?\n\}\n\nfunction fallback/, `function matchRoute(pathname) {
|
|
538
|
-
return routes.find(record => record.path === pathname)
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
function fallback`)
|
|
542
|
-
}
|
|
543
|
-
|
|
544
361
|
async function printNativeEntry(entry, assetsDirectory, base, minify) {
|
|
545
362
|
const output = join(assetsDirectory, entry.path)
|
|
546
363
|
await mkdir(dirname(output), { recursive: true })
|
|
@@ -566,83 +383,6 @@ function runtimeEffects(effects, lifetimes = false) {
|
|
|
566
383
|
}))
|
|
567
384
|
}
|
|
568
385
|
|
|
569
|
-
function printParamEntry(schema, params, searchParams, searchParamsWritable, output, assetsDirectory, base, runtimeName, navigable) {
|
|
570
|
-
const hasSearch = searchParams.length || searchParamsWritable
|
|
571
|
-
const signature = hasSearch ? "pathname, search" : "pathname"
|
|
572
|
-
const prefix = navigable ? `export function initializeParams(${signature}) {\n${searchParamsWritable ? "globalThis.__kSetSearchParams = setSearchParams\n" : ""}` : `${schema ? "let pathname = location.pathname\n" : ""}${hasSearch ? "let search = location.search\n" : ""}`
|
|
573
|
-
const suffix = navigable ? "\n}" : ""
|
|
574
|
-
const pathname = schema ? `const base = ${inlineJson(browserPath(base).slice(1).split("/").filter(Boolean).map(segment => decodeURIComponent(segment)))}
|
|
575
|
-
const schema = ${inlineJson(schema.segments)}
|
|
576
|
-
const params = ${inlineJson(params)}
|
|
577
|
-
let path = pathname
|
|
578
|
-
if (base.length) {
|
|
579
|
-
const pathSegments = path.slice(1).split("/")
|
|
580
|
-
if (pathSegments.length < base.length || base.some((segment, index) => decodeSegment(pathSegments[index], false) !== segment)) throw new Error("Runtime route is outside the configured base")
|
|
581
|
-
path = "/" + pathSegments.slice(base.length).join("/")
|
|
582
|
-
}
|
|
583
|
-
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1)
|
|
584
|
-
const segments = path.slice(1).split("/")
|
|
585
|
-
if (segments.length !== schema.length) throw new Error("Runtime route does not match its fallback pattern")
|
|
586
|
-
const values = Object.create(null)
|
|
587
|
-
for (let index = 0; index < schema.length; index++) {
|
|
588
|
-
const segment = schema[index]
|
|
589
|
-
const value = decodeSegment(segments[index], Boolean(segment.param))
|
|
590
|
-
if (segment.literal !== undefined && value !== segment.literal) throw new Error("Runtime route literal does not match")
|
|
591
|
-
if (segment.param) values[segment.param] = value
|
|
592
|
-
}
|
|
593
|
-
for (const param of params) {
|
|
594
|
-
const value = values[param.name]
|
|
595
|
-
browserState.set(param.id, value)
|
|
596
|
-
commitDom(param.id, value)
|
|
597
|
-
}
|
|
598
|
-
function decodeSegment(raw, param) {
|
|
599
|
-
if (param && /%(?:2f|5c)/i.test(raw)) throw new Error("Runtime route parameter contains an encoded separator")
|
|
600
|
-
let value
|
|
601
|
-
try { value = decodeURIComponent(raw) } catch { throw new Error("Runtime route parameter has malformed encoding") }
|
|
602
|
-
const decodedDots = value.replace(/%2e/gi, ".")
|
|
603
|
-
if (param && (!value || value === "." || value === ".." || decodedDots === "." || decodedDots === ".." || /[\\/?#]/.test(value) || [...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159) || /%(?:2f|5c)/i.test(value))) throw new Error("Runtime route parameter is invalid")
|
|
604
|
-
return value
|
|
605
|
-
}
|
|
606
|
-
` : ""
|
|
607
|
-
const searchInitializer = searchParamsWritable && searchParams.length ? `function initializeSearch(search) {
|
|
608
|
-
const query = new URLSearchParams(search)
|
|
609
|
-
for (const param of ${inlineJson(searchParams)}) {
|
|
610
|
-
const value = query.get(param.name)
|
|
611
|
-
browserState.set(param.id, value)
|
|
612
|
-
commitDom(param.id, value)
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
` : ""
|
|
616
|
-
const query = searchParams.length ? searchParamsWritable ? "initializeSearch(search)\n" : `const query = new URLSearchParams(search)
|
|
617
|
-
for (const param of ${inlineJson(searchParams)}) {
|
|
618
|
-
const value = query.get(param.name)
|
|
619
|
-
browserState.set(param.id, value)
|
|
620
|
-
commitDom(param.id, value)
|
|
621
|
-
}
|
|
622
|
-
` : ""
|
|
623
|
-
const writer = searchParamsWritable ? `
|
|
624
|
-
function setSearchParams(update, replace) {
|
|
625
|
-
const next = update(new URLSearchParams(location.search))
|
|
626
|
-
if (!(next instanceof URLSearchParams)) throw new Error("React Router search parameter updater must return URLSearchParams")
|
|
627
|
-
const url = new URL(location.href)
|
|
628
|
-
url.search = next.toString()
|
|
629
|
-
history[replace ? "replaceState" : "pushState"](null, "", url)
|
|
630
|
-
${searchParams.length ? "initializeSearch(location.search)" : ""}
|
|
631
|
-
}
|
|
632
|
-
${navigable ? "" : `globalThis.__kSetSearchParams = setSearchParams
|
|
633
|
-
addEventListener("popstate", () => ${searchParams.length ? "initializeSearch(location.search)" : "undefined"})`}` : ""
|
|
634
|
-
return `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}
|
|
635
|
-
${searchInitializer}${prefix}${pathname}${query}${suffix}${writer}`
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
export function specializeRuntime(source, events, hasStateSeed) {
|
|
639
|
-
const specialized = specializeEvents(source, events)
|
|
640
|
-
if (hasStateSeed) return specialized
|
|
641
|
-
return specialized
|
|
642
|
-
.replace(" const initialState = document.body.dataset.kState\n", "")
|
|
643
|
-
.replace(/^ if \(initialState\).*\n/m, "")
|
|
644
|
-
}
|
|
645
|
-
|
|
646
386
|
async function writeJavaScript(file, source, minify, define) {
|
|
647
387
|
const code = minify || define ? (await transform(source, { define, format: "esm", legalComments: "none", minify, target: "es2022" })).code : source
|
|
648
388
|
await writeFile(file, code)
|
|
@@ -3866,14 +3606,6 @@ function navigationDomainsOverlap(left, right) {
|
|
|
3866
3606
|
return left.segments.length === right.segments.length && left.segments.every((segment, index) => segment === null || right.segments[index] === null || segment === right.segments[index])
|
|
3867
3607
|
}
|
|
3868
3608
|
|
|
3869
|
-
function specializeNavigationTextDescriptors(source) {
|
|
3870
|
-
const dynamic = source
|
|
3871
|
-
.replace("const textDescriptors = globalThis.__KUDZU_TEXT_BINDINGS__ && typeof document !== \"undefined\" ? JSON.parse(document.body.dataset.kTextBindings ?? \"[]\") : []", "const textDescriptors = () => globalThis.__KUDZU_TEXT_BINDINGS__ ? JSON.parse(document.body.dataset.kTextBindings ?? \"[]\") : []")
|
|
3872
|
-
.replace("const descriptor = textDescriptors[Number(node.data.slice(\"k-text:\".length))]", "const descriptor = textDescriptors()[Number(node.data.slice(\"k-text:\".length))]")
|
|
3873
|
-
if (dynamic === source) throw new Error("Navigation text descriptor specialization did not match binding-runtime.js")
|
|
3874
|
-
return dynamic
|
|
3875
|
-
}
|
|
3876
|
-
|
|
3877
3609
|
function normalizeBase(value) {
|
|
3878
3610
|
if (value == null || value === "" || value === "/") return ""
|
|
3879
3611
|
if (typeof value !== "string" || !value.startsWith("/") || /[?#\0]/.test(value) || /%(?:2f|5c)/i.test(value)) throw new Error("kudzu.config base must be a root-relative path")
|
|
@@ -3906,6 +3638,7 @@ const workerCompiler = createWorkerCompiler({
|
|
|
3906
3638
|
})
|
|
3907
3639
|
|
|
3908
3640
|
const printEffectEntry = createEffectCodegen({ assetPath, inlineJson, relativeModulePath })
|
|
3641
|
+
const printParamEntry = createParamCodegen({ browserPath, inlineJson, relativeModulePath })
|
|
3909
3642
|
const handlerLowering = createHandlerLowering({ cloneAst, synthesizeTree })
|
|
3910
3643
|
const printHandlerModule = createHandlerCodegen({
|
|
3911
3644
|
resolveClientImport: (entry, handlerPath) => entry.package ? entry.target : relativeModulePath(handlerPath, clientModulePath(entry.target))
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { assertCapabilityIR } from "./route-capability-planner.mjs"
|
|
2
|
+
|
|
3
|
+
export function generateListRuntime(source, capabilityIR) {
|
|
4
|
+
assertCapabilityIR(capabilityIR)
|
|
5
|
+
const { lists, effects } = capabilityIR
|
|
6
|
+
let runtime = replaceRequired(source, '"./shared-runtime.js"', '"./kudzu.js"', "shared runtime import")
|
|
7
|
+
runtime = lists.calculated
|
|
8
|
+
? replaceRequired(runtime, '"./binding-runtime.js"', '"./kudzu-binding.js"', "binding runtime import")
|
|
9
|
+
: replaceRequired(runtime, /^const loadListEvaluator[^\n]+\n/m, "", "list evaluator loader")
|
|
10
|
+
runtime = lists.selectors
|
|
11
|
+
? replaceRequired(runtime, '"./collection-selector.js"', '"./kudzu-collection-selector.js"', "collection selector import")
|
|
12
|
+
: replaceRequired(runtime, /^import \{ selectCollection \}[^\n]+\n/m, "", "collection selector import")
|
|
13
|
+
if (!lists.indexes) {
|
|
14
|
+
runtime = replaceSequenceRequired(runtime, [
|
|
15
|
+
["for (const [index, item] of items.entries()) {", "for (const item of items) {", "item iteration"],
|
|
16
|
+
["const key = list.descriptor.key === null ? index : item?.[list.descriptor.key]", "const key = item?.[list.descriptor.key]", "index key"],
|
|
17
|
+
["entries.push({ item, index, key, token, value:", "entries.push({ item, key, token, value:", "indexed entry"],
|
|
18
|
+
["for (const { item, index, key, token, value } of entries) {", "for (const { item, key, token, value } of entries) {", "indexed entry loop"],
|
|
19
|
+
["fillListItem(node, item, list.descriptor.nested, index)", "fillListItem(node, item, list.descriptor.nested)", "indexed item fill", true],
|
|
20
|
+
["fillListItem(node, item, list.descriptor.nested, index, mapListItemParts", "fillListItem(node, item, list.descriptor.nested, 0, mapListItemParts", "mapped item fill"],
|
|
21
|
+
["function addListRoot(list, { item, index = list.roots.size, key, token, value })", "function addListRoot(list, { item, key, token, value })", "indexed list root"],
|
|
22
|
+
["fillListParts(root, listItemParts(root), listItems.get(owner), 0, __KUDZU_LIST_INDEXES__ ? listIndexes.get(owner) ?? 0 : 0)", "fillListParts(root, listItemParts(root), listItems.get(owner), 0)", "indexed root parts"],
|
|
23
|
+
["fillListParts(root, parts, item, revision, index, previous)", "fillListParts(root, parts, item, revision, previous)", "indexed fill call"],
|
|
24
|
+
["function fillListParts(root, parts, item, revision, index = 0, previous)", "function fillListParts(root, parts, item, revision, previous)", "indexed parts signature"],
|
|
25
|
+
["fillListExpressions(root, parts, item, revision, index)", "fillListExpressions(root, parts, item, revision)", "indexed expressions"],
|
|
26
|
+
['value?.type === "list-item" ? serializeItem(item) : value?.type === "list-index" ? index : value', 'value?.type === "list-item" ? serializeItem(item) : value', "index capture", true],
|
|
27
|
+
["evaluate(descriptor, item, index)", "evaluate(descriptor, item)", "indexed evaluator", true],
|
|
28
|
+
["updateListCondition(marker, descriptor.kind, value, item, index)", "updateListCondition(marker, descriptor.kind, value, item)", "indexed condition"],
|
|
29
|
+
["function updateListCondition(marker, kind, value, item, index)", "function updateListCondition(marker, kind, value, item)", "indexed condition signature"],
|
|
30
|
+
["fillListParts(marker, listItemParts(fragment), item, revision, index)", "fillListParts(marker, listItemParts(fragment), item, revision)", "indexed condition parts"],
|
|
31
|
+
["exports[descriptor.handler](item, index, {", "exports[descriptor.handler](item, undefined, {", "indexed state handler call"]
|
|
32
|
+
])
|
|
33
|
+
}
|
|
34
|
+
if (!lists.selectors) runtime = replaceRequired(runtime, " && !list.descriptor.selector", "", "selector guards", true)
|
|
35
|
+
if (!lists.indexes) runtime = replaceSequenceRequired(runtime, [
|
|
36
|
+
[" && !list.descriptor.indexed", "", "indexed guards", true],
|
|
37
|
+
[" && list.descriptor.key !== null", "", "key guards", true],
|
|
38
|
+
["(referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value) || list.descriptor.indexed || list.descriptor.key === null", "referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value", "indexed value comparison"]
|
|
39
|
+
])
|
|
40
|
+
if (lists.rowHooks && !lists.generalRowHooks) runtime = replaceSequenceRequired(replaceRequired(runtime, /\/\* general-row-hooks \*\/[\s\S]*?\/\* general-row-hooks-end \*\/\n/, "", "general row hooks"), [
|
|
41
|
+
["initializeGeneralRowHooks", "initializeRowStates", "general row initializer", true],
|
|
42
|
+
["if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index], roots[index], nested?.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && descriptor.rowStates) for (let index = 0; index < roots.length; index++) initializeRowStates(descriptor, descriptor.keys[index])", "flat row initialization"],
|
|
43
|
+
["if (__KUDZU_LIST_ROW_HOOKS__) initializeRowStates(list.descriptor, key, node, list.owner)", "if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)", "flat row add", true],
|
|
44
|
+
["for (const node of registration.list.roots.values()) deleteRowStates(registration.list.descriptor, ownershipPaths.get(node))", "for (const token of registration.list.roots.keys()) deleteFlatRowStates(registration.list.descriptor, token)", "flat registration cleanup"],
|
|
45
|
+
["deleteRowStates(list.descriptor, ownershipPaths.get(node))", "deleteFlatRowStates(list.descriptor, token)", "flat row cleanup", true],
|
|
46
|
+
[" if (__KUDZU_LIST_ROW_HOOKS__) replaceRowIds(root, rowReplacements.get(root))\n", "", "row ID replacement"],
|
|
47
|
+
[" if (!replacements) return\n", "", "row replacement guard"]
|
|
48
|
+
])
|
|
49
|
+
if (!effects.itemDependencies) runtime = replaceRequired(runtime, ", notifyListItem", "", "item notification import")
|
|
50
|
+
if (!lists.stableFastPaths) runtime = replaceRequired(runtime, /\/\* stable-list-fast-path \*\/[\s\S]*?\/\* stable-list-fast-path-end \*\/\n/, "", "stable list fast path")
|
|
51
|
+
const stylePatch = ` if (target === "style") {
|
|
52
|
+
const style = serializeStyle(value)
|
|
53
|
+
if (style) node.setAttribute("style", style)
|
|
54
|
+
else node.removeAttribute("style")
|
|
55
|
+
return
|
|
56
|
+
}`
|
|
57
|
+
runtime = replaceRequired(runtime, " /* list-style */", lists.styleCount ? stylePatch : "", "list style")
|
|
58
|
+
if (lists.styleCount) runtime = `import { serializeStyle } from "./kudzu-style.js"\n${runtime}`
|
|
59
|
+
return {
|
|
60
|
+
source: runtime,
|
|
61
|
+
define: {
|
|
62
|
+
__KUDZU_LIST_CONDITIONS__: String(lists.conditions),
|
|
63
|
+
__KUDZU_DEEP_LIST_CONDITIONS__: String(lists.deepConditions),
|
|
64
|
+
__KUDZU_LIST_TEXT_RANGES__: String(lists.textRanges),
|
|
65
|
+
__KUDZU_LIST_ATTRIBUTES__: String(lists.attributes),
|
|
66
|
+
__KUDZU_LIST_EVENTS__: String(lists.events),
|
|
67
|
+
__KUDZU_LIST_EXPRESSIONS__: String(lists.expressions),
|
|
68
|
+
__KUDZU_LIST_EXPRESSION_ATTRIBUTES__: String(lists.expressionAttributes),
|
|
69
|
+
__KUDZU_LIST_SEEDS__: String(lists.seeds),
|
|
70
|
+
__KUDZU_LIST_EFFECTS__: String(lists.effects),
|
|
71
|
+
__KUDZU_LIST_ASYNC_PARTS__: String(lists.asyncParts),
|
|
72
|
+
__KUDZU_LIST_MOUNTS__: String(lists.mounts),
|
|
73
|
+
__KUDZU_LIST_ITEM_HOOKS__: String(effects.itemDependencies),
|
|
74
|
+
__KUDZU_LIST_ROW_HOOKS__: String(lists.rowHooks),
|
|
75
|
+
__KUDZU_LIST_ROW_REFS__: String(lists.rowRefs),
|
|
76
|
+
__KUDZU_COMPLEX_LIST_ROW_STATE__: String(lists.complexRowState),
|
|
77
|
+
__KUDZU_NESTED_LISTS__: String(lists.nested),
|
|
78
|
+
__KUDZU_COLLECTION_SELECTORS__: String(lists.selectors),
|
|
79
|
+
__KUDZU_STATIC_COLLECTIONS__: String(lists.static),
|
|
80
|
+
__KUDZU_LIST_INDEXES__: String(lists.indexes),
|
|
81
|
+
__KUDZU_LIST_STABLE_FAST_PATHS__: String(lists.stableFastPaths),
|
|
82
|
+
__KUDZU_SVG_LISTS__: String(lists.svg)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function replaceRequired(source, search, replacement, label, all = false) {
|
|
88
|
+
const output = all ? source.replaceAll(search, replacement) : source.replace(search, replacement)
|
|
89
|
+
if (output === source) throw new Error(`${label} specialization did not match list-runtime.js`)
|
|
90
|
+
return output
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function replaceSequenceRequired(source, replacements) {
|
|
94
|
+
return replacements.reduce((output, [search, replacement, label, all]) => replaceRequired(output, search, replacement, label, all), source)
|
|
95
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { join } from "node:path"
|
|
2
|
+
|
|
3
|
+
export function createParamCodegen({ browserPath, inlineJson, relativeModulePath }) {
|
|
4
|
+
return function printParamEntry(schema, params, searchParams, searchParamsWritable, output, assetsDirectory, base, runtimeName, navigable) {
|
|
5
|
+
const hasSearch = searchParams.length || searchParamsWritable
|
|
6
|
+
const signature = hasSearch ? "pathname, search" : "pathname"
|
|
7
|
+
const prefix = navigable ? `export function initializeParams(${signature}) {\n${searchParamsWritable ? "globalThis.__kSetSearchParams = setSearchParams\n" : ""}` : `${schema ? "let pathname = location.pathname\n" : ""}${hasSearch ? "let search = location.search\n" : ""}`
|
|
8
|
+
const suffix = navigable ? "\n}" : ""
|
|
9
|
+
const pathname = schema ? `const base = ${inlineJson(browserPath(base).slice(1).split("/").filter(Boolean).map(segment => decodeURIComponent(segment)))}
|
|
10
|
+
const schema = ${inlineJson(schema.segments)}
|
|
11
|
+
const params = ${inlineJson(params)}
|
|
12
|
+
let path = pathname
|
|
13
|
+
if (base.length) {
|
|
14
|
+
const pathSegments = path.slice(1).split("/")
|
|
15
|
+
if (pathSegments.length < base.length || base.some((segment, index) => decodeSegment(pathSegments[index], false) !== segment)) throw new Error("Runtime route is outside the configured base")
|
|
16
|
+
path = "/" + pathSegments.slice(base.length).join("/")
|
|
17
|
+
}
|
|
18
|
+
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1)
|
|
19
|
+
const segments = path.slice(1).split("/")
|
|
20
|
+
if (segments.length !== schema.length) throw new Error("Runtime route does not match its fallback pattern")
|
|
21
|
+
const values = Object.create(null)
|
|
22
|
+
for (let index = 0; index < schema.length; index++) {
|
|
23
|
+
const segment = schema[index]
|
|
24
|
+
const value = decodeSegment(segments[index], Boolean(segment.param))
|
|
25
|
+
if (segment.literal !== undefined && value !== segment.literal) throw new Error("Runtime route literal does not match")
|
|
26
|
+
if (segment.param) values[segment.param] = value
|
|
27
|
+
}
|
|
28
|
+
for (const param of params) {
|
|
29
|
+
const value = values[param.name]
|
|
30
|
+
browserState.set(param.id, value)
|
|
31
|
+
commitDom(param.id, value)
|
|
32
|
+
}
|
|
33
|
+
function decodeSegment(raw, param) {
|
|
34
|
+
if (param && /%(?:2f|5c)/i.test(raw)) throw new Error("Runtime route parameter contains an encoded separator")
|
|
35
|
+
let value
|
|
36
|
+
try { value = decodeURIComponent(raw) } catch { throw new Error("Runtime route parameter has malformed encoding") }
|
|
37
|
+
const decodedDots = value.replace(/%2e/gi, ".")
|
|
38
|
+
if (param && (!value || value === "." || value === ".." || decodedDots === "." || decodedDots === ".." || /[\\/?#]/.test(value) || [...value].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) >= 127 && character.charCodeAt(0) <= 159) || /%(?:2f|5c)/i.test(value))) throw new Error("Runtime route parameter is invalid")
|
|
39
|
+
return value
|
|
40
|
+
}
|
|
41
|
+
` : ""
|
|
42
|
+
const searchInitializer = searchParamsWritable && searchParams.length ? `function initializeSearch(search) {
|
|
43
|
+
const query = new URLSearchParams(search)
|
|
44
|
+
for (const param of ${inlineJson(searchParams)}) {
|
|
45
|
+
const value = query.get(param.name)
|
|
46
|
+
browserState.set(param.id, value)
|
|
47
|
+
commitDom(param.id, value)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
` : ""
|
|
51
|
+
const query = searchParams.length ? searchParamsWritable ? "initializeSearch(search)\n" : `const query = new URLSearchParams(search)
|
|
52
|
+
for (const param of ${inlineJson(searchParams)}) {
|
|
53
|
+
const value = query.get(param.name)
|
|
54
|
+
browserState.set(param.id, value)
|
|
55
|
+
commitDom(param.id, value)
|
|
56
|
+
}
|
|
57
|
+
` : ""
|
|
58
|
+
const writer = searchParamsWritable ? `
|
|
59
|
+
function setSearchParams(update, replace) {
|
|
60
|
+
const next = update(new URLSearchParams(location.search))
|
|
61
|
+
if (!(next instanceof URLSearchParams)) throw new Error("React Router search parameter updater must return URLSearchParams")
|
|
62
|
+
const url = new URL(location.href)
|
|
63
|
+
url.search = next.toString()
|
|
64
|
+
history[replace ? "replaceState" : "pushState"](null, "", url)
|
|
65
|
+
${searchParams.length ? "initializeSearch(location.search)" : ""}
|
|
66
|
+
}
|
|
67
|
+
${navigable ? "" : `globalThis.__kSetSearchParams = setSearchParams
|
|
68
|
+
addEventListener("popstate", () => ${searchParams.length ? "initializeSearch(location.search)" : "undefined"})`}` : ""
|
|
69
|
+
return `import { browserState, commitDom } from ${JSON.stringify(relativeModulePath(output, join(assetsDirectory, runtimeName)))}
|
|
70
|
+
${searchInitializer}${prefix}${pathname}${query}${suffix}${writer}`
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export function usesRouteDependencyRuntime({ plan, navigable, hasBindings, hasLists }) {
|
|
2
|
+
assertRouteIR(plan)
|
|
2
3
|
const hasDependencies = plan.effects.some(effect => effect.dependencies?.length)
|
|
3
4
|
return !navigable && hasDependencies && !plan.effects.some(effect => effect.owner) && !hasBindings && !hasLists && !plan.events.some(event => event.native)
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export function planRouteCapabilities(plans, { routes = new Map(), navigationRouteCount = 0 } = {}) {
|
|
8
|
+
for (const plan of plans) assertRouteIR(plan)
|
|
7
9
|
const commandEvents = new Set()
|
|
8
10
|
const nativeEvents = new Set()
|
|
9
11
|
const bindings = { count: 0, text: false, svgConditions: false }
|
|
@@ -90,6 +92,7 @@ export function planRouteCapabilities(plans, { routes = new Map(), navigationRou
|
|
|
90
92
|
lists.mounts ||= lists.conditions || lists.nested
|
|
91
93
|
|
|
92
94
|
return {
|
|
95
|
+
version: 1,
|
|
93
96
|
routes: routeCounts,
|
|
94
97
|
events: { command: [...commandEvents].sort(), native: [...nativeEvents].sort(), hasNativeHandlers: nativeEvents.size > 0 },
|
|
95
98
|
bindings,
|
|
@@ -103,6 +106,38 @@ export function planRouteCapabilities(plans, { routes = new Map(), navigationRou
|
|
|
103
106
|
}
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
function assertRouteIR(plan) {
|
|
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) {
|
|
120
|
+
if (capabilityIR?.version !== 1) throw new Error(`Unsupported CapabilityIR version: ${JSON.stringify(capabilityIR?.version)}`)
|
|
121
|
+
const sections = ["routes", "events", "bindings", "lists", "effects", "captures", "runtime"]
|
|
122
|
+
if (!sections.every(name => isRecord(capabilityIR[name]))) throw new Error("Invalid CapabilityIR v1 structure")
|
|
123
|
+
if (!["behaviors", "regularBehaviors", "regularStateSeeds", "dependencyStateSeeds"].every(name => isCount(capabilityIR.routes[name]))) throw new Error("Invalid CapabilityIR v1 route counts")
|
|
124
|
+
if (!["command", "native"].every(name => Array.isArray(capabilityIR.events[name]) && capabilityIR.events[name].every(event => typeof event === "string")) || typeof capabilityIR.events.hasNativeHandlers !== "boolean") throw new Error("Invalid CapabilityIR v1 events")
|
|
125
|
+
if (!isCount(capabilityIR.bindings.count) || !["text", "svgConditions"].every(name => typeof capabilityIR.bindings[name] === "boolean")) throw new Error("Invalid CapabilityIR v1 bindings")
|
|
126
|
+
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
|
+
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
|
+
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")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const isRecord = value => value !== null && typeof value === "object" && !Array.isArray(value)
|
|
132
|
+
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
|
+
|
|
106
141
|
function hasCaptureType(value, type) {
|
|
107
142
|
if (!value || typeof value !== "object") return false
|
|
108
143
|
if (value.type === type) return true
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { assertCapabilityIR } from "./route-capability-planner.mjs"
|
|
2
|
+
|
|
3
|
+
export function generateCoreRuntime(source, capabilityIR) {
|
|
4
|
+
assertCapabilityIR(capabilityIR)
|
|
5
|
+
const { effects, events, routes } = capabilityIR
|
|
6
|
+
let runtime = specializeRuntime(source, events.command, routes.regularStateSeeds > 0)
|
|
7
|
+
if (!effects.itemDependencies && capabilityIR.runtime.shared) runtime = replaceRequired(runtime, /\/\* list-item-hooks \*\/[\s\S]*?\/\* list-item-hooks-end \*\/\n/, "", "list item hooks", "shared-runtime.js")
|
|
8
|
+
if (effects.navigable) runtime = replaceRequired(runtime, "export function registerCommitter(commit) {\n committers.push(commit)\n}", "export function registerCommitter(commit) {\n committers.push(commit)\n return () => {\n const index = committers.indexOf(commit)\n if (index !== -1) committers.splice(index, 1)\n }\n}", "navigable committer", "shared-runtime.js")
|
|
9
|
+
if (effects.navigableOwners) runtime = replaceSequenceRequired(runtime, [
|
|
10
|
+
["export function registerMountHook(mount) {\n mountHooks.push(mount)\n}", "export function registerMountHook(mount) {\n mountHooks.push(mount)\n return () => {\n const index = mountHooks.indexOf(mount)\n if (index !== -1) mountHooks.splice(index, 1)\n }\n}", "navigable mount hook"],
|
|
11
|
+
["export function registerUnmountHook(unmount) {\n unmountHooks.push(unmount)\n}", "export function registerUnmountHook(unmount) {\n unmountHooks.push(unmount)\n return () => {\n const index = unmountHooks.indexOf(unmount)\n if (index !== -1) unmountHooks.splice(index, 1)\n }\n}", "navigable unmount hook"]
|
|
12
|
+
], "shared-runtime.js")
|
|
13
|
+
return runtime
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function generateEffectRuntime(source, capabilityIR) {
|
|
17
|
+
assertCapabilityIR(capabilityIR)
|
|
18
|
+
const { captures, effects } = capabilityIR
|
|
19
|
+
return {
|
|
20
|
+
source: effects.captures ? replaceRequired(source, '"./serialization.js"', '"./kudzu-serialization.js"', "serialization import", "effect-runtime.js") : replaceRequired(source, /^import[^\n]+\n/, "", "serialization import", "effect-runtime.js"),
|
|
21
|
+
define: {
|
|
22
|
+
"globalThis.__KUDZU_CAPTURE_SETTER__": String(captures.setter),
|
|
23
|
+
"globalThis.__KUDZU_EFFECT_CAPTURES__": String(effects.captures)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function generateBindingRuntime(source, capabilityIR, navigable) {
|
|
29
|
+
assertCapabilityIR(capabilityIR)
|
|
30
|
+
let runtime = replaceRequired(source, '"./shared-runtime.js"', '"./kudzu.js"', "shared runtime import", "binding-runtime.js")
|
|
31
|
+
runtime = replaceRequired(runtime, '"./serialization.js"', '"./kudzu-serialization.js"', "serialization import", "binding-runtime.js")
|
|
32
|
+
runtime = replaceRequired(runtime, '"./style.js"', '"./kudzu-style.js"', "style import", "binding-runtime.js")
|
|
33
|
+
if (navigable) runtime = specializeNavigationTextDescriptors(runtime)
|
|
34
|
+
return {
|
|
35
|
+
source: runtime,
|
|
36
|
+
define: {
|
|
37
|
+
"globalThis.__KUDZU_TEXT_BINDINGS__": String(capabilityIR.bindings.text),
|
|
38
|
+
"globalThis.__KUDZU_SVG_CONDITIONS__": String(capabilityIR.bindings.svgConditions),
|
|
39
|
+
"globalThis.__KUDZU_CAPTURE_STATE__": String(capabilityIR.captures.nestedState)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function generateNativeRuntime(source, capabilityIR) {
|
|
45
|
+
assertCapabilityIR(capabilityIR)
|
|
46
|
+
return {
|
|
47
|
+
source: specializeEvents(replaceRequired(replaceRequired(source, '"./shared-runtime.js"', '"./kudzu.js"', "shared runtime import", "native-runtime.js"), '"./serialization.js"', '"./kudzu-serialization.js"', "serialization import", "native-runtime.js"), capabilityIR.events.native),
|
|
48
|
+
define: { "globalThis.__KUDZU_CAPTURE_SETTER__": String(capabilityIR.captures.setter) }
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function generateNavigationRuntime(source, group) {
|
|
53
|
+
let runtime = replaceSequenceRequired(source, [
|
|
54
|
+
["__KUDZU_NAVIGATION_ROUTES__", JSON.stringify(group.records).replaceAll("<", "\\u003c").replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029"), "route records"],
|
|
55
|
+
["__KUDZU_APPLICATION_ID__", JSON.stringify(group.applicationId), "application ID"],
|
|
56
|
+
["__KUDZU_LAYOUT_ID__", JSON.stringify(group.layoutId), "layout ID"],
|
|
57
|
+
['"./shared-runtime.js"', '"./kudzu.js"', "shared runtime import"]
|
|
58
|
+
], "navigation-runtime.js")
|
|
59
|
+
runtime = specializeNavigationPatterns(runtime, group.records.some(record => record.segments))
|
|
60
|
+
return specializeNavigationEffects(runtime, group.hasEffects || group.hasParams)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function specializeRuntime(source, events, hasStateSeed) {
|
|
64
|
+
const specialized = specializeEvents(source, events)
|
|
65
|
+
if (hasStateSeed) return specialized
|
|
66
|
+
return replaceSequenceRequired(specialized, [
|
|
67
|
+
[" const initialState = document.body.dataset.kState\n", "", "initial state read"],
|
|
68
|
+
[/^ if \(initialState\).*\n/m, "", "initial state parse"]
|
|
69
|
+
], "runtime source")
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function specializeEvents(source, events) {
|
|
73
|
+
return replaceRequired(source, /const eventNames = \[[^\n]+\]/, `const eventNames = ${JSON.stringify(events)}`, "event names", "runtime source")
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function replaceRequired(source, search, replacement, label, file) {
|
|
77
|
+
const output = source.replace(search, replacement)
|
|
78
|
+
if (output === source) throw new Error(`${label} specialization did not match ${file}`)
|
|
79
|
+
return output
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function replaceSequenceRequired(source, replacements, file) {
|
|
83
|
+
return replacements.reduce((output, [search, replacement, label]) => replaceRequired(output, search, replacement, label, file), source)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function specializeNavigationEffects(source, enabled) {
|
|
87
|
+
if (enabled) return source
|
|
88
|
+
return replaceSequenceRequired(source, [
|
|
89
|
+
["const noDispose = async () => {}\nlet routeDispose = noDispose\nlet layoutDispose = noDispose\nconst ready = mountInitial()\n", "", "initial effect lifecycle"],
|
|
90
|
+
[`addEventListener("pagehide", event => {
|
|
91
|
+
if (event.persisted) return
|
|
92
|
+
++revision
|
|
93
|
+
request?.abort()
|
|
94
|
+
void (async () => {
|
|
95
|
+
await routeDispose()
|
|
96
|
+
await layoutDispose()
|
|
97
|
+
})()
|
|
98
|
+
})
|
|
99
|
+
`, "", "pagehide effect lifecycle"],
|
|
100
|
+
[`
|
|
101
|
+
async function mountInitial() {
|
|
102
|
+
try {
|
|
103
|
+
const record = matchRoute(location.pathname)
|
|
104
|
+
if (!record) throw new Error("Initial navigation route does not match")
|
|
105
|
+
const capabilities = await loadCapabilities(validate(document, record))
|
|
106
|
+
capabilities.params?.(location.pathname, location.search)
|
|
107
|
+
layoutDispose = await capabilities.effects?.mountLayoutEffects?.() ?? noDispose
|
|
108
|
+
routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error(error)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
`, "", "initial effect mount"],
|
|
114
|
+
[" await ready\n", "", "initial effect readiness"],
|
|
115
|
+
[" const { incoming, parsed, capabilities } = documentResult\n", " const { incoming, parsed } = documentResult\n", "navigation capability result"],
|
|
116
|
+
[" await routeDispose()\n if (current !== revision) return\n", "", "route effect disposal"],
|
|
117
|
+
[" commit(incoming, parsed.nodes, capabilities.params, url.pathname, url.search)\n", " commit(incoming, parsed.nodes)\n", "navigation capability commit"],
|
|
118
|
+
[" routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose\n", "", "route effect mount"],
|
|
119
|
+
[" return { incoming, parsed, capabilities: await loadCapabilities(parsed), record }\n", " await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))\n return { incoming, parsed, record }\n", "navigation capability load"],
|
|
120
|
+
[`
|
|
121
|
+
async function loadCapabilities(parsed) {
|
|
122
|
+
const modules = await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))
|
|
123
|
+
const params = modules.filter(module => typeof module.initializeParams === "function")
|
|
124
|
+
const effects = modules.filter(module => typeof module.mountRouteEffects === "function")
|
|
125
|
+
if (params.length > 1 || effects.length > 1) throw new Error("Navigation document has duplicate route capabilities")
|
|
126
|
+
return { params: params[0]?.initializeParams, effects: effects[0] }
|
|
127
|
+
}
|
|
128
|
+
`, "", "capability loader"]
|
|
129
|
+
], "navigation-runtime.js")
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function specializeNavigationPatterns(source, enabled) {
|
|
133
|
+
if (enabled) return source
|
|
134
|
+
return replaceRequired(source, /function matchRoute\(pathname\) \{[\s\S]+?\n\}\n\nfunction fallback/, `function matchRoute(pathname) {
|
|
135
|
+
return routes.find(record => record.path === pathname)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function fallback`, "exact route matcher", "navigation-runtime.js")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function specializeNavigationTextDescriptors(source) {
|
|
142
|
+
return replaceSequenceRequired(source, [
|
|
143
|
+
["const textDescriptors = globalThis.__KUDZU_TEXT_BINDINGS__ && typeof document !== \"undefined\" ? JSON.parse(document.body.dataset.kTextBindings ?? \"[]\") : []", "const textDescriptors = () => globalThis.__KUDZU_TEXT_BINDINGS__ ? JSON.parse(document.body.dataset.kTextBindings ?? \"[]\") : []", "text descriptor reader"],
|
|
144
|
+
["const descriptor = textDescriptors[Number(node.data.slice(\"k-text:\".length))]", "const descriptor = textDescriptors()[Number(node.data.slice(\"k-text:\".length))]", "text descriptor lookup"]
|
|
145
|
+
], "binding-runtime.js")
|
|
146
|
+
}
|
package/framework/core.d.ts
CHANGED
|
@@ -131,6 +131,46 @@ export type ListDescriptor = {
|
|
|
131
131
|
valueSeed?: Record<string, string | number | boolean | null>
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
export interface RouteIR {
|
|
135
|
+
version: 1
|
|
136
|
+
states: Array<{ slot: number; id: string; name: string; initialValue: unknown; lifetime?: "layout" | "route"; internal?: true }>
|
|
137
|
+
params: Array<{ name: string; id: string }>
|
|
138
|
+
searchParams: Array<{ name: string; id: string }>
|
|
139
|
+
searchParamsWritable: boolean
|
|
140
|
+
events: Array<{
|
|
141
|
+
event: string
|
|
142
|
+
commands?: Array<[string, string, unknown]>
|
|
143
|
+
native?: { module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }
|
|
144
|
+
}>
|
|
145
|
+
effects: Array<{
|
|
146
|
+
module: string
|
|
147
|
+
handler: string
|
|
148
|
+
states: Record<string, string>
|
|
149
|
+
scope: Record<string, unknown>
|
|
150
|
+
lifetime?: "layout" | "route"
|
|
151
|
+
dependencies?: string[]
|
|
152
|
+
dependencyExpressions?: unknown[]
|
|
153
|
+
dependencyStates?: Record<string, string>
|
|
154
|
+
itemDependencies?: string[]
|
|
155
|
+
listState?: string
|
|
156
|
+
cleanup?: true
|
|
157
|
+
owner?: string
|
|
158
|
+
list?: true
|
|
159
|
+
}>
|
|
160
|
+
bindings: Array<{
|
|
161
|
+
target: string
|
|
162
|
+
state?: string
|
|
163
|
+
module?: string
|
|
164
|
+
handler?: string
|
|
165
|
+
states?: Record<string, string>
|
|
166
|
+
scope?: Record<string, unknown>
|
|
167
|
+
scopeStates?: Record<string, string>
|
|
168
|
+
scopeBindings?: Record<string, unknown>
|
|
169
|
+
}>
|
|
170
|
+
conditions: Array<Record<string, unknown>>
|
|
171
|
+
lists: ListDescriptor[]
|
|
172
|
+
}
|
|
173
|
+
|
|
134
174
|
export function renderPage<Props = Record<string, never>>(
|
|
135
175
|
component: (props: Props) => unknown | Promise<unknown>,
|
|
136
176
|
metadata?: PageMetadata,
|
|
@@ -146,42 +186,5 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
146
186
|
hasListStyles: boolean
|
|
147
187
|
hasStateSeed: boolean
|
|
148
188
|
handlerModules: string[]
|
|
149
|
-
plan:
|
|
150
|
-
states: Array<{ id: string; name: string; initialValue: unknown; lifetime?: "layout" | "route"; internal?: true }>
|
|
151
|
-
params: Array<{ name: string; id: string }>
|
|
152
|
-
searchParams: Array<{ name: string; id: string }>
|
|
153
|
-
searchParamsWritable: boolean
|
|
154
|
-
events: Array<{
|
|
155
|
-
event: string
|
|
156
|
-
commands?: Array<[string, string, unknown]>
|
|
157
|
-
native?: { module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown> }
|
|
158
|
-
}>
|
|
159
|
-
effects: Array<{
|
|
160
|
-
module: string
|
|
161
|
-
handler: string
|
|
162
|
-
states: Record<string, string>
|
|
163
|
-
scope: Record<string, unknown>
|
|
164
|
-
lifetime?: "layout" | "route"
|
|
165
|
-
dependencies?: string[]
|
|
166
|
-
dependencyExpressions?: unknown[]
|
|
167
|
-
dependencyStates?: Record<string, string>
|
|
168
|
-
itemDependencies?: string[]
|
|
169
|
-
listState?: string
|
|
170
|
-
cleanup?: true
|
|
171
|
-
owner?: string
|
|
172
|
-
list?: true
|
|
173
|
-
}>
|
|
174
|
-
bindings: Array<{
|
|
175
|
-
target: string
|
|
176
|
-
state?: string
|
|
177
|
-
module?: string
|
|
178
|
-
handler?: string
|
|
179
|
-
states?: Record<string, string>
|
|
180
|
-
scope?: Record<string, unknown>
|
|
181
|
-
scopeStates?: Record<string, string>
|
|
182
|
-
scopeBindings?: Record<string, unknown>
|
|
183
|
-
}>
|
|
184
|
-
conditions: Array<Record<string, unknown>>
|
|
185
|
-
lists: ListDescriptor[]
|
|
186
|
-
}
|
|
189
|
+
plan: RouteIR
|
|
187
190
|
}>
|
package/framework/core.mjs
CHANGED
|
@@ -527,7 +527,8 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
|
527
527
|
hasStateSeed: initialState.length > 0,
|
|
528
528
|
handlerModules: [...renderContext.handlerModules],
|
|
529
529
|
plan: {
|
|
530
|
-
|
|
530
|
+
version: 1,
|
|
531
|
+
states: Object.entries(renderContext.states).map(([id, state], slot) => ({ slot, id, ...state })),
|
|
531
532
|
params: renderContext.paramEntries,
|
|
532
533
|
searchParams: renderContext.searchParamEntries,
|
|
533
534
|
searchParamsWritable: renderContext.searchParamsWritable,
|