@kudzujs/core 0.7.25 → 0.7.27
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/README.md +2 -2
- package/RELEASES.md +55 -0
- package/framework/README.md +3 -3
- package/framework/binding-runtime.js +18 -2
- package/framework/build.mjs +208 -31
- package/framework/collection-selector.js +54 -41
- package/framework/core.mjs +1 -0
- package/framework/jsx-runtime.d.ts +23 -2
- package/framework/list-runtime.js +6 -5
- package/framework/shared-runtime.js +10 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
10
10
|
|
|
11
11
|
> Experimental `0.7.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
13
|
-
**Latest release: 0.7.
|
|
13
|
+
**Latest release: 0.7.27 - Stateful component migration.** Setter-adapter children can retain ordinary state, IDs, refs, effects, prop-synchronized drafts, and derived JSX locals while Kudzu still emits no browser component runtime. Read the [release notes](./RELEASES.md#0727---stateful-component-migration) or open the [release page](https://kudzujs.cloud/releases/0.7.27).
|
|
14
14
|
|
|
15
15
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
16
16
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
|
@@ -79,7 +79,7 @@ ordinary React-shaped TSX
|
|
|
79
79
|
|
|
80
80
|
- Function components execute at build time and do not survive as browser components.
|
|
81
81
|
- `useState` and reduced `useReducer`, including directly serializable lazy initialization, compile to synchronous logical state and batched direct DOM writes; top-level `useId` and direct intrinsic `forwardRef` authoring erase to static HTML without a component runtime.
|
|
82
|
-
- Conditions, keyed collections, attributes, events, refs, effects, and supported component boundaries compile to route-specific capabilities.
|
|
82
|
+
- Conditions, keyed collections, attributes, events, refs, effects, and supported component boundaries compile to route-specific capabilities. A direct setter may cross one ordinary component boundary through one value-adapter event call; inline or simple `const` setter callbacks and object refs may cross the same direct intrinsic boundary.
|
|
83
83
|
- Build-known data and routes become complete HTML through async components and `getStaticPaths()`.
|
|
84
84
|
- Native document navigation is the default; static routes do not load a client runtime.
|
|
85
85
|
- A named or aliased React Router `Link` with a static root-relative `to` erases to a base-aware native anchor; no router package or runtime is emitted.
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,60 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.27 - Stateful component migration
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.27 lets ordinary setter-adapter components keep their local hooks and derived render values while compiling them into static HTML plus existing route-specific capabilities, not a browser component runtime.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.27
|
|
8
|
+
|
|
9
|
+
- Same-file and relative-imported children receiving one supported setter callback may own multiple directly serializable `useState()` values.
|
|
10
|
+
- A child string draft may initialize from one direct primitive state prop through zero-argument `.toString()`, covering conventional controlled age and numeric inputs.
|
|
11
|
+
- Setter-adapter children may use deterministic build-time `useId()` values and `null`-initialized object refs.
|
|
12
|
+
- Supported child effects reuse existing route ESM, dependency comparison, cleanup, conditional ownership, and stale-work isolation.
|
|
13
|
+
- Conditional removal deletes child state, resolves refs to `null`, and runs cleanup; remount creates fresh ownership and prop dependency effects synchronize drafts to the latest parent value.
|
|
14
|
+
- Inline and simple `const` parent callbacks continue to specialize into the generated child helper instead of becoming serialized function captures.
|
|
15
|
+
- Reactive JSX text and attributes may read recursively chained top-level immutable locals built from supported pure primitive state expressions.
|
|
16
|
+
- Impure derived render calls, non-null child refs, unsupported dynamic state initializers, repeated callback use, and wider callback forwarding retain source-located diagnostics.
|
|
17
|
+
- React Router normalization now safely ignores local export declarations without module specifiers.
|
|
18
|
+
- A migration-derived FIRE age input verifies local draft editing, parent updates, blur validation, effect synchronization, and remount after a hidden parent update.
|
|
19
|
+
- The complete suite passes 121/121 tests, including same-file/imported hook ownership and Chrome lifecycle coverage.
|
|
20
|
+
|
|
21
|
+
### Boundary
|
|
22
|
+
|
|
23
|
+
This remains one compiler-specialized component boundary with one supported setter callback use. Prop-derived state initialization accepts only a direct primitive state prop's zero-argument `.toString()`; other dynamic initializers remain unsupported. Refs must initialize with `null`, effects must use the existing supported callback and dependency forms, and React is never emitted or executed.
|
|
24
|
+
|
|
25
|
+
### Upgrade
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @kudzujs/core@^0.7.27
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 0.7.26 - React-shaped migration and faster lists
|
|
32
|
+
|
|
33
|
+
Kudzu 0.7.26 broadens ordinary React-shaped migration input and removes repeated keyed-row cleanup scans, making the matched 500-card search faster than React without adding a VDOM, hydration, or a retained component tree.
|
|
34
|
+
|
|
35
|
+
### New in 0.7.26
|
|
36
|
+
|
|
37
|
+
- JSX typing accepts React 19 `ReactNode`-shaped component output while preserving Kudzu's compile-time element model.
|
|
38
|
+
- Intrinsic DOM handlers retain contextual event typing through migration-compatible JSX declarations.
|
|
39
|
+
- A direct setter or one-call value-adapter callback may cross one supported component boundary, covering ordinary controlled search inputs without serializing a function.
|
|
40
|
+
- Imported static collections wrapped in TypeScript-only assertions such as `as const` remain analyzable and erase normally.
|
|
41
|
+
- Collection selector execution caches state reads and avoids recursive rest-array, `slice()`, and `map()` allocation in hot expression paths.
|
|
42
|
+
- Safe keyed rows with local state but no row effects, nested lists, or shared text targets release binding and condition registrations directly by state ID instead of rescanning every removed subtree.
|
|
43
|
+
- Rows outside that proven boundary continue through the existing DOM-owned unmount path.
|
|
44
|
+
- A 21-run alternating fresh-profile benchmark measured the 500-card filter at 13.5 ms for Kudzu and 13.9 ms for React, making Kudzu 2.88% faster and 52.30% faster than its preserved 28.3 ms baseline.
|
|
45
|
+
- The same benchmark measured Kudzu's build 11.90% faster, keyed row toggle 7.02% faster, and emitted JavaScript 87.29% smaller by aggregate gzip.
|
|
46
|
+
- The complete suite passes 118/118 tests, including remove-all, unseen-key remount, and fresh row-state coverage for the indexed release path.
|
|
47
|
+
|
|
48
|
+
### Boundary
|
|
49
|
+
|
|
50
|
+
Indexed row release is compiler-selected only when cleanup can be proven from row-owned state IDs and the row has no effects, nested lists, or shared text targets. Effectful, nested, and otherwise lifecycle-bearing rows retain the general ownership cleanup path. Setter adapters remain limited to one supported component boundary and one direct intrinsic handler call. React remains migration input only and is never emitted or executed.
|
|
51
|
+
|
|
52
|
+
### Upgrade
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install @kudzujs/core@^0.7.26
|
|
56
|
+
```
|
|
57
|
+
|
|
3
58
|
## 0.7.25 - Router-shaped native navigation
|
|
4
59
|
|
|
5
60
|
Kudzu 0.7.25 accepts a narrow React Router `useNavigate()` source shape and lowers safe static destinations to native full-document navigation without shipping React Router or adding an SPA runtime.
|
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`. `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 reject it because cloned row templates cannot safely duplicate HTML IDs. Collection memos may start from local array state or a named relative import of an exported JSON-safe `const` array and may read direct local state declared in their dependency array. `build.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.
|
|
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`. 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 reject it because cloned row templates cannot safely duplicate HTML IDs. 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. `build.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
|
A named or aliased `Link` import from `react-router-dom` may render directly with one static root-relative `to` plus native anchor props. The compiler prefixes the configured `base`, changes the element to `<a href>`, and erases the import, so native navigation remains the default and configured navigation groups see an ordinary eligible anchor. Dynamic or relative destinations, `NavLink`, router-only props, spreads, default/namespace imports, and non-JSX uses fail with source diagnostics. No React Router package code or router runtime is emitted.
|
|
8
8
|
|
|
@@ -14,7 +14,7 @@ A named or aliased React Router `useNavigate` import may initialize one top-leve
|
|
|
14
14
|
|
|
15
15
|
Direct `clsx` calls over literal strings, numbers, arrays, object conditions, and conditional expressions are similarly lowered to ordinary concatenation and conditional expressions. The package import is erased, and dynamic classes continue through the existing binding compiler without serializing or shipping the `clsx` function.
|
|
16
16
|
|
|
17
|
-
Repeated ordinary same-file and relative-imported child components execute independently at build time, so each `useState` call receives a distinct concrete state ID while shared native handler modules retain per-element state maps and captures. A direct JSON-safe primitive parent state passed to a destructured child prop remains the same signal for child DOM bindings and effect dependencies; repeated calls own independent effect records, and conditional removal cleans up before remount recreates the effect. Multiple direct primitive dependencies share the existing commit batching path: every value is compared with `Object.is`, and one or more same-turn changes cause one cleanup and rerun. A top-level immutable local derived through a supported pure primitive expression from direct state may also be an effect dependency: source state commits schedule evaluation, the derived result is compared with `Object.is`, and the expression is substituted into setup and cleanup handlers. Effect setup and directly returned cleanup callbacks may each resolve one top-level simple `const` function in the same component; those functions are substituted into the existing handler graph rather than retained in a browser registry. Reactive conditional descriptors own state created by their direct branch: initial visible output reuses the rendered template IDs, removal deletes those slots, and remount recreates them from serialized initial values. Static sibling routes and branches without local state add no ownership metadata, component function, hook dispatcher, or rerender loop.
|
|
17
|
+
Repeated ordinary same-file and relative-imported child components execute independently at build time, so each `useState` call receives a distinct concrete state ID while shared native handler modules retain per-element state maps and captures. A direct JSON-safe primitive parent state passed to a destructured child prop remains the same signal for child DOM bindings and effect dependencies; repeated calls own independent effect records, and conditional removal cleans up before remount recreates the effect. Reactive text and attributes may reference recursively chained top-level immutable locals derived through supported pure primitive expressions from direct state; the compiler substitutes those expressions into the existing binding evaluator and subscribes every source state. Multiple direct primitive dependencies share the existing commit batching path: every value is compared with `Object.is`, and one or more same-turn changes cause one cleanup and rerun. A top-level immutable local derived through a supported pure primitive expression from direct state may also be an effect dependency: source state commits schedule evaluation, the derived result is compared with `Object.is`, and the expression is substituted into setup and cleanup handlers. Effect setup and directly returned cleanup callbacks may each resolve one top-level simple `const` function in the same component; those functions are substituted into the existing handler graph rather than retained in a browser registry. Reactive conditional descriptors own state created by their direct branch: initial visible output reuses the rendered template IDs, removal deletes those slots, and remount recreates them from serialized initial values. Static sibling routes and branches without local state add no ownership metadata, component function, hook dispatcher, or rerender loop.
|
|
18
18
|
|
|
19
19
|
Reduced Zustand migration stores lower to one ordinary layout-lifetime state slot. The compiler accepts one exported `create(set => ({ data, ...actions }))` store with one serializable data property, direct property selectors, and synchronous capture-free actions using one-argument merge-form `set`; selected actions reuse the reducer-style functional update compiler, so same-turn calls observe current logical state and DOM writes still batch. The shared layout must initialize the store before route consumers, outside keyed rows. No Zustand import, store subscription runtime, React hook, or generic external-store capability is emitted.
|
|
20
20
|
|
|
@@ -47,7 +47,7 @@ Rendered collection selectors compile immutable local aliases and inline `(item)
|
|
|
47
47
|
|
|
48
48
|
The reduced `useReducer` form reuses ordinary state slots and React's pure reducer contract. An optional inline, same-file, or relative-imported synchronous one-parameter initializer may derive a directly serializable literal only from its directly serializable initial argument; the compiler substitutes that argument and lowers the call to the ordinary two-argument ownership path. A direct dispatch in a compiled handler becomes a functional `set` whose reducer is bundled from a relative TypeScript module into that handler graph. Pure reducer-owned keyed lists reuse unchanged item identities for reorder, one removal, and append fast paths; ordinary `useState` lists retain full validation. One direct dispatch prop into a same-file or relative-imported synchronous component, including a direct keyed row, is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. A reducer row reads the latest item through the existing list scope and uses the same multiple serializable state, effect, condition, and object-ref specialization as other keyed rows. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Missing directly serializable literal defaults and direct intrinsic rest props in these reducer specializations are substituted at the same call site. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
A direct setter may cross one same-file or relative-imported component boundary when the child invokes it exactly once inside an intrinsic event handler. Inline and simple `const` setter callbacks may use that adapter shape or direct event forwarding. Value adapters such as `event => onValueChange(event.currentTarget.value)` specialize into the parent setter graph instead of serializing a function. The child may also own directly serializable `useState()` values or initialize string state with one direct primitive state prop's zero-argument `.toString()`, plus `useId()` values, supported effects, and object refs initialized with `null`. Other dynamic state initializers remain unsupported. A parent-owned `null`-initialized object ref may cross the same boundary to the direct intrinsic root. These remain compiler-owned descriptors: conditional removal drops the handler with the element, deletes child state, resolves refs to `null`, and runs effect cleanup, while remount creates fresh ownership and dependency effects synchronize prop-derived state without retaining a component instance.
|
|
51
51
|
|
|
52
52
|
`kudzu.config` may opt one emitted shared-layout group into same-document navigation with legacy `navigation: { routes: ["/product", "/items/[id]"] }`, or multiple groups with `navigation: { groups: [{ routes: [...] }, { routes: [...] }] }`. The forms are mutually exclusive. Identities are globally unique emitted exact paths or `runtimeParams` patterns; each group uses one page-exported layout function identity. Runtime records securely match concrete pathnames under `base`, and their cache-safe parameter initializer runs before route DOM/effects mount on every transition. Each group receives a deterministic route-hashed asset specialized to only its records, pattern decoder, and effect/parameter lifecycle needs. Cross-group and ungrouped anchors remain native and are not prefetched; overlapping path domains across groups fail the build. Route effect entries export cache-safe layout and route mount functions: layout effects, including conditional/keyed DOM-owned effects, persist for the group session; route effects receive a fresh owner registry after each route insertion; and non-persisted page disposal cleans route before layout. Direct primitive state, runtime parameter, and keyed-item property dependencies and cleanup are supported. Fragment payloads and coordinated View Transitions are not implemented.
|
|
53
53
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { browserState, mountDom, registerCommitter, registerMountHook, registerUnmountHook, unmountDom } from "./shared-runtime.js"
|
|
1
|
+
import { browserState, mountDom, registerCommitter, registerMountHook, registerStateReleaseHook, registerUnmountHook, releaseState, unmountDom } from "./shared-runtime.js"
|
|
2
2
|
import { deserialize } from "./serialization.js"
|
|
3
3
|
import { serializeStyle } from "./style.js"
|
|
4
4
|
|
|
@@ -63,6 +63,7 @@ registerMountHook(mountBindings)
|
|
|
63
63
|
registerMountHook(mountConditions)
|
|
64
64
|
registerUnmountHook(unmountBindings)
|
|
65
65
|
registerUnmountHook(unmountConditions)
|
|
66
|
+
registerStateReleaseHook(releaseBindings)
|
|
66
67
|
|
|
67
68
|
if (typeof document !== "undefined") mountDom(document)
|
|
68
69
|
|
|
@@ -195,6 +196,21 @@ function unmountConditions(root) {
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
function releaseBindings(id) {
|
|
200
|
+
for (const binding of bindingTargets.get(id) ?? []) {
|
|
201
|
+
for (const [bindingId, entry] of bindingRegistrations.get(binding.node) ?? []) bindingTargets.get(bindingId)?.delete(entry)
|
|
202
|
+
bindingRegistrations.delete(binding.node)
|
|
203
|
+
}
|
|
204
|
+
bindingTargets.delete(id)
|
|
205
|
+
for (const condition of conditionTargets.get(id) ?? []) {
|
|
206
|
+
const registration = conditionRegistrations.get(condition.start)
|
|
207
|
+
for (const [conditionId, entry] of registration?.registrations ?? []) conditionTargets.get(conditionId)?.delete(entry)
|
|
208
|
+
unmountConditionStates(condition, condition.current === "true")
|
|
209
|
+
conditionRegistrations.delete(condition.start)
|
|
210
|
+
}
|
|
211
|
+
conditionTargets.delete(id)
|
|
212
|
+
}
|
|
213
|
+
|
|
198
214
|
function mountConditionStates(condition, truthy, replace) {
|
|
199
215
|
for (const [id, initialValue] of condition.owned?.[truthy ? "true" : "false"] ?? []) {
|
|
200
216
|
if (replace || !browserState.has(id)) browserState.set(id, initialValue !== null && typeof initialValue === "object" ? structuredClone(initialValue) : initialValue)
|
|
@@ -202,7 +218,7 @@ function mountConditionStates(condition, truthy, replace) {
|
|
|
202
218
|
}
|
|
203
219
|
|
|
204
220
|
function unmountConditionStates(condition, truthy) {
|
|
205
|
-
for (const [id] of condition.owned?.[truthy ? "true" : "false"] ?? [])
|
|
221
|
+
for (const [id] of condition.owned?.[truthy ? "true" : "false"] ?? []) releaseState(id)
|
|
206
222
|
}
|
|
207
223
|
|
|
208
224
|
function removeConditionRange(start, end, mount) {
|
package/framework/build.mjs
CHANGED
|
@@ -1790,7 +1790,7 @@ function normalizeReactRouterSyntax(sourceFile, factory, context, base) {
|
|
|
1790
1790
|
const searchHooks = new Set()
|
|
1791
1791
|
const navigateHooks = new Set()
|
|
1792
1792
|
for (const statement of sourceFile.statements) {
|
|
1793
|
-
if ((ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement)) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "react-router-dom") {
|
|
1793
|
+
if ((ts.isExportDeclaration(statement) || ts.isImportDeclaration(statement)) && statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "react-router-dom") {
|
|
1794
1794
|
if (ts.isExportDeclaration(statement)) throw sourceNodeError(statement, sourceFile, "React Router exports are not supported; import Link directly where it renders")
|
|
1795
1795
|
const clause = statement.importClause
|
|
1796
1796
|
if (clause?.isTypeOnly) continue
|
|
@@ -2584,6 +2584,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2584
2584
|
const listConditions = new WeakMap()
|
|
2585
2585
|
const nestedLists = new WeakMap()
|
|
2586
2586
|
const listEffectEntries = new WeakMap()
|
|
2587
|
+
const componentEffectEntries = new WeakMap()
|
|
2587
2588
|
let usesBehavior = false
|
|
2588
2589
|
let usesBinding = false
|
|
2589
2590
|
let usesConditional = false
|
|
@@ -2592,6 +2593,10 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2592
2593
|
let usesListItem = false
|
|
2593
2594
|
let usesRowState = false
|
|
2594
2595
|
let usesRowRef = false
|
|
2596
|
+
let usesComponentState = false
|
|
2597
|
+
let usesComponentId = false
|
|
2598
|
+
let usesComponentRef = false
|
|
2599
|
+
let usesComponentEffects = false
|
|
2595
2600
|
|
|
2596
2601
|
const collect = node => {
|
|
2597
2602
|
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
@@ -2717,7 +2722,47 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2717
2722
|
const fail = (node, message) => {
|
|
2718
2723
|
throw sourceNodeError(node, sourceFile, message)
|
|
2719
2724
|
}
|
|
2725
|
+
const resolveReactiveJsxExpression = (expression, owner, setters) => {
|
|
2726
|
+
const declarations = jsxLocalDeclarations.get(owner)
|
|
2727
|
+
if (!declarations) return expression
|
|
2728
|
+
const substitutions = new Map()
|
|
2729
|
+
const resolving = []
|
|
2730
|
+
const resolve = (name, reference) => {
|
|
2731
|
+
if (substitutions.has(name)) return
|
|
2732
|
+
const entries = declarations.get(name)
|
|
2733
|
+
if (!entries?.length) return
|
|
2734
|
+
if (jsxLocalsByFunction.get(owner)?.has(name)) return
|
|
2735
|
+
if (entries.length !== 1 || entries[0].node.parent?.parent?.parent !== owner?.body) return
|
|
2736
|
+
const cycle = resolving.indexOf(name)
|
|
2737
|
+
if (cycle >= 0) fail(reference, `Reactive JSX local cycle: ${[...resolving.slice(cycle), name].join(" -> ")}`)
|
|
2738
|
+
resolving.push(name)
|
|
2739
|
+
const initializer = entries[0].initializer
|
|
2740
|
+
const visit = node => {
|
|
2741
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, initializer) && declarations.has(node.text)) resolve(node.text, node)
|
|
2742
|
+
ts.forEachChild(node, visit)
|
|
2743
|
+
}
|
|
2744
|
+
visit(initializer)
|
|
2745
|
+
substitutions.set(name, substituteClone(initializer, substitutions, factory, context))
|
|
2746
|
+
resolving.pop()
|
|
2747
|
+
}
|
|
2748
|
+
const visit = node => {
|
|
2749
|
+
if (ts.isIdentifier(node) && isReferenceIdentifier(node) && !isShadowedByParameter(node, expression) && declarations.has(node.text)) resolve(node.text, node)
|
|
2750
|
+
ts.forEachChild(node, visit)
|
|
2751
|
+
}
|
|
2752
|
+
visit(expression)
|
|
2753
|
+
if (!substitutions.size) return expression
|
|
2754
|
+
const expanded = substituteClone(expression, substitutions, factory, context)
|
|
2755
|
+
ts.setParentRecursive(expanded, false)
|
|
2756
|
+
expanded.parent = expression.parent
|
|
2757
|
+
const usedStates = referencedStateNames(expanded, setters)
|
|
2758
|
+
if (!usedStates.size) return expression
|
|
2759
|
+
const captures = captureNames(expanded, expanded, setters)
|
|
2760
|
+
const allowedNames = new Set([...setters.values(), ...captures])
|
|
2761
|
+
collectionExpression(expanded, {}, (node, message) => fail(node, message.replace("Rendered collection", "Reactive JSX local")), allowedNames)
|
|
2762
|
+
return expanded
|
|
2763
|
+
}
|
|
2720
2764
|
const componentSpecializations = new WeakMap()
|
|
2765
|
+
const setterHookHelpers = new WeakMap()
|
|
2721
2766
|
const expandedRowSpecializations = new WeakMap()
|
|
2722
2767
|
const nestedRowSpecializations = new Map()
|
|
2723
2768
|
const reducerComponentCalls = new WeakSet()
|
|
@@ -2842,6 +2887,92 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2842
2887
|
stateBackedComponentRoots.push(specialization.root)
|
|
2843
2888
|
}
|
|
2844
2889
|
}
|
|
2890
|
+
const specializeSetterCallbacks = (call, component, callbackProps, imported) => {
|
|
2891
|
+
if (componentSpecializations.has(call)) fail(call, "Setter callback props cannot be combined with another component specialization")
|
|
2892
|
+
if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, "Setter-callback components must use one destructured props parameter")
|
|
2893
|
+
for (const prop of callbackProps) {
|
|
2894
|
+
const element = component.parameters[0].name.elements.find(entry => !entry.dotDotDotToken && (entry.propertyName ?? entry.name).getText() === prop)
|
|
2895
|
+
if (!element || !ts.isIdentifier(element.name)) fail(call, `Setter-callback component must destructure callback prop ${JSON.stringify(prop)}`)
|
|
2896
|
+
const references = []
|
|
2897
|
+
const collectReferences = node => {
|
|
2898
|
+
if (ts.isIdentifier(node) && node.text === element.name.text && isReferenceIdentifier(node)) references.push(node)
|
|
2899
|
+
ts.forEachChild(node, collectReferences)
|
|
2900
|
+
}
|
|
2901
|
+
collectReferences(component.body)
|
|
2902
|
+
if (references.length !== 1) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be used exactly once in the component`)
|
|
2903
|
+
}
|
|
2904
|
+
const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Setter-callback", false, true, new Set(settersForNode(call, settersByFunction).values()))
|
|
2905
|
+
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
2906
|
+
const substitutions = new Map()
|
|
2907
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
2908
|
+
for (const attribute of attributes.properties) {
|
|
2909
|
+
if (!ts.isJsxAttribute(attribute) || !callbackProps.includes(attribute.name.text) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !ts.isIdentifier(attribute.initializer.expression)) continue
|
|
2910
|
+
const callback = functions.get(attribute.initializer.expression.text)
|
|
2911
|
+
if (callback) substitutions.set(attribute.initializer.expression.text, callback)
|
|
2912
|
+
}
|
|
2913
|
+
if (substitutions.size) {
|
|
2914
|
+
specialization.root = substituteClone(specialization.root, substitutions, factory, context)
|
|
2915
|
+
for (const effect of specialization.effects) effect.call = substituteClone(effect.call, substitutions, factory, context)
|
|
2916
|
+
}
|
|
2917
|
+
}
|
|
2918
|
+
if (imported) synthesizeTree(specialization.root = mergeSpecializedImports(specialization.root, component.getSourceFile(), call, specialization.effects))
|
|
2919
|
+
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
2920
|
+
const owner = nearestFunction(call)
|
|
2921
|
+
const name = `KSetterComponent${Math.max(0, call.pos)}`
|
|
2922
|
+
const effectStatements = specialization.effects.map(entry => {
|
|
2923
|
+
const effectCall = factory.updateCallExpression(entry.call, factory.createIdentifier("__kComponentUseEffect"), entry.call.typeArguments, entry.call.arguments)
|
|
2924
|
+
synthesizeTree(effectCall)
|
|
2925
|
+
const effectSource = entry.source.getSourceFile()
|
|
2926
|
+
componentEffectEntries.set(effectCall, { source: entry.source, sourceFile: effectSource, imports: clientImportBindings(effectSource, effectSource.fileName, sourceFiles) })
|
|
2927
|
+
return factory.createExpressionStatement(effectCall)
|
|
2928
|
+
})
|
|
2929
|
+
const helper = factory.createFunctionDeclaration(
|
|
2930
|
+
undefined,
|
|
2931
|
+
undefined,
|
|
2932
|
+
name,
|
|
2933
|
+
undefined,
|
|
2934
|
+
[],
|
|
2935
|
+
undefined,
|
|
2936
|
+
factory.createBlock([...specialization.hookDeclarations, ...effectStatements, factory.createReturnStatement(specialization.root)], true)
|
|
2937
|
+
)
|
|
2938
|
+
ts.setParentRecursive(helper, false)
|
|
2939
|
+
helper.parent = owner.body
|
|
2940
|
+
const helpers = setterHookHelpers.get(owner.body) ?? []
|
|
2941
|
+
helpers.push(helper)
|
|
2942
|
+
setterHookHelpers.set(owner.body, helpers)
|
|
2943
|
+
const setters = new Map(settersForNode(call, settersByFunction))
|
|
2944
|
+
for (const state of specialization.ordinaryStates) setters.set(state.setter, state.state)
|
|
2945
|
+
settersByFunction.set(helper, setters)
|
|
2946
|
+
usesComponentState ||= specialization.ordinaryStates.length > 0
|
|
2947
|
+
usesComponentId ||= specialization.usesComponentId
|
|
2948
|
+
usesComponentRef ||= specialization.ordinaryRefs.length > 0
|
|
2949
|
+
usesComponentEffects ||= specialization.effects.length > 0
|
|
2950
|
+
specialization.root = factory.createJsxSelfClosingElement(factory.createIdentifier(name), undefined, factory.createJsxAttributes([]))
|
|
2951
|
+
ts.setParentRecursive(specialization.root, false)
|
|
2952
|
+
specialization.root.parent = call.parent
|
|
2953
|
+
}
|
|
2954
|
+
componentSpecializations.set(call, specialization)
|
|
2955
|
+
}
|
|
2956
|
+
for (const [name, component] of components) {
|
|
2957
|
+
for (const call of jsxTagUses(sourceFile, name)) {
|
|
2958
|
+
const callbackProps = jsxSetterCallbackProps(call, settersByFunction.get(nearestFunction(call)) ?? new Map(), functions, reducersForNode(call, reducersByFunction))
|
|
2959
|
+
if (callbackProps.length) specializeSetterCallbacks(call, component.function, callbackProps, false)
|
|
2960
|
+
}
|
|
2961
|
+
}
|
|
2962
|
+
for (const [name, binding] of importBindings) {
|
|
2963
|
+
if (binding.kind === "namespace") continue
|
|
2964
|
+
const calls = jsxTagUses(sourceFile, name)
|
|
2965
|
+
const callbackCalls = calls.map(call => ({ call, callbackProps: jsxSetterCallbackProps(call, settersByFunction.get(nearestFunction(call)) ?? new Map(), functions, reducersForNode(call, reducersByFunction)) })).filter(entry => entry.callbackProps.length)
|
|
2966
|
+
if (!callbackCalls.length) continue
|
|
2967
|
+
const imported = binding.kind === "default" ? "default" : binding.imported
|
|
2968
|
+
let component
|
|
2969
|
+
try {
|
|
2970
|
+
component = resolveComponentExport(binding.target, imported, importedSource, sourceFiles)
|
|
2971
|
+
} catch {
|
|
2972
|
+
fail(callbackCalls[0].call, "Setter callback props require a component imported from a relative TypeScript module")
|
|
2973
|
+
}
|
|
2974
|
+
for (const { call, callbackProps } of callbackCalls) specializeSetterCallbacks(call, component, callbackProps, true)
|
|
2975
|
+
}
|
|
2845
2976
|
for (const [name, component] of components) {
|
|
2846
2977
|
const calls = jsxTagUses(sourceFile, name)
|
|
2847
2978
|
const dispatchCalls = calls.filter(call => jsxCallHasDirectReducerProp(call, reducersForNode(call, reducersByFunction)))
|
|
@@ -3075,6 +3206,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3075
3206
|
}
|
|
3076
3207
|
|
|
3077
3208
|
const visitor = node => {
|
|
3209
|
+
if (ts.isBlock(node) && setterHookHelpers.has(node)) {
|
|
3210
|
+
return ts.visitEachChild(factory.updateBlock(node, [...setterHookHelpers.get(node), ...node.statements]), visitor, context)
|
|
3211
|
+
}
|
|
3078
3212
|
if (specializedDeclarations.has(node)) return node
|
|
3079
3213
|
if (componentSpecializations.has(node)) return ts.visitNode(componentSpecializations.get(node).root, visitor)
|
|
3080
3214
|
|
|
@@ -3100,9 +3234,11 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3100
3234
|
}
|
|
3101
3235
|
|
|
3102
3236
|
const listEffect = ts.isCallExpression(node) ? listEffectEntries.get(node) : undefined
|
|
3103
|
-
|
|
3237
|
+
const componentEffect = ts.isCallExpression(node) ? componentEffectEntries.get(node) : undefined
|
|
3238
|
+
const specializedEffect = listEffect ?? componentEffect
|
|
3239
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && (hasUseEffectImport && node.expression.text === "useEffect" || specializedEffect)) {
|
|
3104
3240
|
const effectFail = (target, message) => {
|
|
3105
|
-
if (
|
|
3241
|
+
if (specializedEffect) throw sourceNodeError(specializedEffect.source, specializedEffect.sourceFile, message)
|
|
3106
3242
|
fail(target, message)
|
|
3107
3243
|
}
|
|
3108
3244
|
if (node.arguments.length !== 2) effectFail(node, "useEffect() requires exactly a callback and literal dependency array")
|
|
@@ -3195,7 +3331,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3195
3331
|
const invalidCleanup = returns.cleanups.find(cleanup => cleanup.parameters.length || cleanup.asteriskToken)
|
|
3196
3332
|
if (invalidCleanup) effectFail(invalidCleanup, "useEffect() cleanup functions cannot declare parameters or be generators")
|
|
3197
3333
|
if (returns.cleanup && callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) effectFail(callback, "useEffect() async callbacks cannot return cleanup functions")
|
|
3198
|
-
const callbackSource =
|
|
3334
|
+
const callbackSource = specializedEffect?.sourceFile ?? sourceFile
|
|
3199
3335
|
const callbackFile = callbackSource.fileName
|
|
3200
3336
|
const workerStart = workerReferences.length
|
|
3201
3337
|
let compiledCallback = dependencySubstitutions.size ? substituteClone(callback, dependencySubstitutions, factory, context) : callback
|
|
@@ -3209,7 +3345,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3209
3345
|
} else {
|
|
3210
3346
|
compiledCallback = rewriteEffectWorkers(compiledCallback, callbackFile, callbackSource, sourceFiles, workerReferences, factory, context)
|
|
3211
3347
|
}
|
|
3212
|
-
const descriptor = compileNativeCallback(compiledCallback, setters, reducersForNode(node, reducersByFunction), factory, effectHandlers,
|
|
3348
|
+
const descriptor = compileNativeCallback(compiledCallback, setters, reducersForNode(node, reducersByFunction), factory, effectHandlers, specializedEffect?.imports ?? importBindings, clientImports, "effect", dependencyItem, true, returns.cleanup)
|
|
3213
3349
|
for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
|
|
3214
3350
|
usesListItem ||= Boolean(itemDependencies.length && !listEffect)
|
|
3215
3351
|
usesBehavior = true
|
|
@@ -3220,7 +3356,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3220
3356
|
factory.createStringLiteral(descriptor.exportName),
|
|
3221
3357
|
descriptor.states,
|
|
3222
3358
|
descriptor.scope,
|
|
3223
|
-
factory.createStringLiteral(
|
|
3359
|
+
factory.createStringLiteral(specializedEffect ? sourceLocation(specializedEffect.source, specializedEffect.sourceFile) : sourceLocation(node, sourceFile)),
|
|
3224
3360
|
returns.cleanup ? factory.createTrue() : factory.createFalse(),
|
|
3225
3361
|
factory.createArrayLiteralExpression(itemDependencies.map(field => factory.createStringLiteral(field))),
|
|
3226
3362
|
hasDerivedDependency ? jsonExpression(dependencyExpressions, factory) : factory.createArrayLiteralExpression(),
|
|
@@ -3228,7 +3364,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3228
3364
|
])
|
|
3229
3365
|
}
|
|
3230
3366
|
|
|
3231
|
-
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && ((node.initializer.expression.text === "useState" || node.initializer.expression.text === "__kRowUseState") && node.initializer.arguments.length === 1 || node.initializer.expression.text === "useReducer" && node.initializer.arguments.length === 2)) {
|
|
3367
|
+
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && ((node.initializer.expression.text === "useState" || node.initializer.expression.text === "__kRowUseState" || node.initializer.expression.text === "__kComponentUseState") && node.initializer.arguments.length === 1 || node.initializer.expression.text === "useReducer" && node.initializer.arguments.length === 2)) {
|
|
3232
3368
|
const stateElement = node.name.elements[0]
|
|
3233
3369
|
if (!stateElement || !ts.isBindingElement(stateElement) || !ts.isIdentifier(stateElement.name)) return node
|
|
3234
3370
|
const initializer = factory.updateCallExpression(node.initializer, node.initializer.expression, node.initializer.typeArguments, [
|
|
@@ -3292,18 +3428,20 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3292
3428
|
if (compiled !== node.expression) return factory.updateJsxExpression(node, compiled)
|
|
3293
3429
|
}
|
|
3294
3430
|
const setters = settersForNode(node, settersByFunction)
|
|
3295
|
-
const
|
|
3296
|
-
const
|
|
3297
|
-
|
|
3431
|
+
const expression = resolveReactiveJsxExpression(node.expression, nearestFunction(node), setters)
|
|
3432
|
+
const usedStates = referencedStateNames(expression, setters)
|
|
3433
|
+
const captures = captureNames(expression, expression, setters)
|
|
3434
|
+
if ((usedStates.size || captures.size) && !ts.isIdentifier(expression) && !containsJsx(expression)) {
|
|
3298
3435
|
usesBehavior = true
|
|
3299
3436
|
usesBinding = true
|
|
3300
|
-
return factory.updateJsxExpression(node, compileReactiveBinding(
|
|
3437
|
+
return factory.updateJsxExpression(node, compileReactiveBinding(expression, setters, factory, context, reactiveBindings, handlerUrl))
|
|
3301
3438
|
}
|
|
3302
3439
|
}
|
|
3303
3440
|
|
|
3304
3441
|
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && !isContextProviderValue(node, contexts) && !/^on/i.test(node.name.text) && !["key", "ref", "dangerouslysetinnerhtml"].includes(node.name.text.toLowerCase())) {
|
|
3305
|
-
const
|
|
3442
|
+
const sourceExpression = node.initializer.expression
|
|
3306
3443
|
const setters = settersForNode(node, settersByFunction)
|
|
3444
|
+
const expression = resolveReactiveJsxExpression(sourceExpression, nearestFunction(node), setters)
|
|
3307
3445
|
const usedStates = referencedStateNames(expression, setters)
|
|
3308
3446
|
const captures = captureNames(expression, expression, setters)
|
|
3309
3447
|
if ((usedStates.size || captures.size) && !ts.isIdentifier(expression)) {
|
|
@@ -3354,6 +3492,10 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3354
3492
|
if (usesListEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kListUseEffect")))
|
|
3355
3493
|
if (usesRowState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kRowUseState")))
|
|
3356
3494
|
if (usesRowRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kRowUseRef")))
|
|
3495
|
+
if (usesComponentState) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useState"), factory.createIdentifier("__kComponentUseState")))
|
|
3496
|
+
if (usesComponentId) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useId"), factory.createIdentifier("__kComponentUseId")))
|
|
3497
|
+
if (usesComponentRef) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useRef"), factory.createIdentifier("__kComponentUseRef")))
|
|
3498
|
+
if (usesComponentEffects) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("useEffect"), factory.createIdentifier("__kComponentUseEffect")))
|
|
3357
3499
|
if (usesBinding || usesConditional) behaviorImports.push(factory.createImportSpecifier(false, factory.createIdentifier("bindingValue"), factory.createIdentifier("__kBindingValue")))
|
|
3358
3500
|
const behaviorImport = factory.createImportDeclaration(
|
|
3359
3501
|
undefined,
|
|
@@ -3719,6 +3861,17 @@ function jsxCallHasDirectStateProp(call, setters) {
|
|
|
3719
3861
|
})
|
|
3720
3862
|
}
|
|
3721
3863
|
|
|
3864
|
+
function jsxSetterCallbackProps(call, setters, functions, reducers) {
|
|
3865
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
3866
|
+
return attributes.properties.flatMap(attribute => {
|
|
3867
|
+
if (!ts.isJsxAttribute(attribute) || !/^on[A-Z]/.test(attribute.name.text) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression) return []
|
|
3868
|
+
const value = unwrapExpression(attribute.initializer.expression)
|
|
3869
|
+
if (ts.isIdentifier(value) && setters.has(value.text)) return [attribute.name.text]
|
|
3870
|
+
const callback = ts.isArrowFunction(value) || ts.isFunctionExpression(value) ? value : ts.isIdentifier(value) ? functions.get(value.text) : undefined
|
|
3871
|
+
return callback && !nativeCaptureNames(callback, setters).size && !referencedReducerDispatches(callback.body, reducers, callback).size && referencedStateNames(callback.body, setters, callback).size ? [attribute.name.text] : []
|
|
3872
|
+
})
|
|
3873
|
+
}
|
|
3874
|
+
|
|
3722
3875
|
function jsxCallHasDirectReducerProp(call, reducers) {
|
|
3723
3876
|
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
3724
3877
|
return attributes.properties.some(attribute => {
|
|
@@ -3979,7 +4132,7 @@ function expandSpecializedRest(root, returned, component, rest, entries, factory
|
|
|
3979
4132
|
return factory.updateJsxElement(root, opening, root.children, root.closingElement)
|
|
3980
4133
|
}
|
|
3981
4134
|
|
|
3982
|
-
function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list", allowComponentRoot = false) {
|
|
4135
|
+
function specializeComponentCall(call, component, sourceFile, factory, context, fail, label = "Keyed list", allowComponentRoot = false, ordinaryHooks = false, ordinaryStateNames = new Set()) {
|
|
3983
4136
|
if (component.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || component.asteriskToken) fail(component, `${label} components must be synchronous`)
|
|
3984
4137
|
if (component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name)) fail(component, `${label} components must use one destructured props parameter`)
|
|
3985
4138
|
const callAttributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
@@ -4040,6 +4193,9 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
4040
4193
|
const hookDeclarations = []
|
|
4041
4194
|
const rowStates = []
|
|
4042
4195
|
const rowRefs = []
|
|
4196
|
+
const ordinaryStates = []
|
|
4197
|
+
const ordinaryRefs = []
|
|
4198
|
+
let usesComponentId = false
|
|
4043
4199
|
if (!ts.isBlock(component.body)) {
|
|
4044
4200
|
returned = component.body
|
|
4045
4201
|
} else {
|
|
@@ -4054,35 +4210,52 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
4054
4210
|
if (!ts.isVariableStatement(statement) || (statement.declarationList.flags & ts.NodeFlags.Const) === 0 || statement.declarationList.declarations.length !== 1) fail(statement, `${label} component locals must be single const declarations`)
|
|
4055
4211
|
const declaration = statement.declarationList.declarations[0]
|
|
4056
4212
|
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState") {
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
const
|
|
4060
|
-
const
|
|
4061
|
-
const
|
|
4213
|
+
const hookLabel = ordinaryHooks ? "Setter-callback component" : "Keyed row"
|
|
4214
|
+
const initialArgument = declaration.initializer.arguments[0]
|
|
4215
|
+
const propReceiver = ordinaryHooks && initialArgument && ts.isCallExpression(initialArgument) && initialArgument.arguments.length === 0 && !initialArgument.questionDotToken && ts.isPropertyAccessExpression(initialArgument.expression) && !initialArgument.expression.questionDotToken && initialArgument.expression.name.text === "toString" && ts.isIdentifier(initialArgument.expression.expression) ? initialArgument.expression.expression : undefined
|
|
4216
|
+
const substitutedProp = propReceiver ? substitutions.get(propReceiver.text) : undefined
|
|
4217
|
+
const propStringInitializer = substitutedProp && ts.isIdentifier(unwrapExpression(substitutedProp)) && ordinaryStateNames.has(unwrapExpression(substitutedProp).text)
|
|
4218
|
+
if (declaration.initializer.arguments.length !== 1 || !isSerializableStateLiteral(initialArgument) && !propStringInitializer) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useState() must use one directly serializable primitive, plain object, or array initial value${ordinaryHooks ? " or direct primitive state prop.toString()" : ""}; other dynamic initializers are not supported`)
|
|
4219
|
+
if (!ts.isArrayBindingPattern(declaration.name) || declaration.name.elements.length !== 2 || declaration.name.elements.some(element => !element || !ts.isBindingElement(element) || !ts.isIdentifier(element.name) || element.initializer || element.dotDotDotToken)) throw sourceNodeError(declaration.name, component.getSourceFile(), `${hookLabel} useState() must use [state, setter] identifier destructuring`)
|
|
4220
|
+
const suffix = `${Math.max(0, call.pos)}_${ordinaryHooks ? ordinaryStates.length : rowStates.length}`
|
|
4221
|
+
const state = ordinaryHooks ? `__kComponentState${suffix}` : `__kRowState${suffix}`
|
|
4222
|
+
const setter = ordinaryHooks ? `__kComponentSetter${suffix}` : `__kRowSetter${suffix}`
|
|
4062
4223
|
substitutions.set(declaration.name.elements[0].name.text, factory.createIdentifier(state))
|
|
4063
4224
|
substitutions.set(declaration.name.elements[1].name.text, factory.createIdentifier(setter))
|
|
4064
4225
|
const binding = factory.createArrayBindingPattern([
|
|
4065
4226
|
factory.createBindingElement(undefined, undefined, factory.createIdentifier(state)),
|
|
4066
4227
|
factory.createBindingElement(undefined, undefined, factory.createIdentifier(setter))
|
|
4067
4228
|
])
|
|
4068
|
-
const initialValue = cloneAst(
|
|
4229
|
+
const initialValue = propStringInitializer ? substituteClone(initialArgument, substitutions, factory, context) : cloneAst(initialArgument, factory, context)
|
|
4069
4230
|
synthesizeTree(initialValue)
|
|
4070
|
-
const initializer = factory.createCallExpression(factory.createIdentifier("__kRowUseState"), undefined, [initialValue])
|
|
4231
|
+
const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseState" : "__kRowUseState"), undefined, [initialValue])
|
|
4071
4232
|
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(binding, undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
4072
|
-
|
|
4233
|
+
if (ordinaryHooks) ordinaryStates.push({ state, setter })
|
|
4234
|
+
else rowStates.push({ state, setter })
|
|
4073
4235
|
continue
|
|
4074
4236
|
}
|
|
4075
4237
|
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useRef") {
|
|
4076
|
-
|
|
4077
|
-
if (
|
|
4078
|
-
|
|
4238
|
+
const hookLabel = ordinaryHooks ? "Setter-callback component" : "Keyed row"
|
|
4239
|
+
if (declaration.initializer.arguments.length !== 1 || declaration.initializer.arguments[0].kind !== ts.SyntaxKind.NullKeyword) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useRef() must use the direct initial value null`)
|
|
4240
|
+
if (!ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.name, component.getSourceFile(), `${hookLabel} useRef() must be assigned to one identifier`)
|
|
4241
|
+
const refs = ordinaryHooks ? ordinaryRefs : rowRefs
|
|
4242
|
+
const name = `${ordinaryHooks ? "__kComponentRef" : "__kRowRef"}${Math.max(0, call.pos)}_${refs.length}`
|
|
4243
|
+
substitutions.set(declaration.name.text, factory.createIdentifier(name))
|
|
4244
|
+
const initializer = factory.createCallExpression(factory.createIdentifier(ordinaryHooks ? "__kComponentUseRef" : "__kRowUseRef"), declaration.initializer.typeArguments?.map(type => cloneAst(type, factory, context)), [factory.createNull()])
|
|
4245
|
+
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
4246
|
+
refs.push({ name })
|
|
4247
|
+
continue
|
|
4248
|
+
}
|
|
4249
|
+
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") {
|
|
4250
|
+
if (!ordinaryHooks) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "useId() is not supported in keyed row components")
|
|
4251
|
+
if (declaration.initializer.arguments.length || !ts.isIdentifier(declaration.name)) throw sourceNodeError(declaration.initializer, component.getSourceFile(), "Setter-callback component useId() must initialize one top-level const identifier without arguments")
|
|
4252
|
+
const name = `__kComponentId${Math.max(0, call.pos)}_${hookDeclarations.length}`
|
|
4079
4253
|
substitutions.set(declaration.name.text, factory.createIdentifier(name))
|
|
4080
|
-
const initializer = factory.createCallExpression(factory.createIdentifier("
|
|
4254
|
+
const initializer = factory.createCallExpression(factory.createIdentifier("__kComponentUseId"), undefined, [])
|
|
4081
4255
|
hookDeclarations.push(factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(factory.createIdentifier(name), undefined, undefined, initializer)], ts.NodeFlags.Const)))
|
|
4082
|
-
|
|
4256
|
+
usesComponentId = true
|
|
4083
4257
|
continue
|
|
4084
4258
|
}
|
|
4085
|
-
if (declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useId") throw sourceNodeError(declaration.initializer, component.getSourceFile(), "useId() is not supported in keyed row components")
|
|
4086
4259
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
|
|
4087
4260
|
const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
|
|
4088
4261
|
calculations.push({ name: declaration.name.text, expression: calculation })
|
|
@@ -4093,12 +4266,12 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
4093
4266
|
let unsupportedHook
|
|
4094
4267
|
const findUnsupportedHook = node => {
|
|
4095
4268
|
if (unsupportedHook) return
|
|
4096
|
-
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ["useState", "useRef"].includes(node.expression.text)) unsupportedHook = node
|
|
4269
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && ["useState", "useRef", "useId"].includes(node.expression.text)) unsupportedHook = node
|
|
4097
4270
|
ts.forEachChild(node, findUnsupportedHook)
|
|
4098
4271
|
}
|
|
4099
4272
|
findUnsupportedHook(returned)
|
|
4100
4273
|
for (const calculation of calculations) findUnsupportedHook(calculation.expression)
|
|
4101
|
-
if (unsupportedHook) throw sourceNodeError(unsupportedHook, component.getSourceFile(),
|
|
4274
|
+
if (unsupportedHook) throw sourceNodeError(unsupportedHook, component.getSourceFile(), `${ordinaryHooks ? "Setter-callback component" : "Keyed row"} ${unsupportedHook.expression.text}() must be one top-level const declaration`)
|
|
4102
4275
|
let root = unwrapExpression(flattenForwardedComponentChildren(substituteClone(returned, substitutions, factory, context), factory, context))
|
|
4103
4276
|
if (rest) root = expandSpecializedRest(root, returned, component, rest, restEntries, factory, context, fail, label)
|
|
4104
4277
|
if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(returned, `${label} component must return one JSX element`)
|
|
@@ -4118,7 +4291,10 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
4118
4291
|
effects,
|
|
4119
4292
|
hookDeclarations,
|
|
4120
4293
|
rowStates,
|
|
4121
|
-
rowRefs
|
|
4294
|
+
rowRefs,
|
|
4295
|
+
ordinaryStates,
|
|
4296
|
+
ordinaryRefs,
|
|
4297
|
+
usesComponentId
|
|
4122
4298
|
}
|
|
4123
4299
|
}
|
|
4124
4300
|
|
|
@@ -4404,7 +4580,7 @@ function identifierReferences(root, name) {
|
|
|
4404
4580
|
}
|
|
4405
4581
|
|
|
4406
4582
|
function unwrapExpression(node) {
|
|
4407
|
-
return ts.isParenthesizedExpression(node) ? unwrapExpression(node.expression) : node
|
|
4583
|
+
return ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isTypeAssertionExpression(node) || ts.isNonNullExpression(node) || ts.isSatisfiesExpression(node) ? unwrapExpression(node.expression) : node
|
|
4408
4584
|
}
|
|
4409
4585
|
|
|
4410
4586
|
function isLocalConst(node) {
|
|
@@ -4712,6 +4888,7 @@ function isReferenceIdentifier(node) {
|
|
|
4712
4888
|
(ts.isVariableDeclaration(parent) && parent.name === node) ||
|
|
4713
4889
|
(ts.isParameter(parent) && parent.name === node) ||
|
|
4714
4890
|
(ts.isFunctionDeclaration(parent) && parent.name === node) ||
|
|
4891
|
+
(ts.isJsxAttribute(parent) && parent.name === node) ||
|
|
4715
4892
|
(ts.isBindingElement(parent) && (parent.name === node || parent.propertyName === node)) ||
|
|
4716
4893
|
ts.isImportSpecifier(parent) || ts.isImportClause(parent)) return false
|
|
4717
4894
|
return true
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
export function selectCollection(anchor, selector = [], readState) {
|
|
2
2
|
let values = anchor == null ? [] : anchor
|
|
3
|
+
const state = readState ? new Map() : undefined
|
|
4
|
+
const read = state ? name => {
|
|
5
|
+
if (!state.has(name)) state.set(name, readState(name))
|
|
6
|
+
return state.get(name)
|
|
7
|
+
} : undefined
|
|
3
8
|
for (const operation of selector) {
|
|
4
|
-
if (operation[0] === "from") values = Array.from(values, operation[1] ? (item, index) => evaluateCollectionExpression(operation[1], item, index,
|
|
9
|
+
if (operation[0] === "from") values = Array.from(values, operation[1] ? (item, index) => evaluateCollectionExpression(operation[1], item, index, read) : undefined)
|
|
5
10
|
else {
|
|
6
11
|
if (!Array.isArray(values)) throw new Error("Rendered collection source must remain an array")
|
|
7
|
-
if (operation[0] === "filter") values = values.filter((item, index) => evaluateCollectionExpression(operation[1], item, index,
|
|
12
|
+
if (operation[0] === "filter") values = values.filter((item, index) => evaluateCollectionExpression(operation[1], item, index, read))
|
|
8
13
|
else if (operation[0] === "flatMap") values = values.flatMap(item => item?.[operation[1]] ?? [])
|
|
9
|
-
else if (operation[0] === "slice") values = values.slice(evaluateCollectionExpression(operation[1], undefined, undefined,
|
|
10
|
-
else if (operation[0] === "sort") values = [...values].sort((left, right) => evaluateCollectionExpression(operation[1], left, right,
|
|
14
|
+
else if (operation[0] === "slice") values = values.slice(evaluateCollectionExpression(operation[1], undefined, undefined, read), operation[2] && evaluateCollectionExpression(operation[2], undefined, undefined, read))
|
|
15
|
+
else if (operation[0] === "sort") values = [...values].sort((left, right) => evaluateCollectionExpression(operation[1], left, right, read))
|
|
11
16
|
}
|
|
12
17
|
}
|
|
13
18
|
if (!Array.isArray(values)) throw new Error("Rendered collection source must remain an array")
|
|
@@ -15,55 +20,63 @@ export function selectCollection(anchor, selector = [], readState) {
|
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
export function evaluateCollectionExpression(expression, item, index, readState) {
|
|
18
|
-
const
|
|
19
|
-
if (kind === "value") return
|
|
23
|
+
const kind = expression[0]
|
|
24
|
+
if (kind === "value") return expression[1]
|
|
20
25
|
if (kind === "undefined") return undefined
|
|
21
26
|
if (kind === "item") return item
|
|
22
27
|
if (kind === "index") return index
|
|
23
28
|
if (kind === "state") {
|
|
24
|
-
if (!readState) throw new Error(`Rendered collection state ${JSON.stringify(
|
|
25
|
-
return readState(
|
|
29
|
+
if (!readState) throw new Error(`Rendered collection state ${JSON.stringify(expression[1])} is not available`)
|
|
30
|
+
return readState(expression[1])
|
|
26
31
|
}
|
|
27
32
|
if (kind === "get") {
|
|
28
|
-
const object = evaluateCollectionExpression(
|
|
29
|
-
return object == null &&
|
|
33
|
+
const object = evaluateCollectionExpression(expression[1], item, index, readState)
|
|
34
|
+
return object == null && expression[3] ? undefined : object[expression[2]]
|
|
30
35
|
}
|
|
31
36
|
if (kind === "unary") {
|
|
32
|
-
const value = evaluateCollectionExpression(
|
|
33
|
-
if (
|
|
34
|
-
if (
|
|
35
|
-
if (
|
|
36
|
-
if (
|
|
37
|
+
const value = evaluateCollectionExpression(expression[2], item, index, readState)
|
|
38
|
+
if (expression[1] === "!") return !value
|
|
39
|
+
if (expression[1] === "+") return +value
|
|
40
|
+
if (expression[1] === "-") return -value
|
|
41
|
+
if (expression[1] === "typeof") return typeof value
|
|
37
42
|
}
|
|
38
43
|
if (kind === "binary") {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
if (
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
if (
|
|
47
|
-
if (
|
|
48
|
-
if (
|
|
49
|
-
if (
|
|
50
|
-
if (
|
|
51
|
-
if (
|
|
52
|
-
if (
|
|
53
|
-
if (
|
|
54
|
-
if (
|
|
55
|
-
if (
|
|
56
|
-
if (
|
|
44
|
+
const operator = expression[1]
|
|
45
|
+
const left = evaluateCollectionExpression(expression[2], item, index, readState)
|
|
46
|
+
if (operator === "&&") return left && evaluateCollectionExpression(expression[3], item, index, readState)
|
|
47
|
+
if (operator === "||") return left || evaluateCollectionExpression(expression[3], item, index, readState)
|
|
48
|
+
if (operator === "??") return left ?? evaluateCollectionExpression(expression[3], item, index, readState)
|
|
49
|
+
const right = evaluateCollectionExpression(expression[3], item, index, readState)
|
|
50
|
+
if (operator === "===") return left === right
|
|
51
|
+
if (operator === "!==") return left !== right
|
|
52
|
+
if (operator === "==") return left == right
|
|
53
|
+
if (operator === "!=") return left != right
|
|
54
|
+
if (operator === "<") return left < right
|
|
55
|
+
if (operator === "<=") return left <= right
|
|
56
|
+
if (operator === ">") return left > right
|
|
57
|
+
if (operator === ">=") return left >= right
|
|
58
|
+
if (operator === "+") return left + right
|
|
59
|
+
if (operator === "-") return left - right
|
|
60
|
+
if (operator === "*") return left * right
|
|
61
|
+
if (operator === "/") return left / right
|
|
62
|
+
if (operator === "%") return left % right
|
|
57
63
|
}
|
|
58
|
-
if (kind === "conditional") return evaluateCollectionExpression(
|
|
59
|
-
if (kind === "array") return
|
|
60
|
-
if (kind === "object") return Object.fromEntries(
|
|
61
|
-
if (kind === "template") return
|
|
64
|
+
if (kind === "conditional") return evaluateCollectionExpression(expression[1], item, index, readState) ? evaluateCollectionExpression(expression[2], item, index, readState) : evaluateCollectionExpression(expression[3], item, index, readState)
|
|
65
|
+
if (kind === "array") return expression.slice(1).map(value => evaluateCollectionExpression(value, item, index, readState))
|
|
66
|
+
if (kind === "object") return Object.fromEntries(expression.slice(1).map(([key, value]) => [key, evaluateCollectionExpression(value, item, index, readState)]))
|
|
67
|
+
if (kind === "template") return expression[1].map((text, offset) => text + (offset < expression[2].length ? evaluateCollectionExpression(expression[2][offset], item, index, readState) : "")).join("")
|
|
62
68
|
if (kind === "call") {
|
|
63
|
-
const receiver = evaluateCollectionExpression(
|
|
64
|
-
return receiver[
|
|
69
|
+
const receiver = evaluateCollectionExpression(expression[1], item, index, readState)
|
|
70
|
+
if (expression.length === 3) return receiver[expression[2]]()
|
|
71
|
+
if (expression.length === 4) return receiver[expression[2]](evaluateCollectionExpression(expression[3], item, index, readState))
|
|
72
|
+
const arguments_ = new Array(expression.length - 3)
|
|
73
|
+
for (let offset = 3; offset < expression.length; offset++) arguments_[offset - 3] = evaluateCollectionExpression(expression[offset], item, index, readState)
|
|
74
|
+
return receiver[expression[2]](...arguments_)
|
|
75
|
+
}
|
|
76
|
+
if (kind === "global" || kind === "math") {
|
|
77
|
+
const arguments_ = new Array(expression.length - 2)
|
|
78
|
+
for (let offset = 2; offset < expression.length; offset++) arguments_[offset - 2] = evaluateCollectionExpression(expression[offset], item, index, readState)
|
|
79
|
+
return (kind === "global" ? globalThis : Math)[expression[1]](...arguments_)
|
|
65
80
|
}
|
|
66
|
-
if (kind === "global") return globalThis[parts[0]](...parts.slice(1).map(value => evaluateCollectionExpression(value, item, index, readState)))
|
|
67
|
-
if (kind === "math") return Math[parts[0]](...parts.slice(1).map(value => evaluateCollectionExpression(value, item, index, readState)))
|
|
68
81
|
throw new Error(`Unsupported rendered collection expression: ${String(kind)}`)
|
|
69
82
|
}
|
package/framework/core.mjs
CHANGED
|
@@ -908,6 +908,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
908
908
|
descriptor.rowRefs = renderContext.listRowRefs.map(({ id }) => id)
|
|
909
909
|
descriptor.mount = true
|
|
910
910
|
}
|
|
911
|
+
if (descriptor.rowStates && !descriptor.effects && !descriptor.nested && !template.includes("data-k-text")) descriptor.fastRelease = true
|
|
911
912
|
const seed = node.ownerField || node.selector.length || node.keyField === null ? undefined : listSeed(node.values, renderContext.listFields)
|
|
912
913
|
if (seed) descriptor.seed = seed
|
|
913
914
|
let current = ""
|
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
export namespace JSX {
|
|
2
|
-
type Element =
|
|
2
|
+
type Element = any
|
|
3
3
|
type Children = unknown
|
|
4
4
|
|
|
5
|
+
type TargetedEvent<Target extends EventTarget, NativeEvent extends Event = Event> = NativeEvent & {
|
|
6
|
+
currentTarget: Target
|
|
7
|
+
target: EventTarget & Target
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type IntrinsicProps<Target extends EventTarget> = Record<string, unknown> & {
|
|
11
|
+
children?: Children
|
|
12
|
+
onBlur?: (event: TargetedEvent<Target, FocusEvent>) => unknown
|
|
13
|
+
onChange?: (event: TargetedEvent<Target>) => unknown
|
|
14
|
+
onClick?: (event: TargetedEvent<Target, MouseEvent>) => unknown
|
|
15
|
+
onFocus?: (event: TargetedEvent<Target, FocusEvent>) => unknown
|
|
16
|
+
onInput?: (event: TargetedEvent<Target, InputEvent>) => unknown
|
|
17
|
+
onKeyDown?: (event: TargetedEvent<Target, KeyboardEvent>) => unknown
|
|
18
|
+
onKeyUp?: (event: TargetedEvent<Target, KeyboardEvent>) => unknown
|
|
19
|
+
onSubmit?: (event: TargetedEvent<Target, SubmitEvent>) => unknown
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
interface IntrinsicAttributes {
|
|
6
23
|
key?: string | number
|
|
7
24
|
}
|
|
8
25
|
|
|
9
|
-
|
|
26
|
+
type IntrinsicElements = {
|
|
27
|
+
[Name in keyof HTMLElementTagNameMap]: IntrinsicProps<HTMLElementTagNameMap[Name]>
|
|
28
|
+
} & {
|
|
29
|
+
[Name in keyof SVGElementTagNameMap]: IntrinsicProps<SVGElementTagNameMap[Name]>
|
|
30
|
+
} & {
|
|
10
31
|
[elementName: string]: Record<string, unknown>
|
|
11
32
|
}
|
|
12
33
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { browserState, mountDom, notifyListItem, registerCommitter, registerMountHook, registerUnmountHook, unmountDom } from "./shared-runtime.js"
|
|
1
|
+
import { browserState, mountDom, notifyListItem, registerCommitter, registerMountHook, registerUnmountHook, releaseState, unmountDom } from "./shared-runtime.js"
|
|
2
2
|
import { selectCollection } from "./collection-selector.js"
|
|
3
3
|
|
|
4
4
|
const listTargets = new Map()
|
|
@@ -219,7 +219,8 @@ function updateList(list) {
|
|
|
219
219
|
}
|
|
220
220
|
for (const [token, node] of list.roots) {
|
|
221
221
|
if (keys.has(token)) continue
|
|
222
|
-
if (
|
|
222
|
+
if (list.descriptor.fastRelease) node.remove()
|
|
223
|
+
else if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) {
|
|
223
224
|
unmountDom(node)
|
|
224
225
|
node.remove()
|
|
225
226
|
} else node.remove()
|
|
@@ -565,7 +566,7 @@ function addListRoot(list, { item, index = list.roots.size, key, token, value })
|
|
|
565
566
|
|
|
566
567
|
function removeListRoot(list, token) {
|
|
567
568
|
const node = list.roots.get(token)
|
|
568
|
-
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) unmountDom(node)
|
|
569
|
+
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount && !list.descriptor.fastRelease) unmountDom(node)
|
|
569
570
|
node.remove()
|
|
570
571
|
if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, ownershipPaths.get(node))
|
|
571
572
|
list.roots.delete(token)
|
|
@@ -1035,7 +1036,7 @@ function initializeRowStates(descriptor, key, root) {
|
|
|
1035
1036
|
}
|
|
1036
1037
|
|
|
1037
1038
|
function deleteFlatRowStates(descriptor, token) {
|
|
1038
|
-
for (const state of descriptor.rowStates)
|
|
1039
|
+
for (const state of descriptor.rowStates) releaseState(flatRowStateId(state.id, token))
|
|
1039
1040
|
}
|
|
1040
1041
|
|
|
1041
1042
|
function flatRowStateId(id, token) {
|
|
@@ -1044,7 +1045,7 @@ function flatRowStateId(id, token) {
|
|
|
1044
1045
|
|
|
1045
1046
|
function deleteRowStates(descriptor, path) {
|
|
1046
1047
|
const statePath = descriptor.ownerField ? path : [path.at(-1).slice(path.at(-1).indexOf("=") + 1)]
|
|
1047
|
-
for (const state of descriptor.rowStates)
|
|
1048
|
+
for (const state of descriptor.rowStates) releaseState(rowStateId(state.id, statePath))
|
|
1048
1049
|
}
|
|
1049
1050
|
function rowStateId(id, path) {
|
|
1050
1051
|
return id.replace("$k", encodeURIComponent(path.join("/")))
|
|
@@ -26,6 +26,7 @@ export const browserState = new Map()
|
|
|
26
26
|
const committers = []
|
|
27
27
|
const mountHooks = []
|
|
28
28
|
const unmountHooks = []
|
|
29
|
+
const stateReleaseHooks = []
|
|
29
30
|
const textTargets = new Map()
|
|
30
31
|
const mountedText = new WeakSet()
|
|
31
32
|
|
|
@@ -59,6 +60,15 @@ export function registerUnmountHook(unmount) {
|
|
|
59
60
|
unmountHooks.push(unmount)
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
export function registerStateReleaseHook(release) {
|
|
64
|
+
stateReleaseHooks.push(release)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function releaseState(id) {
|
|
68
|
+
for (const release of stateReleaseHooks) release(id)
|
|
69
|
+
browserState.delete(id)
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
export function commitDom(id, value) {
|
|
63
73
|
for (const node of textTargets.get(id) ?? []) {
|
|
64
74
|
if (node.isConnected) node.textContent = value
|