@kudzujs/core 0.8.59 → 0.8.61
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION_ROADMAP.md +3 -1
- package/README.md +1 -1
- package/RELEASES.md +58 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/compiler-current-architecture.md +1 -1
- package/docs/next-architecture/large-application-ai-native-roadmap.md +2 -2
- package/docs/next-architecture/versioning.md +3 -1
- package/framework/README.md +1 -1
- package/framework/compiler/effect-analysis.mjs +2 -0
- package/framework/compiler/effect-codegen.mjs +5 -5
- package/framework/compiler/source-compiler.mjs +140 -2
- package/framework/core.mjs +1 -1
- package/package.json +1 -1
package/MIGRATION_ROADMAP.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This document is the source of truth for Kudzu's product direction, architecture invariants, and future development order. Read it before extending React-shaped syntax or browser capabilities.
|
|
4
4
|
|
|
5
|
-
The executable post-`0.8.
|
|
5
|
+
The executable post-`0.8.61` compiler and large-application sequence is maintained in [`docs/next-architecture/large-application-ai-native-roadmap.md`](./docs/next-architecture/large-application-ai-native-roadmap.md). Follow its PR dependencies for implementation work; this document remains authoritative when selecting or accepting a migration capability.
|
|
6
6
|
|
|
7
7
|
[`GOAL_A.md`](./GOAL_A.md) and [`GOAL_B.md`](./GOAL_B.md) are completed capability-validation records. Their commerce and realtime dashboard fixtures prove general lifecycle, navigation, async-workflow, and Worker capabilities; they are not separate product verticals or future priority lists.
|
|
8
8
|
|
|
@@ -96,6 +96,7 @@ This queue orders the next investigations by general migration value. Start only
|
|
|
96
96
|
### Completed In 0.8.0
|
|
97
97
|
|
|
98
98
|
- Relative zero-argument custom hooks expose direct shorthand state/setter pairs and state-capturing callbacks to one top-level caller destructuring.
|
|
99
|
+
- In `0.8.61`, a ClimateCompatibleGrowth-shaped relative `useDebounce(value, delay)` hook may accept one direct primitive state and numeric literal delay, initialize one returned state from the source build value, and own one exact timeout dependency effect with cleanup. Dynamic delays, non-primitive state, aliases, additional effects, intervals, and missing cleanup remain diagnosed.
|
|
99
100
|
- Writable React Router search parameters use native inline `URLSearchParams` updaters, push/replace history, signal recommits, and `popstate` synchronization.
|
|
100
101
|
- Guarded `localStorage` restoration and persistence compose through existing mount/dependency effects, preserving deterministic static fallbacks without a storage runtime.
|
|
101
102
|
- Reachable source compilation excludes unused React/Vite modules while preserving source diagnostics for every reachable module.
|
|
@@ -150,6 +151,7 @@ This queue orders the next investigations by general migration value. Start only
|
|
|
150
151
|
- In `0.8.47`, a specialized setter-callback child may initialize local state directly from one primitive parent state prop. Build scratch reads the parent signal's build value while retaining the structural prop signal for handlers/effects. `0.8.57` extends that exact path to a parent state authored with one JSON-safe plain-object literal; independent child draft state and parent commit behavior pass in the FIRE-derived callback fixture. Array props, aliases, property paths, composed expressions, and object `.toString()` remain diagnosed.
|
|
151
152
|
- In `0.8.58`, a direct keyed item prop may initialize one row-owned object draft. Retained keys preserve that draft and DOM identity through reorder; removal releases it, and re-adding the key clones the current item into fresh state. Aliases, property paths, composed expressions, and indirect item props remain diagnosed.
|
|
152
153
|
- In `0.8.59`, the ClimateCompatibleGrowth teaching-kit `Dropdown` migration may initialize local array state from one direct parent array-state prop and commit it through a direct `on*` callback or matching `set*` setter prop. Parent and child arrays remain independently owned, mounted drafts do not auto-synchronize, and aliases, property paths, composed arrays, indirect callbacks, and additional `set*` forwarding remain diagnosed.
|
|
154
|
+
- In `0.8.60`, that dropdown may preserve its direct `useEffect(() => setSelectedItems(items), [items, setSelectedItems])` synchronization shape. Kudzu proves the matching parent state/setter pair, removes the stable setter dependency, and compares the direct child array-state dependency by identity through existing effect ownership. Composed values, aliases, mismatched pairs, cleanup, and automatic parent-to-draft synchronization remain diagnosed.
|
|
153
155
|
- In `0.8.48`, one setter-callback prop may be called once from each of multiple direct intrinsic event handlers in the specialized leaf. Each call lowers independently to the same parent signal; forwarding fan-out, aliases, non-handler uses, and repeated calls inside one handler remain diagnosed, and no callback registry or handler runtime is added.
|
|
154
156
|
- In `0.8.49`, one component may forward the same destructured setter callback directly through multiple child component `on*` props. Each branch specializes independently to the original parent signal within the existing three-boundary limit; aliases, ordinary props, spreads, and a fourth boundary remain diagnosed.
|
|
155
157
|
|
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
14
14
|
|
|
15
15
|
> Experimental `0.8.x`: the compiler API and supported TSX surface may change.
|
|
16
16
|
|
|
17
|
-
**Latest release: 0.8.
|
|
17
|
+
**Latest release: 0.8.61 - Parameterized debounce hooks.** Relative `useDebounce(state, literalDelay)` hooks compile through existing state, effect, timeout cleanup, and ownership paths without a debounce runtime. Read the [release notes](./RELEASES.md#0861---parameterized-debounce-hooks), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.61), 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,63 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.8.61 - Parameterized debounce hooks
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.61 compiles the common relative `useDebounce(state, literalDelay)` custom-hook shape through existing state and effect ownership.
|
|
6
|
+
|
|
7
|
+
### Changed in 0.8.61
|
|
8
|
+
|
|
9
|
+
- A named or default relative synchronous hook may accept one direct primitive state value and one numeric literal delay.
|
|
10
|
+
- The hook may initialize one state from that value, own one timeout dependency effect with exact cleanup, and directly return the debounced state.
|
|
11
|
+
- The compiler preserves the source signal dependency, treats the literal delay as stable scope, and initializes the debounced signal from the source signal's build value.
|
|
12
|
+
|
|
13
|
+
### Boundaries
|
|
14
|
+
|
|
15
|
+
- The hook body must be the direct `useState(value)`, `useEffect()`, and returned-state shape proven by the ClimateCompatibleGrowth fixture.
|
|
16
|
+
- Dynamic delays, non-primitive source state, aliases, additional statements/effects, intervals, and missing timeout cleanup remain rejected.
|
|
17
|
+
- No debounce/timer runtime, component tree, hydration, or new effect capability is added.
|
|
18
|
+
|
|
19
|
+
### Validation
|
|
20
|
+
|
|
21
|
+
- A fixture reduced from ClimateCompatibleGrowth's MIT-licensed `useDebouce.ts` preserves its generic parameterized custom-hook source shape.
|
|
22
|
+
- Browser coverage proves rapid replacement, pending-timeout cleanup on conditional removal, fresh remount state, and latest-value commit.
|
|
23
|
+
- A static sibling remains zero JavaScript and dynamic delay input fails at its authored call site.
|
|
24
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 230 tests and 174 generated pages.
|
|
25
|
+
|
|
26
|
+
### Upgrade
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install @kudzujs/core@^0.8.61
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 0.8.60 - Array draft effect sync
|
|
33
|
+
|
|
34
|
+
Kudzu 0.8.60 lets a specialized child synchronize direct prop-derived array draft state through the matching parent setter in an ordinary dependency effect.
|
|
35
|
+
|
|
36
|
+
### Changed in 0.8.60
|
|
37
|
+
|
|
38
|
+
- `useEffect(() => { setSelectedItems(items) }, [items, setSelectedItems])` compiles for the matching direct array prop-draft specialization.
|
|
39
|
+
- The compiler proves the parent state/setter pair, removes the stable setter dependency, and tracks the child array state through existing `Object.is` effect scheduling.
|
|
40
|
+
- Parent replacement still does not synchronize the independently owned child draft; the next authored child-state replacement reruns the effect.
|
|
41
|
+
|
|
42
|
+
### Boundaries
|
|
43
|
+
|
|
44
|
+
- The effect must be top-level, synchronous, inline, and contain exactly one direct setter call with exact `[state, setter]` dependencies.
|
|
45
|
+
- Setter aliases, composed values, mismatched parent state/setter pairs, cleanup, and additional callback uses remain rejected.
|
|
46
|
+
- Object and derived array dependencies remain unchanged; no runtime module, EffectIR kind, component tree, or automatic prop synchronization is added.
|
|
47
|
+
|
|
48
|
+
### Validation
|
|
49
|
+
|
|
50
|
+
- The ClimateCompatibleGrowth-derived dropdown now preserves its authored setter effect shape.
|
|
51
|
+
- Browser coverage proves initial setup, child replacement commit, independent parent reset, and effect rerun; a static sibling remains zero JavaScript.
|
|
52
|
+
- Existing effect ownership and object-dependency diagnostics remain covered.
|
|
53
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 228 tests and 174 generated pages.
|
|
54
|
+
|
|
55
|
+
### Upgrade
|
|
56
|
+
|
|
57
|
+
```sh
|
|
58
|
+
npm install @kudzujs/core@^0.8.60
|
|
59
|
+
```
|
|
60
|
+
|
|
3
61
|
## 0.8.59 - Array prop draft state
|
|
4
62
|
|
|
5
63
|
Kudzu 0.8.59 lets a specialized child initialize independent local array draft state directly from one parent array-state prop while preserving an authored direct `set*` setter prop.
|
|
@@ -11,7 +11,7 @@ The top-level [`GOAL_A.md`](../../GOAL_A.md) and [`GOAL_B.md`](../../GOAL_B.md)
|
|
|
11
11
|
| C: state/resource model | Research only | Reduced fixtures expose a limitation |
|
|
12
12
|
| D: routing compatibility | Current behavior preserved | Revisit only with migration evidence and invariant review |
|
|
13
13
|
|
|
14
|
-
The active post-`0.8.
|
|
14
|
+
The active post-`0.8.61` implementation sequence is [`large-application-ai-native-roadmap.md`](./large-application-ai-native-roadmap.md). P0.12 validation, property-level object-state dependencies, direct three-boundary callback/ref ownership, direct primitive, plain-object, array-prop, keyed-item draft state initialization, matching array-draft setter effects, parameterized primitive debounce hooks, repeated direct leaf-handler callback use, direct child callback fan-out, collision-free and private Context action setters, package-neutral shared-state/action IR, browser-only package imports and private mutable refs in owned effects, route/layout CSS closure, structural per-route capability/chunk reporting, signature-keyed runtime families, incremental source and affected-route development builds, measured route-entry output optimization, and the plain TypeScript source-scale fast path are complete. ResourceIR remains research for cross-owner transports and subscriptions; range ownership, virtualization, and optimistic transactions still require independent fixtures. The plan orders compiler semantic generalization, large-application foundations, compatibility boundaries, AI tooling, and production validation without changing the invariants below.
|
|
15
15
|
|
|
16
16
|
## Required Invariants
|
|
17
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Current Compiler Architecture
|
|
2
2
|
|
|
3
|
-
This maps the current `0.8.
|
|
3
|
+
This maps the current `0.8.61` architecture, built on the completed `0.8.23` Goal A compiler foundation. File and function names are the stable references; line numbers are intentionally omitted because later work may still move code.
|
|
4
4
|
|
|
5
5
|
## Responsibility Map
|
|
6
6
|
|
|
@@ -379,7 +379,7 @@ function increment(value) { setCount(value + 1) }; increment(count)
|
|
|
379
379
|
After the relevant P0 foundations, investigate capabilities in this order:
|
|
380
380
|
|
|
381
381
|
1. Property-level derived dependencies over ordinary object state. **Completed in `0.8.40`:** direct property paths and top-level immutable primitive locals over object state reuse tagged DerivedIR, subscribe to the source signal, and compare selected values with `Object.is`; whole-object and dynamic dependencies remain rejected.
|
|
382
|
-
2. Multi-boundary component/prop/callback/ref/context dataflow. **Three-boundary callback/ref ownership completed in `0.8.43`; collision-free Context action-private state completed in `0.8.44`; action-only Provider setter exposure removed in `0.8.46`; direct primitive prop state initialization completed in `0.8.47`; repeated direct leaf-handler callback use completed in `0.8.48`; direct child callback fan-out completed in `0.8.49`; direct plain-object prop state initialization completed in `0.8.57`; direct keyed item draft initialization completed in `0.8.58`; direct array prop draft initialization completed in `0.8.59`:** forwarding preserves parent SignalIR, Context action lowering uses compiler-owned aliases when consumer locals reuse Provider state/setter names, action-required setters may remain compiler-only when their state is publicly exposed, specialized children may seed local state from a direct parent signal authored with a serializable primitive, plain-object, or array literal, keyed rows may seed object draft state from their direct item prop, and one callback may branch through multiple component `on*` props and intrinsic handlers. A direct first-boundary parent setter may also retain its authored matching `set*` prop name; additional forwarding remains restricted to `on*` props. The ClimateCompatibleGrowth-derived dropdown proves independent array drafts
|
|
382
|
+
2. Multi-boundary component/prop/callback/ref/context dataflow. **Three-boundary callback/ref ownership completed in `0.8.43`; collision-free Context action-private state completed in `0.8.44`; action-only Provider setter exposure removed in `0.8.46`; direct primitive prop state initialization completed in `0.8.47`; repeated direct leaf-handler callback use completed in `0.8.48`; direct child callback fan-out completed in `0.8.49`; direct plain-object prop state initialization completed in `0.8.57`; direct keyed item draft initialization completed in `0.8.58`; direct array prop draft initialization completed in `0.8.59`; matching array-draft setter effects completed in `0.8.60`; parameterized primitive debounce hooks completed in `0.8.61`:** forwarding preserves parent SignalIR, Context action lowering uses compiler-owned aliases when consumer locals reuse Provider state/setter names, action-required setters may remain compiler-only when their state is publicly exposed, specialized children may seed local state from a direct parent signal authored with a serializable primitive, plain-object, or array literal, keyed rows may seed object draft state from their direct item prop, and one callback may branch through multiple component `on*` props and intrinsic handlers. A direct first-boundary parent setter may also retain its authored matching `set*` prop name; additional forwarding remains restricted to `on*` props. The ClimateCompatibleGrowth-derived dropdown proves independent array drafts and exact direct setter-effect synchronization, while its relative debounce hook specializes one direct primitive source and literal delay through existing timeout cleanup ownership. A fourth callback boundary, callback aliases/non-handler uses, dynamic debounce delays, non-primitive debounce inputs, mismatched state/setter effect pairs, fully hidden Context state, additional `set*` forwarding, keyed item aliases, property paths, and composed expressions remain fail-closed. Broader prop, callback, ref, and Context graphs remain migration-led work.
|
|
383
383
|
3. Package-neutral shared state/actions and migration of current Zustand internals. **Completed in `0.8.50`:** Zustand source normalization produces one generic shared-state adapter descriptor; selectors and handlers register JSON-safe SharedStateIR/SharedActionIR records, handler lowering consumes package-neutral actions, and existing RouteIR, layout ownership, same-turn updates, navigation persistence, and browser output remain unchanged. Redux/RTK and public adapter APIs remain unsupported.
|
|
384
384
|
4. Browser-only package imports in owned effect/resource modules. **Completed in `0.8.51` for effects:** direct package references in inline effect setup/cleanup callbacks use existing package import records and route-owned effect ESM bundling; build-time component modules and static siblings omit the package. Helper-indirect, render-time, dynamic-import, and ResourceIR package graphs remain unsupported.
|
|
385
385
|
5. ResourceIR from at least two independent WebSocket/SSE/SDK fixtures with the same semantics. **Completed in `0.8.52` without ResourceIR for private ownership:** the E2B terminal and route-owned WebSocket fixtures lower refs used exclusively by one inline effect to invocation-private closure objects, while existing effect ownership supplies replacement, cleanup, stale setter invalidation, navigation, and BFCache disposal. ResourceIR remains unapproved and now requires independent cross-owner transport/subscription fixtures that cannot fit this narrower model.
|
|
@@ -481,4 +481,4 @@ The first comparison is Kudzu versus React + Vite using the same agent, model, t
|
|
|
481
481
|
|
|
482
482
|
## Immediate Decision
|
|
483
483
|
|
|
484
|
-
PR 1 through PR 12, the `0.8.40` property-dependency slice, the `0.8.41` and `0.8.43` direct multi-boundary callback/ref slices, the `0.8.44` Context alias slice, the `0.8.46` action-only Provider setter slice, the `0.8.47` direct primitive, `0.8.57` plain-object prop, `0.8.58` direct keyed item,
|
|
484
|
+
PR 1 through PR 12, the `0.8.40` property-dependency slice, the `0.8.41` and `0.8.43` direct multi-boundary callback/ref slices, the `0.8.44` Context alias slice, the `0.8.46` action-only Provider setter slice, the `0.8.47` direct primitive, `0.8.57` plain-object prop, `0.8.58` direct keyed item, `0.8.59` direct array prop initializer, `0.8.60` matching array-draft setter-effect, and `0.8.61` parameterized primitive debounce-hook slices, the `0.8.48` repeated direct leaf-handler callback slice, the `0.8.49` direct child callback fan-out slice, the `0.8.50` package-neutral shared-state/action slice, the `0.8.51` owned-effect package import slice, the `0.8.52` effect-private mutable-ref slice, the `0.8.53` route/layout CSS closure slice, the `0.8.54` structural per-route capability/chunk report, the `0.8.55` signature-keyed runtime families, item 7 incremental source and affected-route builds, and the `0.8.42` measured route-output optimization are complete. Continue migration-led semantic generalization; keep ResourceIR limited to independent fixtures, and do not add range ownership, virtualization, optimistic transactions, a public adapter/store API, or a router before evidence justifies them.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Planned Version Sequence
|
|
2
2
|
|
|
3
|
-
This is an execution sequence, not release history. `0.8.16` through `0.8.
|
|
3
|
+
This is an execution sequence, not release history. `0.8.16` through `0.8.61` are completed scopes represented by package/release records.
|
|
4
4
|
|
|
5
5
|
Keep each patch behavior-preserving and independently reviewable. If a boundary proves inseparable, revise this plan before combining releases; do not silently broaden a patch.
|
|
6
6
|
|
|
@@ -50,6 +50,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
50
50
|
| `0.8.57` | Initialize specialized child draft state from one direct parent plain-object state prop. | A FIRE-derived editor preserves independent child draft state and parent commit behavior; arrays, aliases, property paths, and composed expressions remain rejected without runtime changes. |
|
|
51
51
|
| `0.8.58` | Initialize keyed row object draft state from the direct keyed item prop. | Todo reducer rows retain independent drafts and DOM identity across reorder, release state on removal, recreate drafts from the current item on remount, reject aliases, and preserve a static zero-JavaScript sibling. |
|
|
52
52
|
| `0.8.59` | Initialize specialized child draft state from one direct parent array-state prop and accept a direct matching `set*` setter prop. | A ClimateCompatibleGrowth-derived dropdown preserves independent array drafts, explicit parent commit, source naming, nearby diagnostics, and a static zero-JavaScript sibling without runtime changes. |
|
|
53
|
+
| `0.8.60` | Synchronize direct prop-derived array draft state through its matching parent setter in a dependency effect. | The ClimateCompatibleGrowth-derived dropdown preserves its exact direct effect/dependency shape, stable setter erasure, array identity comparison, independent parent replacement, nearby diagnostics, and a static zero-JavaScript sibling without runtime changes. |
|
|
54
|
+
| `0.8.61` | Specialize one parameterized relative primitive debounce hook through existing state and dependency-effect ownership. | The ClimateCompatibleGrowth-derived hook preserves direct state initialization, literal delay scope, timeout replacement/cleanup, conditional release, latest-value commit, nearby diagnostics, and a static zero-JavaScript sibling without runtime changes. |
|
|
53
55
|
|
|
54
56
|
## Sequence Rules
|
|
55
57
|
|
package/framework/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Migration source may retain conventional `react` imports for supported named or
|
|
|
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
|
|
|
9
|
-
Native platform work remains ordinary source. A direct async handler or directly returned relative custom-hook callback may call `navigator.clipboard.writeText()` and update application-owned success/failure state; Kudzu emits only its existing route handler ESM. Debounced synchronization uses a dependency effect that creates `setTimeout()` work and directly returns `clearTimeout()` cleanup, reusing dependency, conditional, keyed, and route ownership. One directly returned relative custom-hook callback may own one `null`-initialized private timeout ref when it directly clears the previous value, assigns a numeric-literal-delay `setTimeout()`, and an empty-dependency effect directly clears the timer on cleanup. Kudzu lowers that ref to compiler-owned state shared by existing handler and effect contexts. An ordinary inline effect may exclusively own top-level `useRef(null)` and `useRef(0)` values through direct `.current` references; Kudzu moves them into the setup invocation's closure for browser SDK handles, WebSockets, generation tokens, and animation frames without serialized captures or a resource runtime. Multiple timers, dynamic delays, intervals, ref aliases, cross-effect/event mutable refs, unowned delayed writes, and arbitrary timed callback graphs remain unsupported.
|
|
9
|
+
Native platform work remains ordinary source. A direct async handler or directly returned relative custom-hook callback may call `navigator.clipboard.writeText()` and update application-owned success/failure state; Kudzu emits only its existing route handler ESM. Debounced synchronization uses a dependency effect that creates `setTimeout()` work and directly returns `clearTimeout()` cleanup, reusing dependency, conditional, keyed, and route ownership. A relative `useDebounce(value, delay)` hook may accept one direct primitive state and numeric literal delay, initialize and return one debounced state, and reuse that same effect ownership. One directly returned relative custom-hook callback may own one `null`-initialized private timeout ref when it directly clears the previous value, assigns a numeric-literal-delay `setTimeout()`, and an empty-dependency effect directly clears the timer on cleanup. Kudzu lowers that ref to compiler-owned state shared by existing handler and effect contexts. An ordinary inline effect may exclusively own top-level `useRef(null)` and `useRef(0)` values through direct `.current` references; Kudzu moves them into the setup invocation's closure for browser SDK handles, WebSockets, generation tokens, and animation frames without serialized captures or a resource runtime. Multiple timers, dynamic delays, intervals, ref aliases, cross-effect/event mutable refs, unowned delayed writes, and arbitrary timed callback graphs remain unsupported.
|
|
10
10
|
|
|
11
11
|
Imperative canvas migrations use the same effect ownership rather than a component or canvas runtime. One `null`-initialized canvas DOM ref may feed an inline effect whose local variables persist across a recursive animation-frame callback, an `IntersectionObserver`, and native canvas/window listeners; the returned cleanup must cancel the latest frame, disconnect the observer, and remove every listener. Bare `IntersectionObserver` and `performance` identifiers remain browser globals in emitted effect ESM. Component-level mutable value refs and callbacks shared across effects or JSX handlers remain unsupported; move resource-private state and listeners into the owning effect.
|
|
12
12
|
|
|
@@ -16,6 +16,8 @@ export function analyzeEffectDependencies({ dependencies, node, listEffect, keye
|
|
|
16
16
|
itemDependencies.push(field)
|
|
17
17
|
} else if (dependencyItem && referencesIdentifier(dependency, dependencyItem)) {
|
|
18
18
|
fail(dependency, "useEffect() keyed item dependencies must be direct item.<field> properties")
|
|
19
|
+
} else if (ts.isIdentifier(value) && setters.has(value.text)) {
|
|
20
|
+
continue
|
|
19
21
|
} else {
|
|
20
22
|
ordinaryDependencies.push(dependency)
|
|
21
23
|
}
|
|
@@ -104,7 +104,7 @@ function readDependencies(record) {
|
|
|
104
104
|
${hasDependencyExpressions ? printDerivedDependencyRead("browserState") : ""}
|
|
105
105
|
return (record.effect.dependencies ?? []).map(id => {
|
|
106
106
|
const value = browserState.get(id)
|
|
107
|
-
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
|
|
107
|
+
if (!Array.isArray(value) && value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive or array")
|
|
108
108
|
return value
|
|
109
109
|
})
|
|
110
110
|
}
|
|
@@ -260,7 +260,7 @@ function mount(lifetime) {
|
|
|
260
260
|
${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState") : ""}
|
|
261
261
|
return (record.effect.dependencies ?? []).map(id => {
|
|
262
262
|
const value = __kRuntime.browserState.get(id)
|
|
263
|
-
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
|
|
263
|
+
if (!Array.isArray(value) && value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive or array")
|
|
264
264
|
return value
|
|
265
265
|
})
|
|
266
266
|
}
|
|
@@ -484,7 +484,7 @@ function mount(lifetime) {
|
|
|
484
484
|
${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState") : ""}
|
|
485
485
|
const values = (record.effect.dependencies ?? []).map(id => {
|
|
486
486
|
const value = __kRuntime.browserState.get(id)
|
|
487
|
-
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
|
|
487
|
+
if (!Array.isArray(value) && value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive or array")
|
|
488
488
|
return value
|
|
489
489
|
})
|
|
490
490
|
${hasItemDependencies ? `if (record.effect.itemDependencies) {
|
|
@@ -733,7 +733,7 @@ function readDependencies(record) {
|
|
|
733
733
|
${hasDependencyExpressions ? printDerivedDependencyRead("browserState") : ""}
|
|
734
734
|
const values = (record.effect.dependencies ?? []).map(id => {
|
|
735
735
|
const value = browserState.get(id)
|
|
736
|
-
if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
|
|
736
|
+
if (!Array.isArray(value) && value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive or array")
|
|
737
737
|
return value
|
|
738
738
|
})
|
|
739
739
|
${hasItemDependencies ? `if (record.effect.itemDependencies) {
|
|
@@ -848,7 +848,7 @@ async function flush() {
|
|
|
848
848
|
}
|
|
849
849
|
function readDependency() {
|
|
850
850
|
const next = browserState.get(dependency)
|
|
851
|
-
if (next !== null && typeof next !== "string" && typeof next !== "boolean" && !(typeof next === "number" && Number.isFinite(next) && !Object.is(next, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive")
|
|
851
|
+
if (!Array.isArray(next) && next !== null && typeof next !== "string" && typeof next !== "boolean" && !(typeof next === "number" && Number.isFinite(next) && !Object.is(next, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive or array")
|
|
852
852
|
return next
|
|
853
853
|
}
|
|
854
854
|
function invoke() {
|
|
@@ -240,6 +240,60 @@ function normalizeLazyStateInitializers(sourceFile, factory, context, file, sour
|
|
|
240
240
|
return ts.visitNode(sourceFile, visitor)
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
function parameterizedDebounceHook(hook) {
|
|
244
|
+
if (!hook || hook.parameters.length !== 2 || hook.asteriskToken || hook.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || !ts.isBlock(hook.body) || hook.body.statements.length !== 3) return undefined
|
|
245
|
+
const sourceFile = hook.getSourceFile()
|
|
246
|
+
if (!hasFrameworkImport(sourceFile, "useState") || !hasFrameworkImport(sourceFile, "useEffect")) return undefined
|
|
247
|
+
const [valueParameter, delayParameter] = hook.parameters
|
|
248
|
+
if (!ts.isIdentifier(valueParameter.name) || valueParameter.initializer || valueParameter.dotDotDotToken || !ts.isIdentifier(delayParameter.name) || delayParameter.initializer || delayParameter.dotDotDotToken) return undefined
|
|
249
|
+
const [stateStatement, effectStatement, returnStatement] = hook.body.statements
|
|
250
|
+
if (!ts.isVariableStatement(stateStatement) || !(stateStatement.declarationList.flags & ts.NodeFlags.Const) || stateStatement.declarationList.declarations.length !== 1) return undefined
|
|
251
|
+
const stateDeclaration = stateStatement.declarationList.declarations[0]
|
|
252
|
+
if (!ts.isArrayBindingPattern(stateDeclaration.name) || stateDeclaration.name.elements.length !== 2 || !ts.isIdentifier(stateDeclaration.name.elements[0]?.name) || !ts.isIdentifier(stateDeclaration.name.elements[1]?.name) || !stateDeclaration.initializer || !ts.isCallExpression(stateDeclaration.initializer) || !ts.isIdentifier(stateDeclaration.initializer.expression) || stateDeclaration.initializer.expression.text !== "useState" || stateDeclaration.initializer.arguments.length !== 1) return undefined
|
|
253
|
+
const stateArgument = unwrapExpression(stateDeclaration.initializer.arguments[0])
|
|
254
|
+
const directValue = ts.isIdentifier(stateArgument) && stateArgument.text === valueParameter.name.text
|
|
255
|
+
const normalizedValue = ts.isPropertyAccessExpression(stateArgument) && ts.isIdentifier(stateArgument.expression) && stateArgument.expression.text === valueParameter.name.text && stateArgument.name.text === "value"
|
|
256
|
+
if (!directValue && !normalizedValue) return undefined
|
|
257
|
+
if (!ts.isExpressionStatement(effectStatement) || !ts.isCallExpression(effectStatement.expression) || !ts.isIdentifier(effectStatement.expression.expression) || effectStatement.expression.expression.text !== "useEffect" || effectStatement.expression.arguments.length !== 2) return undefined
|
|
258
|
+
const [setup, dependencies] = effectStatement.expression.arguments
|
|
259
|
+
if ((!ts.isArrowFunction(setup) && !ts.isFunctionExpression(setup)) || setup.parameters.length || setup.asteriskToken || setup.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || !ts.isBlock(setup.body) || setup.body.statements.length !== 2 || !ts.isArrayLiteralExpression(dependencies)) return undefined
|
|
260
|
+
const normalizedDependencies = dependencies.elements.length === 1 && ts.isIdentifier(dependencies.elements[0]) && dependencies.elements[0].text === valueParameter.name.text
|
|
261
|
+
const sourceDependencies = dependencies.elements.length === 2 && ts.isIdentifier(dependencies.elements[0]) && dependencies.elements[0].text === valueParameter.name.text && ts.isIdentifier(dependencies.elements[1]) && dependencies.elements[1].text === delayParameter.name.text
|
|
262
|
+
if (!normalizedDependencies && !sourceDependencies) return undefined
|
|
263
|
+
const timerStatement = setup.body.statements[0]
|
|
264
|
+
if (!ts.isVariableStatement(timerStatement) || !(timerStatement.declarationList.flags & ts.NodeFlags.Const) || timerStatement.declarationList.declarations.length !== 1) return undefined
|
|
265
|
+
const timerDeclaration = timerStatement.declarationList.declarations[0]
|
|
266
|
+
if (!ts.isIdentifier(timerDeclaration.name) || !timerDeclaration.initializer || !ts.isCallExpression(timerDeclaration.initializer) || !ts.isIdentifier(timerDeclaration.initializer.expression) || timerDeclaration.initializer.expression.text !== "setTimeout" || !isUnshadowedGlobal(timerDeclaration.initializer.expression, sourceFile) || timerDeclaration.initializer.arguments.length !== 2 || !ts.isIdentifier(timerDeclaration.initializer.arguments[1]) || timerDeclaration.initializer.arguments[1].text !== delayParameter.name.text) return undefined
|
|
267
|
+
const timeoutCallback = timerDeclaration.initializer.arguments[0]
|
|
268
|
+
const timeoutBody = (ts.isArrowFunction(timeoutCallback) || ts.isFunctionExpression(timeoutCallback)) && !timeoutCallback.parameters.length && !timeoutCallback.asteriskToken && !timeoutCallback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) ? timeoutCallback.body : undefined
|
|
269
|
+
const setterCall = timeoutBody && (ts.isBlock(timeoutBody) ? timeoutBody.statements.length === 1 && ts.isExpressionStatement(timeoutBody.statements[0]) ? timeoutBody.statements[0].expression : undefined : timeoutBody)
|
|
270
|
+
if (!setterCall || !ts.isCallExpression(setterCall) || !ts.isIdentifier(setterCall.expression) || setterCall.expression.text !== stateDeclaration.name.elements[1].name.text || setterCall.arguments.length !== 1 || !ts.isIdentifier(setterCall.arguments[0]) || setterCall.arguments[0].text !== valueParameter.name.text) return undefined
|
|
271
|
+
const cleanupReturn = setup.body.statements[1]
|
|
272
|
+
const cleanup = ts.isReturnStatement(cleanupReturn) && cleanupReturn.expression && (ts.isArrowFunction(cleanupReturn.expression) || ts.isFunctionExpression(cleanupReturn.expression)) && !cleanupReturn.expression.parameters.length ? cleanupReturn.expression : undefined
|
|
273
|
+
const cleanupBody = cleanup && (ts.isBlock(cleanup.body) ? cleanup.body.statements.length === 1 && ts.isExpressionStatement(cleanup.body.statements[0]) ? cleanup.body.statements[0].expression : undefined : cleanup.body)
|
|
274
|
+
if (!cleanupBody || !ts.isCallExpression(cleanupBody) || !ts.isIdentifier(cleanupBody.expression) || cleanupBody.expression.text !== "clearTimeout" || !isUnshadowedGlobal(cleanupBody.expression, sourceFile) || cleanupBody.arguments.length !== 1 || !ts.isIdentifier(cleanupBody.arguments[0]) || cleanupBody.arguments[0].text !== timerDeclaration.name.text) return undefined
|
|
275
|
+
if (!ts.isReturnStatement(returnStatement) || !returnStatement.expression || !ts.isIdentifier(returnStatement.expression) || returnStatement.expression.text !== stateDeclaration.name.elements[0].name.text) return undefined
|
|
276
|
+
return { dependencies, delay: delayParameter.name.text, normalized: normalizedValue && normalizedDependencies, stateCall: stateDeclaration.initializer, state: stateDeclaration.name.elements[0].name.text, setter: stateDeclaration.name.elements[1].name.text, value: valueParameter.name.text }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function normalizeParameterizedDebounceHooks(sourceFile, factory, context) {
|
|
280
|
+
const visitor = node => {
|
|
281
|
+
if (ts.isFunctionDeclaration(node) || ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
282
|
+
const hook = parameterizedDebounceHook(node)
|
|
283
|
+
if (hook && !hook.normalized) {
|
|
284
|
+
const rewrite = current => {
|
|
285
|
+
if (current === hook.stateCall) return factory.updateCallExpression(current, current.expression, current.typeArguments, [factory.createPropertyAccessExpression(factory.createIdentifier(hook.value), "value")])
|
|
286
|
+
if (current === hook.dependencies) return factory.updateArrayLiteralExpression(current, [factory.createIdentifier(hook.value)])
|
|
287
|
+
return ts.visitEachChild(current, rewrite, context)
|
|
288
|
+
}
|
|
289
|
+
return ts.visitEachChild(node, rewrite, context)
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return ts.visitEachChild(node, visitor, context)
|
|
293
|
+
}
|
|
294
|
+
return ts.visitNode(sourceFile, visitor)
|
|
295
|
+
}
|
|
296
|
+
|
|
243
297
|
function normalizeCompilerSource(sourceFile, { base, context, file, importedCollections, importedStaticCollections, sourceFiles, sourceIndex }) {
|
|
244
298
|
const factory = context.factory
|
|
245
299
|
let customHookTimerStates = new Set()
|
|
@@ -250,6 +304,7 @@ function normalizeCompilerSource(sourceFile, { base, context, file, importedColl
|
|
|
250
304
|
source => normalizeMediaQueryExternalStores(source, factory, context),
|
|
251
305
|
source => normalizeReactMigrationSyntax(source, factory, context, importedCollections ?? importedSerializableCollectionNames(source, file, sourceFiles, sourceIndex)),
|
|
252
306
|
source => normalizeNavigatorCapabilityConditions(source, factory, context),
|
|
307
|
+
source => normalizeParameterizedDebounceHooks(source, factory, context),
|
|
253
308
|
source => normalizeEffectPrivateRefs(source, factory, context),
|
|
254
309
|
source => {
|
|
255
310
|
const result = normalizeCustomHookTimerRefs(source, factory, context)
|
|
@@ -365,6 +420,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
365
420
|
const components = new Map()
|
|
366
421
|
const contexts = new Set()
|
|
367
422
|
const customHooks = new Map()
|
|
423
|
+
const parameterizedDebounceCalls = new WeakSet()
|
|
424
|
+
const resolvedParameterizedDebounceHooks = new Map()
|
|
368
425
|
const jsxLocalDeclarations = new Map()
|
|
369
426
|
const jsxLocalsByFunction = new Map()
|
|
370
427
|
const listLocalDeclarations = []
|
|
@@ -550,11 +607,36 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
550
607
|
customHooks.set(key, analysis)
|
|
551
608
|
return analysis
|
|
552
609
|
}
|
|
610
|
+
const resolveParameterizedDebounceHook = binding => {
|
|
611
|
+
const exportName = binding.kind === "default" ? "default" : binding.imported
|
|
612
|
+
const key = `${binding.target}:${exportName}`
|
|
613
|
+
if (resolvedParameterizedDebounceHooks.has(key)) return resolvedParameterizedDebounceHooks.get(key)
|
|
614
|
+
const hook = resolveComponentExport(binding.target, exportName, importedSource, sourceFiles)
|
|
615
|
+
const analysis = parameterizedDebounceHook(hook)
|
|
616
|
+
resolvedParameterizedDebounceHooks.set(key, analysis)
|
|
617
|
+
return analysis
|
|
618
|
+
}
|
|
553
619
|
|
|
554
620
|
const collect = node => {
|
|
555
621
|
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
556
622
|
const callName = ts.isIdentifier(node.initializer.expression) ? node.initializer.expression.text : ""
|
|
557
|
-
|
|
623
|
+
const customHookImport = callName && /^use[A-Z]/.test(callName) && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace" && !resolvedSharedState(importBindings.get(callName))
|
|
624
|
+
const debounce = customHookImport && ts.isIdentifier(node.name) ? resolveParameterizedDebounceHook(importBindings.get(callName)) : undefined
|
|
625
|
+
if (debounce) {
|
|
626
|
+
if (!isLocalConst(node)) throw sourceNodeError(node, sourceFile, "Parameterized debounce hooks must initialize one top-level const identifier")
|
|
627
|
+
const owner = nearestFunction(node)
|
|
628
|
+
const value = node.initializer.arguments[0] && unwrapExpression(node.initializer.arguments[0])
|
|
629
|
+
const delay = node.initializer.arguments[1] && unwrapExpression(node.initializer.arguments[1])
|
|
630
|
+
const setters = owner ? settersByFunction.get(owner) ?? new Map() : new Map()
|
|
631
|
+
if (!owner) throw sourceNodeError(node, sourceFile, "Parameterized debounce hooks cannot be used outside a Kudzu component")
|
|
632
|
+
if (node.initializer.arguments.length !== 2 || !ts.isIdentifier(value) || !new Set(setters.values()).has(value.text) || !componentHasDirectPrimitiveState(owner, value.text)) throw sourceNodeError(node.initializer, sourceFile, "Parameterized debounce hooks require one direct primitive state argument")
|
|
633
|
+
if (!ts.isNumericLiteral(delay)) throw sourceNodeError(node.initializer.arguments[1] ?? node.initializer, sourceFile, "Parameterized debounce hook delays must be numeric literals")
|
|
634
|
+
const syntheticSetter = `__kSetDebounced_${Math.max(0, node.pos)}`
|
|
635
|
+
setters.set(syntheticSetter, node.name.text)
|
|
636
|
+
registerState(owner, node.name.text, syntheticSetter, "custom-hook", node)
|
|
637
|
+
settersByFunction.set(owner, setters)
|
|
638
|
+
parameterizedDebounceCalls.add(node.initializer)
|
|
639
|
+
} else if (customHookImport) {
|
|
558
640
|
if (!isLocalConst(node) || !ts.isObjectBindingPattern(node.name) || node.initializer.arguments.length) throw sourceNodeError(node, sourceFile, "Relative custom hooks must initialize one top-level const object destructuring with no arguments")
|
|
559
641
|
const hook = resolveCustomHook(importBindings.get(callName), node.initializer)
|
|
560
642
|
const names = new Set()
|
|
@@ -711,6 +793,13 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
711
793
|
if (node.parent === sourceFile) {
|
|
712
794
|
components.set(node.name.text, { function: node, declaration: node })
|
|
713
795
|
ensureOwner(node)
|
|
796
|
+
const debounce = parameterizedDebounceHook(node)
|
|
797
|
+
if (debounce) {
|
|
798
|
+
const setters = settersByFunction.get(node) ?? new Map()
|
|
799
|
+
setters.set(`__kDebounceSource_${Math.max(0, node.pos)}`, debounce.value)
|
|
800
|
+
registerState(node, debounce.value, `__kDebounceSource_${Math.max(0, node.pos)}`, "custom-hook", node.parameters[0])
|
|
801
|
+
settersByFunction.set(node, setters)
|
|
802
|
+
}
|
|
714
803
|
}
|
|
715
804
|
}
|
|
716
805
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) {
|
|
@@ -736,7 +825,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
736
825
|
}
|
|
737
826
|
}
|
|
738
827
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "createContext") contexts.add(node.name.text)
|
|
739
|
-
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isLocalConst(node)) {
|
|
828
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && isLocalConst(node) && !(ts.isCallExpression(node.initializer) && parameterizedDebounceCalls.has(node.initializer))) {
|
|
740
829
|
const owner = nearestFunction(node)
|
|
741
830
|
const declarations = jsxLocalDeclarations.get(owner) ?? new Map()
|
|
742
831
|
const entries = declarations.get(node.name.text) ?? []
|
|
@@ -1095,6 +1184,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1095
1184
|
}
|
|
1096
1185
|
collectReferences(component.body)
|
|
1097
1186
|
if (!references.length) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be called by at least one intrinsic event handler`)
|
|
1187
|
+
const effect = directSetterPropEffect(component, prop)
|
|
1188
|
+
if (effect) {
|
|
1189
|
+
if (references.length !== effect.references.size || references.some(reference => !effect.references.has(reference))) fail(references.find(reference => !effect.references.has(reference)) ?? element, `Setter-callback prop ${JSON.stringify(prop)} effect must directly set its prop-derived state with exact [state, setter] dependencies`)
|
|
1190
|
+
continue
|
|
1191
|
+
}
|
|
1098
1192
|
const handlers = new Set()
|
|
1099
1193
|
for (const reference of references) {
|
|
1100
1194
|
if (ts.isJsxExpression(reference.parent) && ts.isJsxAttribute(reference.parent.parent) && /^on[A-Z]/.test(reference.parent.parent.name.text)) {
|
|
@@ -1237,6 +1331,17 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1237
1331
|
const specializeSetterCallbacks = (call, component, callbackProps, imported) => {
|
|
1238
1332
|
if (componentSpecializations.has(call)) fail(call, "Setter callback props cannot be combined with another component specialization")
|
|
1239
1333
|
validateSetterCallbackProps(call, component, callbackProps)
|
|
1334
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
1335
|
+
const setters = settersForNode(call, settersByFunction)
|
|
1336
|
+
for (const prop of callbackProps) {
|
|
1337
|
+
const effect = directSetterPropEffect(component, prop)
|
|
1338
|
+
if (!effect) continue
|
|
1339
|
+
const stateAttribute = attributes.properties.find(entry => ts.isJsxAttribute(entry) && entry.name.text === effect.stateProp)
|
|
1340
|
+
const setterAttribute = attributes.properties.find(entry => ts.isJsxAttribute(entry) && entry.name.text === prop)
|
|
1341
|
+
const stateValue = stateAttribute?.initializer && ts.isJsxExpression(stateAttribute.initializer) ? unwrapExpression(stateAttribute.initializer.expression) : undefined
|
|
1342
|
+
const setterValue = setterAttribute?.initializer && ts.isJsxExpression(setterAttribute.initializer) ? unwrapExpression(setterAttribute.initializer.expression) : undefined
|
|
1343
|
+
if (!ts.isIdentifier(stateValue) || !ts.isIdentifier(setterValue) || setters.get(setterValue.text) !== stateValue.text || !componentHasDirectArrayState(nearestFunction(call), stateValue.text)) fail(setterAttribute ?? call, `Setter-callback effect prop ${JSON.stringify(prop)} must target the same direct array state passed through ${JSON.stringify(effect.stateProp)}`)
|
|
1344
|
+
}
|
|
1240
1345
|
const specialization = specialize(call, component, "Setter-callback", true, true, new Set(settersForNode(call, settersByFunction).values()))
|
|
1241
1346
|
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
1242
1347
|
const substitutions = new Map()
|
|
@@ -1841,6 +1946,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1841
1946
|
}
|
|
1842
1947
|
|
|
1843
1948
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && importBindings.has(node.initializer.expression.text)) {
|
|
1949
|
+
if (parameterizedDebounceCalls.has(node.initializer)) return node
|
|
1844
1950
|
const setters = settersForNode(node, settersByFunction)
|
|
1845
1951
|
const stateNames = new Set(setters.values())
|
|
1846
1952
|
const rewrite = current => {
|
|
@@ -2132,6 +2238,38 @@ function componentHasDirectPropStateInitializer(component) {
|
|
|
2132
2238
|
return component.body.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState" && declaration.initializer.arguments.length === 1 && ts.isIdentifier(declaration.initializer.arguments[0]) && props.has(declaration.initializer.arguments[0].text)))
|
|
2133
2239
|
}
|
|
2134
2240
|
|
|
2241
|
+
function componentHasDirectPrimitiveState(component, state) {
|
|
2242
|
+
return Boolean(component && ts.isBlock(component.body) && component.body.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => ts.isArrayBindingPattern(declaration.name) && ts.isIdentifier(declaration.name.elements[0]?.name) && declaration.name.elements[0].name.text === state && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState" && declaration.initializer.arguments.length === 1 && isPrimitiveDefaultLiteral(unwrapExpression(declaration.initializer.arguments[0])))))
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
function componentHasDirectArrayState(component, state) {
|
|
2246
|
+
return Boolean(component && ts.isBlock(component.body) && component.body.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => ts.isArrayBindingPattern(declaration.name) && ts.isIdentifier(declaration.name.elements[0]?.name) && declaration.name.elements[0].name.text === state && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "useState" && declaration.initializer.arguments.length === 1 && ts.isArrayLiteralExpression(unwrapExpression(declaration.initializer.arguments[0])))))
|
|
2247
|
+
}
|
|
2248
|
+
|
|
2249
|
+
function directSetterPropEffect(component, setterProp) {
|
|
2250
|
+
if (!component || component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name) || !ts.isBlock(component.body)) return undefined
|
|
2251
|
+
const props = component.parameters[0].name.elements.filter(element => !element.dotDotDotToken && ts.isIdentifier(element.name))
|
|
2252
|
+
const setter = props.find(element => !element.propertyName && element.name.text === setterProp)
|
|
2253
|
+
if (!setter) return undefined
|
|
2254
|
+
for (const statement of component.body.statements) {
|
|
2255
|
+
if (!ts.isVariableStatement(statement) || statement.declarationList.declarations.length !== 1) continue
|
|
2256
|
+
const declaration = statement.declarationList.declarations[0]
|
|
2257
|
+
if (!ts.isArrayBindingPattern(declaration.name) || !declaration.initializer || !ts.isCallExpression(declaration.initializer) || !ts.isIdentifier(declaration.initializer.expression) || declaration.initializer.expression.text !== "useState" || declaration.initializer.arguments.length !== 1 || !ts.isIdentifier(declaration.initializer.arguments[0]) || !ts.isIdentifier(declaration.name.elements[0]?.name)) continue
|
|
2258
|
+
const stateProp = props.find(element => !element.propertyName && element.name.text === declaration.initializer.arguments[0].text)
|
|
2259
|
+
if (!stateProp) continue
|
|
2260
|
+
const state = declaration.name.elements[0].name.text
|
|
2261
|
+
for (const effectStatement of component.body.statements) {
|
|
2262
|
+
if (!ts.isExpressionStatement(effectStatement) || !ts.isCallExpression(effectStatement.expression) || !ts.isIdentifier(effectStatement.expression.expression) || effectStatement.expression.expression.text !== "useEffect" || effectStatement.expression.arguments.length !== 2) continue
|
|
2263
|
+
const [callback, dependencies] = effectStatement.expression.arguments
|
|
2264
|
+
if ((!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || !ts.isBlock(callback.body) || callback.body.statements.length !== 1 || !ts.isArrayLiteralExpression(dependencies) || dependencies.elements.length !== 2) continue
|
|
2265
|
+
const body = callback.body.statements[0]
|
|
2266
|
+
if (!ts.isExpressionStatement(body) || !ts.isCallExpression(body.expression) || !ts.isIdentifier(body.expression.expression) || body.expression.expression.text !== setter.name.text || body.expression.arguments.length !== 1 || !ts.isIdentifier(body.expression.arguments[0]) || body.expression.arguments[0].text !== state) continue
|
|
2267
|
+
if (!ts.isIdentifier(dependencies.elements[0]) || dependencies.elements[0].text !== state || !ts.isIdentifier(dependencies.elements[1]) || dependencies.elements[1].text !== setter.name.text) continue
|
|
2268
|
+
return { stateProp: (stateProp.propertyName ?? stateProp.name).getText(), references: new Set([body.expression.expression, dependencies.elements[1]]) }
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2135
2273
|
function supportedSetterCallbackProps(component, props) {
|
|
2136
2274
|
const acceptsSetProp = componentHasDirectPropStateInitializer(component)
|
|
2137
2275
|
return props.filter(prop => /^on[A-Z]/.test(prop) || acceptsSetProp)
|
package/framework/core.mjs
CHANGED
|
@@ -176,7 +176,7 @@ export function useEffect(callback, dependencies, module, handler, states, scope
|
|
|
176
176
|
if (itemDependencies.length && !renderContext.listDepth) throw new Error(`${source} useEffect() item-property dependencies are only supported in direct keyed row components`)
|
|
177
177
|
const derivedSources = new Set(dependencyStates.map(([, dependency]) => dependency))
|
|
178
178
|
const dependencyIds = [...new Set(dependencies.map(dependency => {
|
|
179
|
-
if (!dependency?.[signalMarker] || !derivedSources.has(dependency) && !validEffectDependency(dependency.value)) throw new Error(`${source} useEffect() dependencies must be primitive Kudzu state or runtime parameter identifiers`)
|
|
179
|
+
if (!dependency?.[signalMarker] || !derivedSources.has(dependency) && !validEffectDependency(dependency.value) && !Array.isArray(dependency.value)) throw new Error(`${source} useEffect() dependencies must be primitive or array Kudzu state or runtime parameter identifiers`)
|
|
180
180
|
return dependency.id
|
|
181
181
|
}))]
|
|
182
182
|
const dependencyStateIds = Object.fromEntries(dependencyStates.map(([name, dependency]) => {
|