@kudzujs/core 0.7.5 → 0.7.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/RELEASES.md +73 -0
- package/framework/README.md +4 -2
- package/framework/build.mjs +214 -27
- package/framework/collection-selector.js +23 -19
- package/framework/core.d.ts +2 -2
- package/framework/core.mjs +44 -6
- package/framework/dev-state.js +3 -3
- package/framework/list-runtime.js +123 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Kudzu is designed so ordinary common React-shaped TSX can migrate with minimal s
|
|
|
10
10
|
|
|
11
11
|
> Experimental `0.7.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
13
|
-
**0.7.
|
|
13
|
+
**0.7.8:** Static collection fast paths. Compiler-owned static filters reuse validated item metadata and detached row prototypes, making 1,000-row filtering match Vue and restoring filtered rows faster than React, Vue, and Svelte in the matched benchmark. See [release notes](./RELEASES.md#078---static-collection-fast-paths).
|
|
14
14
|
|
|
15
15
|
Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
|
|
16
16
|
|
|
@@ -74,10 +74,12 @@ export default function Header() {
|
|
|
74
74
|
}
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Kudzu rewrites supported React imports to its compile-time APIs before evaluating the module; neither the React package nor a compatibility runtime enters the deploy output. Named or aliased `useState`, `useReducer`, `useEffect`, `useRef`, `createContext`, and `useContext` imports compile to their canonical forms. Default and namespace imports may call those APIs as direct members such as `React.useState`, and default, namespace, or named `Fragment` also works. `memo(Component)` is erased to a same-file component. Inline `useCallback(function, literalDependencies)` is erased to its function, while inline synchronous `useMemo` callbacks may return one expression over primitive literals/direct local state or an analyzable `filter`, `map`, `flatMap`, and `Array.from` collection pipeline. Scalar expressions inline into existing bindings; collection pipelines lower to existing keyed-list selectors and preserve row identity. Both hooks require inert literal dependency arrays and complete captured-state dependencies; memo locals cannot be duplicated or captured by nested functions. React classes and side-effect or dynamic React imports remain unsupported. A static route using these forms still emits zero JavaScript.
|
|
77
|
+
Kudzu rewrites supported React imports to its compile-time APIs before evaluating the module; neither the React package nor a compatibility runtime enters the deploy output. Named or aliased `useState`, `useReducer`, `useEffect`, `useRef`, `createContext`, and `useContext` imports compile to their canonical forms. Default and namespace imports may call those APIs as direct members such as `React.useState`, and default, namespace, or named `Fragment` also works. `memo(Component)` is erased to a same-file component. Inline `useCallback(function, literalDependencies)` is erased to its function, while inline synchronous `useMemo` callbacks may return one expression over primitive literals/direct local state or an analyzable `filter`, `map`, `flatMap`, and `Array.from` collection pipeline. Collection pipelines may start from local array state or a named relative import whose source is an exported JSON-safe `const` array; imported static collections may be filtered by direct local state listed in the dependency array. Scalar expressions inline into existing bindings; collection pipelines lower to existing keyed-list selectors and preserve row identity. Both hooks require inert literal dependency arrays and complete captured-state dependencies; memo locals cannot be duplicated or captured by nested functions. React classes and side-effect or dynamic React imports remain unsupported. A static route using these forms still emits zero JavaScript.
|
|
78
78
|
|
|
79
79
|
Direct default or named `clsx` imports compile away for string/number literals, literal arrays, literal object conditions, and conditional expressions. Kudzu lowers those calls to ordinary class expressions, so reactive classes reuse existing bindings without shipping `clsx`; spreads, computed object keys, arbitrary calls, and indirect references remain unsupported.
|
|
80
80
|
|
|
81
|
+
Migration source may also retain a reduced Zustand store declared as one exported `const` initialized by a named `create` import. The initializer accepts `set`, returns exactly one directly serializable data property plus synchronous actions, and components select one direct property with `state => state.property`. A shared layout must select the store before its routes use it; Kudzu then owns the data as layout state, inlines action updates into existing handler ESM, and ships neither React nor Zustand. Derived selectors, multiple data properties, middleware, `get`, subscriptions, equality functions, persist/devtools wrappers, async actions, helper captures, replacement updates, and indirect action forwarding remain unsupported.
|
|
82
|
+
|
|
81
83
|
Create `src/pages/index.tsx`:
|
|
82
84
|
|
|
83
85
|
```tsx
|
|
@@ -416,7 +418,9 @@ return <ItemList items={items} />
|
|
|
416
418
|
|
|
417
419
|
The original row component remains reusable across multiple lists and ordinary JSX. State-backed list wrappers and row components are specialized to intrinsic JSX at build time; no component function or component runtime is shipped to the browser. Kudzu emits initial items as static HTML, then adds, removes, updates, styles, conditional branches, and moves keyed elements directly. The map may appear directly in JSX, in one top-level immutable `const` rendered once as a JSX child, or in one synchronous wrapper receiving the state identifier as a direct prop. Existing keys move without remounting, preserving uncontrolled descendant state. Direct `item.<field>` reads use compact markers; derived item expressions compile to external ESM evaluators. Nested item-local `&&` and ternary conditions patch bounded branches and mount or unmount their handlers. Item-local handlers and effects receive the latest JSON-safe item for their key. Effects mount after a row is connected and clean up when it is removed. A direct primitive item dependency such as `[item.name]`, optionally mixed with row or page state, reruns only rows whose selected values changed; unrelated fields and reorder do not rerun it, while removal cleans it up. The item remains stored once in shared list state; runtime descriptors carry a placeholder that the list runtime fills when mounting or updating the keyed root.
|
|
418
420
|
|
|
419
|
-
Rendered collections may use one-use top-level aliases and analyzable pipelines over local array state. Inline arrow callbacks accept `(item)` or `(item, index)`; `filter()` supports pure synchronous expressions, `flatMap()` projects one direct array property, `Array.from()` accepts an optional pure mapper, and the final `map()` may use `key={item.field}` or positional `key={index}`. Field keys preserve the matching DOM node through filtering and reorder. Positional keys deliberately preserve the DOM node at each position while its item changes, matching React key semantics.
|
|
421
|
+
Rendered collections may use one-use top-level aliases and analyzable pipelines over local array state or named relative imports of exported JSON-safe `const` arrays. Inline arrow callbacks accept `(item)` or `(item, index)`; `filter()` supports pure synchronous expressions and direct local-state reads, `flatMap()` projects one direct array property, `Array.from()` accepts an optional pure mapper, and the final `map()` may use `key={item.field}` or positional `key={index}`. Field keys preserve the matching DOM node through filtering and reorder. Positional keys deliberately preserve the DOM node at each position while its item changes, matching React key semantics.
|
|
422
|
+
|
|
423
|
+
Compiler-owned static `filter` collections with structural keyed rows validate source items and key tokens once. Removed rows become detached prototypes; restoration clones fresh nodes and inserts only new runs, so retained keys keep identity while restored keys remount. In a 31-fresh-profile, 4x CPU-throttled Chrome benchmark over 1,000 alternating products, Kudzu measured 37.8 ms to visible rows, 8.4 ms to filter to 500, and 3.6 ms to restore 1,000. React measured 86.3/12.9/6.2 ms, Vue 54.1/8.4/4.3 ms, and Svelte 61.4/11.7/8.3 ms. This is a focused static-filter result, not a claim that rendering 100,000 or 1,000,000 DOM rows is appropriate; paginate or window large datasets so only the visible result set enters the document.
|
|
420
424
|
|
|
421
425
|
A keyed row may contain multiple keyed maps over direct array properties of its item at any nesting depth. This supports recursively nested data populated after mount while preserving keyed DOM identity across updates and reorder:
|
|
422
426
|
|
|
@@ -461,7 +465,7 @@ Initial child rows remain complete HTML. Kudzu stores one child row prototype, i
|
|
|
461
465
|
|
|
462
466
|
In the matched 100-parent/1,000-child fixture, Kudzu measured 1.3/0.4/5.0/0.7 ms for child update and condition change, child reverse, parent reverse, and parent removal. Hand-written Astro/native measured 0.5/0.4/3.9/0.2 ms, Svelte 2.7/1.2/6.7/1.3 ms, Vue 4.9/2.5/6.1/2.2 ms, and React 11.8/5.0/8.2/4.4 ms. Kudzu and Astro emit initial rows while the CSR targets do not, so artifact sizes are not architecture-equivalent.
|
|
463
467
|
|
|
464
|
-
Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. Collections must remain anchored to local array state; inline callbacks accept one or two identifier parameters, and row roots must be intrinsic JSX or supported same-file/relative components with `key={item.<field>}` or `key={index}`. State-backed list wrappers use one destructured props parameter, an intrinsic return root, no effects, and a direct local-state prop. Whole-item, computed, nested, derived, `__proto__`, `prototype`, and `constructor` effect dependencies are rejected. A collection alias may only be rendered once and cannot be read by other JavaScript. Collection callbacks and derived expressions must be pure and synchronous: supported reads, operators, templates, approved read-only methods, deterministic `Math`, and primitive conversion compile; imported callbacks, browser globals, promises, mutation, arbitrary calls, and prototype-sensitive properties fail. Lazy or dynamic keyed-row state initializers, non-`null` refs, callback refs, package/namespace/star row imports, same-file exported rows, reusable aliases, prop spreads/defaults/rest, children, fragments, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
|
|
468
|
+
Each item must be an ordinary plain object with a unique string or finite-number key; nested data may contain only JSON-safe arrays, ordinary plain objects, and primitive values. Null-prototype objects are rejected to preserve JSON round-trip parity. Collections must remain anchored to local array state or a supported static named import; inline callbacks accept one or two identifier parameters, and row roots must be intrinsic JSX or supported same-file/relative components with `key={item.<field>}` or `key={index}`. State-backed list wrappers use one destructured props parameter, an intrinsic return root, no effects, and a direct local-state prop. Whole-item, computed, nested, derived, `__proto__`, `prototype`, and `constructor` effect dependencies are rejected. A collection alias may only be rendered once and cannot be read by other JavaScript. Collection callbacks and derived expressions must be pure and synchronous: supported reads, operators, templates, approved read-only methods, deterministic `Math`, and primitive conversion compile; imported callbacks, browser globals, promises, mutation, arbitrary calls, and prototype-sensitive properties fail. Lazy or dynamic keyed-row state initializers, non-`null` refs, callback refs, package/namespace/star row imports, same-file exported rows, reusable aliases, prop spreads/defaults/rest, children, fragments, and `dangerouslySetInnerHTML` remain unsupported. Keyed rows must be placed inside an explicit `<tbody>`, `<thead>`, or `<tfoot>`.
|
|
465
469
|
|
|
466
470
|
The focused wrapper fixture emits 1,393 B raw / 500 B gzip HTML and 10,719 B raw / 4,665 B gzip JavaScript across its route capabilities. After one warm-up, seven clean builds measured 314.1, 325.3, 322.3, 327.2, 336.1, 322.4, and 315.0 ms, with a 322.4 ms median.
|
|
467
471
|
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.8 - Static collection fast paths
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.8 specializes compiler-owned static `filter` collections so repeated category changes avoid general keyed-list validation and reconciliation.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.8
|
|
8
|
+
|
|
9
|
+
- Static source items, unique keys, source positions, and reference entries validate and cache once when the list mounts.
|
|
10
|
+
- Structural rows removed by a filter become detached prototypes; restoration clones fresh DOM, preserving remount semantics without moving retained keys.
|
|
11
|
+
- Interleaved additions insert only new contiguous runs instead of attaching additions and then reordering the complete list.
|
|
12
|
+
- General local-state lists, dynamic selectors, indexed rows, handlers, effects, refs, and row state retain their existing reconciliation paths.
|
|
13
|
+
- The capability is route-specific and compiles out when no compiler-owned static collection is rendered.
|
|
14
|
+
- The matched route adds 648 B gzip compared with 0.7.7's initial implementation while reducing 500-row restoration from 13.2 ms to 3.6 ms.
|
|
15
|
+
|
|
16
|
+
### Benchmark
|
|
17
|
+
|
|
18
|
+
The matched fixture imports 1,000 alternating products, filters to 500, then restores all 1,000 while checking retained and remounted DOM identity. Thirty-one rotating fresh Chrome 150 profiles on an Apple M3 used 4x CPU throttling. Median visible/filter/restore times were Kudzu 37.8/8.4/3.6 ms, React 86.3/12.9/6.2 ms, Vue 54.1/8.4/4.3 ms, and Svelte 61.4/11.7/8.3 ms. React, Vue, and Svelte used Vite CSR shells while Kudzu emitted initial HTML, so visible-row and artifact comparisons are architecture-dependent.
|
|
19
|
+
|
|
20
|
+
### Boundary
|
|
21
|
+
|
|
22
|
+
The fast path requires a compiler-owned static source, field keys, filter-only selection, and structural rows without handlers, effects, refs, row state, or index dependencies. It is intended for ordinary catalog-sized collections, not direct rendering of 100,000 or 1,000,000 DOM rows; use pagination or windowing for those workloads.
|
|
23
|
+
|
|
24
|
+
### Upgrade
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @kudzujs/core@^0.7.8
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## 0.7.7 - Imported memo collections
|
|
31
|
+
|
|
32
|
+
Kudzu 0.7.7 lets ordinary React migration source filter an imported static catalog through state-dependent `useMemo` while reusing existing keyed-list reconciliation.
|
|
33
|
+
|
|
34
|
+
### New in 0.7.7
|
|
35
|
+
|
|
36
|
+
- Named relative imports of exported JSON-safe `const` arrays can anchor analyzable collection pipelines.
|
|
37
|
+
- Direct local-state reads in collection selectors invalidate the list when their declared state dependencies change.
|
|
38
|
+
- Existing keys retain DOM identity through filtering and restoration; removed keys remount when restored.
|
|
39
|
+
- Compiler-owned static collection state is excluded from development snapshot restoration.
|
|
40
|
+
- Static routes remain JavaScript-free, and interactive routes ship no React runtime or browser memo cache.
|
|
41
|
+
- Focused fixtures verify dependency diagnostics, static output, generated selectors, and browser DOM identity.
|
|
42
|
+
|
|
43
|
+
### Boundary
|
|
44
|
+
|
|
45
|
+
Static collections must be named relative imports of exported JSON-safe `const` arrays. Package, namespace, default, dynamic, mutable, and non-serializable collection sources remain unsupported, as do arbitrary callbacks and general-purpose memo caching.
|
|
46
|
+
|
|
47
|
+
### Upgrade
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install @kudzujs/core@^0.7.7
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 0.7.6 - Zustand-shaped shared stores
|
|
54
|
+
|
|
55
|
+
Kudzu 0.7.6 lets reduced React migration source retain a Zustand `create(set => ...)` store across an explicitly configured shared-layout navigation group.
|
|
56
|
+
|
|
57
|
+
### New in 0.7.6
|
|
58
|
+
|
|
59
|
+
- One exported store with one directly serializable data property and synchronous capture-free actions compiles to one ordinary layout-lifetime Kudzu state slot.
|
|
60
|
+
- Components select the data or an action with direct forms such as `state => state.quantities` and `state => state.add`.
|
|
61
|
+
- Selected actions inline through existing functional state updates, so repeated same-turn calls observe current logical state and DOM writes still batch.
|
|
62
|
+
- Same-group navigation retains the store and layout DOM while incoming route bindings mount against the current value.
|
|
63
|
+
- Neither React, Zustand, a subscription runtime, nor a generic external-store capability enters the deploy output.
|
|
64
|
+
- The shopping fixture verifies two same-turn additions, product-to-cart retention, removal, layout DOM identity, package erasure, and source diagnostics in Chrome.
|
|
65
|
+
|
|
66
|
+
### Boundary
|
|
67
|
+
|
|
68
|
+
The shared layout must initialize the store before route consumers. Derived selectors, multiple data properties, middleware, `get`, subscriptions, equality functions, persist/devtools wrappers, async actions, helper captures, replacement updates, keyed-row initialization, and indirect action forwarding remain unsupported.
|
|
69
|
+
|
|
70
|
+
### Upgrade
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm install @kudzujs/core@^0.7.6
|
|
74
|
+
```
|
|
75
|
+
|
|
3
76
|
## 0.7.5 - Class composition migration
|
|
4
77
|
|
|
5
78
|
Kudzu 0.7.5 lets ordinary React source retain common direct `clsx` calls while compiling them to existing static and reactive class paths.
|
package/framework/README.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
Kudzu specializes ordinary common React-shaped TSX so migrations need minimal source restructuring. Declarative components, collection pipelines, conditions, hooks, and handlers should be lowered at build time rather than replaced with application-owned imperative DOM code. This principle applies across migrations and is not Stay-specific; it does not imply a React package, VDOM, hydration, or ecosystem runtime.
|
|
4
4
|
|
|
5
|
-
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, same-file `memo`, inline `useCallback`, direct-state expression or analyzable collection-pipeline `useMemo`, and default, namespace, or named `Fragment`. `build.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation. Memo wrappers are erased or inlined into existing bindings and keyed-list selectors because no browser component rerender or memo cache exists. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports.
|
|
5
|
+
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, same-file `memo`, inline `useCallback`, direct-state expression or analyzable collection-pipeline `useMemo`, and default, namespace, or named `Fragment`. Collection memos may start from local array state or a named relative import of an exported JSON-safe `const` array and may read direct local state declared in their dependency array. `build.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation. Memo wrappers are erased or inlined into existing bindings and keyed-list selectors because no browser component rerender or memo cache exists. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports.
|
|
6
6
|
|
|
7
7
|
Direct `clsx` calls over literal strings, numbers, arrays, object conditions, and conditional expressions are similarly lowered to ordinary concatenation and conditional expressions. The package import is erased, and dynamic classes continue through the existing binding compiler without serializing or shipping the `clsx` function.
|
|
8
8
|
|
|
9
|
+
Reduced Zustand migration stores lower to one ordinary layout-lifetime state slot. The compiler accepts one exported `create(set => ({ data, ...actions }))` store with one serializable data property, direct property selectors, and synchronous capture-free actions using one-argument merge-form `set`; selected actions reuse the reducer-style functional update compiler, so same-turn calls observe current logical state and DOM writes still batch. The shared layout must initialize the store before route consumers, outside keyed rows. No Zustand import, store subscription runtime, React hook, or generic external-store capability is emitted.
|
|
10
|
+
|
|
9
11
|
- `build.mjs`: TSX compilation, static, `getStaticPaths`, and runtime-fallback routes, base paths, CSS collection, post-build hooks, behavior extraction, static HTML output, and the development server.
|
|
10
12
|
- `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
|
|
11
13
|
- `jsx-runtime.mjs`: automatic JSX runtime used by TypeScript.
|
|
@@ -31,7 +33,7 @@ Inline SVG rendering normalizes an explicit set of common React presentation ali
|
|
|
31
33
|
|
|
32
34
|
Same-file and relative-imported component chains receiving a direct local-state array or keyed item are recursively specialized to intrinsic JSX before keyed-list analysis, so their component functions are not retained in the browser. Rows may own multiple direct-property child maps recursively, nested conditions, latest-item handlers, multiple directly serializable state slots, effects, and `null`-initialized object refs. Structural list sites and ancestor key paths scope hooks across updates and reorder and release them on removal. Handler modules are emitted only when a rendered descriptor references them. Direct JSON-safe primitive keyed-item dependencies subscribe each row record to its owning list commit and compare selected fields after `list-runtime.js` synchronously refreshes the row marker. Only changed rows rerun with the complete latest item; reorder compares equal and unrelated fields do nothing. Builds without item dependencies emit no item reader or list-state subscription code.
|
|
33
35
|
|
|
34
|
-
Rendered collection selectors compile one-use aliases and inline `(item)` or `(item, index)` pipelines over local array state. Supported selectors are pure `filter
|
|
36
|
+
Rendered collection selectors compile one-use aliases and inline `(item)` or `(item, index)` pipelines over local array state or supported static named imports. Supported selectors are pure `filter` with direct local-state reads, direct-property `flatMap`, and `Array.from` before a final keyed `map`; dependency commits re-evaluate the selector against the immutable build-time collection while field keys retain item identity and `key={index}` retains positional identity. Compiler-owned static filters over structural keyed rows validate source references and keys once, retain removed rows as detached prototypes, clone fresh restoration nodes, and insert only new runs without moving retained DOM. This route-specific path is compiled out elsewhere. Compiler-owned collection state is excluded from development snapshot restoration. Arbitrary callbacks, mutation, asynchronous selectors, imported callback functions, prototype-sensitive reads, lazy/dynamic row state initializers, non-`null` or callback refs, and recursive component cycles fail during compilation.
|
|
35
37
|
|
|
36
38
|
The reduced `useReducer` form reuses ordinary state slots and React's pure reducer contract. A direct dispatch in a compiled handler becomes a functional `set` whose reducer is bundled from a relative TypeScript module into that handler graph. Pure reducer-owned keyed lists reuse unchanged item identities for reorder, one removal, and append fast paths; ordinary `useState` lists retain full validation. One direct dispatch prop into a same-file or relative-imported synchronous component, including a direct keyed row, is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. A reducer row reads the latest item through the existing list scope and uses the same multiple serializable state, effect, condition, and object-ref specialization as other keyed rows. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Missing primitive literal defaults in these reducer specializations are substituted at the same call site. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
|
|
37
39
|
|
package/framework/build.mjs
CHANGED
|
@@ -213,6 +213,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
213
213
|
const hasComplexListRowState = plans.some(plan => plan.lists.some(list => list.rowStates?.some(state => state.initialValue !== null && typeof state.initialValue === "object")))
|
|
214
214
|
const hasNestedLists = plans.some(plan => plan.lists.some(list => list.ownerField))
|
|
215
215
|
const hasCollectionSelectors = plans.some(plan => plan.lists.some(list => list.selector))
|
|
216
|
+
const hasStaticCollections = plans.some(plan => plan.lists.some(list => list.static))
|
|
216
217
|
const hasListIndexes = plans.some(plan => plan.lists.some(list => list.indexed))
|
|
217
218
|
const hasListStableFastPaths = plans.some(plan => plan.lists.some(list => !list.children && !list.ownerField && list.key !== null && !list.indexed && !list.reducer && !list.selector))
|
|
218
219
|
const hasGeneralListRowHooks = hasListRowRefs || hasComplexListRowState || plans.some(plan => plan.lists.some(list => list.ownerField && (list.rowStates?.length || list.rowRefs?.length)))
|
|
@@ -343,6 +344,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
343
344
|
__KUDZU_COMPLEX_LIST_ROW_STATE__: String(hasComplexListRowState),
|
|
344
345
|
__KUDZU_NESTED_LISTS__: String(hasNestedLists),
|
|
345
346
|
__KUDZU_COLLECTION_SELECTORS__: String(hasCollectionSelectors),
|
|
347
|
+
__KUDZU_STATIC_COLLECTIONS__: String(hasStaticCollections),
|
|
346
348
|
__KUDZU_LIST_INDEXES__: String(hasListIndexes),
|
|
347
349
|
__KUDZU_LIST_STABLE_FAST_PATHS__: String(hasListStableFastPaths)
|
|
348
350
|
})
|
|
@@ -1791,7 +1793,95 @@ function normalizeClsxSyntax(sourceFile, factory, context) {
|
|
|
1791
1793
|
return ts.visitNode(sourceFile, visitor)
|
|
1792
1794
|
}
|
|
1793
1795
|
|
|
1794
|
-
function
|
|
1796
|
+
function analyzeZustandStores(sourceFile) {
|
|
1797
|
+
const createNames = new Set()
|
|
1798
|
+
for (const statement of sourceFile.statements) {
|
|
1799
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "zustand") continue
|
|
1800
|
+
const bindings = statement.importClause?.namedBindings
|
|
1801
|
+
if (statement.importClause?.name || !bindings || !ts.isNamedImports(bindings)) throw sourceNodeError(statement, sourceFile, "Zustand migration input requires a named create import")
|
|
1802
|
+
for (const entry of bindings.elements) {
|
|
1803
|
+
if (entry.isTypeOnly) continue
|
|
1804
|
+
if ((entry.propertyName ?? entry.name).text !== "create") throw sourceNodeError(entry, sourceFile, "Only Zustand create is supported")
|
|
1805
|
+
createNames.add(entry.name.text)
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
const stores = new Map()
|
|
1809
|
+
if (!createNames.size) return stores
|
|
1810
|
+
for (const statement of sourceFile.statements) {
|
|
1811
|
+
if (!ts.isVariableStatement(statement) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
|
|
1812
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
1813
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer || !ts.isCallExpression(declaration.initializer) || !ts.isIdentifier(declaration.initializer.expression) || !createNames.has(declaration.initializer.expression.text)) continue
|
|
1814
|
+
const callback = declaration.initializer.arguments[0]
|
|
1815
|
+
if (declaration.initializer.arguments.length !== 1 || !callback || (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) || callback.parameters.length !== 1 || !ts.isIdentifier(callback.parameters[0].name) || callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(declaration.initializer, sourceFile, "Zustand create() requires one synchronous initializer with one set parameter")
|
|
1816
|
+
const body = unwrapExpression(callback.body)
|
|
1817
|
+
if (!ts.isObjectLiteralExpression(body)) throw sourceNodeError(callback.body, sourceFile, "Zustand create() initializer must return one object literal")
|
|
1818
|
+
const data = []
|
|
1819
|
+
const actions = new Map()
|
|
1820
|
+
for (const property of body.properties) {
|
|
1821
|
+
if (!ts.isPropertyAssignment(property) || !property.name || !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) throw sourceNodeError(property, sourceFile, "Zustand store entries must be ordinary properties")
|
|
1822
|
+
const name = property.name.text
|
|
1823
|
+
const value = unwrapExpression(property.initializer)
|
|
1824
|
+
if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) actions.set(name, value)
|
|
1825
|
+
else data.push({ name, value })
|
|
1826
|
+
}
|
|
1827
|
+
if (data.length !== 1 || !isSerializableStateLiteral(data[0].value)) throw sourceNodeError(body, sourceFile, "Zustand migration stores require exactly one directly serializable data property")
|
|
1828
|
+
if (!actions.size) throw sourceNodeError(body, sourceFile, "Zustand migration stores require at least one action")
|
|
1829
|
+
for (const [name, action] of actions) {
|
|
1830
|
+
if (action.asteriskToken || action.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(action, sourceFile, `Zustand action ${JSON.stringify(name)} must be synchronous`)
|
|
1831
|
+
const capture = [...nativeCaptureNames(action, new Map())].find(entry => entry !== callback.parameters[0].name.text)
|
|
1832
|
+
if (capture) throw sourceNodeError(action, sourceFile, `Zustand action ${JSON.stringify(name)} cannot capture ${JSON.stringify(capture)}`)
|
|
1833
|
+
const validateAction = node => {
|
|
1834
|
+
if (ts.isAwaitExpression(node) || ts.isYieldExpression(node) || ts.isNewExpression(node)) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} must be synchronous`)
|
|
1835
|
+
if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["then", "catch", "finally"].includes(node.expression.name.text)) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} cannot schedule asynchronous updates`)
|
|
1836
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === callback.parameters[0].name.text && !isShadowedIdentifier(node.expression, action)) {
|
|
1837
|
+
if (nearestFunction(node) !== action) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} must call set directly`)
|
|
1838
|
+
if (node.arguments.length !== 1) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} set() requires exactly one partial update`)
|
|
1839
|
+
}
|
|
1840
|
+
ts.forEachChild(node, validateAction)
|
|
1841
|
+
}
|
|
1842
|
+
validateAction(action.body)
|
|
1843
|
+
}
|
|
1844
|
+
stores.set(declaration.name.text, { name: declaration.name.text, setName: callback.parameters[0].name.text, field: data[0].name, initialValue: data[0].value, actions, declaration })
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
const visit = node => {
|
|
1848
|
+
const recognized = ts.isIdentifier(node) && ts.isCallExpression(node.parent) && node.parent.expression === node && [...stores.values()].some(store => store.declaration.initializer === node.parent)
|
|
1849
|
+
if (ts.isIdentifier(node) && createNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !recognized) throw sourceNodeError(node, sourceFile, "Zustand create must directly initialize an exported const store")
|
|
1850
|
+
ts.forEachChild(node, visit)
|
|
1851
|
+
}
|
|
1852
|
+
visit(sourceFile)
|
|
1853
|
+
return stores
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
function normalizeZustandMigrationSyntax(sourceFile, factory, context) {
|
|
1857
|
+
const stores = analyzeZustandStores(sourceFile)
|
|
1858
|
+
if (!stores.size) {
|
|
1859
|
+
const declaration = sourceFile.statements.find(statement => ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "zustand" && !statement.importClause?.isTypeOnly)
|
|
1860
|
+
if (declaration) throw sourceNodeError(declaration, sourceFile, "Zustand create must directly initialize an exported const store")
|
|
1861
|
+
return sourceFile
|
|
1862
|
+
}
|
|
1863
|
+
const identity = name => `${relative(sourceDirectory, sourceFile.fileName).replaceAll(sep, "/")}#${name}`
|
|
1864
|
+
const visitor = node => {
|
|
1865
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && stores.has(node.name.text)) {
|
|
1866
|
+
const store = stores.get(node.name.text)
|
|
1867
|
+
return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createCallExpression(factory.createIdentifier("__kCreateStore"), undefined, [
|
|
1868
|
+
factory.createStringLiteral(identity(store.name)),
|
|
1869
|
+
factory.createStringLiteral(store.field),
|
|
1870
|
+
store.initialValue,
|
|
1871
|
+
factory.createArrayLiteralExpression([...store.actions.keys()].map(name => factory.createStringLiteral(name)))
|
|
1872
|
+
]))
|
|
1873
|
+
}
|
|
1874
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "zustand") return undefined
|
|
1875
|
+
return ts.visitEachChild(node, visitor, context)
|
|
1876
|
+
}
|
|
1877
|
+
const normalized = ts.visitNode(sourceFile, visitor)
|
|
1878
|
+
const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports([factory.createImportSpecifier(false, undefined, factory.createIdentifier("__kCreateStore"))])), factory.createStringLiteral("@kudzujs/core"))
|
|
1879
|
+
const statements = [...normalized.statements]
|
|
1880
|
+
statements.splice(statements.findLastIndex(statement => ts.isImportDeclaration(statement)) + 1, 0, declaration)
|
|
1881
|
+
return factory.updateSourceFile(normalized, statements)
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
function normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections = new Set()) {
|
|
1795
1885
|
const supported = new Set(["createContext", "useContext", "useEffect", "useReducer", "useRef", "useState"])
|
|
1796
1886
|
const erased = new Set(["memo", "useCallback", "useMemo"])
|
|
1797
1887
|
const aliases = new Map()
|
|
@@ -1925,14 +2015,15 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
|
1925
2015
|
const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
|
|
1926
2016
|
const owner = nearestFunction(node)
|
|
1927
2017
|
const states = owner ? ownerStateNames(owner) : new Set()
|
|
1928
|
-
const
|
|
1929
|
-
if (!expression || !
|
|
1930
|
-
if (!
|
|
2018
|
+
const collection = expression && reactMemoCollection(expression, states, importedCollections, sourceFile)
|
|
2019
|
+
if (!expression || !collection && !isPureReactMemoExpression(expression)) throw sourceNodeError(callback.body, sourceFile, "React useMemo() callback must return one pure expression or analyzable collection pipeline")
|
|
2020
|
+
if (!collection) {
|
|
1931
2021
|
const unsupported = [...reactMemoReferenceNames(expression)].find(reference => !states.has(reference))
|
|
1932
2022
|
if (unsupported) throw sourceNodeError(expression, sourceFile, `React useMemo() pure expressions may only reference direct local state; found ${JSON.stringify(unsupported)}`)
|
|
1933
2023
|
}
|
|
1934
|
-
const
|
|
1935
|
-
|
|
2024
|
+
const collectionDependencies = collection ? new Set([...collection.selectorStates, ...(collection.static ? [] : [collection.state.text])]) : undefined
|
|
2025
|
+
const stale = collection
|
|
2026
|
+
? [...collectionDependencies].find(state => !dependencies.has(state))
|
|
1936
2027
|
: [...states].find(state => referenceIdentifiers(expression, state).length && !dependencies.has(state))
|
|
1937
2028
|
if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useMemo() must list captured state ${JSON.stringify(stale)} as a dependency`)
|
|
1938
2029
|
return ts.visitNode(expression, visitor)
|
|
@@ -2018,10 +2109,10 @@ function lowerReactMemoCollectionExpression(expression, factory) {
|
|
|
2018
2109
|
return visit(expression)
|
|
2019
2110
|
}
|
|
2020
2111
|
|
|
2021
|
-
function
|
|
2112
|
+
function reactMemoCollection(expression, states, importedCollections, sourceFile) {
|
|
2022
2113
|
const setters = new Map([...states].map(state => [state, state]))
|
|
2023
2114
|
const fail = (node, message) => { throw sourceNodeError(node, sourceFile, message) }
|
|
2024
|
-
return renderedCollectionSource(expression, setters, undefined, fail, new Set())
|
|
2115
|
+
return renderedCollectionSource(expression, setters, undefined, fail, new Set(), importedCollections, states)
|
|
2025
2116
|
}
|
|
2026
2117
|
|
|
2027
2118
|
function reactMemoComponentExpression(identifier, sourceFile, factory, context) {
|
|
@@ -2062,9 +2153,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2062
2153
|
return context => sourceFile => {
|
|
2063
2154
|
const factory = context.factory
|
|
2064
2155
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
2156
|
+
const importedCollections = importedSerializableCollectionNames(sourceFile, file, sourceFiles, sourceIndex)
|
|
2065
2157
|
sourceFile = normalizeClsxSyntax(sourceFile, factory, context)
|
|
2066
2158
|
ts.setParentRecursive(sourceFile, false)
|
|
2067
|
-
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context)
|
|
2159
|
+
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections)
|
|
2160
|
+
ts.setParentRecursive(sourceFile, false)
|
|
2161
|
+
sourceFile = normalizeZustandMigrationSyntax(sourceFile, factory, context)
|
|
2068
2162
|
ts.setParentRecursive(sourceFile, false)
|
|
2069
2163
|
sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
|
|
2070
2164
|
ts.setParentRecursive(sourceFile, false)
|
|
@@ -2077,7 +2171,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2077
2171
|
if (!imported) {
|
|
2078
2172
|
imported = normalizeClsxSyntax(parseSourceFile(target, sourceIndex.get(target)), factory, context)
|
|
2079
2173
|
ts.setParentRecursive(imported, false)
|
|
2080
|
-
imported = normalizeReactMigrationSyntax(imported, factory, context)
|
|
2174
|
+
imported = normalizeReactMigrationSyntax(imported, factory, context, importedSerializableCollectionNames(imported, target, sourceFiles, sourceIndex))
|
|
2175
|
+
ts.setParentRecursive(imported, false)
|
|
2176
|
+
imported = normalizeZustandMigrationSyntax(imported, factory, context)
|
|
2081
2177
|
ts.setParentRecursive(imported, false)
|
|
2082
2178
|
imported = normalizeRenderControlFlow(imported, factory, context)
|
|
2083
2179
|
ts.setParentRecursive(imported, false)
|
|
@@ -2087,6 +2183,16 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2087
2183
|
}
|
|
2088
2184
|
const settersByFunction = new Map()
|
|
2089
2185
|
const reducersByFunction = new Map()
|
|
2186
|
+
const zustandStores = new Map()
|
|
2187
|
+
const resolvedZustandStore = entry => {
|
|
2188
|
+
const exportName = entry.kind === "default" ? "default" : entry.imported
|
|
2189
|
+
const key = `${entry.target}:${exportName}`
|
|
2190
|
+
if (zustandStores.has(key)) return zustandStores.get(key)
|
|
2191
|
+
const targetSource = parseSourceFile(entry.target, sourceIndex.get(entry.target))
|
|
2192
|
+
const store = analyzeZustandStores(targetSource).get(exportName)
|
|
2193
|
+
zustandStores.set(key, store)
|
|
2194
|
+
return store
|
|
2195
|
+
}
|
|
2090
2196
|
const functions = new Map()
|
|
2091
2197
|
const components = new Map()
|
|
2092
2198
|
const contexts = new Set()
|
|
@@ -2111,6 +2217,26 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2111
2217
|
const collect = node => {
|
|
2112
2218
|
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
2113
2219
|
const callName = ts.isIdentifier(node.initializer.expression) ? node.initializer.expression.text : ""
|
|
2220
|
+
if (ts.isIdentifier(node.name) && callName && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace") {
|
|
2221
|
+
const storeImport = importBindings.get(callName)
|
|
2222
|
+
const store = resolvedZustandStore(storeImport)
|
|
2223
|
+
if (store) {
|
|
2224
|
+
const selector = node.initializer.arguments[0]
|
|
2225
|
+
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, "Zustand selectors must be direct arrows such as state => state.quantities")
|
|
2226
|
+
const selected = unwrapExpression(selector.body).name.text
|
|
2227
|
+
const owner = nearestFunction(node)
|
|
2228
|
+
if (!owner) throw sourceNodeError(node, sourceFile, "Zustand stores cannot be used outside a Kudzu component")
|
|
2229
|
+
const setters = settersByFunction.get(owner) ?? new Map()
|
|
2230
|
+
if (selected === store.field) setters.set(`__kStoreState_${node.name.text}`, node.name.text)
|
|
2231
|
+
else if (store.actions.has(selected)) {
|
|
2232
|
+
setters.set(node.name.text, node.name.text)
|
|
2233
|
+
const reducers = reducersByFunction.get(owner) ?? new Map()
|
|
2234
|
+
reducers.set(node.name.text, { state: node.name.text, store, action: selected })
|
|
2235
|
+
reducersByFunction.set(owner, reducers)
|
|
2236
|
+
} else throw sourceNodeError(unwrapExpression(selector.body).name, sourceFile, `Zustand store ${JSON.stringify(store.name)} has no supported property ${JSON.stringify(selected)}`)
|
|
2237
|
+
settersByFunction.set(owner, setters)
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2114
2240
|
if (callName === "useReducer") {
|
|
2115
2241
|
if (!ts.isArrayBindingPattern(node.name)) throw sourceNodeError(node.name, sourceFile, "useReducer() must use [state, dispatch] identifier destructuring")
|
|
2116
2242
|
const [stateElement, dispatchElement] = node.name.elements
|
|
@@ -2192,7 +2318,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2192
2318
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
2193
2319
|
for (const [name, entries] of declarations) {
|
|
2194
2320
|
for (const declaration of entries) {
|
|
2195
|
-
const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) })
|
|
2321
|
+
const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) }, new Set(), importedCollections)
|
|
2196
2322
|
if (!parts) continue
|
|
2197
2323
|
const uses = []
|
|
2198
2324
|
const collectUses = node => {
|
|
@@ -2386,7 +2512,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2386
2512
|
return
|
|
2387
2513
|
}
|
|
2388
2514
|
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
2389
|
-
const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction), jsxLocalDeclarations.get(nearestFunction(node)), fail)
|
|
2515
|
+
const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction), jsxLocalDeclarations.get(nearestFunction(node)), fail, new Set(), importedCollections)
|
|
2390
2516
|
if (parts) {
|
|
2391
2517
|
for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.add(declaration)
|
|
2392
2518
|
rawRenderedLists.push({ node, parts })
|
|
@@ -2702,14 +2828,16 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2702
2828
|
if (listParts) {
|
|
2703
2829
|
usesBehavior = true
|
|
2704
2830
|
usesList = true
|
|
2705
|
-
|
|
2831
|
+
const arguments_ = [
|
|
2706
2832
|
listParts.state,
|
|
2707
2833
|
listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
|
|
2708
2834
|
ts.visitNode(listParts.callback, visitor),
|
|
2709
2835
|
factory.createStringLiteral(listParts.ownerField ?? ""),
|
|
2710
2836
|
jsonExpression(listParts.selector ?? [], factory),
|
|
2711
2837
|
listParts.indexed ? factory.createTrue() : factory.createFalse()
|
|
2712
|
-
]
|
|
2838
|
+
]
|
|
2839
|
+
if (listParts.selectorStates?.size) arguments_.push(factory.createArrayLiteralExpression([...listParts.selectorStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
|
|
2840
|
+
return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
|
|
2713
2841
|
}
|
|
2714
2842
|
const conditional = conditionalParts(node.expression)
|
|
2715
2843
|
if (conditional) {
|
|
@@ -2895,11 +3023,11 @@ function containsRenderControl(root, knownLocals) {
|
|
|
2895
3023
|
return found
|
|
2896
3024
|
}
|
|
2897
3025
|
|
|
2898
|
-
function keyedListParts(expression, setters, declarations, fail, aliases = new Set()) {
|
|
3026
|
+
function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set()) {
|
|
2899
3027
|
const value = unwrapExpression(expression)
|
|
2900
3028
|
const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
|
|
2901
3029
|
if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
|
|
2902
|
-
const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases)
|
|
3030
|
+
const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()))
|
|
2903
3031
|
if (!collection?.state) return undefined
|
|
2904
3032
|
if (directFrom) collection.selector.push(["from", undefined])
|
|
2905
3033
|
const callback = directFrom ? value.arguments[1] : value.arguments[0]
|
|
@@ -2933,16 +3061,17 @@ function nestedKeyedListParts(expression, parentItem, fail) {
|
|
|
2933
3061
|
return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
|
|
2934
3062
|
}
|
|
2935
3063
|
|
|
2936
|
-
function renderedCollectionSource(expression, setters, declarations, fail, aliases) {
|
|
3064
|
+
function renderedCollectionSource(expression, setters, declarations, fail, aliases, importedCollections = new Set(), stateNames = new Set()) {
|
|
2937
3065
|
const value = unwrapExpression(expression)
|
|
2938
3066
|
if (ts.isIdentifier(value)) {
|
|
2939
|
-
if ([...setters.values()].includes(value.text)) return { state: value, selector: [] }
|
|
3067
|
+
if ([...setters.values()].includes(value.text)) return { state: value, selector: [], selectorStates: new Set() }
|
|
3068
|
+
if (importedCollections.has(value.text)) return { state: value, static: true, selector: [], selectorStates: new Set() }
|
|
2940
3069
|
const entries = declarations?.get(value.text)
|
|
2941
3070
|
if (!entries) return undefined
|
|
2942
3071
|
if (entries.length !== 1 || aliases.has(value.text) || entries[0].node.parent?.parent?.parent !== nearestFunction(entries[0].node)?.body) fail(value, `Rendered collection alias "${value.text}" must be one top-level immutable local`)
|
|
2943
3072
|
if (identifierReferenceCount(nearestFunction(entries[0].node).body, value.text) !== 1) fail(value, `Rendered collection alias "${value.text}" may only be rendered once`)
|
|
2944
3073
|
aliases.add(value.text)
|
|
2945
|
-
const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases)
|
|
3074
|
+
const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
2946
3075
|
aliases.delete(value.text)
|
|
2947
3076
|
return source && { ...source, aliasDeclarations: [...(source.aliasDeclarations ?? []), entries[0].node] }
|
|
2948
3077
|
}
|
|
@@ -2951,14 +3080,15 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
2951
3080
|
const method = value.expression.name.text
|
|
2952
3081
|
if (method === "filter") {
|
|
2953
3082
|
if (value.arguments.length !== 1) fail(value, "Rendered collection filter() requires one inline predicate")
|
|
2954
|
-
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases)
|
|
3083
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
2955
3084
|
if (!source) return undefined
|
|
2956
3085
|
const parameters = collectionParameters(value.arguments[0], "Rendered collection filter()", fail)
|
|
2957
|
-
|
|
3086
|
+
const selectorStates = new Set(source.selectorStates)
|
|
3087
|
+
return { ...source, selector: [...source.selector, ["filter", collectionExpression(unwrapExpression(value.arguments[0].body), parameters, fail, stateNames, selectorStates)]], selectorStates }
|
|
2958
3088
|
}
|
|
2959
3089
|
if (method === "flatMap") {
|
|
2960
3090
|
if (value.arguments.length !== 1) fail(value, "Rendered collection flatMap() requires one inline projector")
|
|
2961
|
-
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases)
|
|
3091
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
2962
3092
|
if (!source) return undefined
|
|
2963
3093
|
const parameters = collectionParameters(value.arguments[0], "Rendered collection flatMap()", fail)
|
|
2964
3094
|
const field = directProperty(value.arguments[0].body, parameters.item)
|
|
@@ -2969,12 +3099,14 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
2969
3099
|
}
|
|
2970
3100
|
if (isArrayFromCall(value)) {
|
|
2971
3101
|
if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered Array.from() requires an anchor and optional inline mapper")
|
|
2972
|
-
const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases)
|
|
3102
|
+
const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
2973
3103
|
if (!source) return undefined
|
|
2974
3104
|
let mapper
|
|
2975
3105
|
if (value.arguments[1]) {
|
|
2976
3106
|
const parameters = collectionParameters(value.arguments[1], "Rendered Array.from() mapper", fail)
|
|
2977
|
-
|
|
3107
|
+
const selectorStates = new Set(source.selectorStates)
|
|
3108
|
+
mapper = collectionExpression(unwrapExpression(value.arguments[1].body), parameters, fail, stateNames, selectorStates)
|
|
3109
|
+
source.selectorStates = selectorStates
|
|
2978
3110
|
}
|
|
2979
3111
|
return { ...source, selector: [...source.selector, ["from", mapper]] }
|
|
2980
3112
|
}
|
|
@@ -2989,7 +3121,7 @@ function collectionParameters(callback, label, fail) {
|
|
|
2989
3121
|
return { item: callback.parameters[0].name.text, index: callback.parameters[1]?.name.text }
|
|
2990
3122
|
}
|
|
2991
3123
|
|
|
2992
|
-
function collectionExpression(expression, parameters, fail) {
|
|
3124
|
+
function collectionExpression(expression, parameters, fail, stateNames = new Set(), selectorStates = new Set()) {
|
|
2993
3125
|
const encode = node => {
|
|
2994
3126
|
node = unwrapExpression(node)
|
|
2995
3127
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return ["value", ts.isNumericLiteral(node) ? Number(node.text) : node.text]
|
|
@@ -3000,6 +3132,10 @@ function collectionExpression(expression, parameters, fail) {
|
|
|
3000
3132
|
if (node.text === parameters.item) return ["item"]
|
|
3001
3133
|
if (node.text === parameters.index) return ["index"]
|
|
3002
3134
|
if (node.text === "undefined") return ["undefined"]
|
|
3135
|
+
if (stateNames.has(node.text)) {
|
|
3136
|
+
selectorStates.add(node.text)
|
|
3137
|
+
return ["state", node.text]
|
|
3138
|
+
}
|
|
3003
3139
|
fail(node, `Rendered collection expression identifier "${node.text}" is not allowed`)
|
|
3004
3140
|
}
|
|
3005
3141
|
if (ts.isPropertyAccessExpression(node)) {
|
|
@@ -3717,7 +3853,7 @@ function compileNativeCallback(expression, setters, reducers, factory, entries,
|
|
|
3717
3853
|
const allCaptures = nativeCaptureNames(expression, setters)
|
|
3718
3854
|
const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
|
|
3719
3855
|
const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
|
|
3720
|
-
imports.push(...[...usedReducers].map(name => reducers.get(name).import))
|
|
3856
|
+
imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
|
|
3721
3857
|
const captures = new Set([...allCaptures].filter(name => !importBindings.has(name)))
|
|
3722
3858
|
for (const entry of imports) clientImports.add(entry.target)
|
|
3723
3859
|
const usedStates = nativeStateNames(expression, setters)
|
|
@@ -4038,6 +4174,20 @@ function clientImportBindings(sourceFile, file, sourceFiles) {
|
|
|
4038
4174
|
return bindings
|
|
4039
4175
|
}
|
|
4040
4176
|
|
|
4177
|
+
function importedSerializableCollectionNames(sourceFile, file, sourceFiles, sourceIndex) {
|
|
4178
|
+
const names = new Set()
|
|
4179
|
+
for (const [name, binding] of clientImportBindings(sourceFile, file, sourceFiles)) {
|
|
4180
|
+
if (binding.kind !== "named") continue
|
|
4181
|
+
const imported = parseSourceFile(binding.target, sourceIndex.get(binding.target))
|
|
4182
|
+
for (const statement of imported.statements) {
|
|
4183
|
+
if (!ts.isVariableStatement(statement) || !(statement.declarationList.flags & ts.NodeFlags.Const) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
|
|
4184
|
+
const declaration = statement.declarationList.declarations.find(entry => ts.isIdentifier(entry.name) && entry.name.text === binding.imported)
|
|
4185
|
+
if (declaration?.initializer && ts.isArrayLiteralExpression(unwrapExpression(declaration.initializer)) && isSerializableStateLiteral(declaration.initializer)) names.add(name)
|
|
4186
|
+
}
|
|
4187
|
+
}
|
|
4188
|
+
return names
|
|
4189
|
+
}
|
|
4190
|
+
|
|
4041
4191
|
function resolveComponentExport(file, exportName, getSource, sourceFiles, trail = []) {
|
|
4042
4192
|
const key = `${file}:${exportName}`
|
|
4043
4193
|
if (trail.includes(key)) throw new Error(`Imported keyed list component re-export cycle: ${[...trail, key].map(entry => relative(root, entry.slice(0, entry.lastIndexOf(":")))).join(" -> ")}`)
|
|
@@ -4544,13 +4694,17 @@ function printNativeHandler({ exportName, expression, captures, setters, reducer
|
|
|
4544
4694
|
const transformer = context => root => {
|
|
4545
4695
|
const visitor = node => {
|
|
4546
4696
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
|
|
4697
|
+
const reducer = reducers.get(node.expression.text)
|
|
4698
|
+
if (reducer.store) return zustandActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)))
|
|
4547
4699
|
if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
|
|
4548
|
-
return reducerDispatch(factory,
|
|
4700
|
+
return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
|
|
4549
4701
|
}
|
|
4550
4702
|
if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
|
|
4703
|
+
if (reducers.get(node.name.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
|
|
4551
4704
|
return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
|
|
4552
4705
|
}
|
|
4553
4706
|
if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
|
|
4707
|
+
if (reducers.get(node.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
|
|
4554
4708
|
return reducerReference(factory, reducers.get(node.text))
|
|
4555
4709
|
}
|
|
4556
4710
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
|
|
@@ -4665,11 +4819,44 @@ function reducerReference(factory, reducer) {
|
|
|
4665
4819
|
}
|
|
4666
4820
|
|
|
4667
4821
|
function reducerDispatch(factory, reducer, action) {
|
|
4822
|
+
if (reducer.store) return zustandActionDispatch(factory, reducer, [action])
|
|
4668
4823
|
const previous = factory.createUniqueName("__kPrevious")
|
|
4669
4824
|
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]))
|
|
4670
4825
|
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
|
|
4671
4826
|
}
|
|
4672
4827
|
|
|
4828
|
+
function zustandActionDispatch(factory, reducer, args) {
|
|
4829
|
+
const previous = factory.createUniqueName("__kPrevious")
|
|
4830
|
+
const current = factory.createUniqueName("__kStore")
|
|
4831
|
+
const updateValue = factory.createUniqueName("__kUpdate")
|
|
4832
|
+
const partial = factory.createUniqueName("__kPartial")
|
|
4833
|
+
const action = factory.createUniqueName("__kAction")
|
|
4834
|
+
const set = factory.createIdentifier(reducer.store.setName)
|
|
4835
|
+
const merge = factory.createExpressionStatement(factory.createBinaryExpression(current, factory.createToken(ts.SyntaxKind.EqualsToken), factory.createObjectLiteralExpression([
|
|
4836
|
+
factory.createSpreadAssignment(current),
|
|
4837
|
+
factory.createSpreadAssignment(partial)
|
|
4838
|
+
])))
|
|
4839
|
+
const setBody = factory.createBlock([
|
|
4840
|
+
factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(partial, undefined, undefined, factory.createConditionalExpression(
|
|
4841
|
+
factory.createBinaryExpression(factory.createTypeOfExpression(updateValue), factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), factory.createStringLiteral("function")),
|
|
4842
|
+
undefined,
|
|
4843
|
+
factory.createCallExpression(updateValue, undefined, [current]),
|
|
4844
|
+
undefined,
|
|
4845
|
+
updateValue
|
|
4846
|
+
))], ts.NodeFlags.Const)),
|
|
4847
|
+
merge
|
|
4848
|
+
], true)
|
|
4849
|
+
const body = factory.createBlock([
|
|
4850
|
+
factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(current, undefined, undefined, factory.createObjectLiteralExpression([factory.createPropertyAssignment(reducer.store.field, previous)]))], ts.NodeFlags.Let)),
|
|
4851
|
+
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)),
|
|
4852
|
+
factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(action, undefined, undefined, synthesizeTree(reducer.store.actions.get(reducer.action)))], ts.NodeFlags.Const)),
|
|
4853
|
+
factory.createExpressionStatement(factory.createCallExpression(action, undefined, args)),
|
|
4854
|
+
factory.createReturnStatement(factory.createPropertyAccessExpression(current, reducer.store.field))
|
|
4855
|
+
], true)
|
|
4856
|
+
const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), body)
|
|
4857
|
+
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
|
|
4858
|
+
}
|
|
4859
|
+
|
|
4673
4860
|
function printReactiveBinding({ exportName, expression, captures, states }) {
|
|
4674
4861
|
const factory = ts.factory
|
|
4675
4862
|
const transformer = context => root => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export function selectCollection(anchor, selector = []) {
|
|
1
|
+
export function selectCollection(anchor, selector = [], readState) {
|
|
2
2
|
let values = anchor == null ? [] : anchor
|
|
3
3
|
for (const operation of selector) {
|
|
4
|
-
if (operation[0] === "from") values = Array.from(values, operation[1] ? (item, index) => evaluateCollectionExpression(operation[1], item, index) : undefined)
|
|
4
|
+
if (operation[0] === "from") values = Array.from(values, operation[1] ? (item, index) => evaluateCollectionExpression(operation[1], item, index, readState) : undefined)
|
|
5
5
|
else {
|
|
6
6
|
if (!Array.isArray(values)) throw new Error("Rendered collection source must remain an array")
|
|
7
|
-
if (operation[0] === "filter") values = values.filter((item, index) => evaluateCollectionExpression(operation[1], item, index))
|
|
7
|
+
if (operation[0] === "filter") values = values.filter((item, index) => evaluateCollectionExpression(operation[1], item, index, readState))
|
|
8
8
|
else if (operation[0] === "flatMap") values = values.flatMap(item => item?.[operation[1]] ?? [])
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -12,29 +12,33 @@ export function selectCollection(anchor, selector = []) {
|
|
|
12
12
|
return values
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function evaluateCollectionExpression(expression, item, index) {
|
|
15
|
+
function evaluateCollectionExpression(expression, item, index, readState) {
|
|
16
16
|
const [kind, ...parts] = expression
|
|
17
17
|
if (kind === "value") return parts[0]
|
|
18
18
|
if (kind === "undefined") return undefined
|
|
19
19
|
if (kind === "item") return item
|
|
20
20
|
if (kind === "index") return index
|
|
21
|
+
if (kind === "state") {
|
|
22
|
+
if (!readState) throw new Error(`Rendered collection state ${JSON.stringify(parts[0])} is not available`)
|
|
23
|
+
return readState(parts[0])
|
|
24
|
+
}
|
|
21
25
|
if (kind === "get") {
|
|
22
|
-
const object = evaluateCollectionExpression(parts[0], item, index)
|
|
26
|
+
const object = evaluateCollectionExpression(parts[0], item, index, readState)
|
|
23
27
|
return object == null && parts[2] ? undefined : object[parts[1]]
|
|
24
28
|
}
|
|
25
29
|
if (kind === "unary") {
|
|
26
|
-
const value = evaluateCollectionExpression(parts[1], item, index)
|
|
30
|
+
const value = evaluateCollectionExpression(parts[1], item, index, readState)
|
|
27
31
|
if (parts[0] === "!") return !value
|
|
28
32
|
if (parts[0] === "+") return +value
|
|
29
33
|
if (parts[0] === "-") return -value
|
|
30
34
|
if (parts[0] === "typeof") return typeof value
|
|
31
35
|
}
|
|
32
36
|
if (kind === "binary") {
|
|
33
|
-
const left = evaluateCollectionExpression(parts[1], item, index)
|
|
34
|
-
if (parts[0] === "&&") return left && evaluateCollectionExpression(parts[2], item, index)
|
|
35
|
-
if (parts[0] === "||") return left || evaluateCollectionExpression(parts[2], item, index)
|
|
36
|
-
if (parts[0] === "??") return left ?? evaluateCollectionExpression(parts[2], item, index)
|
|
37
|
-
const right = evaluateCollectionExpression(parts[2], item, index)
|
|
37
|
+
const left = evaluateCollectionExpression(parts[1], item, index, readState)
|
|
38
|
+
if (parts[0] === "&&") return left && evaluateCollectionExpression(parts[2], item, index, readState)
|
|
39
|
+
if (parts[0] === "||") return left || evaluateCollectionExpression(parts[2], item, index, readState)
|
|
40
|
+
if (parts[0] === "??") return left ?? evaluateCollectionExpression(parts[2], item, index, readState)
|
|
41
|
+
const right = evaluateCollectionExpression(parts[2], item, index, readState)
|
|
38
42
|
if (parts[0] === "===") return left === right
|
|
39
43
|
if (parts[0] === "!==") return left !== right
|
|
40
44
|
if (parts[0] === "==") return left == right
|
|
@@ -49,15 +53,15 @@ function evaluateCollectionExpression(expression, item, index) {
|
|
|
49
53
|
if (parts[0] === "/") return left / right
|
|
50
54
|
if (parts[0] === "%") return left % right
|
|
51
55
|
}
|
|
52
|
-
if (kind === "conditional") return evaluateCollectionExpression(parts[0], item, index) ? evaluateCollectionExpression(parts[1], item, index) : evaluateCollectionExpression(parts[2], item, index)
|
|
53
|
-
if (kind === "array") return parts.map(value => evaluateCollectionExpression(value, item, index))
|
|
54
|
-
if (kind === "object") return Object.fromEntries(parts.map(([key, value]) => [key, evaluateCollectionExpression(value, item, index)]))
|
|
55
|
-
if (kind === "template") return parts[0].map((text, offset) => text + (offset < parts[1].length ? evaluateCollectionExpression(parts[1][offset], item, index) : "")).join("")
|
|
56
|
+
if (kind === "conditional") return evaluateCollectionExpression(parts[0], item, index, readState) ? evaluateCollectionExpression(parts[1], item, index, readState) : evaluateCollectionExpression(parts[2], item, index, readState)
|
|
57
|
+
if (kind === "array") return parts.map(value => evaluateCollectionExpression(value, item, index, readState))
|
|
58
|
+
if (kind === "object") return Object.fromEntries(parts.map(([key, value]) => [key, evaluateCollectionExpression(value, item, index, readState)]))
|
|
59
|
+
if (kind === "template") return parts[0].map((text, offset) => text + (offset < parts[1].length ? evaluateCollectionExpression(parts[1][offset], item, index, readState) : "")).join("")
|
|
56
60
|
if (kind === "call") {
|
|
57
|
-
const receiver = evaluateCollectionExpression(parts[0], item, index)
|
|
58
|
-
return receiver[parts[1]](...parts.slice(2).map(value => evaluateCollectionExpression(value, item, index)))
|
|
61
|
+
const receiver = evaluateCollectionExpression(parts[0], item, index, readState)
|
|
62
|
+
return receiver[parts[1]](...parts.slice(2).map(value => evaluateCollectionExpression(value, item, index, readState)))
|
|
59
63
|
}
|
|
60
|
-
if (kind === "global") return globalThis[parts[0]](...parts.slice(1).map(value => evaluateCollectionExpression(value, item, index)))
|
|
61
|
-
if (kind === "math") return Math[parts[0]](...parts.slice(1).map(value => evaluateCollectionExpression(value, item, index)))
|
|
64
|
+
if (kind === "global") return globalThis[parts[0]](...parts.slice(1).map(value => evaluateCollectionExpression(value, item, index, readState)))
|
|
65
|
+
if (kind === "math") return Math[parts[0]](...parts.slice(1).map(value => evaluateCollectionExpression(value, item, index, readState)))
|
|
62
66
|
throw new Error(`Unsupported rendered collection expression: ${String(kind)}`)
|
|
63
67
|
}
|
package/framework/core.d.ts
CHANGED
|
@@ -31,7 +31,7 @@ export function nativeBehavior(module: string, handler: string, states: Array<[s
|
|
|
31
31
|
export function binding(value: unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
|
|
32
32
|
export function bindingValue(value: unknown): unknown
|
|
33
33
|
export function conditional(kind: "and" | "ternary", value: unknown, truthy: () => unknown, falsy: () => unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
|
|
34
|
-
export function list(items: unknown, keyField: string | null, render: (item: unknown, index: number) => unknown, ownerField?: string, selector?: unknown[], indexed?: boolean): unknown
|
|
34
|
+
export function list(items: unknown, keyField: string | null, render: (item: unknown, index: number) => unknown, ownerField?: string, selector?: unknown[], indexed?: boolean, selectorStates?: Array<[string, unknown]>): unknown
|
|
35
35
|
export function listField(read: () => unknown, field: string): unknown
|
|
36
36
|
export function listExpression(read: () => unknown, module: string, handler: string): unknown
|
|
37
37
|
export function listItem(): unknown
|
|
@@ -89,7 +89,7 @@ export function renderPage<Props = Record<string, never>>(
|
|
|
89
89
|
hasStateSeed: boolean
|
|
90
90
|
handlerModules: string[]
|
|
91
91
|
plan: {
|
|
92
|
-
states: Array<{ id: string; name: string; initialValue: unknown; lifetime?: "layout" | "route" }>
|
|
92
|
+
states: Array<{ id: string; name: string; initialValue: unknown; lifetime?: "layout" | "route"; internal?: true }>
|
|
93
93
|
params: Array<{ name: string; id: string }>
|
|
94
94
|
events: Array<{
|
|
95
95
|
event: string
|
package/framework/core.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { serializeStyle } from "./style.js"
|
|
|
2
2
|
import { selectCollection } from "./collection-selector.js"
|
|
3
3
|
|
|
4
4
|
const signalMarker = Symbol("kudzu.signal")
|
|
5
|
+
const internalStateMarker = Symbol("kudzu.internal-state")
|
|
5
6
|
const setterMarker = Symbol("kudzu.setter")
|
|
6
7
|
const reducerDispatchMarker = Symbol("kudzu.reducerDispatch")
|
|
7
8
|
const reducerStateMarker = Symbol("kudzu.reducerState")
|
|
@@ -73,6 +74,32 @@ export function useReducer(reducer, initialValue, name) {
|
|
|
73
74
|
return [state, dispatch]
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
export function __kCreateStore(name, field, initialValue, actions) {
|
|
78
|
+
return selector => {
|
|
79
|
+
if (!renderContext) throw new Error("Zustand stores can only be read while rendering a Kudzu component")
|
|
80
|
+
let store = renderContext.stores.get(name)
|
|
81
|
+
if (!store) {
|
|
82
|
+
if (renderContext.scoped && renderContext.renderScope !== "layout") throw new Error(`Zustand store ${JSON.stringify(name)} must be initialized by the shared layout before route components use it`)
|
|
83
|
+
if (renderContext.listRoot || renderContext.listRowRoot || renderContext.listTemplate) throw new Error(`Zustand store ${JSON.stringify(name)} cannot be initialized inside a keyed row`)
|
|
84
|
+
const [state] = useState(initialValue, `${name}.${field}`)
|
|
85
|
+
store = { [field]: state }
|
|
86
|
+
for (const action of actions) {
|
|
87
|
+
const marker = () => {
|
|
88
|
+
throw new Error("Zustand actions are compiled into browser handlers")
|
|
89
|
+
}
|
|
90
|
+
Object.defineProperties(marker, {
|
|
91
|
+
[signalMarker]: { value: true },
|
|
92
|
+
id: { value: state.id },
|
|
93
|
+
value: { get: () => state.value }
|
|
94
|
+
})
|
|
95
|
+
store[action] = marker
|
|
96
|
+
}
|
|
97
|
+
renderContext.stores.set(name, store)
|
|
98
|
+
}
|
|
99
|
+
return selector(store)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
76
103
|
export function useParams() {
|
|
77
104
|
if (renderContext?.renderScope === "layout") throw new Error("useParams() is only supported in route scope")
|
|
78
105
|
if (!renderContext?.runtimeParamNames?.length) throw new Error("useParams() requires export const runtimeParams = true on a bracket page")
|
|
@@ -104,6 +131,14 @@ function createSignal(id, value) {
|
|
|
104
131
|
}
|
|
105
132
|
}
|
|
106
133
|
|
|
134
|
+
function createInternalState(initialValue) {
|
|
135
|
+
const id = nextRenderId("s")
|
|
136
|
+
const signal = createSignal(id, initialValue)
|
|
137
|
+
signal[internalStateMarker] = true
|
|
138
|
+
renderContext.states[id] = { name: id, initialValue, internal: true, ...(renderContext.scoped ? { lifetime: renderContext.renderScope } : {}) }
|
|
139
|
+
return signal
|
|
140
|
+
}
|
|
141
|
+
|
|
107
142
|
export function useEffect(callback, dependencies, module, handler, states, scope, source, cleanup, itemDependencies = []) {
|
|
108
143
|
if (!renderContext) throw new Error("useEffect() can only run while rendering a Kudzu component")
|
|
109
144
|
if (typeof callback !== "function" || !Array.isArray(dependencies) || !module || !handler) throw new Error("useEffect() must be compiled with a literal dependency array")
|
|
@@ -221,8 +256,11 @@ export function stateConditional(kind, state, truthy, falsy) {
|
|
|
221
256
|
return { [conditionalMarker]: true, kind, value: state.value, truthy, falsy, state: state.id }
|
|
222
257
|
}
|
|
223
258
|
|
|
224
|
-
export function list(items, keyField, render, ownerField, selector = [], indexed = false) {
|
|
225
|
-
if (
|
|
259
|
+
export function list(items, keyField, render, ownerField, selector = [], indexed = false, selectorStates = []) {
|
|
260
|
+
if (Array.isArray(items)) items = createInternalState(items)
|
|
261
|
+
if (!items?.[signalMarker] || !Array.isArray(items.value)) throw new Error("A keyed list must use local array state or a supported imported static array")
|
|
262
|
+
const selectorStateMap = new Map(selectorStates)
|
|
263
|
+
for (const [name, state] of selectorStateMap) if (!state?.[signalMarker]) throw new Error(`Rendered collection selector state ${JSON.stringify(name)} must be framework state`)
|
|
226
264
|
let values = items.value
|
|
227
265
|
if (ownerField) {
|
|
228
266
|
const owner = renderContext?.listRoot ?? renderContext?.listRowRoot
|
|
@@ -231,7 +269,7 @@ export function list(items, keyField, render, ownerField, selector = [], indexed
|
|
|
231
269
|
values = renderContext.listTemplate ? [] : owner.item?.[ownerField]
|
|
232
270
|
if (!Array.isArray(values) && values != null) throw new Error(`Nested keyed list property "${ownerField}" must remain an array`)
|
|
233
271
|
}
|
|
234
|
-
values = selectCollection(values, selector)
|
|
272
|
+
values = selectCollection(values, selector, name => selectorStateMap.get(name)?.value)
|
|
235
273
|
const keys = new Set()
|
|
236
274
|
for (const [index, item] of values.entries()) {
|
|
237
275
|
const key = keyField === null ? index : item?.[keyField]
|
|
@@ -242,7 +280,7 @@ export function list(items, keyField, render, ownerField, selector = [], indexed
|
|
|
242
280
|
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
243
281
|
keys.add(token)
|
|
244
282
|
}
|
|
245
|
-
return { [listMarker]: true, items, values, keyField, render, ownerField, selector, indexed }
|
|
283
|
+
return { [listMarker]: true, items, values, keyField, render, ownerField, selector, indexed, selectorStates }
|
|
246
284
|
}
|
|
247
285
|
|
|
248
286
|
export function listField(read, field) {
|
|
@@ -368,7 +406,7 @@ function serializeCapture(name, value, seen) {
|
|
|
368
406
|
}
|
|
369
407
|
|
|
370
408
|
export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
371
|
-
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerModules: new Set(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
409
|
+
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], stores: new Map(), states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerModules: new Set(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
372
410
|
|
|
373
411
|
try {
|
|
374
412
|
const page = { [routeScopeMarker]: true, component, props }
|
|
@@ -775,7 +813,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
775
813
|
const ownerTemplate = Boolean(node.ownerField && renderContext.listTemplate)
|
|
776
814
|
const rowList = node.ownerField ? nextRowList() : undefined
|
|
777
815
|
const id = rowList?.id ?? nextRenderId("l")
|
|
778
|
-
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.values.map((item, index) => node.keyField === null ? index : item[node.keyField]), ...(node.ownerField ? { ownerField: node.ownerField } : {}), ...(node.selector.length ? { selector: node.selector } : {}), ...(node.indexed ? { indexed: true } : {}), ...(!node.selector.length && node.keyField !== null && node.items[reducerStateMarker] ? { reducer: true } : {}) }
|
|
816
|
+
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.values.map((item, index) => node.keyField === null ? index : item[node.keyField]), ...(node.items[internalStateMarker] ? { static: true } : {}), ...(node.ownerField ? { ownerField: node.ownerField } : {}), ...(node.selector.length ? { selector: node.selector } : {}), ...(node.selectorStates.length ? { selectorStates: Object.fromEntries(node.selectorStates.map(([name, state]) => [name, state.id])) } : {}), ...(node.indexed ? { indexed: true } : {}), ...(!node.selector.length && node.keyField !== null && node.items[reducerStateMarker] ? { reducer: true } : {}) }
|
|
779
817
|
if (ownerTemplate) {
|
|
780
818
|
ownerRoot.descriptor.children ??= []
|
|
781
819
|
ownerRoot.descriptor.children.push({ id, field: node.ownerField, key: node.keyField, ...(node.selector.length ? { selector: node.selector } : {}) })
|
package/framework/dev-state.js
CHANGED
|
@@ -2,9 +2,9 @@ const maxAge = 10000
|
|
|
2
2
|
|
|
3
3
|
export function stateSchema(states) {
|
|
4
4
|
const occurrences = new Map()
|
|
5
|
-
for (const { name } of states) if (typeof name === "string") occurrences.set(name, (occurrences.get(name) ?? 0) + 1)
|
|
6
|
-
return states.flatMap(({ id, name }) => {
|
|
7
|
-
return typeof id === "string" && occurrences.get(name) === 1 ? [[id, name]] : []
|
|
5
|
+
for (const { name, internal } of states) if (!internal && typeof name === "string") occurrences.set(name, (occurrences.get(name) ?? 0) + 1)
|
|
6
|
+
return states.flatMap(({ id, name, internal }) => {
|
|
7
|
+
return !internal && typeof id === "string" && occurrences.get(name) === 1 ? [[id, name]] : []
|
|
8
8
|
})
|
|
9
9
|
}
|
|
10
10
|
|
|
@@ -47,6 +47,7 @@ function mountLists(root) {
|
|
|
47
47
|
const templateRoot = __KUDZU_NESTED_LISTS__ ? nested.templateRoot : start.content.firstElementChild
|
|
48
48
|
if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeGeneralRowHooks(descriptor, descriptor.keys[index], roots[index], nested?.owner)
|
|
49
49
|
const parts = listItemPartPlan(templateRoot, descriptor.nested)
|
|
50
|
+
const staticRows = __KUDZU_STATIC_COLLECTIONS__ && descriptor.static && parts.directFill ? new Map() : undefined
|
|
50
51
|
for (const root of roots) {
|
|
51
52
|
if (__KUDZU_LIST_CONDITIONS__ && descriptor.conditions) {
|
|
52
53
|
if (__KUDZU_NESTED_LISTS__ && descriptor.ownerField) itemPartPlans.set(root, parts)
|
|
@@ -56,11 +57,16 @@ function mountLists(root) {
|
|
|
56
57
|
}
|
|
57
58
|
if (__KUDZU_LIST_SEEDS__ && descriptor.seed && !browserState.has(descriptor.state)) browserState.set(descriptor.state, roots.map((root, index) => seedListItem(root, descriptor, index)))
|
|
58
59
|
const items = browserState.get(descriptor.state)
|
|
60
|
+
const staticEntries = __KUDZU_STATIC_COLLECTIONS__ && descriptor.static && descriptor.key !== null && !descriptor.indexed && descriptor.selector?.every(operation => operation[0] === "filter") && parts.directFill
|
|
61
|
+
? cacheStaticListEntries(descriptor, items)
|
|
62
|
+
: undefined
|
|
59
63
|
const list = {
|
|
60
64
|
start,
|
|
61
65
|
descriptor,
|
|
62
66
|
...(__KUDZU_NESTED_LISTS__ ? { templateRoot, ...(nested.childPrototypes?.size ? { childPrototypes: nested.childPrototypes } : {}) } : {}),
|
|
63
67
|
parts,
|
|
68
|
+
...(__KUDZU_STATIC_COLLECTIONS__ && staticRows ? { staticRows } : {}),
|
|
69
|
+
...(__KUDZU_STATIC_COLLECTIONS__ && staticEntries ? { staticEntries, staticPositions: staticEntries.positions } : {}),
|
|
64
70
|
seedFields: __KUDZU_LIST_SEEDS__ && descriptor.seed && Object.keys(descriptor.seed),
|
|
65
71
|
roots: new Map(roots.map((node, index) => [keyToken(descriptor.keys[index]), node])),
|
|
66
72
|
...(__KUDZU_LIST_STABLE_FAST_PATHS__ ? { orderedRoots: roots } : {}),
|
|
@@ -78,8 +84,14 @@ function mountLists(root) {
|
|
|
78
84
|
ownedLists.set(list.owner, lists)
|
|
79
85
|
listRegistrations.set(start, { list, owner: list.owner })
|
|
80
86
|
} else {
|
|
81
|
-
|
|
82
|
-
|
|
87
|
+
if (__KUDZU_COLLECTION_SELECTORS__ && descriptor.selectorStates) {
|
|
88
|
+
const states = [...new Set([descriptor.state, ...Object.values(descriptor.selectorStates)])]
|
|
89
|
+
for (const state of states) register(listTargets, state, list)
|
|
90
|
+
listRegistrations.set(start, { states, list })
|
|
91
|
+
} else {
|
|
92
|
+
register(listTargets, descriptor.state, list)
|
|
93
|
+
listRegistrations.set(start, { state: descriptor.state, list })
|
|
94
|
+
}
|
|
83
95
|
}
|
|
84
96
|
updateList(list)
|
|
85
97
|
}
|
|
@@ -97,9 +109,17 @@ function unregisterList(start) {
|
|
|
97
109
|
if (lists?.get(registration.list.descriptor.id) === registration.list) lists.delete(registration.list.descriptor.id)
|
|
98
110
|
if (!lists?.size) ownedLists.delete(registration.owner)
|
|
99
111
|
} else {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
if (__KUDZU_COLLECTION_SELECTORS__ && registration.states) {
|
|
113
|
+
for (const state of registration.states) {
|
|
114
|
+
const lists = listTargets.get(state)
|
|
115
|
+
lists?.delete(registration.list)
|
|
116
|
+
if (!lists?.size) listTargets.delete(state)
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
const lists = listTargets.get(registration.state)
|
|
120
|
+
lists?.delete(registration.list)
|
|
121
|
+
if (!lists?.size) listTargets.delete(registration.state)
|
|
122
|
+
}
|
|
103
123
|
}
|
|
104
124
|
if (__KUDZU_LIST_ROW_HOOKS__ && registration.list.descriptor.rowStates) for (const node of registration.list.roots.values()) deleteRowStates(registration.list.descriptor, ownershipPaths.get(node))
|
|
105
125
|
}
|
|
@@ -112,12 +132,13 @@ function updateList(list) {
|
|
|
112
132
|
? listItems.get(list.owner)?.[list.descriptor.ownerField]
|
|
113
133
|
: browserState.get(list.descriptor.state)
|
|
114
134
|
if (__KUDZU_NESTED_LISTS__ && list.descriptor.ownerField && items == null) items = []
|
|
115
|
-
if (__KUDZU_COLLECTION_SELECTORS__) items = selectCollection(items, list.descriptor.selector)
|
|
135
|
+
if (__KUDZU_COLLECTION_SELECTORS__) items = selectCollection(items, list.descriptor.selector, name => browserState.get(list.descriptor.selectorStates?.[name]))
|
|
116
136
|
if (!Array.isArray(items)) throw new Error(list.descriptor.ownerField ? `Nested keyed list property "${list.descriptor.ownerField}" must remain an array` : "Keyed list state must remain an array")
|
|
117
137
|
if (__KUDZU_NESTED_LISTS__ && (list.descriptor.children || list.descriptor.ownerField) && !list.descriptor.indexed && !list.descriptor.selector && list.descriptor.key !== null && list.items && updateNestedList(list, items)) return
|
|
118
138
|
if (__KUDZU_NESTED_LISTS__ && list.descriptor.children) validateChildLists(items, list.descriptor.children)
|
|
119
139
|
if (list.descriptor.reducer && !list.descriptor.selector && list.descriptor.key !== null && list.items && updateReducerList(list, items)) return
|
|
120
140
|
if (__KUDZU_LIST_STABLE_FAST_PATHS__ && list.descriptor.key !== null && !list.descriptor.indexed && !list.descriptor.selector && !list.descriptor.reducer && !list.descriptor.children && !list.descriptor.ownerField && list.items && updateStableList(list, items)) return
|
|
141
|
+
if (__KUDZU_STATIC_COLLECTIONS__ && list.staticEntries && updateStaticFilterList(list, items)) return
|
|
121
142
|
const entries = []
|
|
122
143
|
const keys = new Set()
|
|
123
144
|
const seen = new Set()
|
|
@@ -174,14 +195,18 @@ function updateList(list) {
|
|
|
174
195
|
for (const { item, index, key, token, value } of entries) {
|
|
175
196
|
let node = list.roots.get(token)
|
|
176
197
|
if (!node) {
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
const staticRoot = __KUDZU_STATIC_COLLECTIONS__ && list.staticRows?.get(token)?.cloneNode(true)
|
|
199
|
+
node = staticRoot ?? (__KUDZU_NESTED_LISTS__ ? list.templateRoot : list.start.content.firstElementChild)?.cloneNode(true)
|
|
200
|
+
if (!staticRoot && node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
179
201
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
180
202
|
node.removeAttribute("data-k-list-root")
|
|
181
203
|
if (__KUDZU_NESTED_LISTS__ && list.childPrototypes) childPrototypes.set(node, list.childPrototypes)
|
|
182
204
|
if (__KUDZU_LIST_ROW_HOOKS__) initializeGeneralRowHooks(list.descriptor, key, node, list.owner)
|
|
183
|
-
|
|
184
|
-
|
|
205
|
+
if (staticRoot) listItems.set(node, item)
|
|
206
|
+
else if (list.parts.directFill) {
|
|
207
|
+
listItems.set(node, item)
|
|
208
|
+
fillStructuralListParts(list.parts, node, item)
|
|
209
|
+
} else fillListItem(node, item, list.descriptor.nested, index, mapListItemParts(list.parts, node, list.descriptor.nested))
|
|
185
210
|
additions.append(node)
|
|
186
211
|
added = true
|
|
187
212
|
} else if ((referenceOnly ? listItems.get(node) !== item : list.values.get(token) !== value) || list.descriptor.indexed || list.descriptor.key === null) {
|
|
@@ -199,20 +224,38 @@ function updateList(list) {
|
|
|
199
224
|
} else node.remove()
|
|
200
225
|
if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, ownershipPaths.get(node))
|
|
201
226
|
}
|
|
227
|
+
let ordered = false
|
|
202
228
|
if (added) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
229
|
+
const addedNodes = __KUDZU_LIST_MOUNTS__ && list.descriptor.mount ? [...additions.childNodes] : undefined
|
|
230
|
+
let seenAddition = false
|
|
231
|
+
let interleaved = false
|
|
232
|
+
for (const [, node] of next) {
|
|
233
|
+
if (node.parentNode === additions) seenAddition = true
|
|
234
|
+
else if (seenAddition) {
|
|
235
|
+
interleaved = true
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (interleaved) {
|
|
240
|
+
const run = parent.ownerDocument.createDocumentFragment()
|
|
241
|
+
for (const [, node] of next) {
|
|
242
|
+
if (node.parentNode === additions) run.append(node)
|
|
243
|
+
else if (run.firstChild) parent.insertBefore(run, node)
|
|
244
|
+
}
|
|
245
|
+
if (run.firstChild) parent.insertBefore(run, list.boundary)
|
|
246
|
+
ordered = true
|
|
207
247
|
} else parent.insertBefore(additions, list.boundary)
|
|
248
|
+
if (addedNodes) for (const node of addedNodes) mountDom(node)
|
|
208
249
|
list.container ??= parent
|
|
209
250
|
}
|
|
210
251
|
let anchor = list.boundary
|
|
211
252
|
let misplaced = 0
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
253
|
+
if (!ordered) {
|
|
254
|
+
for (let index = next.length - 1; index >= 0; index--) {
|
|
255
|
+
const node = next[index][1]
|
|
256
|
+
if (node.nextSibling !== anchor) misplaced++
|
|
257
|
+
anchor = node
|
|
258
|
+
}
|
|
216
259
|
}
|
|
217
260
|
if (misplaced > next.length / 2) {
|
|
218
261
|
const reordered = parent.ownerDocument.createDocumentFragment()
|
|
@@ -232,6 +275,68 @@ function updateList(list) {
|
|
|
232
275
|
list.items = items
|
|
233
276
|
}
|
|
234
277
|
|
|
278
|
+
function updateStaticFilterList(list, items) {
|
|
279
|
+
const entries = new Array(items.length)
|
|
280
|
+
for (let index = 0; index < items.length; index++) {
|
|
281
|
+
const entry = list.staticEntries.get(items[index])
|
|
282
|
+
if (!entry || !list.roots.has(entry.token) && !list.staticRows.has(entry.token)) return false
|
|
283
|
+
entries[index] = entry
|
|
284
|
+
}
|
|
285
|
+
let selectedIndex = 0
|
|
286
|
+
for (const [token, node] of list.roots) {
|
|
287
|
+
const position = list.staticPositions.get(token)
|
|
288
|
+
while (entries[selectedIndex]?.position < position) selectedIndex++
|
|
289
|
+
if (entries[selectedIndex]?.token === token) {
|
|
290
|
+
selectedIndex++
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
list.staticRows.set(token, node)
|
|
294
|
+
node.remove()
|
|
295
|
+
}
|
|
296
|
+
const parent = list.container ?? list.start.parentNode
|
|
297
|
+
const run = parent.ownerDocument.createDocumentFragment()
|
|
298
|
+
const next = new Array(entries.length)
|
|
299
|
+
for (let index = 0; index < entries.length; index++) {
|
|
300
|
+
const entry = entries[index]
|
|
301
|
+
let node = list.roots.get(entry.token)
|
|
302
|
+
if (!node) {
|
|
303
|
+
node = list.staticRows.get(entry.token).cloneNode(true)
|
|
304
|
+
listItems.set(node, entry.item)
|
|
305
|
+
run.append(node)
|
|
306
|
+
} else if (run.firstChild) parent.insertBefore(run, node)
|
|
307
|
+
next[index] = [entry.token, node]
|
|
308
|
+
}
|
|
309
|
+
if (run.firstChild) parent.insertBefore(run, list.boundary)
|
|
310
|
+
list.roots = new Map(next)
|
|
311
|
+
if (__KUDZU_LIST_STABLE_FAST_PATHS__) list.orderedRoots = next.map(([, node]) => node)
|
|
312
|
+
list.values.clear()
|
|
313
|
+
list.items = items
|
|
314
|
+
list.container ??= parent
|
|
315
|
+
return true
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function cacheStaticListEntries(descriptor, items) {
|
|
319
|
+
if (!Array.isArray(items)) return undefined
|
|
320
|
+
const entries = new WeakMap()
|
|
321
|
+
const positions = new Map()
|
|
322
|
+
const keys = new Set()
|
|
323
|
+
const seen = new Set()
|
|
324
|
+
for (let position = 0; position < items.length; position++) {
|
|
325
|
+
const item = items[position]
|
|
326
|
+
assertListItem(item)
|
|
327
|
+
assertListValue(item, seen, true)
|
|
328
|
+
const key = item[descriptor.key]
|
|
329
|
+
if (!validListKey(key)) throw new Error(`Keyed list key "${descriptor.key}" must be a string or finite number`)
|
|
330
|
+
const token = keyToken(key)
|
|
331
|
+
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
332
|
+
keys.add(token)
|
|
333
|
+
entries.set(item, { item, key, token, value: item, position })
|
|
334
|
+
positions.set(token, position)
|
|
335
|
+
}
|
|
336
|
+
entries.positions = positions
|
|
337
|
+
return entries
|
|
338
|
+
}
|
|
339
|
+
|
|
235
340
|
/* stable-list-fast-path */
|
|
236
341
|
function updateStableList(list, items) {
|
|
237
342
|
const previous = list.items
|