@kudzujs/core 0.8.58 → 0.8.60
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/PERFORMANCE.md +5 -1
- package/README.md +1 -1
- package/RELEASES.md +60 -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 +66 -7
- 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.59` 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
|
|
|
@@ -149,6 +149,8 @@ This queue orders the next investigations by general migration value. Start only
|
|
|
149
149
|
- In `0.8.46`, the same Notes migration may omit action-only setters from its public Context type and authored Provider value when the corresponding state remains exposed. Kudzu restores those setters only in build scratch so existing concrete state operations, RouteIR signals, CRUD behavior, and zero-Context runtime output remain unchanged. Fully hidden action state and exposed setters without state remain diagnosed.
|
|
150
150
|
- 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
151
|
- 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
|
+
- 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.
|
|
153
|
+
- 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.
|
|
152
154
|
- 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.
|
|
153
155
|
- 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.
|
|
154
156
|
|
package/PERFORMANCE.md
CHANGED
|
@@ -6,7 +6,11 @@ Reproducibility classes: `npm run benchmark`, `npm run benchmark:keyed`, `npm ru
|
|
|
6
6
|
|
|
7
7
|
The maintained 2026-08-13 comparison used Node 24.14.0 and an Intel Core i5-9500 Linux x64 host, one warm-up, and seven alternating fresh-process samples against clean `v0.8.44`. A narrow fast path skips Kudzu semantic transformation for 450 plain `.ts` modules whose runtime edges are exclusively resolvable relative TypeScript imports or exports; all other modules retain the existing transformer. Compile median fell from 2,323.9 ms to 1,413.2 ms (39.2%) and clean-build median from 3,325.3 ms to 2,382.4 ms (28.4%); every paired sample improved. Compile peak-RSS median fell from 571.2 MiB to 552.6 MiB, while build peak RSS was 570.9 MiB versus 568.8 MiB. Compiler scratch fell from 7,328,390 to 1,971,061 bytes. Both targets emitted the same 50 static HTML files, 10,980 bytes, and deploy SHA-256 `e107d78a7f55bc8a1af0ea6e53efeffa19b3d44d21c892484d103fa346e7ba7b`. This is a source-scale compiler comparison, not a cross-framework result.
|
|
8
8
|
|
|
9
|
-
## Current 0.8.
|
|
9
|
+
## Current 0.8.59 Release Snapshot
|
|
10
|
+
|
|
11
|
+
Kudzu 0.8.59 broadens existing compile-time setter-child specialization to one direct parent array-state prop and one direct `set*` setter prop in that prop-derived state shape. It adds no browser runtime code and makes no timing claim.
|
|
12
|
+
|
|
13
|
+
## Maintained 0.8.58 Release Snapshot
|
|
10
14
|
|
|
11
15
|
Kudzu 0.8.58 adds one narrow keyed-item initializer descriptor to the existing complex row-state path. Routes without that descriptor, including static siblings, retain their existing capability output. This release makes no timing claim.
|
|
12
16
|
|
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.60 - Array draft effect sync.** A specialized child can synchronize direct prop-derived array draft state through its matching parent setter in an ordinary dependency effect without a component runtime. Read the [release notes](./RELEASES.md#0860---array-draft-effect-sync), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.60), 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,65 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.8.60 - Array draft effect sync
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
### Changed in 0.8.60
|
|
8
|
+
|
|
9
|
+
- `useEffect(() => { setSelectedItems(items) }, [items, setSelectedItems])` compiles for the matching direct array prop-draft specialization.
|
|
10
|
+
- 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.
|
|
11
|
+
- Parent replacement still does not synchronize the independently owned child draft; the next authored child-state replacement reruns the effect.
|
|
12
|
+
|
|
13
|
+
### Boundaries
|
|
14
|
+
|
|
15
|
+
- The effect must be top-level, synchronous, inline, and contain exactly one direct setter call with exact `[state, setter]` dependencies.
|
|
16
|
+
- Setter aliases, composed values, mismatched parent state/setter pairs, cleanup, and additional callback uses remain rejected.
|
|
17
|
+
- Object and derived array dependencies remain unchanged; no runtime module, EffectIR kind, component tree, or automatic prop synchronization is added.
|
|
18
|
+
|
|
19
|
+
### Validation
|
|
20
|
+
|
|
21
|
+
- The ClimateCompatibleGrowth-derived dropdown now preserves its authored setter effect shape.
|
|
22
|
+
- Browser coverage proves initial setup, child replacement commit, independent parent reset, and effect rerun; a static sibling remains zero JavaScript.
|
|
23
|
+
- Existing effect ownership and object-dependency diagnostics remain covered.
|
|
24
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 228 tests and 174 generated pages.
|
|
25
|
+
|
|
26
|
+
### Upgrade
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install @kudzujs/core@^0.8.60
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 0.8.59 - Array prop draft state
|
|
33
|
+
|
|
34
|
+
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.
|
|
35
|
+
|
|
36
|
+
### Changed in 0.8.59
|
|
37
|
+
|
|
38
|
+
- `const [draft, setDraft] = useState(selectedItems)` compiles when `selectedItems` is one direct prop backed by a parent `useState([...])` literal.
|
|
39
|
+
- A direct parent setter may enter that prop-draft specialization through an `on*` prop or an authored `set*` prop such as `setSelectedItems`.
|
|
40
|
+
- Parent and child retain distinct state IDs and independently serialized arrays.
|
|
41
|
+
- Mounted drafts remain local until the authored handler explicitly commits them to parent state.
|
|
42
|
+
|
|
43
|
+
### Boundaries
|
|
44
|
+
|
|
45
|
+
- `set*` support is limited to prop-derived state components; unrelated effect setter props retain their existing path.
|
|
46
|
+
- Additional forwarding remains `on*`-only.
|
|
47
|
+
- Aliases, property paths, array spreads, composed expressions, indirect callbacks, and automatic prop synchronization remain rejected.
|
|
48
|
+
- No runtime module, VDOM, hydration, or retained component tree is added.
|
|
49
|
+
|
|
50
|
+
### Validation
|
|
51
|
+
|
|
52
|
+
- A fixture reduced from ClimateCompatibleGrowth's MIT-licensed teaching-kit `Dropdown` preserves its direct `selectedItems` and `setSelectedItems` source shape.
|
|
53
|
+
- Browser coverage proves child draft independence, parent replacement without draft synchronization, explicit commit, and a static zero-JavaScript sibling.
|
|
54
|
+
- Existing conditional-effect `setResult` props remain outside setter-callback specialization.
|
|
55
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 225 tests and 174 generated pages.
|
|
56
|
+
|
|
57
|
+
### Upgrade
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
npm install @kudzujs/core@^0.8.59
|
|
61
|
+
```
|
|
62
|
+
|
|
3
63
|
## 0.8.58 - Keyed item draft state
|
|
4
64
|
|
|
5
65
|
Kudzu 0.8.58 lets a keyed row initialize independent local object draft state directly from its keyed item 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.60` 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, 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.60` 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`:** 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
|
|
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`:** 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, exact direct setter-effect synchronization through existing effect ownership, stable setter-dependency erasure, no automatic mounted prop synchronization, and zero-JavaScript static sibling output. A fourth callback boundary, callback aliases/non-handler uses, 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,
|
|
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, and `0.8.60` matching array-draft setter-effect 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.60` 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
|
|
|
@@ -49,6 +49,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
49
49
|
| `0.8.56` | Recompile and rerender only source-affected routes during development. | A route-owned helper edit recompiles two of four modules, rerenders one of two pages, and emits byte-identical deploy output to a fresh full build. |
|
|
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
|
+
| `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. |
|
|
52
54
|
|
|
53
55
|
## Sequence Rules
|
|
54
56
|
|
package/framework/README.md
CHANGED
|
@@ -90,7 +90,7 @@ Rendered collection selectors compile immutable local aliases and inline `(item)
|
|
|
90
90
|
|
|
91
91
|
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.
|
|
92
92
|
|
|
93
|
-
A direct setter or inline/simple `const` setter callback may cross one same-file or relative-imported component boundary and two additional direct forwarding components before an intrinsic handler invokes it exactly once. Value adapters such as `event => onValueChange(event.currentTarget.value)` specialize into the parent setter graph instead of serializing a function.
|
|
93
|
+
A direct setter or inline/simple `const` setter callback may cross one same-file or relative-imported component boundary and two additional direct forwarding components before an intrinsic handler invokes it exactly once. Value adapters such as `event => onValueChange(event.currentTarget.value)` specialize into the parent setter graph instead of serializing a function. The first boundary may pass a direct parent setter through an `on*` or `set*` prop; each additional forwarding component must destructure and directly forward the callback once as an `on*` JSX prop. Fourth-boundary forwarding, additional `set*` forwarding, aliases, spreads, intermediate adapters, and repeated uses remain unsupported. Specialized children may own directly serializable `useState()` values or initialize state from one direct parent state prop authored with a serializable primitive, plain-object, or array literal; zero-argument `.toString()` remains primitive-only. Prop aliases, property paths, and composed initializers remain unsupported. Specialized children may also own `useId()` values, supported effects, and object refs initialized with `null`. Same-file and relative-imported presentation components recursively specialize away; nested hooks join the same generated owner only on unconditional or statically truthy paths. A parent-owned `null`-initialized object ref may follow the proven tree 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.
|
|
94
94
|
|
|
95
95
|
`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.
|
|
96
96
|
|
|
@@ -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() {
|
|
@@ -1095,6 +1095,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1095
1095
|
}
|
|
1096
1096
|
collectReferences(component.body)
|
|
1097
1097
|
if (!references.length) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be called by at least one intrinsic event handler`)
|
|
1098
|
+
const effect = directSetterPropEffect(component, prop)
|
|
1099
|
+
if (effect) {
|
|
1100
|
+
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`)
|
|
1101
|
+
continue
|
|
1102
|
+
}
|
|
1098
1103
|
const handlers = new Set()
|
|
1099
1104
|
for (const reference of references) {
|
|
1100
1105
|
if (ts.isJsxExpression(reference.parent) && ts.isJsxAttribute(reference.parent.parent) && /^on[A-Z]/.test(reference.parent.parent.name.text)) {
|
|
@@ -1157,7 +1162,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1157
1162
|
for (const state of aggregate.ordinaryStates) setters.set(state.setter, state.state)
|
|
1158
1163
|
const stateOwners = new Map(parentStateOwners)
|
|
1159
1164
|
for (const state of aggregate.ordinaryStates) stateOwners.set(state.state, state.analysisReference)
|
|
1160
|
-
const callbackProps = jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction))
|
|
1165
|
+
const callbackProps = supportedSetterCallbackProps(component, jsxSetterCallbackProps(node, setters, functionsForNode(node), reducersForNode(node, reducersByFunction)))
|
|
1161
1166
|
const callbackSubstitutions = new Map()
|
|
1162
1167
|
if (callbackProps.length) {
|
|
1163
1168
|
if (!callbackDepth) fail(node, "Setter callbacks cannot cross more than three component boundaries")
|
|
@@ -1237,6 +1242,17 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1237
1242
|
const specializeSetterCallbacks = (call, component, callbackProps, imported) => {
|
|
1238
1243
|
if (componentSpecializations.has(call)) fail(call, "Setter callback props cannot be combined with another component specialization")
|
|
1239
1244
|
validateSetterCallbackProps(call, component, callbackProps)
|
|
1245
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
1246
|
+
const setters = settersForNode(call, settersByFunction)
|
|
1247
|
+
for (const prop of callbackProps) {
|
|
1248
|
+
const effect = directSetterPropEffect(component, prop)
|
|
1249
|
+
if (!effect) continue
|
|
1250
|
+
const stateAttribute = attributes.properties.find(entry => ts.isJsxAttribute(entry) && entry.name.text === effect.stateProp)
|
|
1251
|
+
const setterAttribute = attributes.properties.find(entry => ts.isJsxAttribute(entry) && entry.name.text === prop)
|
|
1252
|
+
const stateValue = stateAttribute?.initializer && ts.isJsxExpression(stateAttribute.initializer) ? unwrapExpression(stateAttribute.initializer.expression) : undefined
|
|
1253
|
+
const setterValue = setterAttribute?.initializer && ts.isJsxExpression(setterAttribute.initializer) ? unwrapExpression(setterAttribute.initializer.expression) : undefined
|
|
1254
|
+
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)}`)
|
|
1255
|
+
}
|
|
1240
1256
|
const specialization = specialize(call, component, "Setter-callback", true, true, new Set(settersForNode(call, settersByFunction).values()))
|
|
1241
1257
|
if (specialization.hookDeclarations.length || specialization.effects.length) {
|
|
1242
1258
|
const substitutions = new Map()
|
|
@@ -1295,7 +1311,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1295
1311
|
}
|
|
1296
1312
|
for (const [name, component] of components) {
|
|
1297
1313
|
for (const call of jsxTagUses(sourceFile, name)) {
|
|
1298
|
-
const callbackProps = jsxSetterCallbackProps(call, settersByFunction.get(nearestFunction(call)) ?? new Map(), functionsForNode(call), reducersForNode(call, reducersByFunction))
|
|
1314
|
+
const callbackProps = supportedSetterCallbackProps(component.function, jsxSetterCallbackProps(call, settersByFunction.get(nearestFunction(call)) ?? new Map(), functionsForNode(call), reducersForNode(call, reducersByFunction)))
|
|
1299
1315
|
if (callbackProps.length) specializeSetterCallbacks(call, component.function, callbackProps, false)
|
|
1300
1316
|
}
|
|
1301
1317
|
}
|
|
@@ -1311,7 +1327,10 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1311
1327
|
} catch {
|
|
1312
1328
|
fail(callbackCalls[0].call, "Setter callback props require a component imported from a relative TypeScript module")
|
|
1313
1329
|
}
|
|
1314
|
-
for (const { call, callbackProps } of callbackCalls)
|
|
1330
|
+
for (const { call, callbackProps } of callbackCalls) {
|
|
1331
|
+
const supportedProps = supportedSetterCallbackProps(component, callbackProps)
|
|
1332
|
+
if (supportedProps.length) specializeSetterCallbacks(call, component, supportedProps, true)
|
|
1333
|
+
}
|
|
1315
1334
|
}
|
|
1316
1335
|
for (const [name, component] of components) {
|
|
1317
1336
|
const calls = jsxTagUses(sourceFile, name)
|
|
@@ -2114,14 +2133,54 @@ function jsxCallHasDirectStateProp(call, setters) {
|
|
|
2114
2133
|
function jsxSetterCallbackProps(call, setters, functions, reducers) {
|
|
2115
2134
|
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
2116
2135
|
return attributes.properties.flatMap(attribute => {
|
|
2117
|
-
if (!ts.isJsxAttribute(attribute) ||
|
|
2136
|
+
if (!ts.isJsxAttribute(attribute) || !attribute.initializer || !ts.isJsxExpression(attribute.initializer) || !attribute.initializer.expression) return []
|
|
2118
2137
|
const value = unwrapExpression(attribute.initializer.expression)
|
|
2119
|
-
if (ts.isIdentifier(value) && setters.has(value.text)) return [attribute.name.text]
|
|
2138
|
+
if (ts.isIdentifier(value) && setters.has(value.text) && (/^on[A-Z]/.test(attribute.name.text) || /^set[A-Z]/.test(attribute.name.text))) return [attribute.name.text]
|
|
2139
|
+
if (!/^on[A-Z]/.test(attribute.name.text)) return []
|
|
2120
2140
|
const callback = ts.isArrowFunction(value) || ts.isFunctionExpression(value) ? value : ts.isIdentifier(value) ? functions.get(value.text) : undefined
|
|
2121
2141
|
return callback && !nativeCaptureNames(callback, setters).size && !referencedReducerDispatches(callback.body, reducers, callback).size && referencedStateNames(callback.body, setters, callback).size ? [attribute.name.text] : []
|
|
2122
2142
|
})
|
|
2123
2143
|
}
|
|
2124
2144
|
|
|
2145
|
+
function componentHasDirectPropStateInitializer(component) {
|
|
2146
|
+
if (!component || component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name) || !ts.isBlock(component.body)) return false
|
|
2147
|
+
const props = new Set(component.parameters[0].name.elements.filter(element => !element.dotDotDotToken && ts.isIdentifier(element.name)).map(element => element.name.text))
|
|
2148
|
+
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)))
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
function componentHasDirectArrayState(component, state) {
|
|
2152
|
+
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])))))
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function directSetterPropEffect(component, setterProp) {
|
|
2156
|
+
if (!component || component.parameters.length !== 1 || !ts.isObjectBindingPattern(component.parameters[0].name) || !ts.isBlock(component.body)) return undefined
|
|
2157
|
+
const props = component.parameters[0].name.elements.filter(element => !element.dotDotDotToken && ts.isIdentifier(element.name))
|
|
2158
|
+
const setter = props.find(element => !element.propertyName && element.name.text === setterProp)
|
|
2159
|
+
if (!setter) return undefined
|
|
2160
|
+
for (const statement of component.body.statements) {
|
|
2161
|
+
if (!ts.isVariableStatement(statement) || statement.declarationList.declarations.length !== 1) continue
|
|
2162
|
+
const declaration = statement.declarationList.declarations[0]
|
|
2163
|
+
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
|
|
2164
|
+
const stateProp = props.find(element => !element.propertyName && element.name.text === declaration.initializer.arguments[0].text)
|
|
2165
|
+
if (!stateProp) continue
|
|
2166
|
+
const state = declaration.name.elements[0].name.text
|
|
2167
|
+
for (const effectStatement of component.body.statements) {
|
|
2168
|
+
if (!ts.isExpressionStatement(effectStatement) || !ts.isCallExpression(effectStatement.expression) || !ts.isIdentifier(effectStatement.expression.expression) || effectStatement.expression.expression.text !== "useEffect" || effectStatement.expression.arguments.length !== 2) continue
|
|
2169
|
+
const [callback, dependencies] = effectStatement.expression.arguments
|
|
2170
|
+
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
|
|
2171
|
+
const body = callback.body.statements[0]
|
|
2172
|
+
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
|
|
2173
|
+
if (!ts.isIdentifier(dependencies.elements[0]) || dependencies.elements[0].text !== state || !ts.isIdentifier(dependencies.elements[1]) || dependencies.elements[1].text !== setter.name.text) continue
|
|
2174
|
+
return { stateProp: (stateProp.propertyName ?? stateProp.name).getText(), references: new Set([body.expression.expression, dependencies.elements[1]]) }
|
|
2175
|
+
}
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
function supportedSetterCallbackProps(component, props) {
|
|
2180
|
+
const acceptsSetProp = componentHasDirectPropStateInitializer(component)
|
|
2181
|
+
return props.filter(prop => /^on[A-Z]/.test(prop) || acceptsSetProp)
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2125
2184
|
function jsxCallHasDirectReducerProp(call, reducers) {
|
|
2126
2185
|
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
2127
2186
|
return attributes.properties.some(attribute => {
|
|
@@ -2498,10 +2557,10 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2498
2557
|
const substitutedState = substitutedProp && ts.isIdentifier(unwrapExpression(substitutedProp)) ? unwrapExpression(substitutedProp).text : undefined
|
|
2499
2558
|
const directProp = ts.isIdentifier(initialArgument)
|
|
2500
2559
|
const parentInitializer = substitutedState ? directStateInitializer(call, substitutedState) : undefined
|
|
2501
|
-
const propInitializer = ordinaryHooks && substitutedState && ordinaryStateNames.has(substitutedState) && parentInitializer && (isPrimitiveDefaultLiteral(parentInitializer) || directProp && ts.isObjectLiteralExpression(parentInitializer))
|
|
2560
|
+
const propInitializer = ordinaryHooks && substitutedState && ordinaryStateNames.has(substitutedState) && parentInitializer && (isPrimitiveDefaultLiteral(parentInitializer) || directProp && (ts.isObjectLiteralExpression(parentInitializer) || ts.isArrayLiteralExpression(parentInitializer)))
|
|
2502
2561
|
const rowItemProp = propReceiver && elements.find(element => !element.dotDotDotToken && ts.isIdentifier(element.name) && element.name.text === propReceiver.text)
|
|
2503
2562
|
const rowItemInitializer = !ordinaryHooks && directProp && substitutedState && rowItemProp && directProps.has((rowItemProp.propertyName ?? rowItemProp.name).text)
|
|
2504
|
-
if (declaration.initializer.arguments.length !== 1 || !isSerializableStateLiteral(initialArgument) && !propInitializer && !rowItemInitializer) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useState() must use one directly serializable primitive, plain object, or array initial value${ordinaryHooks ? " or direct state prop initialized with a primitive
|
|
2563
|
+
if (declaration.initializer.arguments.length !== 1 || !isSerializableStateLiteral(initialArgument) && !propInitializer && !rowItemInitializer) throw sourceNodeError(declaration.initializer, component.getSourceFile(), `${hookLabel} useState() must use one directly serializable primitive, plain object, or array initial value${ordinaryHooks ? " or direct state prop initialized with a primitive, plain object, or array; .toString() requires a primitive prop" : " or the direct keyed item prop"}; other dynamic initializers are not supported`)
|
|
2505
2564
|
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`)
|
|
2506
2565
|
const suffix = `${Math.max(0, call.pos)}_${ordinaryHooks ? ordinaryStates.length : rowStates.length}`
|
|
2507
2566
|
const state = ordinaryHooks ? `__kComponentState${suffix}` : `__kRowState${suffix}`
|
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]) => {
|