@kudzujs/core 0.8.48 → 0.8.50
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 -2
- package/PERFORMANCE.md +9 -1
- package/README.md +1 -1
- package/RELEASES.md +58 -0
- package/docs/next-architecture/README.md +1 -1
- package/docs/next-architecture/compiler-current-architecture.md +4 -4
- package/docs/next-architecture/large-application-ai-native-roadmap.md +5 -5
- package/docs/next-architecture/versioning.md +3 -1
- package/framework/README.md +3 -3
- package/framework/compiler/descriptor-session.mjs +1 -0
- package/framework/compiler/handler-lowering.mjs +10 -9
- package/framework/compiler/ir/module-ir.mjs +24 -1
- package/framework/compiler/source-compiler.mjs +27 -13
- package/framework/compiler/zustand-pass.mjs +28 -6
- package/framework/core.mjs +8 -8
- 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.50` 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
|
|
|
@@ -61,7 +61,7 @@ The following are available building blocks, not future vertical roadmaps:
|
|
|
61
61
|
- Async build-time components, `getStaticPaths()`, runtime path parameters, metadata, base paths, public assets, CSS, CSS Modules, and post-build hooks.
|
|
62
62
|
- React import normalization for supported named/aliased hooks, direct `React.*` members, fragments, same-file `memo`, inline `useCallback`, direct intrinsic `forwardRef`, top-level `useId`, and analyzable `useMemo` expressions and collections.
|
|
63
63
|
- Function components, props, children, context, direct bindings, conditions, controlled form properties, refs, and synchronous or async handlers.
|
|
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 proven by migration fixtures.
|
|
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
66
|
- 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
67
|
- Complete-document native fallback and optional shared-layout navigation groups with parameter initialization, finite prefetch caching, focus, history, cleanup, and independent capability output.
|
|
@@ -146,6 +146,7 @@ This queue orders the next investigations by general migration value. Start only
|
|
|
146
146
|
- 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.
|
|
147
147
|
- 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; direct object/array props, aliases, and composed initializer expressions remain diagnosed.
|
|
148
148
|
- 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.
|
|
149
|
+
- 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.
|
|
149
150
|
|
|
150
151
|
### Completed In 0.8.10
|
|
151
152
|
|
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.50 Release Snapshot
|
|
10
|
+
|
|
11
|
+
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
|
+
|
|
13
|
+
## Maintained 0.8.49 Release Snapshot
|
|
14
|
+
|
|
15
|
+
Kudzu 0.8.49 broadens compiler-only setter-callback validation to direct child fan-out while retaining the existing command descriptors and browser runtimes. No new performance claim is made.
|
|
16
|
+
|
|
17
|
+
## Maintained 0.8.48 Release Snapshot
|
|
10
18
|
|
|
11
19
|
Kudzu 0.8.48 broadens compiler-only setter-callback validation to multiple direct intrinsic handlers while retaining the existing command descriptors 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.50 - Package-neutral shared state IR.** Reduced Zustand migration now lowers through validated `SharedStateIR` and `SharedActionIR` records before generic signal and handler consumers. Existing layout ownership, action behavior, and source diagnostics remain unchanged without adding a public store API or runtime. Read the [release notes](./RELEASES.md#0850---package-neutral-shared-state-ir), open the [release page](https://github.com/kudzujs/kudzu/releases/tag/v0.8.50), or follow the [architecture packet](./docs/next-architecture/README.md).
|
|
18
18
|
|
|
19
19
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
20
20
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.8.50 - Package-neutral shared state IR
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.50 contains reduced Zustand migration syntax behind package-neutral shared-state and action records before generic signal and handler consumers.
|
|
6
|
+
|
|
7
|
+
### Changed in 0.8.50
|
|
8
|
+
|
|
9
|
+
- ModuleIR v2 records JSON-safe `SharedStateIR` identities, fields, and initial values.
|
|
10
|
+
- `SharedActionIR` records reference their owning shared state through validated slots.
|
|
11
|
+
- HandlerIR records the shared actions used by route-specific native handlers.
|
|
12
|
+
- The Zustand adapter preserves package-specific source diagnostics while generic compiler and build-time rendering paths consume shared-state metadata.
|
|
13
|
+
|
|
14
|
+
### Output and performance
|
|
15
|
+
|
|
16
|
+
- Existing RouteIR layout state, functional action updates, same-turn behavior, and enhanced-navigation persistence remain unchanged.
|
|
17
|
+
- React, Zustand, subscriptions, and a generic store runtime remain absent from deploy output.
|
|
18
|
+
- No public store or compatibility-adapter API is added, and this release makes no new performance claim.
|
|
19
|
+
|
|
20
|
+
### Validation
|
|
21
|
+
|
|
22
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 215 tests and 165 generated pages.
|
|
23
|
+
- Focused ModuleIR tests validate JSON round-tripping and reject dangling shared-action state references.
|
|
24
|
+
- The maintained Zustand browser journey retains repeated same-turn updates, batched effects, layout identity, navigation persistence, and removal behavior.
|
|
25
|
+
|
|
26
|
+
### Upgrade
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @kudzujs/core@^0.8.50
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 0.8.49 - Setter callback fan-out
|
|
33
|
+
|
|
34
|
+
Kudzu 0.8.49 lets one component forward the same destructured setter callback directly through multiple child component `on*` props.
|
|
35
|
+
|
|
36
|
+
### Changed in 0.8.49
|
|
37
|
+
|
|
38
|
+
- A specialized component may pass one callback prop directly to multiple child controls.
|
|
39
|
+
- Each child branch specializes independently to the original parent state signal.
|
|
40
|
+
- Fan-out composes with multiple direct intrinsic handlers within the existing three-boundary limit.
|
|
41
|
+
- Aliases, ordinary prop names, spreads, intermediate adapters, and a fourth callback boundary remain source-diagnosed.
|
|
42
|
+
|
|
43
|
+
### Output and performance
|
|
44
|
+
|
|
45
|
+
- Every leaf continues to emit its existing concrete state-operation descriptor.
|
|
46
|
+
- No callback registry, component runtime, browser runtime module, or public Kudzu API is added.
|
|
47
|
+
- This release makes no new performance claim.
|
|
48
|
+
|
|
49
|
+
### Validation
|
|
50
|
+
|
|
51
|
+
- `npm run check`, `npm test`, and `npm run test:package` pass with all 214 tests and 164 generated pages.
|
|
52
|
+
- A relative component fans one callback out to two child controls whose click descriptors target the same parent signal.
|
|
53
|
+
- Existing repeated-call and fourth-boundary negative fixtures retain their source diagnostics.
|
|
54
|
+
|
|
55
|
+
### Upgrade
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npm install @kudzujs/core@^0.8.49
|
|
59
|
+
```
|
|
60
|
+
|
|
3
61
|
## 0.8.48 - Multi-handler setter callbacks
|
|
4
62
|
|
|
5
63
|
Kudzu 0.8.48 lets one compiler-specialized setter-callback prop be called once from each of multiple direct intrinsic event handlers.
|
|
@@ -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.50` 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, measured route-entry output optimization, and the plain TypeScript source-scale fast path are complete. Browser-only package imports in owned effect/resource modules remain migration-backed work. 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.50` 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
|
|
|
@@ -16,14 +16,14 @@ This maps the current `0.8.48` architecture, built on the completed `0.8.23` Goa
|
|
|
16
16
|
| Shared AST/scope helpers | [`framework/compiler/ast-helpers.mjs`](../../framework/compiler/ast-helpers.mjs) | Binding, scope, reference, effect-return, and source-location analysis. |
|
|
17
17
|
| Source-local binding index | [`framework/compiler/analysis/binding-index.mjs`](../../framework/compiler/analysis/binding-index.mjs) | After normalization, assigns deterministic lexical slots and classifies local, parameter, import, capture, global, and unresolved references. Native handler, effect, binding, list evaluator, optimized-command, and effect-resource consumers use complete index-owned AST; synthesized expressions retain the existing fallback. |
|
|
18
18
|
| Pure collection language | [`framework/compiler/collection-analysis.mjs`](../../framework/compiler/collection-analysis.mjs) | Analyzes collection roots/selectors and serializes the allowed pure expression language used by lists and derived dependencies. |
|
|
19
|
-
| Main semantic analysis | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `createKudzuTransformer()` | Produces transformed source plus explicit component, handler, binding, derived, keyed, and effect ownership results. |
|
|
19
|
+
| Main semantic analysis | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `createKudzuTransformer()` | Produces transformed source plus explicit component, shared-state/action, handler, binding, derived, keyed, and effect ownership results. Zustand package syntax is resolved by its adapter before generic shared-state consumers. |
|
|
20
20
|
| Component ownership analysis | [`framework/compiler/analysis/component-analysis.mjs`](../../framework/compiler/analysis/component-analysis.mjs) | Produces ComponentAnalysis v2 with ordered JSON-safe owner and specialization slots for state, setters, props, refs, IDs, direct SignalIR links, structural OwnerRefs, source-local SiteIds, and source provenance. Three direct callback/ref component boundaries specialize into the same parent signal and intrinsic ownership; Context action-private setters may remain compiler-only and receive collision-free consumer-local aliases; AST identity remains private to its source-local session. |
|
|
21
|
-
| Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes ModuleIR v2 with deterministic SymbolRef, SignalIR, HandlerIR, BindingIR, DerivedIR, EffectIR, KeyedBlockIR, and ImportIR slots. One fail-closed boundary validates every source-local and component ownership edge before build-module generation. |
|
|
21
|
+
| Per-source descriptor registration | [`framework/compiler/descriptor-session.mjs`](../../framework/compiler/descriptor-session.mjs), `createSemanticArtifact()`, `createDescriptorSession()` | Keeps AST descriptors private during analysis, then finalizes ModuleIR v2 with deterministic SymbolRef, SharedStateIR, SharedActionIR, SignalIR, HandlerIR, BindingIR, DerivedIR, EffectIR, KeyedBlockIR, and ImportIR slots. One fail-closed boundary validates every source-local and component ownership edge before build-module generation. |
|
|
22
22
|
| Route artifact graph | [`framework/compiler/route-build-record.mjs`](../../framework/compiler/route-build-record.mjs), `createRouteBuildRecord()`, `planRouteArtifacts()` | Validates each rendered route's RouteIR, capabilities, entry paths, styles, and exact handler/effect references. Handler modules, Workers, package-client modules, and bundle entry/chunk closure derive from structural edges without serialized output searches or formatted composite keys. |
|
|
23
23
|
| Route contract validation | [`framework/compiler/route-ir.mjs`](../../framework/compiler/route-ir.mjs), `assertRouteIR()` | Fails before artifact selection for invalid state/parameter identity, commands, native/effect captures and dependencies, reactive descriptors, conditions, keyed-list identity/ownership, marker fields, or JSON safety. Immutable in-memory contracts validate once by identity. |
|
|
24
24
|
| Command IR and codegen | [`framework/compiler/optimize/command-specialization.mjs`](../../framework/compiler/optimize/command-specialization.mjs), [`framework/compiler/ir/module-ir.mjs`](../../framework/compiler/ir/module-ir.mjs), [`framework/compiler/codegen/command-codegen.mjs`](../../framework/compiler/codegen/command-codegen.mjs) | Direct commands use the existing fast path; proven immutable state aliases and one-call local helpers specialize to the same JSON-safe command ModuleIR. Recursion, escape, mutation, and dynamic helper dispatch fail explicitly, while unrelated handlers retain native ESM. Codegen emits the existing `__kBehavior` AST and command ABI. |
|
|
25
25
|
| Source compilation | [`framework/compiler/source-compiler.mjs`](../../framework/compiler/source-compiler.mjs), `compileSource()` | Runs TypeScript with the Kudzu transformer, rejects surviving React/Router references, and returns a JSON-safe project-relative build module, component analysis, ModuleIR, optional handler module, and imported assets without filesystem writes. |
|
|
26
|
-
| Handler/evaluator lowering | [`framework/compiler/handler-lowering.mjs`](../../framework/compiler/handler-lowering.mjs) | Completes source-local callback
|
|
26
|
+
| Handler/evaluator lowering | [`framework/compiler/handler-lowering.mjs`](../../framework/compiler/handler-lowering.mjs) | Completes source-local callback, package-neutral shared-action, binding, and list AST rewriting and diagnostics before the JSON-safe IR boundary. |
|
|
27
27
|
| Handler module codegen | [`framework/compiler/handler-codegen.mjs`](../../framework/compiler/handler-codegen.mjs) | Renders finalized ordered imports and concatenates generated module-export source without TypeScript AST or semantic discovery. |
|
|
28
28
|
| Effect analysis | [`framework/compiler/effect-analysis.mjs`](../../framework/compiler/effect-analysis.mjs) | Classifies ordered signal, ordinary object-property DerivedIR, and keyed-item dependencies and validates cleanup-owned browser resources before EffectIR registration. Object sources keep one signal subscription while the existing tagged evaluator and `Object.is` compare the selected primitive value. |
|
|
29
29
|
| Worker graph | [`framework/compiler/worker-compiler.mjs`](../../framework/compiler/worker-compiler.mjs) | Returns functional Worker rewrite results and JSON-safe EffectIR edges, validates relative graphs, emits content-hashed ESM, and resolves placeholders only for rendered effects. |
|
|
@@ -183,7 +183,7 @@ P0 creates the semantic base required by every later large-application capabilit
|
|
|
183
183
|
- Add an internal compatibility adapter registry.
|
|
184
184
|
- Consolidate React compatibility ownership behind one adapter contract.
|
|
185
185
|
- Register React Router ownership without changing native navigation semantics.
|
|
186
|
-
- Move Zustand-specific logic out of generic core, handler, and state paths onto package-neutral shared-state/action IR.
|
|
186
|
+
- Move Zustand-specific logic out of generic core, handler, and state paths onto package-neutral shared-state/action IR. **Completed in `0.8.50`:** the Zustand pass preserves package diagnostics while generic compiler consumers use validated SharedStateIR/SharedActionIR identities and references; no public adapter or store runtime was added.
|
|
187
187
|
- Characterize Redux/RTK, TanStack Query, React Hook Form, Zod, Radix/Headless UI/MUI, CSS-in-JS, animation, chart, drag/drop, virtualization, auth, REST, and GraphQL against real application fixtures.
|
|
188
188
|
- Prefer native packages and Web APIs, then compilation, deterministic migration, adapters, partial support, or explicit unsupported status in that order.
|
|
189
189
|
- Publish no public adapter API until an external adapter requires independent versioning.
|
|
@@ -379,8 +379,8 @@ 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`:** 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
|
|
383
|
-
3. Package-neutral shared state/actions and migration of current Zustand internals.
|
|
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
|
+
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.
|
|
385
385
|
5. ResourceIR from at least two independent WebSocket/SSE/SDK fixtures with the same semantics.
|
|
386
386
|
6. Route/layout capability and CSS chunk closure.
|
|
@@ -405,7 +405,7 @@ Current factual baseline:
|
|
|
405
405
|
|---|---|
|
|
406
406
|
| React source | Compiled, partial |
|
|
407
407
|
| React Router | Compiled, partial |
|
|
408
|
-
| Zustand | Compiled, partial,
|
|
408
|
+
| Zustand | Compiled, partial, isolated through internal shared-state/action IR |
|
|
409
409
|
| Redux/RTK | Unsupported |
|
|
410
410
|
| TanStack Query | Migrated recipe, partial |
|
|
411
411
|
| React Hook Form | Migrated recipe, partial |
|
|
@@ -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, 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, and the `0.8.42` measured route-output optimization are complete. Continue with **browser-only package imports in owned effect/resource modules** only from a real migration blocker. 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.50` 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
|
|
|
@@ -39,6 +39,8 @@ Keep each patch behavior-preserving and independently reviewable. If a boundary
|
|
|
39
39
|
| `0.8.46` | Remove action-only setter fields from authored Context Provider APIs. | Compiler scratch restores required setters while Notes CRUD, RouteIR signals, concrete state operations, diagnostics, and zero Context runtime output pass. |
|
|
40
40
|
| `0.8.47` | Initialize specialized child state from one direct primitive parent state prop. | Build-known primitive seeds, independent state ownership, existing handler specialization, and negative object/expression diagnostics pass without a runtime change. |
|
|
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
|
+
| `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
|
+
| `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. |
|
|
42
44
|
|
|
43
45
|
## Sequence Rules
|
|
44
46
|
|
package/framework/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Direct `clsx` calls over literal strings, numbers, arrays, object conditions, an
|
|
|
28
28
|
|
|
29
29
|
Repeated ordinary same-file and relative-imported child components execute independently at build time, so each `useState` call receives a distinct concrete state ID while shared native handler modules retain per-element state maps and captures. A direct JSON-safe primitive parent state passed to a destructured child prop remains the same signal for child DOM bindings and effect dependencies; repeated calls own independent effect records, and conditional removal cleans up before remount recreates the effect. Reactive text and attributes may reference recursively chained top-level immutable locals derived through supported pure primitive expressions from direct state; the compiler substitutes those expressions into the existing binding evaluator and subscribes every source state. Fixed-locale `new Intl.NumberFormat("literal").format(Math.round(expression))` display chains reuse that binding ESM, while dynamic locales and options remain unsupported. Multiple direct primitive dependencies share the existing commit batching path: every value is compared with `Object.is`, and one or more same-turn changes cause one cleanup and rerun. A top-level immutable local derived through a supported pure primitive expression from direct state may also be an effect dependency: source state commits schedule evaluation, the derived result is compared with `Object.is`, and the expression is substituted into setup and cleanup handlers. Effect setup and directly returned cleanup callbacks may each resolve one top-level simple `const` function in the same component; those functions are substituted into the existing handler graph rather than retained in a browser registry. Reactive conditional descriptors own state created by their direct branch: initial visible output reuses the rendered template IDs, removal deletes those slots, and remount recreates them from serialized initial values. Static sibling routes and branches without local state add no ownership metadata, component function, hook dispatcher, or rerender loop.
|
|
30
30
|
|
|
31
|
-
Reduced Zustand migration stores lower to one ordinary layout-lifetime state slot. The compiler accepts one exported `create(set => ({ data, ...actions }))` store with one serializable data property, direct property selectors, and synchronous capture-free actions using one-argument merge-form `set`; selected actions reuse the
|
|
31
|
+
Reduced Zustand migration stores lower through package-neutral `SharedStateIR` and `SharedActionIR` records to one ordinary layout-lifetime state slot. The compiler accepts one exported `create(set => ({ data, ...actions }))` store with one serializable data property, direct property selectors, and synchronous capture-free actions using one-argument merge-form `set`; selected actions reuse the generic functional shared-action compiler, so same-turn calls observe current logical state and DOM writes still batch. The shared layout must initialize the store before route consumers, outside keyed rows. No Zustand import, public store API, subscription runtime, React hook, or generic external-store capability is emitted.
|
|
32
32
|
|
|
33
33
|
- `build.mjs`: TSX compilation, static, `getStaticPaths`, and runtime-fallback routes, base paths, CSS collection, post-build hooks, behavior extraction, and static HTML output.
|
|
34
34
|
- `compiler/ast-helpers.mjs`: shared TypeScript AST traversal, scope, binding, effect-return, and source-location helpers used by compiler passes.
|
|
@@ -36,7 +36,7 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
|
|
|
36
36
|
- `compiler/animation-frame-pass.mjs`: effect-owned animation-frame ref validation and plain mutable effect-scope lowering.
|
|
37
37
|
- `compiler/browser-signal-passes.mjs`: static media-query external-store and navigator capability-condition normalization.
|
|
38
38
|
- `compiler/collection-analysis.mjs`: pure collection expression IR, selector pipeline, alias, and imported-transform analysis.
|
|
39
|
-
- `compiler/descriptor-session.mjs`: private source-local descriptor registration and deterministic JSON-safe HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, EffectIR, import, and client-root finalization.
|
|
39
|
+
- `compiler/descriptor-session.mjs`: private source-local descriptor registration and deterministic JSON-safe SharedStateIR, SharedActionIR, HandlerIR, BindingIR, DerivedIR, KeyedBlockIR, EffectIR, import, and client-root finalization.
|
|
40
40
|
- `compiler/effect-analysis.mjs`: ordered effect dependency classification and cleanup-owned browser resource validation before EffectIR registration.
|
|
41
41
|
- `compiler/normalization-pipeline.mjs`: ordered source normalization with parent-pointer repair after each structurally changed source root.
|
|
42
42
|
- `compiler/param-codegen.mjs`: runtime pathname and search-parameter entry generation from rendered route descriptors.
|
|
@@ -44,7 +44,7 @@ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slo
|
|
|
44
44
|
- `compiler/custom-hook-timer-pass.mjs`: private custom-hook timeout-ref validation and compiler-owned timer-state lowering.
|
|
45
45
|
- `compiler/effect-codegen.mjs`: route-specific ordinary, owned, and navigable effect entry generation.
|
|
46
46
|
- `compiler/optimize/command-specialization.mjs`, `compiler/ir/module-ir.mjs`, and `compiler/codegen/command-codegen.mjs`: JSON-safe command, keyed, and effect ownership registration, sparse per-source ModuleIR, and source-analysis-free existing behavior-call generation.
|
|
47
|
-
- `compiler/handler-lowering.mjs`: source-local native/effect callback, reducer, Context,
|
|
47
|
+
- `compiler/handler-lowering.mjs`: source-local native/effect callback, reducer, Context, package-neutral shared-action, reactive-binding, and list-evaluator AST lowering before IR finalization.
|
|
48
48
|
- `compiler/handler-codegen.mjs`: AST-free ordered import rendering and finalized handler/binding export-source assembly.
|
|
49
49
|
- `compiler/render-control-pass.mjs`: render-function early-return and exhaustive adjacent-assignment normalization.
|
|
50
50
|
- `compiler/router-pass.mjs`: React Router import validation and native Link, pathname parameter, search parameter, and imperative navigation lowering.
|
|
@@ -244,6 +244,7 @@ export function createDescriptorSession({ semantic, handlerUrl, factory, context
|
|
|
244
244
|
snapshot: lowered.captureSnapshots.includes(name)
|
|
245
245
|
})),
|
|
246
246
|
imports: entry.imports.map(entry => importSlot(importRecord(entry))),
|
|
247
|
+
actions: [...entry.reducers.values()].flatMap(reducer => reducer.sharedAction ? [reducer.sharedAction.slot] : []),
|
|
247
248
|
code: lowered.code,
|
|
248
249
|
...(source(entry.expression) ? { source: source(entry.expression) } : {})
|
|
249
250
|
})
|
|
@@ -21,18 +21,18 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
|
|
|
21
21
|
ts.setParentRecursive(call, false)
|
|
22
22
|
return ts.visitNode(call, visitor)
|
|
23
23
|
}
|
|
24
|
-
if (reducer.
|
|
24
|
+
if (reducer.sharedAction) return sharedActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)))
|
|
25
25
|
if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
|
|
26
26
|
return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
|
|
27
27
|
}
|
|
28
28
|
if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && matchesExternalReference(node.name, expression, bindingIndex)) {
|
|
29
29
|
if (reducers.get(node.name.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
|
|
30
|
-
if (reducers.get(node.name.text).
|
|
30
|
+
if (reducers.get(node.name.text).sharedAction) throw sourceNodeError(node, expression.getSourceFile(), `${reducers.get(node.name.text).sourceKind} actions must be called directly inside an event handler`)
|
|
31
31
|
return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
|
|
32
32
|
}
|
|
33
33
|
if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && matchesExternalReference(node, expression, bindingIndex)) {
|
|
34
34
|
if (reducers.get(node.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
|
|
35
|
-
if (reducers.get(node.text).
|
|
35
|
+
if (reducers.get(node.text).sharedAction) throw sourceNodeError(node, expression.getSourceFile(), `${reducers.get(node.text).sourceKind} actions must be called directly inside an event handler`)
|
|
36
36
|
return reducerReference(factory, reducers.get(node.text))
|
|
37
37
|
}
|
|
38
38
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && matchesExternalReference(node.expression, expression, bindingIndex)) {
|
|
@@ -156,19 +156,20 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
function reducerDispatch(factory, reducer, action) {
|
|
159
|
-
if (reducer.
|
|
159
|
+
if (reducer.sharedAction) return sharedActionDispatch(factory, reducer, [action])
|
|
160
160
|
const previous = factory.createUniqueName("__kPrevious")
|
|
161
161
|
const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createCallExpression(factory.createIdentifier(reducer.reducer), undefined, [previous, action]))
|
|
162
162
|
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
function
|
|
165
|
+
function sharedActionDispatch(factory, reducer, args) {
|
|
166
166
|
const previous = factory.createUniqueName("__kPrevious")
|
|
167
167
|
const current = factory.createUniqueName("__kStore")
|
|
168
168
|
const updateValue = factory.createUniqueName("__kUpdate")
|
|
169
169
|
const partial = factory.createUniqueName("__kPartial")
|
|
170
170
|
const action = factory.createUniqueName("__kAction")
|
|
171
|
-
const
|
|
171
|
+
const shared = reducer.sharedAction
|
|
172
|
+
const set = factory.createIdentifier(shared.setName)
|
|
172
173
|
const merge = factory.createExpressionStatement(factory.createBinaryExpression(current, factory.createToken(ts.SyntaxKind.EqualsToken), factory.createObjectLiteralExpression([
|
|
173
174
|
factory.createSpreadAssignment(current),
|
|
174
175
|
factory.createSpreadAssignment(partial)
|
|
@@ -184,11 +185,11 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
|
|
|
184
185
|
merge
|
|
185
186
|
], true)
|
|
186
187
|
const body = factory.createBlock([
|
|
187
|
-
factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(current, undefined, undefined, factory.createObjectLiteralExpression([factory.createPropertyAssignment(
|
|
188
|
+
factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(current, undefined, undefined, factory.createObjectLiteralExpression([factory.createPropertyAssignment(shared.field, previous)]))], ts.NodeFlags.Let)),
|
|
188
189
|
factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(set, undefined, undefined, factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, updateValue)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), setBody))], ts.NodeFlags.Const)),
|
|
189
|
-
factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(action, undefined, undefined, synthesizeTree(
|
|
190
|
+
factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(action, undefined, undefined, synthesizeTree(shared.implementation))], ts.NodeFlags.Const)),
|
|
190
191
|
factory.createExpressionStatement(factory.createCallExpression(action, undefined, args)),
|
|
191
|
-
factory.createReturnStatement(factory.createPropertyAccessExpression(current,
|
|
192
|
+
factory.createReturnStatement(factory.createPropertyAccessExpression(current, shared.field))
|
|
192
193
|
], true)
|
|
193
194
|
const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), body)
|
|
194
195
|
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function createModuleIR(file) {
|
|
2
|
-
return { version: 2, file, symbols: [], signals: [], handlers: [], bindings: [], derived: [], effects: [], keyedBlocks: [], imports: [], clientModules: [] }
|
|
2
|
+
return { version: 2, file, symbols: [], sharedStates: [], sharedActions: [], signals: [], handlers: [], bindings: [], derived: [], effects: [], keyedBlocks: [], imports: [], clientModules: [] }
|
|
3
3
|
}
|
|
4
4
|
|
|
5
5
|
export function assertModuleIRReferences(moduleIR, componentAnalysis) {
|
|
@@ -15,6 +15,8 @@ export function assertModuleIRReferences(moduleIR, componentAnalysis) {
|
|
|
15
15
|
})
|
|
16
16
|
}
|
|
17
17
|
indexed("SymbolRef", moduleIR.symbols)
|
|
18
|
+
indexed("SharedStateIR", moduleIR.sharedStates)
|
|
19
|
+
indexed("SharedActionIR", moduleIR.sharedActions)
|
|
18
20
|
indexed("SignalIR", moduleIR.signals)
|
|
19
21
|
indexed("HandlerIR", moduleIR.handlers)
|
|
20
22
|
indexed("BindingIR", moduleIR.bindings)
|
|
@@ -22,6 +24,7 @@ export function assertModuleIRReferences(moduleIR, componentAnalysis) {
|
|
|
22
24
|
indexed("EffectIR", moduleIR.effects)
|
|
23
25
|
indexed("KeyedBlockIR", moduleIR.keyedBlocks)
|
|
24
26
|
indexed("ImportIR", moduleIR.imports)
|
|
27
|
+
for (const action of moduleIR.sharedActions) slot(moduleIR.sharedStates, action.state, `SharedActionIR ${action.slot}`, "SharedStateIR")
|
|
25
28
|
if (componentAnalysis) {
|
|
26
29
|
indexed("Component owner", componentAnalysis.owners)
|
|
27
30
|
indexed("Component specialization", componentAnalysis.specializations)
|
|
@@ -54,6 +57,7 @@ export function assertModuleIRReferences(moduleIR, componentAnalysis) {
|
|
|
54
57
|
if (!reference || typeof reference !== "object") throw new Error(`${label} must be a StateRef`)
|
|
55
58
|
if (reference.kind === "module-symbol") return moduleSymbol(reference.symbol, label)
|
|
56
59
|
if (reference.kind === "symbol") return slot(moduleIR.symbols, reference.symbol, label, "SymbolRef")
|
|
60
|
+
if (reference.kind === "shared-state") return slot(moduleIR.sharedStates, reference.sharedState, label, "SharedStateIR")
|
|
57
61
|
if (reference.kind !== "state") throw new Error(`${label} has invalid kind ${JSON.stringify(reference.kind)}`)
|
|
58
62
|
const owner = ownerRef(reference.owner, `${label} owner`)
|
|
59
63
|
const states = reference.owner.kind === "component" || reference.owner.kind === "specialization" ? owner.states : []
|
|
@@ -74,6 +78,7 @@ export function assertModuleIRReferences(moduleIR, componentAnalysis) {
|
|
|
74
78
|
for (const [index, signal] of (handler.signals ?? []).entries()) slot(moduleIR.signals, signal.signal, `HandlerIR ${handler.slot} signal ${index}`, "SignalIR")
|
|
75
79
|
for (const [index, imported] of (handler.imports ?? []).entries()) slot(moduleIR.imports, imported, `HandlerIR ${handler.slot} import ${index}`, "ImportIR")
|
|
76
80
|
for (const [index, capture] of (handler.captures ?? []).entries()) if (capture.symbol !== undefined) slot(moduleIR.symbols, capture.symbol, `HandlerIR ${handler.slot} capture ${index}`, "SymbolRef")
|
|
81
|
+
for (const [index, action] of (handler.actions ?? []).entries()) slot(moduleIR.sharedActions, action, `HandlerIR ${handler.slot} action ${index}`, "SharedActionIR")
|
|
77
82
|
if (handler.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, handler.keyedBlock, `HandlerIR ${handler.slot} keyed block`, "KeyedBlockIR")
|
|
78
83
|
}
|
|
79
84
|
for (const binding of moduleIR.bindings) {
|
|
@@ -149,6 +154,24 @@ export function registerSignal(moduleIR, reference, debugName) {
|
|
|
149
154
|
return signal
|
|
150
155
|
}
|
|
151
156
|
|
|
157
|
+
export function registerSharedState(moduleIR, descriptor) {
|
|
158
|
+
let state = moduleIR.sharedStates.find(entry => entry.identity === descriptor.identity && entry.field === descriptor.field)
|
|
159
|
+
if (!state) {
|
|
160
|
+
state = { ...descriptor, slot: moduleIR.sharedStates.length }
|
|
161
|
+
moduleIR.sharedStates.push(state)
|
|
162
|
+
}
|
|
163
|
+
return state
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function registerSharedAction(moduleIR, descriptor) {
|
|
167
|
+
let action = moduleIR.sharedActions.find(entry => entry.state === descriptor.state && entry.name === descriptor.name)
|
|
168
|
+
if (!action) {
|
|
169
|
+
action = { ...descriptor, slot: moduleIR.sharedActions.length }
|
|
170
|
+
moduleIR.sharedActions.push(action)
|
|
171
|
+
}
|
|
172
|
+
return action
|
|
173
|
+
}
|
|
174
|
+
|
|
152
175
|
export function registerCommandHandler(moduleIR, commands, source) {
|
|
153
176
|
const handler = {
|
|
154
177
|
slot: moduleIR.handlers.length,
|
|
@@ -12,6 +12,7 @@ import { captureNames, createDescriptorSession, createSemanticArtifact, nativeCa
|
|
|
12
12
|
import { analyzeEffectDependencies, validateEffectOwnedBrowserResources } from "./effect-analysis.mjs"
|
|
13
13
|
import { createHandlerCodegen } from "./handler-codegen.mjs"
|
|
14
14
|
import { createHandlerLowering } from "./handler-lowering.mjs"
|
|
15
|
+
import { registerSharedAction, registerSharedState } from "./ir/module-ir.mjs"
|
|
15
16
|
import { createCommandSpecializer } from "./optimize/command-specialization.mjs"
|
|
16
17
|
import { applyNormalizationPasses } from "./normalization-pipeline.mjs"
|
|
17
18
|
import { assetPath, relativeModulePath, withBase } from "./path-helpers.mjs"
|
|
@@ -346,14 +347,14 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
346
347
|
const stateOwnersByFunction = new Map()
|
|
347
348
|
const localStateSettersByFunction = new Map()
|
|
348
349
|
const reducersByFunction = new Map()
|
|
349
|
-
const
|
|
350
|
-
const
|
|
350
|
+
const sharedStateAdapters = new Map()
|
|
351
|
+
const resolvedSharedState = entry => {
|
|
351
352
|
const exportName = entry.kind === "default" ? "default" : entry.imported
|
|
352
353
|
const key = `${entry.target}:${exportName}`
|
|
353
|
-
if (
|
|
354
|
+
if (sharedStateAdapters.has(key)) return sharedStateAdapters.get(key)
|
|
354
355
|
const targetSource = parseSourceFile(entry.target, sourceIndex.get(entry.target))
|
|
355
356
|
const store = analyzeZustandStores(targetSource).get(exportName)
|
|
356
|
-
|
|
357
|
+
sharedStateAdapters.set(key, store)
|
|
357
358
|
return store
|
|
358
359
|
}
|
|
359
360
|
const functions = new Map()
|
|
@@ -552,7 +553,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
552
553
|
const collect = node => {
|
|
553
554
|
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
554
555
|
const callName = ts.isIdentifier(node.initializer.expression) ? node.initializer.expression.text : ""
|
|
555
|
-
if (callName && /^use[A-Z]/.test(callName) && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace" && !
|
|
556
|
+
if (callName && /^use[A-Z]/.test(callName) && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace" && !resolvedSharedState(importBindings.get(callName))) {
|
|
556
557
|
if (!isLocalConst(node) || !ts.isObjectBindingPattern(node.name) || node.initializer.arguments.length) throw sourceNodeError(node, sourceFile, "Relative custom hooks must initialize one top-level const object destructuring with no arguments")
|
|
557
558
|
const hook = resolveCustomHook(importBindings.get(callName), node.initializer)
|
|
558
559
|
const names = new Set()
|
|
@@ -635,26 +636,31 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
635
636
|
}
|
|
636
637
|
if (ts.isIdentifier(node.name) && callName && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace") {
|
|
637
638
|
const storeImport = importBindings.get(callName)
|
|
638
|
-
const store =
|
|
639
|
+
const store = resolvedSharedState(storeImport)
|
|
639
640
|
if (store) {
|
|
640
641
|
const selector = node.initializer.arguments[0]
|
|
641
|
-
if (node.initializer.arguments.length !== 1 || !selector || !ts.isArrowFunction(selector) || selector.parameters.length !== 1 || !ts.isIdentifier(selector.parameters[0].name) || !ts.isPropertyAccessExpression(unwrapExpression(selector.body)) || !ts.isIdentifier(unwrapExpression(selector.body).expression) || unwrapExpression(selector.body).expression.text !== selector.parameters[0].name.text) throw sourceNodeError(node.initializer, sourceFile,
|
|
642
|
+
if (node.initializer.arguments.length !== 1 || !selector || !ts.isArrowFunction(selector) || selector.parameters.length !== 1 || !ts.isIdentifier(selector.parameters[0].name) || !ts.isPropertyAccessExpression(unwrapExpression(selector.body)) || !ts.isIdentifier(unwrapExpression(selector.body).expression) || unwrapExpression(selector.body).expression.text !== selector.parameters[0].name.text) throw sourceNodeError(node.initializer, sourceFile, `${store.sourceKind} selectors must be direct arrows such as ${store.selectorExample}`)
|
|
642
643
|
const selected = unwrapExpression(selector.body).name.text
|
|
643
644
|
const owner = nearestFunction(node)
|
|
644
|
-
if (!owner) throw sourceNodeError(node, sourceFile,
|
|
645
|
+
if (!owner) throw sourceNodeError(node, sourceFile, `${store.sourceKind} stores cannot be used outside a Kudzu component`)
|
|
645
646
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
647
|
+
const sharedState = registerSharedState(moduleIR, { identity: store.identity, field: store.field, initialValue: store.initialData })
|
|
648
|
+
const sharedReference = { kind: "shared-state", sharedState: sharedState.slot }
|
|
646
649
|
if (selected === store.field) {
|
|
647
650
|
const setter = `__kStoreState_${node.name.text}`
|
|
648
651
|
setters.set(setter, node.name.text)
|
|
649
|
-
registerState(owner, node.name.text, setter, "
|
|
652
|
+
registerState(owner, node.name.text, setter, "shared-state", node)
|
|
653
|
+
stateOwnersByFunction.get(owner).set(node.name.text, sharedReference)
|
|
650
654
|
}
|
|
651
655
|
else if (store.actions.has(selected)) {
|
|
652
656
|
setters.set(node.name.text, node.name.text)
|
|
653
|
-
registerState(owner, node.name.text, node.name.text, "
|
|
657
|
+
registerState(owner, node.name.text, node.name.text, "shared-action", node)
|
|
658
|
+
stateOwnersByFunction.get(owner).set(node.name.text, sharedReference)
|
|
659
|
+
const action = registerSharedAction(moduleIR, { state: sharedState.slot, name: selected })
|
|
654
660
|
const reducers = reducersByFunction.get(owner) ?? new Map()
|
|
655
|
-
reducers.set(node.name.text, { state: node.name.text, store, action: selected })
|
|
661
|
+
reducers.set(node.name.text, { state: node.name.text, sourceKind: store.sourceKind, sharedAction: { ...action, setName: store.setName, field: store.field, implementation: store.actions.get(selected) } })
|
|
656
662
|
reducersByFunction.set(owner, reducers)
|
|
657
|
-
} else throw sourceNodeError(unwrapExpression(selector.body).name, sourceFile,
|
|
663
|
+
} else throw sourceNodeError(unwrapExpression(selector.body).name, sourceFile, `${store.sourceKind} store ${JSON.stringify(store.name)} has no supported property ${JSON.stringify(selected)}`)
|
|
658
664
|
settersByFunction.set(owner, setters)
|
|
659
665
|
}
|
|
660
666
|
}
|
|
@@ -1088,9 +1094,17 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1088
1094
|
}
|
|
1089
1095
|
collectReferences(component.body)
|
|
1090
1096
|
if (!references.length) fail(element, `Setter-callback prop ${JSON.stringify(prop)} must be called by at least one intrinsic event handler`)
|
|
1091
|
-
if (references.length === 1 && ts.isJsxExpression(references[0].parent) && ts.isJsxAttribute(references[0].parent.parent) && /^on[A-Z]/.test(references[0].parent.parent.name.text)) continue
|
|
1092
1097
|
const handlers = new Set()
|
|
1093
1098
|
for (const reference of references) {
|
|
1099
|
+
if (ts.isJsxExpression(reference.parent) && ts.isJsxAttribute(reference.parent.parent) && /^on[A-Z]/.test(reference.parent.parent.name.text)) {
|
|
1100
|
+
const attribute = reference.parent.parent
|
|
1101
|
+
const tag = attribute.parent?.parent && (ts.isJsxOpeningElement(attribute.parent.parent) || ts.isJsxSelfClosingElement(attribute.parent.parent)) ? attribute.parent.parent.tagName : undefined
|
|
1102
|
+
if (ts.isIdentifier(tag) && tag.text[0] !== tag.text[0].toLowerCase()) continue
|
|
1103
|
+
if (ts.isIdentifier(tag) && !handlers.has(attribute)) {
|
|
1104
|
+
handlers.add(attribute)
|
|
1105
|
+
continue
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1094
1108
|
if (!ts.isCallExpression(reference.parent) || reference.parent.expression !== reference) fail(reference, `Setter-callback prop ${JSON.stringify(prop)} must be called directly inside an intrinsic event handler`)
|
|
1095
1109
|
let attribute
|
|
1096
1110
|
for (let current = reference.parent; current && current !== component.body; current = current.parent) {
|
|
@@ -51,7 +51,18 @@ export function createZustandPass({ isSerializableStateLiteral, nativeCaptureNam
|
|
|
51
51
|
}
|
|
52
52
|
validateAction(action.body)
|
|
53
53
|
}
|
|
54
|
-
stores.set(declaration.name.text, {
|
|
54
|
+
stores.set(declaration.name.text, {
|
|
55
|
+
name: declaration.name.text,
|
|
56
|
+
identity: `${relative(sourceDirectory, sourceFile.fileName).replaceAll(sep, "/")}#${declaration.name.text}`,
|
|
57
|
+
sourceKind: "Zustand",
|
|
58
|
+
selectorExample: "state => state.quantities",
|
|
59
|
+
setName: callback.parameters[0].name.text,
|
|
60
|
+
field: data[0].name,
|
|
61
|
+
initialValue: data[0].value,
|
|
62
|
+
initialData: serializableLiteralValue(data[0].value),
|
|
63
|
+
actions,
|
|
64
|
+
declaration
|
|
65
|
+
})
|
|
55
66
|
}
|
|
56
67
|
}
|
|
57
68
|
const visit = node => {
|
|
@@ -70,22 +81,22 @@ export function createZustandPass({ isSerializableStateLiteral, nativeCaptureNam
|
|
|
70
81
|
if (declaration) throw sourceNodeError(declaration, sourceFile, "Zustand create must directly initialize an exported const store")
|
|
71
82
|
return sourceFile
|
|
72
83
|
}
|
|
73
|
-
const identity = name => `${relative(sourceDirectory, sourceFile.fileName).replaceAll(sep, "/")}#${name}`
|
|
74
84
|
const visitor = node => {
|
|
75
85
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && stores.has(node.name.text)) {
|
|
76
86
|
const store = stores.get(node.name.text)
|
|
77
|
-
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createCallExpression(factory.createIdentifier("
|
|
78
|
-
factory.createStringLiteral(
|
|
87
|
+
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createCallExpression(factory.createIdentifier("__kCreateSharedState"), undefined, [
|
|
88
|
+
factory.createStringLiteral(store.identity),
|
|
79
89
|
factory.createStringLiteral(store.field),
|
|
80
90
|
store.initialValue,
|
|
81
|
-
factory.createArrayLiteralExpression([...store.actions.keys()].map(name => factory.createStringLiteral(name)))
|
|
91
|
+
factory.createArrayLiteralExpression([...store.actions.keys()].map(name => factory.createStringLiteral(name))),
|
|
92
|
+
factory.createStringLiteral(store.sourceKind)
|
|
82
93
|
]))
|
|
83
94
|
}
|
|
84
95
|
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "zustand") return undefined
|
|
85
96
|
return ts.visitEachChild(node, visitor, context)
|
|
86
97
|
}
|
|
87
98
|
const normalized = ts.visitNode(sourceFile, visitor)
|
|
88
|
-
const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports([factory.createImportSpecifier(false, undefined, factory.createIdentifier("
|
|
99
|
+
const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports([factory.createImportSpecifier(false, undefined, factory.createIdentifier("__kCreateSharedState"))])), factory.createStringLiteral("@kudzujs/core"))
|
|
89
100
|
const statements = [...normalized.statements]
|
|
90
101
|
statements.splice(statements.findLastIndex(statement => ts.isImportDeclaration(statement)) + 1, 0, declaration)
|
|
91
102
|
return factory.updateSourceFile(normalized, statements)
|
|
@@ -93,3 +104,14 @@ export function createZustandPass({ isSerializableStateLiteral, nativeCaptureNam
|
|
|
93
104
|
|
|
94
105
|
return { analyzeZustandStores, normalizeZustandMigrationSyntax }
|
|
95
106
|
}
|
|
107
|
+
|
|
108
|
+
function serializableLiteralValue(node) {
|
|
109
|
+
const value = unwrapExpression(node)
|
|
110
|
+
if (ts.isStringLiteral(value) || ts.isNumericLiteral(value)) return ts.isNumericLiteral(value) ? Number(value.text) : value.text
|
|
111
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword) return true
|
|
112
|
+
if (value.kind === ts.SyntaxKind.FalseKeyword) return false
|
|
113
|
+
if (value.kind === ts.SyntaxKind.NullKeyword) return null
|
|
114
|
+
if (ts.isPrefixUnaryExpression(value)) return value.operator === ts.SyntaxKind.MinusToken ? -Number(value.operand.text) : Number(value.operand.text)
|
|
115
|
+
if (ts.isArrayLiteralExpression(value)) return value.elements.map(serializableLiteralValue)
|
|
116
|
+
return Object.fromEntries(value.properties.map(property => [property.name.text, serializableLiteralValue(property.initializer)]))
|
|
117
|
+
}
|
package/framework/core.mjs
CHANGED
|
@@ -80,18 +80,18 @@ export function useReducer(reducer, initialValue, name) {
|
|
|
80
80
|
return [state, dispatch]
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
export function
|
|
83
|
+
export function __kCreateSharedState(name, field, initialValue, actions, sourceKind = "Shared state") {
|
|
84
84
|
return selector => {
|
|
85
|
-
if (!renderContext) throw new Error(
|
|
86
|
-
let store = renderContext.
|
|
85
|
+
if (!renderContext) throw new Error(`${sourceKind} stores can only be read while rendering a Kudzu component`)
|
|
86
|
+
let store = renderContext.sharedStates.get(name)
|
|
87
87
|
if (!store) {
|
|
88
|
-
if (renderContext.scoped && renderContext.renderScope !== "layout") throw new Error(
|
|
89
|
-
if (renderContext.listRoot || renderContext.listRowRoot || renderContext.listTemplate) throw new Error(
|
|
88
|
+
if (renderContext.scoped && renderContext.renderScope !== "layout") throw new Error(`${sourceKind} store ${JSON.stringify(name)} must be initialized by the shared layout before route components use it`)
|
|
89
|
+
if (renderContext.listRoot || renderContext.listRowRoot || renderContext.listTemplate) throw new Error(`${sourceKind} store ${JSON.stringify(name)} cannot be initialized inside a keyed row`)
|
|
90
90
|
const [state] = useState(initialValue, `${name}.${field}`)
|
|
91
91
|
store = { [field]: state }
|
|
92
92
|
for (const action of actions) {
|
|
93
93
|
const marker = () => {
|
|
94
|
-
throw new Error(
|
|
94
|
+
throw new Error(`${sourceKind} actions are compiled into browser handlers`)
|
|
95
95
|
}
|
|
96
96
|
Object.defineProperties(marker, {
|
|
97
97
|
[signalMarker]: { value: true },
|
|
@@ -100,7 +100,7 @@ export function __kCreateStore(name, field, initialValue, actions) {
|
|
|
100
100
|
})
|
|
101
101
|
store[action] = marker
|
|
102
102
|
}
|
|
103
|
-
renderContext.
|
|
103
|
+
renderContext.sharedStates.set(name, store)
|
|
104
104
|
}
|
|
105
105
|
return selector(store)
|
|
106
106
|
}
|
|
@@ -458,7 +458,7 @@ function serializeCapture(name, value, seen) {
|
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
461
|
-
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, nextId: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [],
|
|
461
|
+
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, nextId: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], sharedStates: new Map(), states: {}, textStates: new Set(), conditionStates: new Set(), conditionOwnedStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerReferences: new Map(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, searchParams: new Map(), searchParamEntries: [], searchParamsWritable: false, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
462
462
|
|
|
463
463
|
try {
|
|
464
464
|
const page = { [routeScopeMarker]: true, component, props }
|