@kudzujs/core 0.14.3 → 0.16.1
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 +2 -1
- package/PERFORMANCE.md +98 -0
- package/README.md +2 -2
- package/RELEASES.md +70 -0
- package/docs/next-architecture/0.9-implementation-plan.md +2 -2
- package/docs/next-architecture/0.9-semantic-compression.md +3 -3
- package/docs/next-architecture/README.md +2 -2
- package/docs/next-architecture/application-capability-release-plan.md +119 -3
- package/docs/next-architecture/compiler-current-architecture.md +1 -1
- package/docs/next-architecture/large-application-ai-native-roadmap.md +4 -4
- package/docs/next-architecture/versioning.md +2 -2
- package/framework/README.md +1 -1
- package/framework/compiler/effect-private-ref-pass.mjs +62 -2
- package/framework/compiler/route-capability-planner.mjs +2 -2
- package/framework/compiler/route-ir.mjs +1 -1
- package/framework/compiler/source-compiler.mjs +34 -10
- package/framework/core.mjs +11 -8
- package/framework/list-runtime.js +4 -3
- package/package.json +3 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -51,7 +51,7 @@ Syntax compatibility does not mean reproducing React wholesale. Kudzu accepts th
|
|
|
51
51
|
- stronger JavaScript failure resilience;
|
|
52
52
|
- no remaining material large-route build-scaling weakness.
|
|
53
53
|
|
|
54
|
-
The
|
|
54
|
+
The completed 0.9 plan defines the cross-framework comparison and release gate. The active application packet is `0.16.2` in the capability release plan below. Benchmark-only feature omission, unmatched accessibility, weighted scores that hide losses, and unrecorded environment differences do not count as proof.
|
|
55
55
|
|
|
56
56
|
### 0.10.0 Through 0.21.x: Application Capability Release Train
|
|
57
57
|
|
|
@@ -128,6 +128,7 @@ The following are available building blocks, not future vertical roadmaps:
|
|
|
128
128
|
- `useState`, independent repeated non-keyed child state with conditional mount ownership, reduced relative-imported `useReducer`, direct dispatch specialization, and reduced Zustand-shaped shared state lowered through package-neutral shared-state/action IR and proven by migration fixtures.
|
|
129
129
|
- Mount and dependency effects with cleanup, route/layout lifetimes, conditional/keyed ownership, stale-write isolation, and relative TypeScript Workers.
|
|
130
130
|
- Direct browser-only package references in inline effect setup/cleanup callbacks bundle into route-owned effect ESM without entering build-time component execution or static sibling output.
|
|
131
|
+
- A CodeMirror-class editor may retain one package instance across one mount effect and later dependency update effects through a null-initialized ref with one direct setup assignment and null-reset cleanup. The ref lowers to existing owner-scoped object state; package DOM, bidirectional updates, errors, accessibility, conditional disposal, and fresh remount require no widget runtime or React island.
|
|
131
132
|
- Keyed local-state and imported static collections, pure selectors, nested direct-property lists, recursive and directly exported row specialization, static/keyed component reuse, analyzable specialized prop spreads, serializable literal defaults, direct intrinsic rest forwarding, forwarded JSX children, latest-item handlers, row state/effects/refs, and stable keyed identity.
|
|
132
133
|
- Direct object-state props with one-segment binding/effect/list consumers when the child directly maps an array field; ComponentAnalysis links every consumer to the parent SignalIR without authored field state or a browser component.
|
|
133
134
|
- Complete-document native fallback and optional shared-layout navigation groups with parameter initialization, finite prefetch caching, focus, history, cleanup, and independent capability output.
|
package/PERFORMANCE.md
CHANGED
|
@@ -2,6 +2,104 @@
|
|
|
2
2
|
|
|
3
3
|
Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm run benchmark:native`, `npm run benchmark:module-cache`, `npm run benchmark:project-navigation`, `npm run benchmark:project-state`, and `npm run benchmark:source-scale` are maintained in this repository; `npm run benchmark:commerce` is a maintained paired runner over the public external storefront; older excluded-workspace sections are historical provenance only and are not current framework rankings.
|
|
4
4
|
|
|
5
|
+
## 0.16.1 Retained Editor Ownership
|
|
6
|
+
|
|
7
|
+
Measured 2026-08-28 on Linux x64 with Node 24.14.0 and Chrome. The new
|
|
8
|
+
CodeMirror fixture emits 7 JavaScript files totaling 214,968 raw / 71,607
|
|
9
|
+
aggregate gzip bytes, while its static sibling emits zero JavaScript. These
|
|
10
|
+
bytes belong to the real editor package and existing selected effect/runtime
|
|
11
|
+
capabilities; no retained-ref runtime file or shared widget runtime was added.
|
|
12
|
+
|
|
13
|
+
Required Chrome proves initial content, retained package DOM across application
|
|
14
|
+
updates, editor-to-state updates, accessible error recovery, conditional
|
|
15
|
+
disposal/remount, and document disposal. The maintained Worker benchmark remains
|
|
16
|
+
byte-identical to 0.15.1 at 907 raw / 477 gzip B for the Worker graph and 14,456
|
|
17
|
+
raw / 6,159 gzip B for the window graph. The generated 50-route, 500-module
|
|
18
|
+
source-scale fixture retains compiler digest
|
|
19
|
+
`7c366079a984b3d62fb19a26305326d3278f029d2193d39340ddd3a4be5adc6e` and
|
|
20
|
+
output digest `e107d78a7f55bc8a1af0ea6e53efeffa19b3d44d21c892484d103fa346e7ba7b`.
|
|
21
|
+
Timing from this Linux host is recorded only as a local check and is not compared
|
|
22
|
+
with the prior macOS release sweep.
|
|
23
|
+
|
|
24
|
+
## 0.15.1 Maintained Benchmark Sweep
|
|
25
|
+
|
|
26
|
+
Remeasured 2026-08-27 on macOS arm64 with Node 24.14.0, Apple M4 hardware,
|
|
27
|
+
16 GiB RAM, and Chrome 151.0.7922.174. Candidate comparisons use clean
|
|
28
|
+
`v0.14.3` except project-state scale, whose maintained contract compares the
|
|
29
|
+
published `0.10.0` baseline. Every paired compiler/native/commerce output digest
|
|
30
|
+
or manifest is identical unless the row below records route-specific `0.15.1`
|
|
31
|
+
output.
|
|
32
|
+
|
|
33
|
+
| Maintained runner | Current `0.15.1` result |
|
|
34
|
+
|---|---|
|
|
35
|
+
| `npm run benchmark` | Worker-effects clean-build median 220.1 ms; Worker graph 907 raw / 477 gzip B; window graph 14,456 / 6,159 B. |
|
|
36
|
+
| `npm run benchmark:keyed` | 2,000-row build 233.2 ms; append 2.2 ms, filter 3.9 ms, restore 15.6 ms, reverse 5.3 ms medians; 30,048 raw / 11,748 gzip B JavaScript. |
|
|
37
|
+
| `RUNS=7 npm run benchmark:native` | `v0.14.3` / `0.15.1` dispatch medians 3.5 / 3.4 ms for 5,000 clicks; identical 14,354 raw / 6,470 gzip B JavaScript and zero changed files. |
|
|
38
|
+
| `RUNS=7 WARMUPS=1 npm run benchmark:module-cache` | `v0.14.3` / `0.15.1` medians 248.338 / 249.692 ms and 296.9 / 296.8 MiB peak RSS; identical 454,007-byte result and digest. |
|
|
39
|
+
| `npm run benchmark:source-scale` | 50 routes, 500 modules, 50,550 lines: `v0.14.3` / `0.15.1` compile medians 499.3 / 498.9 ms and clean builds 851.5 / 853.8 ms; identical compiler and output digests. |
|
|
40
|
+
| `RUNS=7 npm run benchmark:commerce` | 1,011-page storefront: `v0.14.3` / `0.15.1` clean-build medians 1,854.6 / 1,822.1 ms; all 1,053 files and 10,140,618 output bytes match. |
|
|
41
|
+
| `npm run benchmark:project-navigation` | Seven-profile table update median 0.4 ms and navigation median 2.3 ms; 17 session JavaScript files total 77,779 raw / 27,050 gzip B. |
|
|
42
|
+
| `npm run benchmark:project-state` | One-, eight-, and 32-state commit medians 0.2, 0.3, and 0.4 ms; deploy digest `3f83712254d95f1cf59c5034f363ea906406efa1bd9539f3bb8b9a0d54a3fdc5`. |
|
|
43
|
+
| `npm run benchmark:project-list-decision` | Direct load 138.2 ms with 90,023 nodes; pagination load/range 39.6/5.8 ms with 2,867 nodes; window load/range 40.7/5.8 ms with 2,868 nodes. |
|
|
44
|
+
|
|
45
|
+
The paired timing ranges overlap at module-cache and source-scale granularity,
|
|
46
|
+
so no compiler regression or improvement is claimed. Native output is byte
|
|
47
|
+
identical. Pagination remains selected over the timing-tied authored window: it
|
|
48
|
+
uses one listener instead of two, has lower median heap (1,485,168 versus
|
|
49
|
+
1,586,528 B), and preserves native page, focus, keyboard, and variable-row-height
|
|
50
|
+
behavior without fixed-height range policy.
|
|
51
|
+
|
|
52
|
+
## 0.15.1 Public Cross-Framework Commerce Fixture
|
|
53
|
+
|
|
54
|
+
Measured 2026-08-27 from public fixture commit
|
|
55
|
+
`f2d5be1a516c539e30f7125f6870d42b1dd02ecd` with 1,000 products and Kudzu
|
|
56
|
+
linked to this `0.15.1` checkout. The matched variants use Astro 7.1.3 with
|
|
57
|
+
React 19.2.8, React Router 8.3.0, TanStack Start 1.168.32, and Next.js 16.2.11.
|
|
58
|
+
All five builds completed before the same asset and browser harnesses ran.
|
|
59
|
+
|
|
60
|
+
| Variant | Initial JavaScript across six routes, gzip | Total output | First reliable click after first paint | Degraded capabilities |
|
|
61
|
+
|---|---:|---:|---:|---:|
|
|
62
|
+
| Kudzu 0.15.1 | 4.2-9.9 KiB | 9.42 MiB | 300 ms | 15 / 18 |
|
|
63
|
+
| Astro 7.1.3 + React | 60.6-61.1 KiB | 13.95 MiB | 1,500 ms | 12 / 18 |
|
|
64
|
+
| React Router 8.3.0 | 103.8-104.6 KiB | 7.04 MiB | 2,000 ms | 8 / 18 |
|
|
65
|
+
| TanStack Start 1.168.32 | 103.5-104.1 KiB | 9.87 MiB | 2,000 ms | 9 / 18 |
|
|
66
|
+
| Next.js 16.2.11 | 144.2-146.2 KiB | 35.76 MiB | 3,000 ms | 8 / 18 |
|
|
67
|
+
|
|
68
|
+
The initial-JavaScript column records what Chrome actually received for home,
|
|
69
|
+
search, collection, product, policy, and checkout routes. Browser results are
|
|
70
|
+
medians from seven fresh sessions under 4x CPU slowdown and Chrome Slow 4G.
|
|
71
|
+
Reliable click is the first tested delay with zero lost add-to-cart attempts in
|
|
72
|
+
seven isolated sessions. The resilience score covers six matched capabilities
|
|
73
|
+
under blocked JavaScript, a two-second script delay, and one missing script.
|
|
74
|
+
Pre-rendered content and native links remain usable without JavaScript in every
|
|
75
|
+
variant; the score does not treat read-only fallback as full interactivity.
|
|
76
|
+
|
|
77
|
+
This is the current executable public commerce comparison, not a replacement
|
|
78
|
+
for the broader historical 0.9 C1/C2/C5 contracts. That ignored local workspace
|
|
79
|
+
is unavailable in current checkouts, so its React, Vue, Svelte, and Astro result
|
|
80
|
+
remains provenance rather than a current ranking. The public commerce fixture
|
|
81
|
+
does not contain Vue or Svelte variants.
|
|
82
|
+
|
|
83
|
+
## 0.15.1 Key-Scoped Native Popover Intake
|
|
84
|
+
|
|
85
|
+
Measured 2026-08-27 on macOS arm64 with Node 24.14.0 and Chrome
|
|
86
|
+
151.0.7922.174. The maintained project fixture uses native Popover with one
|
|
87
|
+
key-scoped `useId()` relationship per project row. Required Chrome verifies
|
|
88
|
+
Escape, light dismiss, trigger focus restoration, reorder identity, inserted
|
|
89
|
+
and remounted IDs, and enhanced-navigation cleanup.
|
|
90
|
+
|
|
91
|
+
The application emits 44 files totaling 171,202 raw / 53,629 aggregate gzip
|
|
92
|
+
bytes with deploy SHA-256
|
|
93
|
+
`46ba45b4a423b9627c6ede5f8dd3a667f593cb5401454ae42ecc66207b07b323`.
|
|
94
|
+
The maintained two-route session contains 17 JavaScript files totaling 77,779
|
|
95
|
+
raw / 27,050 aggregate gzip bytes. `/help` remains 0 B JavaScript.
|
|
96
|
+
|
|
97
|
+
Seven fresh Chrome profiles record table update samples of `[0.4, 0.4, 0.5,
|
|
98
|
+
0.4, 0.5, 0.5, 0.5]` ms, a 0.5 ms median and 0.4/0.5 ms range. Navigation
|
|
99
|
+
samples are `[4, 2.4, 2.6, 2.5, 2.5, 2.8, 2.4]` ms, a 2.5 ms median and 2.4/4
|
|
100
|
+
ms range. These ranges overlap prior retained-path evidence; no timing
|
|
101
|
+
improvement or regression is claimed.
|
|
102
|
+
|
|
5
103
|
## 0.14.3 10,000-Item Browser Decision Intake
|
|
6
104
|
|
|
7
105
|
Measured 2026-08-27 on Linux x64 with Node 24.14.0 and Chrome
|
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
12
12
|
|
|
13
13
|
*Watch: React-shaped TSX to static HTML and route-specific ESM in 22 seconds.*
|
|
14
14
|
|
|
15
|
-
> Experimental `0.
|
|
15
|
+
> Experimental `0.15.x`: the compiler API and supported TSX surface may change.
|
|
16
16
|
|
|
17
|
-
**Latest release: 0.
|
|
17
|
+
**Latest release: 0.16.1 - Retained external editor ownership.** A CodeMirror-class editor can retain one package instance across mount and dependency effects while Kudzu keeps disposal, remount, errors, and static exclusion owner-bounded. Read the [release notes](./RELEASES.md#0161---retained-external-editor-ownership), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.16.1), 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,75 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.16.1 - Retained External Editor Ownership
|
|
4
|
+
|
|
5
|
+
Kudzu 0.16.1 lets one CodeMirror-class package instance remain owned across a
|
|
6
|
+
mount effect and later dependency update effects without a React island or
|
|
7
|
+
generic widget runtime.
|
|
8
|
+
|
|
9
|
+
### Changed in 0.16.1
|
|
10
|
+
|
|
11
|
+
- Lowers one proven unattached `useRef(null)` retained-instance handle to
|
|
12
|
+
existing owner-scoped object state.
|
|
13
|
+
- Requires one empty-dependency mount effect, one direct setup assignment,
|
|
14
|
+
direct null-reset cleanup, and read-only later dependency effects.
|
|
15
|
+
- Preserves editor DOM identity across application-state and editor-to-state
|
|
16
|
+
updates, including accessible package errors and recovery.
|
|
17
|
+
- Disposes on conditional and document release; remount receives a fresh handle.
|
|
18
|
+
- Rejects malformed cleanup, update-effect writes, multiple retained handles,
|
|
19
|
+
aliases, event use, and attached retained refs.
|
|
20
|
+
- Adds no IR kind, runtime concept, resource registry, React, or widget runtime.
|
|
21
|
+
- Updates `create-kudzu@0.1.127` to generate projects on
|
|
22
|
+
`@kudzujs/core@^0.16.1`.
|
|
23
|
+
|
|
24
|
+
### Output And Browser Evidence
|
|
25
|
+
|
|
26
|
+
- The CodeMirror fixture emits 7 JavaScript files totaling 214,968 raw /
|
|
27
|
+
71,607 aggregate gzip bytes; its static sibling emits zero JavaScript.
|
|
28
|
+
- Required Chrome proves initial mount, retained application updates,
|
|
29
|
+
editor-to-state updates, package-error recovery, disposal, and fresh remount.
|
|
30
|
+
- The smaller Typed.js boundary fixture proves the same existing effect owner
|
|
31
|
+
without any production compiler change.
|
|
32
|
+
|
|
33
|
+
### Upgrade
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
npm install @kudzujs/core@^0.16.1
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## 0.15.1 - Key-Scoped Native Popovers
|
|
40
|
+
|
|
41
|
+
Kudzu 0.15.1 lets ordinary keyed row components use `useId()` for native
|
|
42
|
+
Popover triggers and intrinsic ID relationships without duplicate cloned IDs.
|
|
43
|
+
|
|
44
|
+
### Changed in 0.15.1
|
|
45
|
+
|
|
46
|
+
- Key-scoped keyed-row `useId()` through the existing list ownership path.
|
|
47
|
+
- Rewrites cloned `id`, `htmlFor`, static ARIA IDREF, `form`, `headers`, `list`,
|
|
48
|
+
and `popovertarget` attributes.
|
|
49
|
+
- Rejects keyed IDs used in text, handlers, aliases, arbitrary attributes, or
|
|
50
|
+
component prop forwarding.
|
|
51
|
+
- Proves native Popover Escape, light dismiss, trigger focus restoration,
|
|
52
|
+
retained reorder identity, inserted and remounted IDs, and route cleanup.
|
|
53
|
+
- Adds no overlay runtime, focus manager, positioning engine, Portal/Slot
|
|
54
|
+
reconciliation, retained component tree, or public API.
|
|
55
|
+
- Updates `create-kudzu@0.1.126` to generate projects on
|
|
56
|
+
`@kudzujs/core@^0.15.1`.
|
|
57
|
+
|
|
58
|
+
### Output And Browser Evidence
|
|
59
|
+
|
|
60
|
+
- The maintained application emits 44 files totaling 171,202 raw / 53,629
|
|
61
|
+
aggregate gzip bytes; `/help` remains 0 B JavaScript.
|
|
62
|
+
- The two-route session remains 17 JavaScript files totaling 77,779 raw /
|
|
63
|
+
27,050 aggregate gzip bytes.
|
|
64
|
+
- Seven Chrome profiles record a 0.5 ms table-update median and 2.5 ms
|
|
65
|
+
navigation median, within prior retained-path ranges.
|
|
66
|
+
|
|
67
|
+
### Upgrade
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
npm install @kudzujs/core@^0.15.1
|
|
71
|
+
```
|
|
72
|
+
|
|
3
73
|
## 0.14.3 - 10,000-Item Browser Decision
|
|
4
74
|
|
|
5
75
|
Kudzu 0.14.3 measures direct DOM, pagination, and scroll-window alternatives
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Document Role
|
|
4
4
|
|
|
5
|
-
This document is the detailed delivery
|
|
5
|
+
This document is the completed detailed delivery record for [`0.9-semantic-compression.md`](./0.9-semantic-compression.md). It retains the executable sessions, artifacts, tests, measurements, reviews, and stop conditions that produced `0.9.0`; current execution follows the application capability release plan.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
`0.9.0` is released and this plan is historical. The current package version remains the version recorded in `package.json`; package metadata, release notes, public support claims, and generator ranges still change only through the active release procedure after every gate passes.
|
|
8
8
|
|
|
9
9
|
Use this document to answer:
|
|
10
10
|
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
This is the completed execution record after `0.8.62`. Every release gate in this document is satisfied by `0.9.0`; follow-on model-driven delivery work remains separately gated for 1.0.
|
|
6
6
|
|
|
7
|
-
The
|
|
7
|
+
The historical session-by-session decomposition, implementation seams, benchmark restoration work, report templates, and release procedure are retained in [`0.9-implementation-plan.md`](./0.9-implementation-plan.md). This document remains authoritative for the completed 0.9 direction, order, and release gates; current execution follows the application capability release plan.
|
|
8
8
|
|
|
9
|
-
Read documents in this order
|
|
9
|
+
Read documents in this order when reviewing the completed 0.9 work:
|
|
10
10
|
|
|
11
11
|
1. [`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) for product invariants and feature selection.
|
|
12
|
-
2. This document for the
|
|
12
|
+
2. This document for the completed 0.9 scope and work order.
|
|
13
13
|
3. [`0.9-implementation-plan.md`](./0.9-implementation-plan.md) for executable session packets and evidence.
|
|
14
14
|
4. [`compiler-current-architecture.md`](./compiler-current-architecture.md) for current owners and data flow.
|
|
15
15
|
5. [`performance-gates.md`](./performance-gates.md) for required measurements.
|
|
@@ -7,7 +7,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
|
|
|
7
7
|
| Goal | Decision | Start condition |
|
|
8
8
|
|---|---|---|
|
|
9
9
|
| A: compiler foundation | Complete in `0.8.23` | The no-write source compiler, RouteIR v1, CapabilityIR v1, generator boundaries, and output baseline are recorded. |
|
|
10
|
-
| B: optimization benchmarks |
|
|
10
|
+
| B: optimization benchmarks | Maintained gate | Keyed restoration, no-op normalization, exact route-entry transform reuse, and identical route-entry file sharing are retained; continue only when another current fixture reproduces a material loss. |
|
|
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
|
|
|
@@ -15,7 +15,7 @@ The completed `0.9.0` milestone is recorded in [`0.9-semantic-compression.md`](.
|
|
|
15
15
|
|
|
16
16
|
[`1.0-large-application-compatibility-audit.md`](./1.0-large-application-compatibility-audit.md) records the first post-0.9 probes against Memos, Apache Answer, and Actual Budget. The audit finds that reduced slices build but whole-application source retention and behavior parity do not yet pass.
|
|
17
17
|
|
|
18
|
-
[`application-capability-release-plan.md`](./application-capability-release-plan.md) is the authoritative post-0.9 execution queue. It assigns one application-capability section to each minor release and one independently accepted evidence packet to each patch release from `0.10.0` through the `1.0.0` gate. It supersedes the provisional 0.10/0.11/0.12 tool-first ordering in the completed 0.9 handoff without rewriting that historical record.
|
|
18
|
+
[`application-capability-release-plan.md`](./application-capability-release-plan.md) is the authoritative post-0.9 execution queue, currently at `0.16.2`. It assigns one application-capability section to each minor release and one independently accepted evidence packet to each patch release from `0.10.0` through the `1.0.0` gate. It supersedes the provisional 0.10/0.11/0.12 tool-first ordering in the completed 0.9 handoff without rewriting that historical record.
|
|
19
19
|
|
|
20
20
|
## Required Invariants
|
|
21
21
|
|
|
@@ -959,7 +959,7 @@ projects on `@kudzujs/core@^0.14.1`.
|
|
|
959
959
|
- **Done condition:** the full journey passes with explicit network and memory
|
|
960
960
|
bounds.
|
|
961
961
|
|
|
962
|
-
|
|
962
|
+
Published release evidence complete. The maintained project application adds one
|
|
963
963
|
intrinsic sentinel, an owned native `IntersectionObserver`, cursor and request
|
|
964
964
|
primitive state, an owned fetch with `AbortController`, immutable keyed append,
|
|
965
965
|
duplicate ID suppression, and explicit loading/error/retry/end UI. The authored
|
|
@@ -1030,6 +1030,13 @@ virtual range ownership.
|
|
|
1030
1030
|
- **Done condition:** range semantics are package-neutral, optional, measured,
|
|
1031
1031
|
and absent from non-virtual routes.
|
|
1032
1032
|
|
|
1033
|
+
Closed by stop condition. The `0.14.3` decision selected pagination over both
|
|
1034
|
+
direct 10,000-row DOM and the authored fixed-height scroll window. No independent
|
|
1035
|
+
application fixture proves pagination or application-owned bounded ranges
|
|
1036
|
+
insufficient, against the required three. Kudzu therefore adds no visible-range
|
|
1037
|
+
owner, measurement policy, range primitive, or virtualization runtime and moves
|
|
1038
|
+
to the next evidence-ready application capability.
|
|
1039
|
+
|
|
1033
1040
|
## `0.15.x`: Overlay And Layer Ownership
|
|
1034
1041
|
|
|
1035
1042
|
### `0.15.0`: Native Dialog Baseline
|
|
@@ -1040,6 +1047,32 @@ virtual range ownership.
|
|
|
1040
1047
|
- **Stop condition:** a custom modal/focus-trap runtime replaces `<dialog>`.
|
|
1041
1048
|
- **Done condition:** project delete and edit confirmation pass accessibly.
|
|
1042
1049
|
|
|
1050
|
+
Validation evidence is complete and the packet closed without consuming a release
|
|
1051
|
+
because no production compiler or runtime changed. The maintained
|
|
1052
|
+
project application replaces immediate keyed project deletion with one shared
|
|
1053
|
+
native `<dialog>`, one route-owned object ref, and one pending project ID state.
|
|
1054
|
+
Existing row callback specialization opens the browser top layer; native cancel,
|
|
1055
|
+
explicit cancel, and confirmation handlers close it without a Portal, focus trap,
|
|
1056
|
+
overlay runtime, or retained component tree.
|
|
1057
|
+
|
|
1058
|
+
Required Chrome proves modal top-layer behavior, native initial focus, explicit
|
|
1059
|
+
cancel, Escape-equivalent `requestClose()`, trigger-focus restoration, confirmed
|
|
1060
|
+
keyed deletion with fallback focus, retained sibling identity, and exact top-layer
|
|
1061
|
+
release on enhanced route navigation. The static `/help` sibling remains 0 B
|
|
1062
|
+
JavaScript. The slice adds no semantic primitive, IR kind, compiler pass,
|
|
1063
|
+
production compiler/runtime line, normalization rule, adapter, runtime concept,
|
|
1064
|
+
or public API. The application emits 44 files totaling 169,778 raw / 53,294
|
|
1065
|
+
aggregate gzip bytes with deploy SHA-256
|
|
1066
|
+
`91c5517e69c341bbd2051f07b3f0ad7df627098ba8e1984561268ea8666f3407`.
|
|
1067
|
+
The maintained two-route session remains 17 JavaScript files totaling 77,453 raw
|
|
1068
|
+
/ 26,905 aggregate gzip bytes.
|
|
1069
|
+
|
|
1070
|
+
Seven fresh macOS arm64 Chrome 151 profiles record table update samples of
|
|
1071
|
+
`[0.5, 0.6, 0.6, 0.6, 0.6, 0.4, 0.5]` ms, a 0.6 ms median and 0.4/0.6 ms range,
|
|
1072
|
+
plus navigation samples of `[2.3, 2.6, 2.9, 2.6, 2.5, 2.7, 2.5]` ms, a 2.6 ms
|
|
1073
|
+
median and 2.3/2.9 ms range. These retained-path ranges overlap the previous
|
|
1074
|
+
same-environment evidence, so no timing improvement or regression is claimed.
|
|
1075
|
+
|
|
1043
1076
|
### `0.15.1`: Popover, Dropdown, And Menu Behavior
|
|
1044
1077
|
|
|
1045
1078
|
- **Purpose:** establish the native-first non-modal overlay boundary.
|
|
@@ -1050,6 +1083,48 @@ virtual range ownership.
|
|
|
1050
1083
|
- **Stop condition:** package Portal/Slot reconciliation enters the runtime.
|
|
1051
1084
|
- **Done condition:** supported native and custom boundaries are explicit.
|
|
1052
1085
|
|
|
1086
|
+
Completed with the maintained project application as the qualifying fixture. A
|
|
1087
|
+
keyed project row now authors one top-level `useId()`, a native
|
|
1088
|
+
`popovertarget`, and a native `popover="auto"` action group. The compiler scopes
|
|
1089
|
+
the generated ID by the existing keyed ownership path and the list runtime
|
|
1090
|
+
rewrites only intrinsic `id`, `for`, static ARIA IDREF, `form`, `headers`,
|
|
1091
|
+
`list`, and `popovertarget` attributes. Unsupported ID escapes receive a source
|
|
1092
|
+
diagnostic. Native Chrome behavior supplies the top layer, Escape, light
|
|
1093
|
+
dismiss, and trigger focus restoration; ordinary keyed ownership preserves IDs
|
|
1094
|
+
across reorder, scopes inserted rows, recreates deterministic IDs after
|
|
1095
|
+
remove/re-add, and releases an open popover during enhanced navigation. The
|
|
1096
|
+
authored overlay uses static anchor positioning and `role="group"`, so menu
|
|
1097
|
+
roving focus is not applicable.
|
|
1098
|
+
|
|
1099
|
+
The slice adds no semantic primitive, IR kind, compiler pass, runtime concept,
|
|
1100
|
+
public API, Portal/Slot reconciliation, focus manager, positioning engine, or
|
|
1101
|
+
retained overlay tree. It adds 52 and removes 24 production lines across the
|
|
1102
|
+
source compiler, RouteIR validation, capability projection, build renderer, and
|
|
1103
|
+
existing list runtime. The maintained application emits 44 files totaling
|
|
1104
|
+
171,202 raw / 53,629 aggregate gzip bytes with deploy SHA-256
|
|
1105
|
+
`46ba45b4a423b9627c6ede5f8dd3a667f593cb5401454ae42ecc66207b07b323`.
|
|
1106
|
+
The two-route session remains 17 JavaScript files totaling 77,779 raw / 27,050
|
|
1107
|
+
aggregate gzip bytes, a 326 raw / 145 gzip byte increase from `0.15.0`; the
|
|
1108
|
+
static `/help` sibling remains 0 B JavaScript. Familiar React-shaped `useId()`
|
|
1109
|
+
authoring remains intact, so no application adapter or migration-only source
|
|
1110
|
+
structure is added.
|
|
1111
|
+
|
|
1112
|
+
Seven fresh macOS arm64 Chrome 151 profiles record table update samples of
|
|
1113
|
+
`[0.4, 0.4, 0.5, 0.4, 0.5, 0.5, 0.5]` ms, a 0.5 ms median and 0.4/0.5 ms range,
|
|
1114
|
+
plus navigation samples of `[4, 2.4, 2.6, 2.5, 2.5, 2.8, 2.4]` ms, a 2.5 ms
|
|
1115
|
+
median and 2.4/4 ms range. These retained-path ranges overlap prior evidence,
|
|
1116
|
+
so no timing improvement or regression is claimed.
|
|
1117
|
+
|
|
1118
|
+
Release transaction complete at commit
|
|
1119
|
+
`2dd5c409d3553cdf0b04080f1ba7c39b4541c523` and immutable tag `v0.15.1`.
|
|
1120
|
+
CI run `33069675599` passed both jobs. The GitHub release is
|
|
1121
|
+
[`v0.15.1`](https://github.com/kudzujs/kudzu/releases/tag/v0.15.1), and publish
|
|
1122
|
+
workflow run `33071439012` published and verified `@kudzujs/core@0.15.1` and
|
|
1123
|
+
`create-kudzu@0.1.126`. A fresh registry install imported `useId()` from core
|
|
1124
|
+
and confirmed both exact package versions. The first publish attempt stopped at
|
|
1125
|
+
the existing concurrent Chrome timing gate before any publish step; the exact
|
|
1126
|
+
same immutable tag passed on the release-event retry.
|
|
1127
|
+
|
|
1053
1128
|
### `0.15.2`: Toast And Notification Ownership
|
|
1054
1129
|
|
|
1055
1130
|
- **Purpose:** provide application feedback across route mutations.
|
|
@@ -1061,6 +1136,15 @@ virtual range ownership.
|
|
|
1061
1136
|
- **Done condition:** notifications remain owner-bounded and absent from unused
|
|
1062
1137
|
route output.
|
|
1063
1138
|
|
|
1139
|
+
**Result:** closed by ordinary layout composition. The maintained project
|
|
1140
|
+
application now owns one deduplicated notification queue in layout state, renders
|
|
1141
|
+
keyed rows in a native polite live region, and uses one cleanup-owned sequential
|
|
1142
|
+
timeout plus explicit dismissal. Required Chrome preserves the live-region and
|
|
1143
|
+
row identities through enhanced route replacement, proves timeout removal and
|
|
1144
|
+
disposal, and keeps `/help` at zero JavaScript. The 45-file deploy totals 193,770
|
|
1145
|
+
raw / 60,227 aggregate gzip bytes. No scheduler, primitive, runtime concept, or
|
|
1146
|
+
production source changed, so no release was consumed.
|
|
1147
|
+
|
|
1064
1148
|
### `0.15.3`: Layer Owner Decision
|
|
1065
1149
|
|
|
1066
1150
|
- **Purpose:** decide whether logical ownership of DOM rendered outside its
|
|
@@ -1071,6 +1155,13 @@ virtual range ownership.
|
|
|
1071
1155
|
- **Done condition:** no new concept is added, or a minimal owner edge with exact
|
|
1072
1156
|
cleanup/focus order is approved.
|
|
1073
1157
|
|
|
1158
|
+
**Result:** closed with no owner edge. Native dialog and Popover already satisfy
|
|
1159
|
+
modal and non-modal top-layer behavior, while maintained dropdown, tooltip,
|
|
1160
|
+
notification, and menu fixtures compose in their authored ranges. Zero fixtures
|
|
1161
|
+
fail because DOM must render outside its authored range, below the required three
|
|
1162
|
+
independent failures. Portal source compatibility alone remains insufficient;
|
|
1163
|
+
no production source or release changed.
|
|
1164
|
+
|
|
1074
1165
|
## `0.16.x`: External Complex UI
|
|
1075
1166
|
|
|
1076
1167
|
### `0.16.0`: Bounded External UI Contract
|
|
@@ -1084,6 +1175,12 @@ virtual range ownership.
|
|
|
1084
1175
|
- **Done condition:** package ownership remains inside effect/handler ESM and no
|
|
1085
1176
|
generic widget runtime is added.
|
|
1086
1177
|
|
|
1178
|
+
**Result:** closed through existing effect ownership. A real Typed.js instance
|
|
1179
|
+
creates DOM from one attached host ref, replaces on application-state dependency
|
|
1180
|
+
change, disposes on conditional/document release, and remounts fresh. The package
|
|
1181
|
+
is absent from the complete zero-JavaScript static sibling. No compiler, IR,
|
|
1182
|
+
runtime concept, or production source changed, so no release was consumed.
|
|
1183
|
+
|
|
1087
1184
|
### `0.16.1`: Real Editor Lifecycle
|
|
1088
1185
|
|
|
1089
1186
|
- **Purpose:** integrate one real CodeMirror-class editor.
|
|
@@ -1093,6 +1190,18 @@ virtual range ownership.
|
|
|
1093
1190
|
- **Done condition:** the native package owns its DOM and Kudzu owns only its
|
|
1094
1191
|
bounded lifecycle.
|
|
1095
1192
|
|
|
1193
|
+
**Result:** complete. The Apache Answer-derived CodeMirror fixture exposes the
|
|
1194
|
+
first real retained-instance failure: an unattached `useRef(null)` shared by one
|
|
1195
|
+
mount/cleanup effect and a later dependency update effect was incorrectly treated
|
|
1196
|
+
as a DOM ref. Kudzu now lowers exactly one proven retained handle to existing
|
|
1197
|
+
owner-scoped object state. Required Chrome proves initial value, retained
|
|
1198
|
+
application updates, editor-to-state updates, package-error recovery, accessible
|
|
1199
|
+
textbox naming, conditional disposal, fresh remount, and document disposal. The
|
|
1200
|
+
fixture emits 214,968 raw / 71,607 aggregate gzip JavaScript bytes while its
|
|
1201
|
+
static sibling emits zero. One existing normalization pass grows; semantic
|
|
1202
|
+
concepts, IR kinds, runtime concepts, runtime files, and public APIs remain
|
|
1203
|
+
unchanged.
|
|
1204
|
+
|
|
1096
1205
|
### `0.16.2`: Chart And Map Lifecycle
|
|
1097
1206
|
|
|
1098
1207
|
- **Purpose:** validate the same contract against unrelated canvas/SVG/map DOM.
|
|
@@ -1477,5 +1586,12 @@ release transaction where possible or document and publish a forward-fix patch.
|
|
|
1477
1586
|
| `0.14.0` | Released | Preserve native table CRUD, retained identity, keyboard access, and measured update evidence. | None |
|
|
1478
1587
|
| `0.14.1` | Released | Preserve immutable object-state replacement, nested keyed identity, latest handlers, and exact row release. | None |
|
|
1479
1588
|
| `0.14.2` | Released | Preserve bounded cursor loading, duplicate suppression, retry/end behavior, retained identity, and route cleanup. | None |
|
|
1480
|
-
| `0.14.3` |
|
|
1481
|
-
| `0.14.4`
|
|
1589
|
+
| `0.14.3` | Released | Preserve the published 10,000-item pagination decision and measurement evidence. | None |
|
|
1590
|
+
| `0.14.4` | Closed by stop condition | Preserve pagination as the selected strategy until three independent fixtures authorize virtual ownership review. | Evidence favors pagination; zero qualifying fixtures |
|
|
1591
|
+
| `0.15.0` | Closed by native composition | Preserve the native dialog, focus, cancellation, keyed deletion, and route cleanup evidence. | No production change; no release consumed |
|
|
1592
|
+
| `0.15.1` | Released | Preserve key-scoped IDs, native Popover behavior, retained keyed identity, and route cleanup evidence. | None |
|
|
1593
|
+
| `0.15.2` | Closed by layout composition | Preserve owner-bounded queue, deduplication, dismissal, route persistence, live-region, and timer cleanup evidence. | No production change; no release consumed |
|
|
1594
|
+
| `0.15.3` | Closed by stop condition | Preserve native top-layer and in-place composition until three independent authored-range failures exist. | Zero qualifying failures; no release consumed |
|
|
1595
|
+
| `0.16.0` | Closed by existing effect ownership | Preserve real package mount, replacement, disposal, remount, and static exclusion evidence. | No production change; no release consumed |
|
|
1596
|
+
| `0.16.1` | Released | Preserve the retained CodeMirror instance boundary, bidirectional updates, error recovery, accessibility, and exact cleanup. | None |
|
|
1597
|
+
| `0.16.2` | Active | Validate the same lifecycle against one unrelated real chart or map package. | None |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Current Compiler Architecture
|
|
2
2
|
|
|
3
|
-
This maps the current `0.
|
|
3
|
+
This maps the current `0.16.1` architecture, built on the completed `0.9.0` semantic-compression release and `0.8.23` Goal A compiler foundation. The active application packet is `0.16.2`; file and function names are the stable references, while line numbers are intentionally omitted because later work may still move code.
|
|
4
4
|
|
|
5
5
|
## Responsibility Map
|
|
6
6
|
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
## Status
|
|
4
4
|
|
|
5
|
-
Completed compiler-foundation record and longer-term plan after `0.8.35`. The
|
|
5
|
+
Completed compiler-foundation record and longer-term plan after `0.8.35`. The [`0.9-semantic-compression.md`](./0.9-semantic-compression.md) execution queue is complete. The active queue for current work is [`application-capability-release-plan.md`](./application-capability-release-plan.md) at packet `0.16.2`. This document does not mark any remaining planned capability as supported and does not authorize a React runtime, VDOM, hydration, retained browser component tree, generic rerenderer, public store/query/resource API, SPA router, or islands.
|
|
6
6
|
|
|
7
|
-
[`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains
|
|
7
|
+
[`MIGRATION_ROADMAP.md`](../../MIGRATION_ROADMAP.md) remains the product authority for product invariants and fixture-driven feature selection. [`application-capability-release-plan.md`](./application-capability-release-plan.md) is authoritative for current work order and evidence at packet `0.16.2`; this plan retains the completed foundation, deferred program, and long-term production gates. If implementation evidence changes either boundary, update the relevant document before broadening a patch.
|
|
8
8
|
|
|
9
9
|
## Product Outcome
|
|
10
10
|
|
|
@@ -453,7 +453,7 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
453
453
|
|
|
454
454
|
## Production Gates Before 1.0
|
|
455
455
|
|
|
456
|
-
- The
|
|
456
|
+
- The completed `0.9.0` cross-framework gate proves lower browser cost and matched user-facing performance against React + Vite, Vue, Svelte, and Astro before AI productivity claims are considered.
|
|
457
457
|
- The maintained AI delivery suite uses the same model, tools, requirements, budgets, and acceptance checks across frameworks, includes failed attempts, and establishes the highest success rate plus lowest median cost per successful task before `1.0.0`.
|
|
458
458
|
- Async native and effect work cannot write after ownership release.
|
|
459
459
|
- Build output is staged, collision-safe, and rollback/recovery guarded.
|
|
@@ -483,4 +483,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
483
483
|
|
|
484
484
|
## Immediate Decision
|
|
485
485
|
|
|
486
|
-
|
|
486
|
+
All listed foundation and `0.9` slices are complete. Current work continues at `0.16.2` Chart And Map Lifecycle under the [`application-capability-release-plan.md`](./application-capability-release-plan.md); keep ResourceIR limited to qualifying independent fixtures, and do not add range ownership, virtualization, optimistic transactions, a public adapter/store API, or a router before evidence justifies them.
|
|
@@ -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.62` are completed scopes represented by package/release records. The
|
|
3
|
+
This is an execution sequence, not release history. `0.8.16` through `0.8.62` are completed scopes represented by package/release records. The `0.10.0` through `1.0.0` minor/patch sequence is maintained in [`application-capability-release-plan.md`](./application-capability-release-plan.md), currently at `0.16.2`; that plan supersedes the provisional tool-first 0.10/0.11/0.12 ordering in the completed 0.9 handoff.
|
|
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
|
|
|
@@ -66,7 +66,7 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
66
66
|
|
|
67
67
|
## Generator Versions
|
|
68
68
|
|
|
69
|
-
`create-kudzu@0.1.
|
|
69
|
+
`create-kudzu@0.1.127` retains the explicit install instructions and generates projects with `@kudzujs/core@^0.16.1`.
|
|
70
70
|
|
|
71
71
|
## Release Boundary
|
|
72
72
|
|
package/framework/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Kudzu specializes ordinary common React-shaped TSX so migrations need minimal source restructuring. Declarative components, collection pipelines, conditions, hooks, and handlers should be lowered at build time rather than replaced with application-owned imperative DOM code. This principle applies across migrations and is not Stay-specific; it does not imply a React package, VDOM, hydration, or ecosystem runtime.
|
|
4
4
|
|
|
5
|
-
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, same-file `memo`, inline `useCallback`, direct-state expression or analyzable collection-pipeline `useMemo`, direct intrinsic `forwardRef`, top-level `const` identifiers initialized by `useId()`, and default, namespace, or named `Fragment`. A named or default zero-argument custom hook imported from a relative TypeScript module may return direct shorthand state/setter pairs and callbacks that capture those states, including a callback that resets multiple states to direct primitive literals; callers use one top-level `const` object destructuring without aliases, defaults, or rest. Relative Context-hook actions may require exposed Provider state/setter fields without forcing those hidden fields into public consumer destructuring; compiler-owned collision-free aliases preserve same-named consumer locals. Kudzu's JSX declarations accept ReactNode-shaped component returns and contextually type common intrinsic DOM events, so strict React component props do not need migration-only `unknown` or explicit event annotations. `forwardRef()` accepts one inline synchronous `(props, ref)` function and requires the object ref exactly once on its direct intrinsic root; the compiler removes `ref` from props/rest and erases the wrapper. `useId()` becomes a deterministic build-time HTML ID and emits no browser capability; keyed rows
|
|
5
|
+
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, same-file `memo`, inline `useCallback`, direct-state expression or analyzable collection-pipeline `useMemo`, direct intrinsic `forwardRef`, top-level `const` identifiers initialized by `useId()`, and default, namespace, or named `Fragment`. A named or default zero-argument custom hook imported from a relative TypeScript module may return direct shorthand state/setter pairs and callbacks that capture those states, including a callback that resets multiple states to direct primitive literals; callers use one top-level `const` object destructuring without aliases, defaults, or rest. Relative Context-hook actions may require exposed Provider state/setter fields without forcing those hidden fields into public consumer destructuring; compiler-owned collision-free aliases preserve same-named consumer locals. Kudzu's JSX declarations accept ReactNode-shaped component returns and contextually type common intrinsic DOM events, so strict React component props do not need migration-only `unknown` or explicit event annotations. `forwardRef()` accepts one inline synchronous `(props, ref)` function and requires the object ref exactly once on its direct intrinsic root; the compiler removes `ref` from props/rest and erases the wrapper. `useId()` becomes a deterministic build-time HTML ID and emits no browser capability; keyed rows key-scope direct intrinsic ID and ID-reference attributes through existing list ownership by row key. Collection memos may start from local array state or a named relative import of an exported JSON-safe `const` array, including type-only `as const` and `satisfies` wrappers, and may read direct local state declared in their dependency array. `compiler/react-migration-pass.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation. Memo wrappers are erased or inlined into existing bindings and keyed-list selectors because no browser component rerender or memo cache exists. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports.
|
|
6
6
|
|
|
7
7
|
Compilation begins from page entries and follows relative runtime imports, re-exports, and validated Worker references; unreachable TypeScript migration files are not transformed. Direct maps over imported immutable JSON-safe arrays fold to literals for zero-JavaScript static rows. Synchronous relative calculation functions may return objects whose direct static fields feed reactive JSX bindings; build rendering uses current signal values and route-specific binding ESM reevaluates the same helper after state commits. One direct array field may instead feed a keyed intrinsic map: its evaluator refreshes a compiler-owned array anchor before the existing list reconciler runs, preserving keyed DOM and SVG identity without a calculation runtime. That field must remain a JSON-safe array after every source-state commit. Package imports have a separate narrow boundary: direct references inside intrinsic JSX event callbacks are erased from build modules and bundled into route handler ESM, while render-time, effect, helper-indirect, and mixed package use fails.
|
|
8
8
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from "typescript"
|
|
2
|
-
import { effectReturns, importDeclarationNames, isNodeWithin, isShadowedIdentifier, nearestFunction, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
2
|
+
import { effectReturns, importDeclarationNames, isNodeWithin, isShadowedIdentifier, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
3
3
|
|
|
4
4
|
export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
|
|
5
5
|
const frameCall = (node, name) => ts.isCallExpression(node) && (
|
|
@@ -24,8 +24,10 @@ export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
|
|
|
24
24
|
}
|
|
25
25
|
const hasUseRefImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useRef"))
|
|
26
26
|
const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
|
|
27
|
+
const hasUseStateImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useState"))
|
|
27
28
|
const privateRefs = new Map()
|
|
28
29
|
const effectRefs = new Map()
|
|
30
|
+
const retainedRefs = new Map()
|
|
29
31
|
const registerPrivateRef = (node, callback) => {
|
|
30
32
|
const initializer = ts.isNumericLiteral(node.initializer.arguments[0]) ? factory.createNumericLiteral(0) : factory.createNull()
|
|
31
33
|
privateRefs.set(node, callback)
|
|
@@ -42,20 +44,60 @@ export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
|
|
|
42
44
|
const owner = nearestFunction(node)
|
|
43
45
|
if (!owner?.body || !ts.isBlock(owner.body)) return
|
|
44
46
|
const references = referenceIdentifiers(owner.body, node.name.text)
|
|
47
|
+
const attachedToJsx = references.some(reference => ts.isJsxExpression(reference.parent) && ts.isJsxAttribute(reference.parent.parent) && reference.parent.parent.name.text === "ref")
|
|
45
48
|
const invalidReference = references.find(reference => !ts.isPropertyAccessExpression(reference.parent) || reference.parent.expression !== reference || reference.parent.name.text !== "current")
|
|
46
49
|
const accesses = references.filter(reference => ts.isPropertyAccessExpression(reference.parent) && reference.parent.expression === reference && reference.parent.name.text === "current").map(reference => reference.parent)
|
|
50
|
+
const mutations = accesses.filter(access =>
|
|
51
|
+
(ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && access.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment) ||
|
|
52
|
+
((ts.isPrefixUnaryExpression(access.parent) || ts.isPostfixUnaryExpression(access.parent)) && access.parent.operand === access && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(access.parent.operator)) ||
|
|
53
|
+
(ts.isDeleteExpression(access.parent) && access.parent.expression === access)
|
|
54
|
+
)
|
|
47
55
|
const frameAssignments = accesses.filter(access => ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && frameCall(unwrapExpression(access.parent.right), "requestAnimationFrame") && unshadowedFrameCall(unwrapExpression(access.parent.right), owner))
|
|
48
56
|
const statement = node.parent?.parent
|
|
49
57
|
const topLevelOwner = owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile
|
|
50
58
|
const topLevelConst = topLevelOwner && ts.isVariableStatement(statement) && (statement.declarationList.flags & ts.NodeFlags.Const) && statement.declarationList.declarations.length === 1 && statement.parent === owner.body
|
|
51
59
|
if (frameAssignments.length && invalidReference) throw sourceNodeError(invalidReference, sourceFile, "Animation frame refs may only use direct .current reads and assignments")
|
|
52
60
|
if (frameAssignments.length && !topLevelConst) throw sourceNodeError(node, sourceFile, "Animation frame refs must be one top-level component const")
|
|
61
|
+
if (attachedToJsx && mutations.length) throw sourceNodeError(mutations[0], sourceFile, "JSX object refs may not assign to ref.current")
|
|
53
62
|
if (!topLevelConst || invalidReference || !accesses.length) return
|
|
54
63
|
const effectCalls = owner.body.statements.flatMap(statement => hasUseEffectImport && ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "useEffect" && !isShadowedIdentifier(statement.expression.expression, sourceFile) ? [statement.expression] : [])
|
|
55
64
|
const effects = effectCalls.filter(effect => {
|
|
56
65
|
const callback = effect.arguments[0]
|
|
57
66
|
return callback && accesses.every(access => isNodeWithin(access, callback))
|
|
58
67
|
})
|
|
68
|
+
if (!frameAssignments.length && refInitializer?.kind === ts.SyntaxKind.NullKeyword && !invalidReference && !effects.length) {
|
|
69
|
+
const owners = effectCalls.filter(effect => {
|
|
70
|
+
const callback = effect.arguments[0]
|
|
71
|
+
return callback && accesses.some(access => isNodeWithin(access, callback))
|
|
72
|
+
})
|
|
73
|
+
const callbacks = owners.map(effect => effect.arguments[0])
|
|
74
|
+
const directCallbacks = callbacks.every(callback => (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && ts.isBlock(callback.body))
|
|
75
|
+
const everyAccessOwned = accesses.every(access => callbacks.filter(callback => isNodeWithin(access, callback)).length === 1)
|
|
76
|
+
const mountEffects = owners.filter(effect => ts.isArrayLiteralExpression(effect.arguments[1]) && !effect.arguments[1].elements.length)
|
|
77
|
+
const updateEffects = owners.filter(effect => ts.isArrayLiteralExpression(effect.arguments[1]) && effect.arguments[1].elements.length)
|
|
78
|
+
if (owners.length >= 2 && directCallbacks && everyAccessOwned && mountEffects.length === 1 && updateEffects.length === owners.length - 1 && owners[0] === mountEffects[0]) {
|
|
79
|
+
const mount = mountEffects[0]
|
|
80
|
+
const callback = mount.arguments[0]
|
|
81
|
+
const returns = effectReturns(callback)
|
|
82
|
+
const cleanup = returns.cleanups.length === 1 ? returns.cleanups[0] : undefined
|
|
83
|
+
const writes = accesses.filter(access => ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken)
|
|
84
|
+
const setupWrites = writes.filter(access => isNodeWithin(access, callback) && (!cleanup || !isNodeWithin(access, cleanup)))
|
|
85
|
+
const cleanupWrites = cleanup ? writes.filter(access => isNodeWithin(access, cleanup)) : []
|
|
86
|
+
const setupStatement = setupWrites[0]?.parent.parent
|
|
87
|
+
const cleanupStatement = cleanupWrites[0]?.parent.parent
|
|
88
|
+
const directSetup = setupWrites.length === 1 && ts.isExpressionStatement(setupStatement) && setupStatement.parent === callback.body && unwrapExpression(setupWrites[0].parent.right).kind !== ts.SyntaxKind.NullKeyword
|
|
89
|
+
const directCleanup = cleanup && ts.isBlock(cleanup.body) && cleanupWrites.length === 1 && ts.isExpressionStatement(cleanupStatement) && cleanupStatement.parent === cleanup.body && unwrapExpression(cleanupWrites[0].parent.right).kind === ts.SyntaxKind.NullKeyword
|
|
90
|
+
const updateWrites = mutations.some(access => updateEffects.some(effect => isNodeWithin(access, effect.arguments[0])))
|
|
91
|
+
if (directSetup && directCleanup && !updateWrites) {
|
|
92
|
+
if (!hasUseStateImport) throw sourceNodeError(node, sourceFile, "Retained instance refs require a named useState import")
|
|
93
|
+
const setterName = `__kSetRetainedRef_${node.name.text}_${node.pos}`
|
|
94
|
+
if (referencesIdentifier(owner.body, setterName)) throw sourceNodeError(node, sourceFile, "Retained instance ref conflicts with a compiler-owned binding")
|
|
95
|
+
retainedRefs.set(node, setterName)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (owners.length >= 2) throw sourceNodeError(node, sourceFile, "Retained instance refs require one empty-dependency mount effect with one direct assignment and null-reset cleanup followed by read-only dependency effects")
|
|
100
|
+
}
|
|
59
101
|
if (!frameAssignments.length) {
|
|
60
102
|
const callback = effects.length === 1 ? effects[0].arguments[0] : undefined
|
|
61
103
|
if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && ts.isBlock(callback.body)) {
|
|
@@ -107,9 +149,27 @@ export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
|
|
|
107
149
|
registerPrivateRef(node, callback)
|
|
108
150
|
}
|
|
109
151
|
inspect(sourceFile)
|
|
110
|
-
|
|
152
|
+
const retainedByOwner = new Map()
|
|
153
|
+
for (const declaration of retainedRefs.keys()) {
|
|
154
|
+
const owner = nearestFunction(declaration)
|
|
155
|
+
const refs = retainedByOwner.get(owner) ?? []
|
|
156
|
+
refs.push(declaration)
|
|
157
|
+
retainedByOwner.set(owner, refs)
|
|
158
|
+
}
|
|
159
|
+
for (const refs of retainedByOwner.values()) if (refs.length > 1) throw sourceNodeError(refs[1], sourceFile, "Components may own only one retained instance ref")
|
|
160
|
+
if (!privateRefs.size && !retainedRefs.size) return sourceFile
|
|
111
161
|
const visitor = node => {
|
|
112
162
|
if (ts.isVariableStatement(node) && node.declarationList.declarations.length === 1 && privateRefs.has(node.declarationList.declarations[0])) return undefined
|
|
163
|
+
if (ts.isVariableDeclaration(node) && retainedRefs.has(node)) {
|
|
164
|
+
const binding = factory.createArrayBindingPattern([
|
|
165
|
+
factory.createBindingElement(undefined, undefined, node.name),
|
|
166
|
+
factory.createBindingElement(undefined, undefined, retainedRefs.get(node))
|
|
167
|
+
])
|
|
168
|
+
const initializer = factory.createCallExpression(factory.createIdentifier("useState"), undefined, [
|
|
169
|
+
factory.createObjectLiteralExpression([factory.createPropertyAssignment("current", factory.createNull())])
|
|
170
|
+
])
|
|
171
|
+
return factory.updateVariableDeclaration(node, binding, node.exclamationToken, undefined, initializer)
|
|
172
|
+
}
|
|
113
173
|
const refs = effectRefs.get(node)
|
|
114
174
|
if (refs && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && ts.isBlock(node.body)) {
|
|
115
175
|
const body = ts.visitEachChild(node.body, visitor, context)
|
|
@@ -68,7 +68,7 @@ export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}
|
|
|
68
68
|
lists.expressionAttributes ||= Boolean(list.expressionAttributes)
|
|
69
69
|
lists.seeds ||= Boolean(list.seed || list.valueSeed)
|
|
70
70
|
lists.effects ||= Boolean(list.effects)
|
|
71
|
-
lists.rowHooks ||= Boolean(list.rowStates?.length || list.rowRefs?.length)
|
|
71
|
+
lists.rowHooks ||= Boolean(list.rowStates?.length || list.rowRefs?.length || list.rowIds?.length)
|
|
72
72
|
lists.rowRefs ||= Boolean(list.rowRefs?.length)
|
|
73
73
|
lists.complexRowState ||= Boolean(list.rowStates?.some(state => state.initializer === "list-item" || state.initialValue !== null && typeof state.initialValue === "object"))
|
|
74
74
|
lists.nested ||= Boolean(list.ownerField)
|
|
@@ -77,7 +77,7 @@ export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}
|
|
|
77
77
|
lists.static ||= Boolean(list.static)
|
|
78
78
|
lists.indexes ||= Boolean(list.indexed)
|
|
79
79
|
lists.stableFastPaths ||= !list.children && !list.ownerField && list.key !== null && !list.indexed && !list.reducer && !list.selector
|
|
80
|
-
lists.generalRowHooks ||= Boolean(list.ownerField && (list.rowStates?.length || list.rowRefs?.length))
|
|
80
|
+
lists.generalRowHooks ||= Boolean(list.rowIds?.length || list.ownerField && (list.rowStates?.length || list.rowRefs?.length))
|
|
81
81
|
lists.mounts ||= Boolean(list.mount)
|
|
82
82
|
}
|
|
83
83
|
}
|
|
@@ -164,7 +164,7 @@ function assertList(list, index, ids, stateIds, listIds, parentByChild) {
|
|
|
164
164
|
parentByChild.set(child.id, list.id)
|
|
165
165
|
}
|
|
166
166
|
for (const row of list.rowStates ?? []) if (!isRecord(row) || !rowTemplate(row.id) || !Object.hasOwn(row, "initialValue") || row.initializer !== undefined && row.initializer !== "list-item") throw new Error(`${label} has invalid row state`)
|
|
167
|
-
for (const row of [...(list.rowRefs ?? []), ...(list.rowConditions ?? [])]) if (!rowTemplate(row)) throw new Error(`${label} has invalid row ownership ID`)
|
|
167
|
+
for (const row of [...(list.rowRefs ?? []), ...(list.rowIds ?? []), ...(list.rowConditions ?? [])]) if (!rowTemplate(row)) throw new Error(`${label} has invalid row ownership ID`)
|
|
168
168
|
if (list.seed && list.valueSeed || list.seed !== undefined && !validSeed(list.seed) || list.valueSeed !== undefined && !validSeed(list.valueSeed)) throw new Error(`${label} has invalid seed`)
|
|
169
169
|
}
|
|
170
170
|
|
|
@@ -517,6 +517,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
517
517
|
let usesListItem = false
|
|
518
518
|
let usesRowState = false
|
|
519
519
|
let usesRowRef = false
|
|
520
|
+
let usesRowId = false
|
|
520
521
|
let usesComponentState = false
|
|
521
522
|
let usesComponentId = false
|
|
522
523
|
let usesComponentRef = false
|
|
@@ -1199,7 +1200,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1199
1200
|
...result.ordinaryStates.map(({ state, setter, source }) => ({ name: state, setter, kind: "component", ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
|
|
1200
1201
|
],
|
|
1201
1202
|
refs: [...result.rowRefs.map(({ name, source }) => ({ name, kind: "row", ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })), ...result.ordinaryRefs.map(({ name, source }) => ({ name, kind: "component", ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))],
|
|
1202
|
-
ids: result.ordinaryIds.map(({ name, source }) => ({ name, ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))
|
|
1203
|
+
ids: [...result.rowIds.map(({ name, source }) => ({ name, kind: "row", ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) })), ...result.ordinaryIds.map(({ name, source }) => ({ name, kind: "component", ...(analysisSite(source, "hook") ? { site: analysisSite(source, "hook") } : {}), ...(analysisSource(source) ? { source: analysisSource(source) } : {}) }))]
|
|
1203
1204
|
})
|
|
1204
1205
|
result.propStateOwners = new Map(result.props.flatMap(prop => {
|
|
1205
1206
|
const expression = result.propExpressions.get(prop.name)
|
|
@@ -1216,7 +1217,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1216
1217
|
return result
|
|
1217
1218
|
}
|
|
1218
1219
|
const registerRowHooks = (call, specialization) => {
|
|
1219
|
-
if (!specialization.rowStates.length && !specialization.rowRefs.length) return
|
|
1220
|
+
if (!specialization.rowStates.length && !specialization.rowRefs.length && !specialization.rowIds.length) return
|
|
1220
1221
|
let owner
|
|
1221
1222
|
for (let current = call.parent; current; current = current.parent) {
|
|
1222
1223
|
if (isFunctionLike(current) && settersByFunction.has(current)) {
|
|
@@ -1236,6 +1237,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1236
1237
|
rowHookCalls.push(call)
|
|
1237
1238
|
usesRowState ||= specialization.rowStates.length > 0
|
|
1238
1239
|
usesRowRef ||= specialization.rowRefs.length > 0
|
|
1240
|
+
usesRowId ||= specialization.rowIds.length > 0
|
|
1239
1241
|
}
|
|
1240
1242
|
const mergeSpecializedImports = (root, componentSource, call, effects = []) => {
|
|
1241
1243
|
const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
|
|
@@ -1716,7 +1718,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1716
1718
|
let count = 0
|
|
1717
1719
|
const visit = (node, currentAggregate = aggregate) => {
|
|
1718
1720
|
if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) {
|
|
1719
|
-
const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], specializations: [] }
|
|
1721
|
+
const nestedAggregate = { calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], rowIds: [], specializations: [] }
|
|
1720
1722
|
for (const argument of node.arguments) visit(argument, nestedAggregate)
|
|
1721
1723
|
if (nestedAggregate.hookDeclarations.length || nestedAggregate.effects.length) nestedRowSpecializations.set(`${node.pos}:${node.end}`, nestedAggregate)
|
|
1722
1724
|
return
|
|
@@ -1749,6 +1751,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1749
1751
|
currentAggregate.hookDeclarations.push(...specialization.hookDeclarations)
|
|
1750
1752
|
currentAggregate.rowStates.push(...specialization.rowStates)
|
|
1751
1753
|
currentAggregate.rowRefs.push(...specialization.rowRefs)
|
|
1754
|
+
currentAggregate.rowIds.push(...specialization.rowIds)
|
|
1752
1755
|
}
|
|
1753
1756
|
replacements.set(node, specialization.root)
|
|
1754
1757
|
count++
|
|
@@ -1784,7 +1787,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1784
1787
|
}
|
|
1785
1788
|
for (const { node, parts: originalParts } of rawRenderedLists) {
|
|
1786
1789
|
if (keyedListParentTag(node) === "table") throw new Error("Keyed table rows must be wrapped in <tbody>, <thead>, or <tfoot>")
|
|
1787
|
-
const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], ordinaryStates: [] }
|
|
1790
|
+
const specialization = componentSpecializations.get(originalParts.root) ?? { root: originalParts.root, calculations: [], effects: [], hookDeclarations: [], rowStates: [], rowRefs: [], rowIds: [], ordinaryStates: [] }
|
|
1788
1791
|
const componentSource = specialization.componentSource ?? sourceFile
|
|
1789
1792
|
specialization.root = expandKeyedComponents(specialization.root, componentSource, specialization.component ? [specialization.component] : [], specialization)
|
|
1790
1793
|
if (specialization.imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, componentSource, originalParts.root, specialization.effects))
|
|
@@ -1811,6 +1814,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1811
1814
|
specializations: [specialization.analysis?.slot, ...(specialization.specializations ?? [])].filter(slot => slot !== undefined),
|
|
1812
1815
|
rowStates: [...specialization.rowStates, ...specialization.ordinaryStates],
|
|
1813
1816
|
rowRefs: specialization.rowRefs,
|
|
1817
|
+
rowIds: specialization.rowIds,
|
|
1814
1818
|
analysisStateOwners: new Map([...stateOwnersForNode(originalParts.root), ...(specialization.propStateOwners ?? []), ...[...specialization.rowStates, ...specialization.ordinaryStates].map(state => [state.state, state.analysisReference])])
|
|
1815
1819
|
}
|
|
1816
1820
|
for (const calculation of specialization.calculations) {
|
|
@@ -2297,6 +2301,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2297
2301
|
if (usesListEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kListUseEffect")))
|
|
2298
2302
|
if (usesRowState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kRowUseState")))
|
|
2299
2303
|
if (usesRowRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kRowUseRef")))
|
|
2304
|
+
if (usesRowId) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useId"), factory.createIdentifier("__kRowUseId")))
|
|
2300
2305
|
if (usesComponentState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kComponentUseState")))
|
|
2301
2306
|
if (usesComponentId) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useId"), factory.createIdentifier("__kComponentUseId")))
|
|
2302
2307
|
if (usesComponentRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kComponentUseRef")))
|
|
@@ -2656,7 +2661,6 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
|
|
|
2656
2661
|
if (!ts.isIdentifier(tag) || tag.text[0] !== tag.text[0].toLowerCase()) fail(node, "Keyed list items must use intrinsic JSX elements")
|
|
2657
2662
|
}
|
|
2658
2663
|
const visit = node => {
|
|
2659
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === "useId") fail(node, "useId() is not supported in keyed rows")
|
|
2660
2664
|
if (ts.isJsxFragment(node)) fail(node, "Fragments are not supported in keyed lists")
|
|
2661
2665
|
if (ts.isJsxElement(node) || ts.isJsxSelfClosingElement(node)) validateElement(node)
|
|
2662
2666
|
if (node !== root && ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "map" && containsJsx(node)) fail(node, nestedDiagnostic)
|
|
@@ -2697,6 +2701,7 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
|
|
|
2697
2701
|
specializations: [specialization?.analysis?.slot, ...(specialization?.specializations ?? [])].filter(slot => slot !== undefined),
|
|
2698
2702
|
rowStates: specializedStates,
|
|
2699
2703
|
rowRefs: specialization?.rowRefs ?? [],
|
|
2704
|
+
rowIds: specialization?.rowIds ?? [],
|
|
2700
2705
|
analysisStateOwners: new Map([...(parts.analysisStateOwners ?? []), ...(specialization?.propStateOwners ?? []), ...specializedStates.map(state => [state.state, state.analysisReference])])
|
|
2701
2706
|
}
|
|
2702
2707
|
for (const calculation of specialization?.calculations ?? []) {
|
|
@@ -2747,6 +2752,8 @@ function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpeci
|
|
|
2747
2752
|
return analysis
|
|
2748
2753
|
}
|
|
2749
2754
|
|
|
2755
|
+
const keyedIdReferenceAttributes = new Set(["id", "htmlFor", "aria-activedescendant", "aria-controls", "aria-describedby", "aria-details", "aria-errormessage", "aria-flowto", "aria-labelledby", "aria-owns", "form", "headers", "list", "popovertarget"])
|
|
2756
|
+
|
|
2750
2757
|
function directConstObjectLiteral(expression, call) {
|
|
2751
2758
|
expression = unwrapExpression(expression)
|
|
2752
2759
|
if (ts.isObjectLiteralExpression(expression)) return expression
|
|
@@ -2947,6 +2954,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2947
2954
|
const hookDeclarations = []
|
|
2948
2955
|
const rowStates = []
|
|
2949
2956
|
const rowRefs = []
|
|
2957
|
+
const rowIds = []
|
|
2950
2958
|
const ordinaryStates = []
|
|
2951
2959
|
const ordinaryRefs = []
|
|
2952
2960
|
const ordinaryIds = []
|
|
@@ -3012,13 +3020,14 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
3012
3020
|
continue
|
|
3013
3021
|
}
|
|
3014
3022
|
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") {
|
|
3015
|
-
|
|
3016
|
-
if (declaration.initializer.arguments.length || !ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.initializer, component.getSourceFile(),
|
|
3017
|
-
const
|
|
3023
|
+
const hookLabel = ordinaryHooks ? "Setter-callback component" : "Keyed row"
|
|
3024
|
+
if (declaration.initializer.arguments.length || !ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useId() must initialize one top-level const identifier without arguments`)
|
|
3025
|
+
const ids = ordinaryHooks ? ordinaryIds : rowIds
|
|
3026
|
+
const name = `${ordinaryHooks ? "__kComponentId" : "__kRowId"}${Math.max(0, call.pos)}_${ids.length}`
|
|
3018
3027
|
substitutions.set(declaration.name.text, factory.createIdentifier(name))
|
|
3019
|
-
const initializer = factory.createCallExpression(factory.createIdentifier("__kComponentUseId"), undefined, [])
|
|
3028
|
+
const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseId" : "__kRowUseId"), undefined, [])
|
|
3020
3029
|
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
3021
|
-
|
|
3030
|
+
ids.push({ name, local: declaration.name.text, source: declaration })
|
|
3022
3031
|
continue
|
|
3023
3032
|
}
|
|
3024
3033
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
|
|
@@ -3029,6 +3038,10 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
3029
3038
|
}
|
|
3030
3039
|
returned = last.expression
|
|
3031
3040
|
}
|
|
3041
|
+
for (const id of rowIds) {
|
|
3042
|
+
for (const reference of referenceIdentifiers(returned, id.local)) if (!validKeyedIdReference(reference)) throw sourceNodeError(reference, component.getSourceFile(), "Keyed row useId() values may only be used in intrinsic id and ID-reference attributes")
|
|
3043
|
+
for (const calculation of calculations) if (referenceIdentifiers(calculation.expression, id.local).length) throw sourceNodeError(calculation.expression, component.getSourceFile(), "Keyed row useId() values may only be used directly in intrinsic id and ID-reference attributes")
|
|
3044
|
+
}
|
|
3032
3045
|
let unsupportedHook
|
|
3033
3046
|
const findUnsupportedHook = node => {
|
|
3034
3047
|
if (unsupportedHook) return
|
|
@@ -3058,6 +3071,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
3058
3071
|
hookDeclarations,
|
|
3059
3072
|
rowStates,
|
|
3060
3073
|
rowRefs,
|
|
3074
|
+
rowIds,
|
|
3061
3075
|
ordinaryStates,
|
|
3062
3076
|
ordinaryRefs,
|
|
3063
3077
|
ordinaryIds,
|
|
@@ -3068,6 +3082,16 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
3068
3082
|
}
|
|
3069
3083
|
}
|
|
3070
3084
|
|
|
3085
|
+
function validKeyedIdReference(reference) {
|
|
3086
|
+
for (let current = reference.parent; current; current = current.parent) {
|
|
3087
|
+
if (!ts.isJsxAttribute(current)) continue
|
|
3088
|
+
const element = current.parent?.parent
|
|
3089
|
+
const tag = element?.tagName
|
|
3090
|
+
return keyedIdReferenceAttributes.has(current.name.text) && ts.isIdentifier(tag) && tag.text[0] === tag.text[0].toLowerCase()
|
|
3091
|
+
}
|
|
3092
|
+
return false
|
|
3093
|
+
}
|
|
3094
|
+
|
|
3071
3095
|
function isSerializableStateLiteral(node) {
|
|
3072
3096
|
const value = unwrapExpression(node)
|
|
3073
3097
|
if (isPrimitiveDefaultLiteral(value)) return true
|
package/framework/core.mjs
CHANGED
|
@@ -49,8 +49,7 @@ let renderContext
|
|
|
49
49
|
|
|
50
50
|
export function useId() {
|
|
51
51
|
if (!renderContext) throw new Error("useId() can only run while rendering a Kudzu component")
|
|
52
|
-
|
|
53
|
-
return `k-${nextRenderId("i")}`
|
|
52
|
+
return `k-${renderContext.listRoot || renderContext.listRowRoot ? nextRowRenderId("i") : nextRenderId("i")}`
|
|
54
53
|
}
|
|
55
54
|
|
|
56
55
|
export function useState(initialValue, name, initializer) {
|
|
@@ -484,7 +483,7 @@ function serializeCapture(name, value, seen) {
|
|
|
484
483
|
}
|
|
485
484
|
|
|
486
485
|
export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
487
|
-
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", applicationRoute: metadata.applicationRoute, counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, nextId: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], captureCache: new WeakMap(), sharedStates: new Map(), states: {}, textStates: new Set(), conditionStates: new Set(), conditionOwnedStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerReferences: new Map(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, searchParams: new Map(), searchParamEntries: [], searchParamsWritable: false, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
486
|
+
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", applicationRoute: metadata.applicationRoute, counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, nextId: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowIds: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], captureCache: new WeakMap(), sharedStates: new Map(), states: {}, textStates: new Set(), conditionStates: new Set(), conditionOwnedStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerReferences: new Map(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, searchParams: new Map(), searchParamEntries: [], searchParamsWritable: false, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
488
487
|
|
|
489
488
|
try {
|
|
490
489
|
const page = { [routeScopeMarker]: true, component, props }
|
|
@@ -941,6 +940,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
941
940
|
const previousListEffectOwners = renderContext.listEffectOwners
|
|
942
941
|
const previousListRowStates = renderContext.listRowStates
|
|
943
942
|
const previousListRowRefs = renderContext.listRowRefs
|
|
943
|
+
const previousListRowIds = renderContext.listRowIds
|
|
944
944
|
const previousListRowConditions = renderContext.listRowConditions
|
|
945
945
|
const previousListRowLists = renderContext.listRowLists
|
|
946
946
|
try {
|
|
@@ -948,10 +948,11 @@ async function renderList(node, namespace, selectValue) {
|
|
|
948
948
|
renderContext.listEffectOwners = rowList?.effectOwners ?? []
|
|
949
949
|
renderContext.listRowStates = rowList?.rowStates ?? []
|
|
950
950
|
renderContext.listRowRefs = rowList?.rowRefs ?? []
|
|
951
|
+
renderContext.listRowIds = rowList?.rowIds ?? []
|
|
951
952
|
renderContext.listRowConditions = rowList?.rowConditions ?? []
|
|
952
953
|
renderContext.listRowLists = rowList?.rowLists ?? []
|
|
953
954
|
renderContext.listFields = new Set([node.keyField])
|
|
954
|
-
const templateRoot = { id, state: node.items.id, descriptor, template: true, effects: [], item: {}, path: ownerRoot?.path ?? [], rowIndexes: { s: 0, r: 0, c: 0, l: 0 } }
|
|
955
|
+
const templateRoot = { id, state: node.items.id, descriptor, template: true, effects: [], item: {}, path: ownerRoot?.path ?? [], rowIndexes: { s: 0, r: 0, i: 0, c: 0, l: 0 } }
|
|
955
956
|
renderContext.listRoot = templateRoot
|
|
956
957
|
renderContext.listRowRoot = templateRoot
|
|
957
958
|
const template = await renderNode(node.render({}, 0), namespace, selectValue)
|
|
@@ -974,6 +975,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
974
975
|
descriptor.rowRefs = renderContext.listRowRefs.map(({ id }) => id)
|
|
975
976
|
descriptor.mount = true
|
|
976
977
|
}
|
|
978
|
+
if (renderContext.listRowIds.length) descriptor.rowIds = renderContext.listRowIds.map(({ id }) => id)
|
|
977
979
|
if (descriptor.rowStates && !descriptor.effects && !descriptor.nested && !template.includes("data-k-text")) descriptor.fastRelease = true
|
|
978
980
|
const valueSeed = node.ownerField || node.keyField === null ? undefined : listSeed(node.values, renderContext.listFields)
|
|
979
981
|
const seed = node.selector.length ? undefined : valueSeed
|
|
@@ -984,7 +986,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
984
986
|
renderContext.listInitialMarkers = Boolean(descriptor.conditions)
|
|
985
987
|
for (const [index, item] of node.values.entries()) {
|
|
986
988
|
const key = node.keyField === null ? index : item[node.keyField]
|
|
987
|
-
renderContext.listRoot = { id, state: node.items.id, descriptor, key, template: false, effects: [], item, path: [...(ownerRoot?.path ?? []), `${id}=${typeof key}:${key}`], rowIndexes: { s: 0, r: 0, c: 0, l: 0 } }
|
|
989
|
+
renderContext.listRoot = { id, state: node.items.id, descriptor, key, template: false, effects: [], item, path: [...(ownerRoot?.path ?? []), `${id}=${typeof key}:${key}`], rowIndexes: { s: 0, r: 0, i: 0, c: 0, l: 0 } }
|
|
988
990
|
renderContext.listRowRoot = renderContext.listRoot
|
|
989
991
|
current += await renderNode(node.render(item, index), namespace, selectValue)
|
|
990
992
|
}
|
|
@@ -1007,6 +1009,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
1007
1009
|
renderContext.listEffectOwners = previousListEffectOwners
|
|
1008
1010
|
renderContext.listRowStates = previousListRowStates
|
|
1009
1011
|
renderContext.listRowRefs = previousListRowRefs
|
|
1012
|
+
renderContext.listRowIds = previousListRowIds
|
|
1010
1013
|
renderContext.listRowConditions = previousListRowConditions
|
|
1011
1014
|
renderContext.listRowLists = previousListRowLists
|
|
1012
1015
|
renderContext.listDepth--
|
|
@@ -1017,7 +1020,7 @@ function nextRowList() {
|
|
|
1017
1020
|
const root = renderContext.listRoot ?? renderContext.listRowRoot
|
|
1018
1021
|
const index = root.rowIndexes.l++
|
|
1019
1022
|
if (renderContext.listTemplate) {
|
|
1020
|
-
const entry = renderContext.listRowLists[index] ?? { id: nextRenderId("l"), rowLists: [], rowStates: [], rowRefs: [], rowConditions: [], effectOwners: [], planned: false }
|
|
1023
|
+
const entry = renderContext.listRowLists[index] ?? { id: nextRenderId("l"), rowLists: [], rowStates: [], rowRefs: [], rowIds: [], rowConditions: [], effectOwners: [], planned: false }
|
|
1021
1024
|
renderContext.listRowLists[index] = entry
|
|
1022
1025
|
return entry
|
|
1023
1026
|
}
|
|
@@ -1035,7 +1038,7 @@ function nextRenderId(kind) {
|
|
|
1035
1038
|
function nextRowRenderId(kind, initialValue, initializer) {
|
|
1036
1039
|
const root = renderContext.listRoot ?? renderContext.listRowRoot
|
|
1037
1040
|
const index = root.rowIndexes[kind]++
|
|
1038
|
-
const entries = kind === "s" ? renderContext.listRowStates : kind === "r" ? renderContext.listRowRefs : renderContext.listRowConditions
|
|
1041
|
+
const entries = kind === "s" ? renderContext.listRowStates : kind === "r" ? renderContext.listRowRefs : kind === "i" ? renderContext.listRowIds : renderContext.listRowConditions
|
|
1039
1042
|
if (renderContext.listTemplate) {
|
|
1040
1043
|
const entry = entries[index]
|
|
1041
1044
|
if (entry) return entry.id
|
|
@@ -1044,7 +1047,7 @@ function nextRowRenderId(kind, initialValue, initializer) {
|
|
|
1044
1047
|
return id
|
|
1045
1048
|
}
|
|
1046
1049
|
const entry = entries[index]
|
|
1047
|
-
if (!entry) throw new Error(`Keyed row ${kind === "s" ? "state hooks" : kind === "r" ? "ref hooks" : "conditionals"} must have the same order for every item`)
|
|
1050
|
+
if (!entry) throw new Error(`Keyed row ${kind === "s" ? "state hooks" : kind === "r" ? "ref hooks" : kind === "i" ? "ID hooks" : "conditionals"} must have the same order for every item`)
|
|
1048
1051
|
return rowRenderId(entry.id, root.descriptor.ownerField ? root.path : [`${typeof root.key}:${root.key}`])
|
|
1049
1052
|
}
|
|
1050
1053
|
|
|
@@ -19,6 +19,7 @@ const conditionTemplates = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
|
|
|
19
19
|
const itemPartsSelector = `[data-k-list-text]${__KUDZU_LIST_ATTRIBUTES__ ? ",[data-k-list-attrs]" : ""}${__KUDZU_LIST_EVENTS__ ? ",[data-k-list-events]" : ""}${__KUDZU_LIST_EXPRESSIONS__ ? ",[data-k-list-expression]" : ""}${__KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? ",[data-k-list-expression-attrs]" : ""}${__KUDZU_LIST_CONDITIONS__ ? ",[data-k-list-condition]" : ""}${__KUDZU_LIST_EFFECTS__ ? ",[data-k-effects]" : ""}`
|
|
20
20
|
const childPrototypes = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
|
|
21
21
|
const itemPartPlans = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
|
|
22
|
+
const rowIdAttributes = new Set(["id", "for", "aria-activedescendant", "aria-controls", "aria-describedby", "aria-details", "aria-errormessage", "aria-flowto", "aria-labelledby", "aria-owns", "form", "headers", "list", "popovertarget"])
|
|
22
23
|
|
|
23
24
|
function commitLists(id) {
|
|
24
25
|
const lists = listTargets.get(id)
|
|
@@ -742,7 +743,7 @@ function listItemPartPlan(template, nested = false, descriptor) {
|
|
|
742
743
|
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [location(node), descriptor, node]) : [],
|
|
743
744
|
effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(location) : [],
|
|
744
745
|
rowIds: structural && __KUDZU_GENERAL_ROW_HOOKS__ && hasRowHooks(descriptor) ? source.flatMap(node => {
|
|
745
|
-
const attributes = [...node.attributes].filter(attribute => attribute.name.startsWith("data-k-") && attribute.value.includes("$k")).map(attribute => attribute.name)
|
|
746
|
+
const attributes = [...node.attributes].filter(attribute => (attribute.name.startsWith("data-k-") || rowIdAttributes.has(attribute.name)) && attribute.value.includes("$k")).map(attribute => attribute.name)
|
|
746
747
|
return attributes.length ? [[location(node), attributes]] : []
|
|
747
748
|
}) : []
|
|
748
749
|
}
|
|
@@ -1086,7 +1087,7 @@ function replaceOwnedRowIds(root) {
|
|
|
1086
1087
|
if (!directRowReplacements.delete(root)) return replaceRowIds(root, rowReplacements.get(root))
|
|
1087
1088
|
const path = listRowPaths.get(root)
|
|
1088
1089
|
const replace = node => {
|
|
1089
|
-
for (const attribute of [...node.attributes]) if (attribute.name.startsWith("data-k-") && attribute.value.includes("$k")) attribute.value = attribute.value.replaceAll("$k", path)
|
|
1090
|
+
for (const attribute of [...node.attributes]) if ((attribute.name.startsWith("data-k-") || rowIdAttributes.has(attribute.name)) && attribute.value.includes("$k")) attribute.value = attribute.value.replaceAll("$k", path)
|
|
1090
1091
|
for (const child of node.children) replace(child)
|
|
1091
1092
|
for (const child of node.content?.children ?? []) replace(child)
|
|
1092
1093
|
}
|
|
@@ -1095,7 +1096,7 @@ function replaceOwnedRowIds(root) {
|
|
|
1095
1096
|
/* general-row-hooks-end */
|
|
1096
1097
|
|
|
1097
1098
|
function hasRowHooks(descriptor) {
|
|
1098
|
-
return Boolean(descriptor.rowStates?.length || descriptor.rowRefs?.length || descriptor.rowConditions?.length)
|
|
1099
|
+
return Boolean(descriptor.rowStates?.length || descriptor.rowRefs?.length || descriptor.rowIds?.length || descriptor.rowConditions?.length)
|
|
1099
1100
|
}
|
|
1100
1101
|
|
|
1101
1102
|
function listLifecycle(descriptor, template) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kudzujs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -73,6 +73,8 @@
|
|
|
73
73
|
"typescript": "^5.9.2"
|
|
74
74
|
},
|
|
75
75
|
"devDependencies": {
|
|
76
|
+
"@codemirror/view": "^6.43.9",
|
|
77
|
+
"typed.js": "^3.0.0",
|
|
76
78
|
"wrangler": "^4.112.0"
|
|
77
79
|
}
|
|
78
80
|
}
|