@kudzujs/core 0.14.3 → 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 +20 -0
- package/README.md +1 -1
- package/RELEASES.md +34 -0
- package/docs/next-architecture/application-capability-release-plan.md +69 -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 +1 -1
package/PERFORMANCE.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
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
|
+
|
|
5
25
|
## 0.14.3 10,000-Item Browser Decision Intake
|
|
6
26
|
|
|
7
27
|
Measured 2026-08-27 on Linux x64 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,39 @@
|
|
|
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
|
+
|
|
3
37
|
## 0.14.3 - 10,000-Item Browser Decision
|
|
4
38
|
|
|
5
39
|
Kudzu 0.14.3 measures direct DOM, pagination, and scroll-window alternatives
|
|
@@ -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,31 @@ 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
|
+
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
|
+
|
|
1043
1075
|
### `0.15.1`: Popover, Dropdown, And Menu Behavior
|
|
1044
1076
|
|
|
1045
1077
|
- **Purpose:** establish the native-first non-modal overlay boundary.
|
|
@@ -1050,6 +1082,38 @@ virtual range ownership.
|
|
|
1050
1082
|
- **Stop condition:** package Portal/Slot reconciliation enters the runtime.
|
|
1051
1083
|
- **Done condition:** supported native and custom boundaries are explicit.
|
|
1052
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
|
+
|
|
1053
1117
|
### `0.15.2`: Toast And Notification Ownership
|
|
1054
1118
|
|
|
1055
1119
|
- **Purpose:** provide application feedback across route mutations.
|
|
@@ -1477,5 +1541,8 @@ release transaction where possible or document and publish a forward-fix patch.
|
|
|
1477
1541
|
| `0.14.0` | Released | Preserve native table CRUD, retained identity, keyboard access, and measured update evidence. | None |
|
|
1478
1542
|
| `0.14.1` | Released | Preserve immutable object-state replacement, nested keyed identity, latest handlers, and exact row release. | None |
|
|
1479
1543
|
| `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`
|
|
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) {
|