@kudzujs/core 0.14.2 → 0.15.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/PERFORMANCE.md +48 -0
- package/README.md +1 -1
- package/RELEASES.md +73 -0
- package/docs/next-architecture/application-capability-release-plan.md +89 -2
- package/docs/next-architecture/versioning.md +1 -1
- package/framework/README.md +1 -1
- 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 +2 -1
package/PERFORMANCE.md
CHANGED
|
@@ -2,6 +2,54 @@
|
|
|
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.15.1 Key-Scoped Native Popover Intake
|
|
6
|
+
|
|
7
|
+
Measured 2026-08-27 on macOS arm64 with Node 24.14.0 and Chrome
|
|
8
|
+
151.0.7922.174. The maintained project fixture uses native Popover with one
|
|
9
|
+
key-scoped `useId()` relationship per project row. Required Chrome verifies
|
|
10
|
+
Escape, light dismiss, trigger focus restoration, reorder identity, inserted
|
|
11
|
+
and remounted IDs, and enhanced-navigation cleanup.
|
|
12
|
+
|
|
13
|
+
The application emits 44 files totaling 171,202 raw / 53,629 aggregate gzip
|
|
14
|
+
bytes with deploy SHA-256
|
|
15
|
+
`46ba45b4a423b9627c6ede5f8dd3a667f593cb5401454ae42ecc66207b07b323`.
|
|
16
|
+
The maintained two-route session contains 17 JavaScript files totaling 77,779
|
|
17
|
+
raw / 27,050 aggregate gzip bytes. `/help` remains 0 B JavaScript.
|
|
18
|
+
|
|
19
|
+
Seven fresh Chrome profiles record table update samples of `[0.4, 0.4, 0.5,
|
|
20
|
+
0.4, 0.5, 0.5, 0.5]` ms, a 0.5 ms median and 0.4/0.5 ms range. Navigation
|
|
21
|
+
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
|
|
22
|
+
ms range. These ranges overlap prior retained-path evidence; no timing
|
|
23
|
+
improvement or regression is claimed.
|
|
24
|
+
|
|
25
|
+
## 0.14.3 10,000-Item Browser Decision Intake
|
|
26
|
+
|
|
27
|
+
Measured 2026-08-27 on Linux x64 with Node 24.14.0 and Chrome
|
|
28
|
+
142.0.7444.175 using `RUNS=7 npm run benchmark:project-list-decision`.
|
|
29
|
+
The tracked fixture rotates three fresh-profile routes, forces garbage collection,
|
|
30
|
+
and records navigation completion, range replacement, DOM counters, JavaScript
|
|
31
|
+
heap, native edit identity, and bounded-row release.
|
|
32
|
+
|
|
33
|
+
| Strategy | Load median | Range median | Rows | DOM nodes median | JS heap median |
|
|
34
|
+
|---|---:|---:|---:|---:|---:|
|
|
35
|
+
| Direct DOM | 806.6 ms | n/a | 10,000 | 90,023 | 535,572 B |
|
|
36
|
+
| Pagination | 165.4 ms | 12.1 ms | 100 | 1,470 | 1,407,412 B |
|
|
37
|
+
| Scroll window | 161.2 ms | 16.5 ms | 100 | 1,471 | 1,507,712 B |
|
|
38
|
+
|
|
39
|
+
Direct load samples are `[767, 790.1, 882.8, 908.8, 753, 826.7, 806.6]`
|
|
40
|
+
ms. Pagination load/range samples are `[253.6, 138.6, 157, 133.7, 181.9,
|
|
41
|
+
202, 165.4]` / `[10.8, 22.1, 15.8, 13.7, 12.1, 10.5, 11]` ms. Scroll
|
|
42
|
+
window load/range samples are `[156.7, 154.6, 163.4, 161.2, 175.9, 203.5,
|
|
43
|
+
137.1]` / `[21.3, 16.4, 14.6, 17.1, 15.9, 16.5, 20.4]` ms.
|
|
44
|
+
|
|
45
|
+
The direct route is static and therefore has the lowest JavaScript heap; that
|
|
46
|
+
does not offset its 61.2x DOM-node count and 4.9x load median. Pagination and the
|
|
47
|
+
scroll window have overlapping load ranges, while pagination has the lower range
|
|
48
|
+
median, lower JavaScript heap, no scroll listener, native focus/page semantics,
|
|
49
|
+
and no fixed-row-height policy. Pagination is selected for 10,000-item project
|
|
50
|
+
tables. The window experiment adds no framework primitive and does not authorize
|
|
51
|
+
`0.14.4`; three independent failing fixtures remain required.
|
|
52
|
+
|
|
5
53
|
## 0.14.2 Infinite Loading Composition
|
|
6
54
|
|
|
7
55
|
Measured 2026-08-26 on macOS arm64 with Node 24.14.0 and Chrome
|
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.14.x`: the compiler API and supported TSX surface may change.
|
|
16
16
|
|
|
17
|
-
**Latest release: 0.
|
|
17
|
+
**Latest release: 0.15.1 - Key-scoped native popovers.** Keyed row `useId()` now scopes native Popover triggers and intrinsic ID references by row key without adding an overlay runtime. Read the [release notes](./RELEASES.md#0151---key-scoped-native-popovers), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.15.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,78 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.15.1 - Key-Scoped Native Popovers
|
|
4
|
+
|
|
5
|
+
Kudzu 0.15.1 lets ordinary keyed row components use `useId()` for native
|
|
6
|
+
Popover triggers and intrinsic ID relationships without duplicate cloned IDs.
|
|
7
|
+
|
|
8
|
+
### Changed in 0.15.1
|
|
9
|
+
|
|
10
|
+
- Key-scoped keyed-row `useId()` through the existing list ownership path.
|
|
11
|
+
- Rewrites cloned `id`, `htmlFor`, static ARIA IDREF, `form`, `headers`, `list`,
|
|
12
|
+
and `popovertarget` attributes.
|
|
13
|
+
- Rejects keyed IDs used in text, handlers, aliases, arbitrary attributes, or
|
|
14
|
+
component prop forwarding.
|
|
15
|
+
- Proves native Popover Escape, light dismiss, trigger focus restoration,
|
|
16
|
+
retained reorder identity, inserted and remounted IDs, and route cleanup.
|
|
17
|
+
- Adds no overlay runtime, focus manager, positioning engine, Portal/Slot
|
|
18
|
+
reconciliation, retained component tree, or public API.
|
|
19
|
+
- Updates `create-kudzu@0.1.126` to generate projects on
|
|
20
|
+
`@kudzujs/core@^0.15.1`.
|
|
21
|
+
|
|
22
|
+
### Output And Browser Evidence
|
|
23
|
+
|
|
24
|
+
- The maintained application emits 44 files totaling 171,202 raw / 53,629
|
|
25
|
+
aggregate gzip bytes; `/help` remains 0 B JavaScript.
|
|
26
|
+
- The two-route session remains 17 JavaScript files totaling 77,779 raw /
|
|
27
|
+
27,050 aggregate gzip bytes.
|
|
28
|
+
- Seven Chrome profiles record a 0.5 ms table-update median and 2.5 ms
|
|
29
|
+
navigation median, within prior retained-path ranges.
|
|
30
|
+
|
|
31
|
+
### Upgrade
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install @kudzujs/core@^0.15.1
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 0.14.3 - 10,000-Item Browser Decision
|
|
38
|
+
|
|
39
|
+
Kudzu 0.14.3 measures direct DOM, pagination, and scroll-window alternatives
|
|
40
|
+
for one deterministic 10,000-project table and selects native pagination.
|
|
41
|
+
|
|
42
|
+
### Changed in 0.14.3
|
|
43
|
+
|
|
44
|
+
- Added a tracked three-route large-list decision fixture.
|
|
45
|
+
- Added rotating fresh-Chrome load, range, DOM, heap, and identity measurement.
|
|
46
|
+
- Rejected direct 10,000-row DOM after its 90,023-node result.
|
|
47
|
+
- Rejected fixed-height scroll windowing for the project table because pagination
|
|
48
|
+
is faster to change range, uses less heap, and preserves native page/focus and
|
|
49
|
+
variable-row-height behavior.
|
|
50
|
+
- Added no range primitive, virtualization runtime, compiler pass, or public API.
|
|
51
|
+
- Updated `create-kudzu@0.1.125` to generate projects on
|
|
52
|
+
`@kudzujs/core@^0.14.3`.
|
|
53
|
+
|
|
54
|
+
### Browser Evidence
|
|
55
|
+
|
|
56
|
+
- Direct DOM loads in 806.6 ms median with 90,023 DOM nodes.
|
|
57
|
+
- Pagination loads in 165.4 ms and changes its 100-row page in 12.1 ms median
|
|
58
|
+
with 1,470 DOM nodes.
|
|
59
|
+
- The scroll window loads in 161.2 ms and changes range in 16.5 ms median with
|
|
60
|
+
1,471 DOM nodes.
|
|
61
|
+
- Both bounded strategies release an edited off-range row and restore fresh
|
|
62
|
+
native input identity.
|
|
63
|
+
|
|
64
|
+
### Decision
|
|
65
|
+
|
|
66
|
+
Use pagination for 10,000-item project tables. `0.14.4` virtual range ownership
|
|
67
|
+
remains blocked until three independent real fixtures prove pagination and
|
|
68
|
+
application-owned bounded ranges insufficient.
|
|
69
|
+
|
|
70
|
+
### Upgrade
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
npm install @kudzujs/core@^0.14.3
|
|
74
|
+
```
|
|
75
|
+
|
|
3
76
|
## 0.14.2 - Infinite Loading Composition
|
|
4
77
|
|
|
5
78
|
Kudzu 0.14.2 composes bounded sentinel-driven loading from ordinary state,
|
|
@@ -999,6 +999,25 @@ delta is claimed. `0.14.2` publishes this accepted packet, and
|
|
|
999
999
|
- **Done condition:** the selected strategy and thresholds are recorded without
|
|
1000
1000
|
assuming direct 10,000-row rendering is acceptable.
|
|
1001
1001
|
|
|
1002
|
+
Decision evidence is complete. A tracked three-route fixture and
|
|
1003
|
+
`npm run benchmark:project-list-decision` now compare one static 10,000-row
|
|
1004
|
+
table with pagination and a scroll-driven 100-row window over the same
|
|
1005
|
+
deterministic records and native edit controls. Seven rotating fresh Chrome 142
|
|
1006
|
+
profiles show direct DOM
|
|
1007
|
+
at an 806.6 ms load median and 90,023 DOM nodes, versus 165.4/161.2 ms and a
|
|
1008
|
+
1,470/1,471-node median for pagination and the scroll window. Direct JavaScript
|
|
1009
|
+
heap is lower because its route is static; DOM size and load remain materially
|
|
1010
|
+
worse.
|
|
1011
|
+
|
|
1012
|
+
Pagination is selected. Its 12.1 ms range median is lower than the window's 16.5
|
|
1013
|
+
ms, it uses 100,300 fewer median JavaScript-heap bytes, and it preserves native
|
|
1014
|
+
page, keyboard, focus, and variable-row-height behavior without a scroll listener
|
|
1015
|
+
or measurement policy. Both bounded routes release an edited off-range row and
|
|
1016
|
+
restore fresh native input identity. The experiment adds no semantic primitive,
|
|
1017
|
+
IR kind, compiler pass, production compiler/runtime line, runtime concept, or
|
|
1018
|
+
public API. `0.14.4` remains blocked because this one fixture cannot authorize
|
|
1019
|
+
virtual range ownership.
|
|
1020
|
+
|
|
1002
1021
|
### `0.14.4`: Virtual Range Ownership
|
|
1003
1022
|
|
|
1004
1023
|
- **Purpose:** implement bounded DOM only if `0.14.3` and three independent
|
|
@@ -1011,6 +1030,13 @@ delta is claimed. `0.14.2` publishes this accepted packet, and
|
|
|
1011
1030
|
- **Done condition:** range semantics are package-neutral, optional, measured,
|
|
1012
1031
|
and absent from non-virtual routes.
|
|
1013
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
|
+
|
|
1014
1040
|
## `0.15.x`: Overlay And Layer Ownership
|
|
1015
1041
|
|
|
1016
1042
|
### `0.15.0`: Native Dialog Baseline
|
|
@@ -1021,6 +1047,31 @@ delta is claimed. `0.14.2` publishes this accepted packet, and
|
|
|
1021
1047
|
- **Stop condition:** a custom modal/focus-trap runtime replaces `<dialog>`.
|
|
1022
1048
|
- **Done condition:** project delete and edit confirmation pass accessibly.
|
|
1023
1049
|
|
|
1050
|
+
Implementation evidence is local and release gates remain pending. The maintained
|
|
1051
|
+
project application replaces immediate keyed project deletion with one shared
|
|
1052
|
+
native `<dialog>`, one route-owned object ref, and one pending project ID state.
|
|
1053
|
+
Existing row callback specialization opens the browser top layer; native cancel,
|
|
1054
|
+
explicit cancel, and confirmation handlers close it without a Portal, focus trap,
|
|
1055
|
+
overlay runtime, or retained component tree.
|
|
1056
|
+
|
|
1057
|
+
Required Chrome proves modal top-layer behavior, native initial focus, explicit
|
|
1058
|
+
cancel, Escape-equivalent `requestClose()`, trigger-focus restoration, confirmed
|
|
1059
|
+
keyed deletion with fallback focus, retained sibling identity, and exact top-layer
|
|
1060
|
+
release on enhanced route navigation. The static `/help` sibling remains 0 B
|
|
1061
|
+
JavaScript. The slice adds no semantic primitive, IR kind, compiler pass,
|
|
1062
|
+
production compiler/runtime line, normalization rule, adapter, runtime concept,
|
|
1063
|
+
or public API. The application emits 44 files totaling 169,778 raw / 53,294
|
|
1064
|
+
aggregate gzip bytes with deploy SHA-256
|
|
1065
|
+
`91c5517e69c341bbd2051f07b3f0ad7df627098ba8e1984561268ea8666f3407`.
|
|
1066
|
+
The maintained two-route session remains 17 JavaScript files totaling 77,453 raw
|
|
1067
|
+
/ 26,905 aggregate gzip bytes.
|
|
1068
|
+
|
|
1069
|
+
Seven fresh macOS arm64 Chrome 151 profiles record table update samples of
|
|
1070
|
+
`[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,
|
|
1071
|
+
plus navigation samples of `[2.3, 2.6, 2.9, 2.6, 2.5, 2.7, 2.5]` ms, a 2.6 ms
|
|
1072
|
+
median and 2.3/2.9 ms range. These retained-path ranges overlap the previous
|
|
1073
|
+
same-environment evidence, so no timing improvement or regression is claimed.
|
|
1074
|
+
|
|
1024
1075
|
### `0.15.1`: Popover, Dropdown, And Menu Behavior
|
|
1025
1076
|
|
|
1026
1077
|
- **Purpose:** establish the native-first non-modal overlay boundary.
|
|
@@ -1031,6 +1082,38 @@ delta is claimed. `0.14.2` publishes this accepted packet, and
|
|
|
1031
1082
|
- **Stop condition:** package Portal/Slot reconciliation enters the runtime.
|
|
1032
1083
|
- **Done condition:** supported native and custom boundaries are explicit.
|
|
1033
1084
|
|
|
1085
|
+
Completed with the maintained project application as the qualifying fixture. A
|
|
1086
|
+
keyed project row now authors one top-level `useId()`, a native
|
|
1087
|
+
`popovertarget`, and a native `popover="auto"` action group. The compiler scopes
|
|
1088
|
+
the generated ID by the existing keyed ownership path and the list runtime
|
|
1089
|
+
rewrites only intrinsic `id`, `for`, static ARIA IDREF, `form`, `headers`,
|
|
1090
|
+
`list`, and `popovertarget` attributes. Unsupported ID escapes receive a source
|
|
1091
|
+
diagnostic. Native Chrome behavior supplies the top layer, Escape, light
|
|
1092
|
+
dismiss, and trigger focus restoration; ordinary keyed ownership preserves IDs
|
|
1093
|
+
across reorder, scopes inserted rows, recreates deterministic IDs after
|
|
1094
|
+
remove/re-add, and releases an open popover during enhanced navigation. The
|
|
1095
|
+
authored overlay uses static anchor positioning and `role="group"`, so menu
|
|
1096
|
+
roving focus is not applicable.
|
|
1097
|
+
|
|
1098
|
+
The slice adds no semantic primitive, IR kind, compiler pass, runtime concept,
|
|
1099
|
+
public API, Portal/Slot reconciliation, focus manager, positioning engine, or
|
|
1100
|
+
retained overlay tree. It adds 52 and removes 24 production lines across the
|
|
1101
|
+
source compiler, RouteIR validation, capability projection, build renderer, and
|
|
1102
|
+
existing list runtime. The maintained application emits 44 files totaling
|
|
1103
|
+
171,202 raw / 53,629 aggregate gzip bytes with deploy SHA-256
|
|
1104
|
+
`46ba45b4a423b9627c6ede5f8dd3a667f593cb5401454ae42ecc66207b07b323`.
|
|
1105
|
+
The two-route session remains 17 JavaScript files totaling 77,779 raw / 27,050
|
|
1106
|
+
aggregate gzip bytes, a 326 raw / 145 gzip byte increase from `0.15.0`; the
|
|
1107
|
+
static `/help` sibling remains 0 B JavaScript. Familiar React-shaped `useId()`
|
|
1108
|
+
authoring remains intact, so no application adapter or migration-only source
|
|
1109
|
+
structure is added.
|
|
1110
|
+
|
|
1111
|
+
Seven fresh macOS arm64 Chrome 151 profiles record table update samples of
|
|
1112
|
+
`[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,
|
|
1113
|
+
plus navigation samples of `[4, 2.4, 2.6, 2.5, 2.5, 2.8, 2.4]` ms, a 2.5 ms
|
|
1114
|
+
median and 2.4/4 ms range. These retained-path ranges overlap prior evidence,
|
|
1115
|
+
so no timing improvement or regression is claimed.
|
|
1116
|
+
|
|
1034
1117
|
### `0.15.2`: Toast And Notification Ownership
|
|
1035
1118
|
|
|
1036
1119
|
- **Purpose:** provide application feedback across route mutations.
|
|
@@ -1457,5 +1540,9 @@ release transaction where possible or document and publish a forward-fix patch.
|
|
|
1457
1540
|
| `0.13.3` | Released | Preserve the published upload validation, cancellation, retry, route cleanup, and attachment mutation evidence. | None |
|
|
1458
1541
|
| `0.14.0` | Released | Preserve native table CRUD, retained identity, keyboard access, and measured update evidence. | None |
|
|
1459
1542
|
| `0.14.1` | Released | Preserve immutable object-state replacement, nested keyed identity, latest handlers, and exact row release. | None |
|
|
1460
|
-
| `0.14.2` |
|
|
1461
|
-
| `0.14.3`
|
|
1543
|
+
| `0.14.2` | Released | Preserve bounded cursor loading, duplicate suppression, retry/end behavior, retained identity, and route cleanup. | None |
|
|
1544
|
+
| `0.14.3` | Released | Preserve the published 10,000-item pagination decision and measurement evidence. | None |
|
|
1545
|
+
| `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 |
|
|
1546
|
+
| `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 |
|
|
1547
|
+
| `0.15.1` | Released | Preserve key-scoped IDs, native Popover behavior, retained keyed identity, and route cleanup evidence. | None |
|
|
1548
|
+
| `0.15.2` | Active | Begin with a real toast and notification ownership fixture. | None |
|
|
@@ -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.126` retains the explicit install instructions and generates projects with `@kudzujs/core@^0.15.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
|
|
|
@@ -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.15.1",
|
|
4
4
|
"description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -61,6 +61,7 @@
|
|
|
61
61
|
"benchmark:module-cache": "node test/module-cache-performance.mjs",
|
|
62
62
|
"benchmark:native": "node test/native-performance.mjs",
|
|
63
63
|
"benchmark:project-navigation": "node test/project-navigation-performance.mjs",
|
|
64
|
+
"benchmark:project-list-decision": "node test/project-list-decision-performance.mjs",
|
|
64
65
|
"benchmark:project-state": "node test/project-state-performance.mjs",
|
|
65
66
|
"benchmark:source-scale": "node test/source-scale-performance.mjs",
|
|
66
67
|
"prepublishOnly": "npm run check && npm test",
|