@kudzujs/core 0.8.50 → 0.8.52
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 +5 -4
- package/PERFORMANCE.md +9 -1
- package/README.md +1 -1
- package/RELEASES.md +63 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/compiler-current-architecture.md +1 -1
- package/docs/next-architecture/goal-c-state-resource-research.md +12 -8
- package/docs/next-architecture/large-application-ai-native-roadmap.md +3 -3
- package/docs/next-architecture/versioning.md +3 -1
- package/framework/README.md +1 -1
- package/framework/compiler/{animation-frame-pass.mjs → effect-private-ref-pass.mjs} +49 -20
- package/framework/compiler/source-compiler.mjs +28 -7
- 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.52` 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
|
|
|
@@ -63,6 +63,7 @@ The following are available building blocks, not future vertical roadmaps:
|
|
|
63
63
|
- Function components, props, children, context, direct bindings, conditions, controlled form properties, refs, and synchronous or async handlers.
|
|
64
64
|
- `useState`, independent repeated non-keyed child state with conditional mount ownership, reduced relative-imported `useReducer`, direct dispatch specialization, and reduced Zustand-shaped shared state lowered through package-neutral shared-state/action IR and proven by migration fixtures.
|
|
65
65
|
- Mount and dependency effects with cleanup, route/layout lifetimes, conditional/keyed ownership, stale-write isolation, and relative TypeScript Workers.
|
|
66
|
+
- Direct browser-only package references in inline effect setup/cleanup callbacks bundle into route-owned effect ESM without entering build-time component execution or static sibling output.
|
|
66
67
|
- Keyed local-state and imported static collections, pure selectors, nested direct-property lists, recursive and directly exported row specialization, static/keyed component reuse, analyzable specialized prop spreads, serializable literal defaults, direct intrinsic rest forwarding, forwarded JSX children, latest-item handlers, row state/effects/refs, and stable keyed identity.
|
|
67
68
|
- Complete-document native fallback and optional shared-layout navigation groups with parameter initialization, finite prefetch caching, focus, history, cleanup, and independent capability output.
|
|
68
69
|
- React-shaped SVG presentation attributes, direct `clsx` lowering, fixed-locale reactive `Intl.NumberFormat` display formatting, relative TypeScript handler helpers, and source-located diagnostics.
|
|
@@ -97,7 +98,7 @@ This queue orders the next investigations by general migration value. Start only
|
|
|
97
98
|
- Guarded `localStorage` restoration and persistence compose through existing mount/dependency effects, preserving deterministic static fallbacks without a storage runtime.
|
|
98
99
|
- Reachable source compilation excludes unused React/Vite modules while preserving source diagnostics for every reachable module.
|
|
99
100
|
- Direct imported immutable maps fold to static HTML, and relative structured calculations reevaluate consumed scalar fields through existing binding ESM.
|
|
100
|
-
- Direct event-
|
|
101
|
+
- Direct event-handler and inline owned-effect package imports bundle into route handler/effect ESM without executing during static rendering.
|
|
101
102
|
- Production Tailwind output, Inter assets, direct native SVG charts, and Excel export compose without adding Tailwind, Recharts, or a package runtime to Kudzu.
|
|
102
103
|
- FIRE migration validation covers all fourteen routes, URL/storage updates and reload, reset, presets, chart identity, Quiz recommendation, keyed Debt updates, Excel workbook creation, and zero-JavaScript Home/Books/Apps output.
|
|
103
104
|
|
|
@@ -172,7 +173,7 @@ This queue orders the next investigations by general migration value. Start only
|
|
|
172
173
|
### Completed In 0.8.13
|
|
173
174
|
|
|
174
175
|
- A reduced fixture from [Memos](https://github.com/usememos/memos) preserves its memo-outline scroll spy: native heading links, active `aria-current`, smooth scrolling, hash replacement, capture-phase scroll/resize listeners, and animation-frame coalescing with cleanup.
|
|
175
|
-
-
|
|
176
|
+
- Top-level `useRef(null)` and `useRef(0)` values used exclusively through direct `.current` references in one inline effect graph lower to invocation-private closure objects in effect ESM. Existing effect ownership handles SDK/WebSocket handles, generation invalidation, animation frames, dependency replacement, listener removal, BFCache-aware document disposal, and cleanup without serialized captures, ResourceIR, or a retained component.
|
|
176
177
|
- Chrome validation covers burst coalescing, active-heading updates, outline clicks, hash replacement, pending-frame cancellation, listener cleanup, and a zero-JavaScript static sibling. Ref aliases, cross-effect/event use, nonzero initializers, multiple scheduling assignments, and missing cancellation remain unsupported.
|
|
177
178
|
- A reduced fixture from [Excalidraw](https://github.com/excalidraw/excalidraw) preserves its active-room progressive sharing shape: a readonly collaboration URL, a Web Share button gated by `"share" in navigator`, direct `navigator.share()`, clipboard fallback, and accessible application-owned status.
|
|
178
179
|
- Browser capability conditions lower to a false static state plus one existing mount effect and state-owned conditional branch. Supported browsers mount the Share DOM and handler; unsupported browsers keep both out of the document and accessibility tree. Node's build-time `navigator` can no longer fold browser capability UI incorrectly.
|
|
@@ -271,7 +272,7 @@ This queue orders the next investigations by general migration value. Start only
|
|
|
271
272
|
|
|
272
273
|
### Characterized In 0.8.27
|
|
273
274
|
|
|
274
|
-
-
|
|
275
|
+
- Reduced E2B Dashboard terminal and route-owned WebSocket fixtures establish callback-shared mutable browser resources, asynchronous generation invalidation, dependency replacement, exact cleanup, and BFCache retain/resume/discard behavior. Exclusive effect-private refs now compile through ordinary effect ESM; shared transports and cross-owner subscriptions remain Goal C research.
|
|
275
276
|
- Unsupported page-level mutable value refs fail during source analysis with a source location and the existing effect-owned animation-frame exception; no resource API or runtime is added.
|
|
276
277
|
- The large-application and AI-native execution plan orders symbol resolution, semantic operations, project/module analysis, IR authority, application foundations, ecosystem compatibility, AI tooling, and scale validation before broad feature work.
|
|
277
278
|
|
package/PERFORMANCE.md
CHANGED
|
@@ -6,7 +6,15 @@ 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.52 Release Snapshot
|
|
10
|
+
|
|
11
|
+
Kudzu 0.8.52 moves proven effect-private mutable refs from component scope into existing setup-invocation closures. The refs add no serialized route scope, shared runtime, or static-sibling JavaScript; no new performance claim is made.
|
|
12
|
+
|
|
13
|
+
## Maintained 0.8.51 Release Snapshot
|
|
14
|
+
|
|
15
|
+
Kudzu 0.8.51 broadens compiler-only package-reference routing into existing owned effect ESM and bundling. Static siblings and routes without the effect retain zero package bytes; no new performance claim is made.
|
|
16
|
+
|
|
17
|
+
## Maintained 0.8.50 Release Snapshot
|
|
10
18
|
|
|
11
19
|
Kudzu 0.8.50 changes compiler and build-time shared-state metadata while retaining existing RouteIR state ownership, action handler output, and browser runtimes. No new performance claim is made.
|
|
12
20
|
|
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.52 - Effect-private mutable refs.** Top-level `useRef(null)` and `useRef(0)` values used exclusively by one inline effect graph lower to invocation-private closure objects. Browser SDK handles, WebSockets, generation tokens, animation frames, replacement, and cleanup retain ordinary React-shaped source without ResourceIR or a runtime. Read the [release notes](./RELEASES.md#0852---effect-private-mutable-refs), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.52), 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,68 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.8.52 - Effect-private mutable refs
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.52 compiles component-authored mutable refs when one inline effect exclusively owns their complete lifecycle.
|
|
6
|
+
|
|
7
|
+
### Changed in 0.8.52
|
|
8
|
+
|
|
9
|
+
- Top-level `useRef(null)` and `useRef(0)` values lower when every direct `.current` reference belongs to one inline block-bodied effect setup, its nested callbacks, and cleanup.
|
|
10
|
+
- The compiler removes those component declarations and creates fresh mutable objects inside each effect setup invocation.
|
|
11
|
+
- Existing effect ownership handles dependency replacement, stale setter invalidation, conditional/keyed ownership, navigation, document disposal, and BFCache preservation.
|
|
12
|
+
- Animation-frame refs use the same invocation-private lowering while retaining exact scheduler, reset, and cancellation validation.
|
|
13
|
+
- JSX DOM refs remain component-owned; aliases, nonzero initializers, cross-effect/event use, missing cleanup invalidation, and escaped ref objects remain source-diagnosed.
|
|
14
|
+
|
|
15
|
+
### Migration evidence
|
|
16
|
+
|
|
17
|
+
- The E2B terminal fixture now preserves asynchronous generation invalidation, BFCache retain/resume, discard cleanup, and a static sibling without ResourceIR.
|
|
18
|
+
- A native WebSocket fixture verifies dependency replacement, listener removal, stale callback rejection, distinct connection ownership, and one close per connection.
|
|
19
|
+
- Mutable refs shared by multiple effects retain a focused source diagnostic.
|
|
20
|
+
|
|
21
|
+
### Output and performance
|
|
22
|
+
|
|
23
|
+
- Private refs stay inside existing route-specific effect handler ESM and are absent from build-time captures and RouteIR scope.
|
|
24
|
+
- Static sibling routes remain zero JavaScript.
|
|
25
|
+
- No ResourceIR record, component runtime, resource API, scheduler, or new performance claim is added.
|
|
26
|
+
|
|
27
|
+
### Validation
|
|
28
|
+
|
|
29
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 219 tests and 167 generated pages.
|
|
30
|
+
|
|
31
|
+
### Upgrade
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install @kudzujs/core@^0.8.52
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 0.8.51 - Owned effect package imports
|
|
38
|
+
|
|
39
|
+
Kudzu 0.8.51 lets inline owned effect setup and cleanup callbacks reference browser-only package imports directly.
|
|
40
|
+
|
|
41
|
+
### Changed in 0.8.51
|
|
42
|
+
|
|
43
|
+
- Direct package references are accepted inside inline `useEffect` setup callbacks and their directly returned inline cleanup functions.
|
|
44
|
+
- Existing package import records feed the route-specific effect handler ESM and package bundler.
|
|
45
|
+
- Build scratch replaces package-owning effect callbacks with inert functions because build-time rendering records ownership without executing effect code.
|
|
46
|
+
- Render-time, helper-indirect, mixed, side-effect, and dynamic package imports remain source-diagnosed.
|
|
47
|
+
|
|
48
|
+
### Output and performance
|
|
49
|
+
|
|
50
|
+
- Package code is retained only by routes whose effect graph references it.
|
|
51
|
+
- Build-time component modules and static sibling routes omit the package and its JavaScript.
|
|
52
|
+
- No package runtime, ResourceIR, public adapter API, or new performance claim is added.
|
|
53
|
+
|
|
54
|
+
### Validation
|
|
55
|
+
|
|
56
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 217 tests and 166 generated pages.
|
|
57
|
+
- A direct setup/cleanup fixture bundles TypeScript package data into one owned effect handler while its static sibling ships zero JavaScript.
|
|
58
|
+
- Focused negative fixtures retain diagnostics for render-time and helper-indirect package references.
|
|
59
|
+
|
|
60
|
+
### Upgrade
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install @kudzujs/core@^0.8.51
|
|
64
|
+
```
|
|
65
|
+
|
|
3
66
|
## 0.8.50 - Package-neutral shared state IR
|
|
4
67
|
|
|
5
68
|
Kudzu 0.8.50 contains reduced Zustand migration syntax behind package-neutral shared-state and action records before generic signal and handler consumers.
|
|
@@ -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.52` 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 prop state initialization, 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, measured route-entry output optimization, and the plain TypeScript source-scale fast path are complete. ResourceIR remains research for cross-owner transports and subscriptions. 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.52` 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
|
|
|
@@ -58,17 +58,21 @@ The repository-owned fixture at `test/fixtures/goal-c-e2b-terminal` preserves th
|
|
|
58
58
|
- `pageshow.persisted` resuming the retained handle;
|
|
59
59
|
- one unrelated static sibling route.
|
|
60
60
|
|
|
61
|
-
The
|
|
61
|
+
The fixture now compiles through a narrow effect-private ref specialization. Because both refs are initialized with `null` or `0` and every direct `.current` reference belongs to one inline effect graph, the compiler removes their component declarations and recreates them inside each setup invocation. Promise continuations, page lifecycle listeners, and cleanup share the same ordinary JavaScript closure. Persisted `pagehide` retains that invocation; non-persisted disposal invalidates late setters and runs the authored close path. No ref capture, ResourceIR record, or resource runtime is emitted, and the static sibling remains JavaScript-free.
|
|
62
62
|
|
|
63
|
-
|
|
64
|
-
src/pages/index.tsx:7:25 Mutable value useRef() is unsupported except for an effect-owned useRef(0) animation-frame handle; otherwise keep resource-private mutable values inside the owning effect
|
|
65
|
-
```
|
|
63
|
+
### Route-Owned WebSocket Reduced Fixture
|
|
66
64
|
|
|
67
|
-
The
|
|
65
|
+
The repository-owned fixture at `test/fixtures/goal-c-route-websocket` adds an independent native WebSocket case with a narrower route lifetime:
|
|
68
66
|
|
|
69
|
-
|
|
67
|
+
- one `null`-initialized socket handle ref shared by setup and cleanup;
|
|
68
|
+
- one numeric generation ref invalidating callbacks from replaced connections;
|
|
69
|
+
- a primitive room dependency that replaces the connection;
|
|
70
|
+
- exact listener removal and socket closure on replacement or route unmount;
|
|
71
|
+
- one unrelated static sibling route.
|
|
72
|
+
|
|
73
|
+
This fixture uses the same effect-private lowering. On dependency replacement, the previous invocation is invalidated and cleaned before a new closure and socket are created. The regression test executes the bundled effect handler with a fake WebSocket and verifies listener removal, stale callback rejection, one close per connection, and distinct replacement sockets. Its static sibling remains JavaScript-free.
|
|
70
74
|
|
|
71
|
-
|
|
75
|
+
Both fixtures therefore resolve without ResourceIR: their mutable handles are private implementation details of one effect invocation, while existing effect ownership already supplies replacement, cleanup, navigation, and BFCache disposal. The remaining Goal C pressure is cross-owner sharing: document/layout transports with independently mounted subscribers, reconnect/replay state, or handles intentionally shared with event handlers cannot use this specialization.
|
|
72
76
|
|
|
73
77
|
## Decision Boundary
|
|
74
78
|
|
|
@@ -81,4 +85,4 @@ Research may produce notes, fixtures, measurements, or a narrow compiler-special
|
|
|
81
85
|
- [ ] Classify build-known, browser-only, layout-owned, route-owned, and DOM-owned data.
|
|
82
86
|
- [ ] Test existing compiler capabilities before proposing a new one.
|
|
83
87
|
- [ ] Record unresolved semantics instead of filling them with a generic runtime.
|
|
84
|
-
- [
|
|
88
|
+
- [x] Stop at research unless a separate implementation decision is approved; the effect-private specialization was explicitly approved without approving ResourceIR.
|
|
@@ -381,8 +381,8 @@ After the relevant P0 foundations, investigate capabilities in this order:
|
|
|
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
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`:** 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 primitive parent signal, and one callback may branch through multiple component `on*` props and intrinsic handlers. A fourth callback boundary, callback aliases/non-handler uses, fully hidden Context state, and object/array prop initializers 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
|
-
4. Browser-only package imports in owned effect/resource modules.
|
|
385
|
-
5. ResourceIR from at least two independent WebSocket/SSE/SDK fixtures with the same semantics.
|
|
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
|
+
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.
|
|
386
386
|
6. Route/layout capability and CSS chunk closure.
|
|
387
387
|
7. Incremental source and affected-route builds.
|
|
388
388
|
8. Range ownership and virtualization only after a real data-heavy fixture establishes direct DOM limits.
|
|
@@ -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 prop initializer slice, 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, and the `0.8.42` measured route-output optimization are complete. Continue
|
|
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 prop initializer slice, 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, and the `0.8.42` measured route-output optimization are complete. Continue ResourceIR research only from cross-owner transport/subscription fixtures that cannot use effect-private closure ownership; do not skip directly to a public adapter/store API, router, virtualization, or unsupported ecosystem package feature.
|
|
@@ -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.52` 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
|
|
|
@@ -41,6 +41,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
41
41
|
| `0.8.48` | Allow one setter-callback prop in multiple direct intrinsic handlers. | Each handler lowers once to the same parent signal; repeated same-handler calls and escaped uses fail without a runtime change. |
|
|
42
42
|
| `0.8.49` | Fan one setter-callback prop out through multiple direct child component event props. | Every branch lowers to the original parent signal; aliases, ordinary props, spreads, and the fourth boundary fail without a runtime change. |
|
|
43
43
|
| `0.8.50` | Isolate reduced Zustand migration behind package-neutral shared-state and action IR. | Validated shared identities/actions preserve RouteIR, layout lifetime, same-turn updates, diagnostics, browser behavior, and zero store runtime. |
|
|
44
|
+
| `0.8.51` | Bundle direct browser-only package references from inline owned effect setup/cleanup callbacks. | Package code stays in referenced route effect ESM; build scratch and static siblings omit it while indirect/render-time uses fail. |
|
|
45
|
+
| `0.8.52` | Lower effect-exclusive mutable refs into setup-invocation closures. | E2B and WebSocket fixtures preserve generation, replacement, BFCache, and cleanup without captures, ResourceIR, or static-sibling JavaScript. |
|
|
44
46
|
|
|
45
47
|
## Sequence Rules
|
|
46
48
|
|
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 effect may
|
|
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.
|
|
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
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from "typescript"
|
|
2
2
|
import { effectReturns, importDeclarationNames, isShadowedIdentifier, nearestFunction, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
3
3
|
|
|
4
|
-
export function
|
|
4
|
+
export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
|
|
5
5
|
const frameCall = (node, name) => ts.isCallExpression(node) && (
|
|
6
6
|
ts.isIdentifier(node.expression) && node.expression.text === name ||
|
|
7
7
|
ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === "window" && node.expression.name.text === name
|
|
@@ -28,32 +28,51 @@ export function normalizeEffectAnimationFrameRefs(sourceFile, factory, context)
|
|
|
28
28
|
}
|
|
29
29
|
const hasUseRefImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useRef"))
|
|
30
30
|
const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && !statement.importClause?.isTypeOnly && ts.isStringLiteral(statement.moduleSpecifier) && ["react", "@kudzujs/core"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
|
|
31
|
-
const
|
|
31
|
+
const privateRefs = new Map()
|
|
32
|
+
const effectRefs = new Map()
|
|
33
|
+
const registerPrivateRef = (node, callback) => {
|
|
34
|
+
const initializer = ts.isNumericLiteral(node.initializer.arguments[0]) ? factory.createNumericLiteral(0) : factory.createNull()
|
|
35
|
+
privateRefs.set(node, callback)
|
|
36
|
+
const refs = effectRefs.get(callback) ?? []
|
|
37
|
+
refs.push({ name: node.name.text, initializer })
|
|
38
|
+
effectRefs.set(callback, refs)
|
|
39
|
+
}
|
|
32
40
|
const inspect = node => {
|
|
33
|
-
|
|
41
|
+
const refInitializer = ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer) ? node.initializer.arguments[0] : undefined
|
|
42
|
+
if (!hasUseRefImport || !ts.isVariableDeclaration(node) || !ts.isIdentifier(node.name) || !node.initializer || !ts.isCallExpression(node.initializer) || !ts.isIdentifier(node.initializer.expression) || node.initializer.expression.text !== "useRef" || isShadowedIdentifier(node.initializer.expression, sourceFile) || node.initializer.arguments.length !== 1 || !(refInitializer?.kind === ts.SyntaxKind.NullKeyword || ts.isNumericLiteral(refInitializer) && Number(refInitializer.text) === 0)) {
|
|
34
43
|
ts.forEachChild(node, inspect)
|
|
35
44
|
return
|
|
36
45
|
}
|
|
37
46
|
const owner = nearestFunction(node)
|
|
38
47
|
if (!owner?.body || !ts.isBlock(owner.body)) return
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
ts.forEachChild(current, collect)
|
|
43
|
-
}
|
|
44
|
-
collect(owner.body)
|
|
48
|
+
const references = referenceIdentifiers(owner.body, node.name.text)
|
|
49
|
+
const invalidReference = references.find(reference => !ts.isPropertyAccessExpression(reference.parent) || reference.parent.expression !== reference || reference.parent.name.text !== "current")
|
|
50
|
+
const accesses = references.filter(reference => ts.isPropertyAccessExpression(reference.parent) && reference.parent.expression === reference && reference.parent.name.text === "current").map(reference => reference.parent)
|
|
45
51
|
const frameAssignments = accesses.filter(access => ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && frameCall(unwrapExpression(access.parent.right), "requestAnimationFrame") && unshadowedFrameCall(unwrapExpression(access.parent.right), owner))
|
|
46
|
-
if (!frameAssignments.length) return
|
|
47
|
-
const invalidReference = referenceIdentifiers(owner.body, node.name.text).find(reference => !ts.isPropertyAccessExpression(reference.parent) || reference.parent.expression !== reference || reference.parent.name.text !== "current")
|
|
48
|
-
if (invalidReference) throw sourceNodeError(invalidReference, sourceFile, "Animation frame refs may only use direct .current reads and assignments")
|
|
49
52
|
const statement = node.parent?.parent
|
|
50
53
|
const topLevelOwner = owner.parent === sourceFile || ts.isVariableDeclaration(owner.parent) && owner.parent.parent?.parent?.parent === sourceFile
|
|
51
|
-
|
|
54
|
+
const topLevelConst = topLevelOwner && ts.isVariableStatement(statement) && (statement.declarationList.flags & ts.NodeFlags.Const) && statement.declarationList.declarations.length === 1 && statement.parent === owner.body
|
|
55
|
+
if (frameAssignments.length && invalidReference) throw sourceNodeError(invalidReference, sourceFile, "Animation frame refs may only use direct .current reads and assignments")
|
|
56
|
+
if (frameAssignments.length && !topLevelConst) throw sourceNodeError(node, sourceFile, "Animation frame refs must be one top-level component const")
|
|
57
|
+
if (!topLevelConst || invalidReference || !accesses.length) return
|
|
58
|
+
const effectCalls = owner.body.statements.flatMap(statement => hasUseEffectImport && ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "useEffect" && !isShadowedIdentifier(statement.expression.expression, sourceFile) ? [statement.expression] : [])
|
|
59
|
+
const effects = effectCalls.filter(effect => {
|
|
60
|
+
const callback = effect.arguments[0]
|
|
61
|
+
return callback && accesses.every(access => inside(access, callback))
|
|
62
|
+
})
|
|
63
|
+
if (!frameAssignments.length) {
|
|
64
|
+
const callback = effects.length === 1 ? effects[0].arguments[0] : undefined
|
|
65
|
+
if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && ts.isBlock(callback.body)) {
|
|
66
|
+
const cleanups = effectReturns(callback).cleanups
|
|
67
|
+
const cleanupWrites = cleanups.length === 1 && accesses.some(access => inside(access, cleanups[0]) && ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && access.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
|
|
68
|
+
if (!cleanupWrites) throw sourceNodeError(node, sourceFile, "Effect-private refs require one cleanup that directly resets or invalidates ref.current")
|
|
69
|
+
registerPrivateRef(node, callback)
|
|
70
|
+
}
|
|
71
|
+
return
|
|
72
|
+
}
|
|
52
73
|
if (frameAssignments.length !== 1) throw sourceNodeError(node, sourceFile, "Animation frame refs require one direct ref.current = requestAnimationFrame(callback) assignment")
|
|
53
74
|
const frame = unwrapExpression(frameAssignments[0].parent.right)
|
|
54
75
|
const frameCallback = frame.arguments.length === 1 && ts.isIdentifier(unwrapExpression(frame.arguments[0])) ? unwrapExpression(frame.arguments[0]) : undefined
|
|
55
|
-
const effectCalls = owner.body.statements.flatMap(statement => hasUseEffectImport && ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "useEffect" && !isShadowedIdentifier(statement.expression.expression, sourceFile) ? [statement.expression] : [])
|
|
56
|
-
const effects = effectCalls.filter(effect => effect.arguments[0] && inside(frameAssignments[0], effect.arguments[0]))
|
|
57
76
|
if (effects.length !== 1) throw sourceNodeError(node, sourceFile, "Animation frame refs must belong to one inline component effect")
|
|
58
77
|
const effect = effects[0]
|
|
59
78
|
const callback = effect.arguments[0]
|
|
@@ -89,14 +108,24 @@ export function normalizeEffectAnimationFrameRefs(sourceFile, factory, context)
|
|
|
89
108
|
const cleanup = cancellation && returns.cleanups.find(candidate => nearestFunction(cancellation) === candidate)
|
|
90
109
|
const cancellationStatement = cancellation?.parent
|
|
91
110
|
if (cancellations.length !== 1 || !cleanup || !ts.isBlock(cleanup.body) || !ts.isExpressionStatement(cancellationStatement) || !directOrGuarded(cancellationStatement, cleanup.body, node.name.text, false)) throw sourceNodeError(node, sourceFile, "Animation frame refs require direct cancellation in effect cleanup")
|
|
92
|
-
|
|
111
|
+
registerPrivateRef(node, callback)
|
|
93
112
|
}
|
|
94
113
|
inspect(sourceFile)
|
|
95
|
-
if (!
|
|
114
|
+
if (!privateRefs.size) return sourceFile
|
|
96
115
|
const visitor = node => {
|
|
97
|
-
if (
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
if (ts.isVariableStatement(node) && node.declarationList.declarations.length === 1 && privateRefs.has(node.declarationList.declarations[0])) return undefined
|
|
117
|
+
const refs = effectRefs.get(node)
|
|
118
|
+
if (refs && (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) && ts.isBlock(node.body)) {
|
|
119
|
+
const body = ts.visitEachChild(node.body, visitor, context)
|
|
120
|
+
const declarations = refs.map(ref => factory.createVariableStatement(undefined, factory.createVariableDeclarationList([
|
|
121
|
+
factory.createVariableDeclaration(ref.name, undefined, undefined, factory.createObjectLiteralExpression([
|
|
122
|
+
factory.createPropertyAssignment("current", ref.initializer)
|
|
123
|
+
]))
|
|
124
|
+
], ts.NodeFlags.Const)))
|
|
125
|
+
const nextBody = factory.updateBlock(body, [...declarations, ...body.statements])
|
|
126
|
+
if (ts.isArrowFunction(node)) return factory.updateArrowFunction(node, node.modifiers, node.typeParameters, node.parameters, node.type, node.equalsGreaterThanToken, nextBody)
|
|
127
|
+
return factory.updateFunctionExpression(node, node.modifiers, node.asteriskToken, node.name, node.typeParameters, node.parameters, node.type, nextBody)
|
|
128
|
+
}
|
|
100
129
|
return ts.visitEachChild(node, visitor, context)
|
|
101
130
|
}
|
|
102
131
|
return ts.visitNode(sourceFile, visitor)
|
|
@@ -3,7 +3,7 @@ import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "node
|
|
|
3
3
|
import ts from "typescript"
|
|
4
4
|
import { createBindingIndex } from "./analysis/binding-index.mjs"
|
|
5
5
|
import { createComponentAnalysisSession } from "./analysis/component-analysis.mjs"
|
|
6
|
-
import {
|
|
6
|
+
import { normalizeEffectPrivateRefs } from "./effect-private-ref-pass.mjs"
|
|
7
7
|
import { bindingNames, containsJsx, effectReturns, functionVarDeclaresName, importDeclarationNames, isFunctionLike, isLocalConst, isReferenceIdentifier, isShadowedByParameter, isShadowedIdentifier, isUnshadowedGlobal, nearestFunction, referenceIdentifiers, referencesIdentifier, sourceLocation, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
|
|
8
8
|
import { normalizeMediaQueryExternalStores, normalizeNavigatorCapabilityConditions } from "./browser-signal-passes.mjs"
|
|
9
9
|
import { analyzeCollectionPipeline, collectionExpression, collectionParameters, isArrayFromCall, mutatingCollectionMethods as mutatingListMethods, pureCollectionMathMethods as pureMathMethods, pureCollectionMethods as pureListMethods } from "./collection-analysis.mjs"
|
|
@@ -249,7 +249,7 @@ function normalizeCompilerSource(sourceFile, { base, context, file, importedColl
|
|
|
249
249
|
source => normalizeMediaQueryExternalStores(source, factory, context),
|
|
250
250
|
source => normalizeReactMigrationSyntax(source, factory, context, importedCollections ?? importedSerializableCollectionNames(source, file, sourceFiles, sourceIndex)),
|
|
251
251
|
source => normalizeNavigatorCapabilityConditions(source, factory, context),
|
|
252
|
-
source =>
|
|
252
|
+
source => normalizeEffectPrivateRefs(source, factory, context),
|
|
253
253
|
source => {
|
|
254
254
|
const result = normalizeCustomHookTimerRefs(source, factory, context)
|
|
255
255
|
customHookTimerStates = result.timerStates
|
|
@@ -322,8 +322,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
322
322
|
const packageBindings = packageImportBindings(sourceFile)
|
|
323
323
|
for (const [name] of packageBindings) {
|
|
324
324
|
const references = referenceIdentifiers(sourceFile, name)
|
|
325
|
-
const invalid = references.find(reference => !insideJsxEventHandler(reference, sourceFile))
|
|
326
|
-
if (invalid) throw sourceNodeError(invalid, sourceFile, `Package import ${JSON.stringify(name)} may only be referenced directly inside JSX event handlers`)
|
|
325
|
+
const invalid = references.find(reference => !insideJsxEventHandler(reference, sourceFile) && !insideOwnedEffectCallback(reference, sourceFile))
|
|
326
|
+
if (invalid) throw sourceNodeError(invalid, sourceFile, `Package import ${JSON.stringify(name)} may only be referenced directly inside JSX event handlers or owned effect setup/cleanup callbacks`)
|
|
327
327
|
}
|
|
328
328
|
const hasUseEffectImport = sourceFile.statements.some(statement => ts.isImportDeclaration(statement) && ["@kudzujs/core", "react"].includes(statement.moduleSpecifier.text) && statement.importClause?.namedBindings && ts.isNamedImports(statement.importClause.namedBindings) && statement.importClause.namedBindings.elements.some(entry => !entry.propertyName && entry.name.text === "useEffect"))
|
|
329
329
|
const importedSourceCache = new Map()
|
|
@@ -723,7 +723,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
723
723
|
const owner = nearestFunction(node)
|
|
724
724
|
if (owner && node.initializer.expression.text === "useRef") {
|
|
725
725
|
const nullInitializer = node.initializer.arguments.length === 1 && node.initializer.arguments[0].kind === ts.SyntaxKind.NullKeyword
|
|
726
|
-
if (!nullInitializer && owner.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) throw sourceNodeError(node.initializer, sourceFile, "Mutable
|
|
726
|
+
if (!nullInitializer && owner.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.DefaultKeyword)) throw sourceNodeError(node.initializer, sourceFile, "Mutable useRef() values must be referenced exclusively inside one owned effect; DOM refs require useRef(null)")
|
|
727
727
|
if (nullInitializer) {
|
|
728
728
|
ensureOwner(owner)
|
|
729
729
|
componentAnalysis.registerRef(owner, { name: node.name.text, site: analysisSite(node, "hook"), source: analysisSource(node) })
|
|
@@ -1770,7 +1770,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1770
1770
|
const descriptor = descriptors.compileEffectCallback(compiledCallback, {
|
|
1771
1771
|
setters,
|
|
1772
1772
|
reducers: reducersForNode(node, reducersByFunction),
|
|
1773
|
-
importBindings: specializedEffect?.imports ?? importBindings,
|
|
1773
|
+
importBindings: specializedEffect?.imports ?? new Map([...importBindings, ...packageBindings]),
|
|
1774
1774
|
listItem: dependencyItem,
|
|
1775
1775
|
keyedBlock: activeKeyedBlock?.block.slot,
|
|
1776
1776
|
stateOwners: new Map([...stateOwnersForNode(callbackArgument), ...stateOwnersForNode(node), ...(specializedEffectStateOwners.get(node)?.references ?? []), ...(activeStateOwners ?? [])]),
|
|
@@ -1804,8 +1804,11 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1804
1804
|
...(analysisSource(effectSource) ? { source: analysisSource(effectSource) } : {})
|
|
1805
1805
|
})
|
|
1806
1806
|
const dependencyExpressions = effect.dependencies.map((dependency, index) => dependency.kind === "derived" ? moduleIR.derived[dependency.derived].expression : ["state", dependencyEntries[index]?.name ?? ordinaryDependencies[index].text])
|
|
1807
|
+
const buildCallback = [...packageBindings].some(([name]) => referenceIdentifiers(callback, name).length)
|
|
1808
|
+
? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createBlock([], false))
|
|
1809
|
+
: callback
|
|
1807
1810
|
return factory.updateCallExpression(node, node.expression, node.typeArguments, [
|
|
1808
|
-
|
|
1811
|
+
buildCallback,
|
|
1809
1812
|
factory.createArrayLiteralExpression(subscriptionNames.map(name => factory.createIdentifier(name))),
|
|
1810
1813
|
factory.createStringLiteral(handlerUrl),
|
|
1811
1814
|
factory.createStringLiteral(effect.setup.exportName),
|
|
@@ -2153,6 +2156,24 @@ function insideJsxEventHandler(node, root) {
|
|
|
2153
2156
|
return false
|
|
2154
2157
|
}
|
|
2155
2158
|
|
|
2159
|
+
function insideOwnedEffectCallback(node, root) {
|
|
2160
|
+
let callback
|
|
2161
|
+
for (let current = node.parent; current && current !== root.parent; current = current.parent) {
|
|
2162
|
+
if (!isFunctionLike(current)) continue
|
|
2163
|
+
callback = current
|
|
2164
|
+
break
|
|
2165
|
+
}
|
|
2166
|
+
if (!callback) return false
|
|
2167
|
+
if (ts.isCallExpression(callback.parent) && callback.parent.arguments[0] === callback && ts.isIdentifier(callback.parent.expression) && callback.parent.expression.text === "useEffect") return true
|
|
2168
|
+
const returned = callback.parent
|
|
2169
|
+
if (!ts.isReturnStatement(returned)) return false
|
|
2170
|
+
for (let current = returned.parent; current && current !== root.parent; current = current.parent) {
|
|
2171
|
+
if (!isFunctionLike(current)) continue
|
|
2172
|
+
return ts.isCallExpression(current.parent) && current.parent.arguments[0] === current && ts.isIdentifier(current.parent.expression) && current.parent.expression.text === "useEffect"
|
|
2173
|
+
}
|
|
2174
|
+
return false
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2156
2177
|
function validateKeyedList(parts, sourceFile, setters, rowStates, componentSpecializations, expandedRowSpecializations, nestedRowSpecializations, factory, prepareListCallback, bindingIndex) {
|
|
2157
2178
|
const fail = (node, message) => {
|
|
2158
2179
|
throw sourceNodeError(node, sourceFile, message)
|