@kudzujs/core 0.7.6 → 0.7.9
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/GOAL_A.md +75 -146
- package/GOAL_B.md +56 -92
- package/README.md +18 -13
- package/RELEASES.md +73 -0
- package/framework/README.md +3 -3
- package/framework/build.mjs +54 -26
- package/framework/collection-selector.js +23 -19
- package/framework/core.d.ts +2 -2
- package/framework/core.mjs +17 -5
- package/framework/dev-state.js +3 -3
- package/framework/list-runtime.js +123 -18
- package/package.json +1 -1
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.7.9 - Keyed-row prop defaults
|
|
4
|
+
|
|
5
|
+
Kudzu 0.7.9 lets ordinary keyed row components retain destructured primitive literal prop defaults instead of restructuring every call site.
|
|
6
|
+
|
|
7
|
+
### New in 0.7.9
|
|
8
|
+
|
|
9
|
+
- Same-file and relative-imported keyed row components accept string, finite-number, boolean, and `null` defaults.
|
|
10
|
+
- Missing props receive their default literals during existing component specialization.
|
|
11
|
+
- The component call lowers to intrinsic keyed JSX; React imports and the component call do not enter browser output.
|
|
12
|
+
- Rest and nested destructuring remain source-diagnosed, while object, array, computed, and call defaults remain rejected.
|
|
13
|
+
- No browser runtime branch or shared asset bytes are added.
|
|
14
|
+
- The product documents now define commerce and realtime dashboards as validation fixtures under one general React migration roadmap, not separate product verticals.
|
|
15
|
+
|
|
16
|
+
### Boundary
|
|
17
|
+
|
|
18
|
+
This release supports primitive literal defaults on compiler-specialized keyed rows. General prop spreads, rest bindings, non-primitive defaults, specialized `children`, `forwardRef`, and `useId` remain fixture-driven composition work.
|
|
19
|
+
|
|
20
|
+
### Upgrade
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @kudzujs/core@^0.7.9
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 0.7.8 - Static collection fast paths
|
|
27
|
+
|
|
28
|
+
Kudzu 0.7.8 specializes compiler-owned static `filter` collections so repeated category changes avoid general keyed-list validation and reconciliation.
|
|
29
|
+
|
|
30
|
+
### New in 0.7.8
|
|
31
|
+
|
|
32
|
+
- Static source items, unique keys, source positions, and reference entries validate and cache once when the list mounts.
|
|
33
|
+
- Structural rows removed by a filter become detached prototypes; restoration clones fresh DOM, preserving remount semantics without moving retained keys.
|
|
34
|
+
- Interleaved additions insert only new contiguous runs instead of attaching additions and then reordering the complete list.
|
|
35
|
+
- General local-state lists, dynamic selectors, indexed rows, handlers, effects, refs, and row state retain their existing reconciliation paths.
|
|
36
|
+
- The capability is route-specific and compiles out when no compiler-owned static collection is rendered.
|
|
37
|
+
- 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.
|
|
38
|
+
|
|
39
|
+
### Benchmark
|
|
40
|
+
|
|
41
|
+
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.
|
|
42
|
+
|
|
43
|
+
### Boundary
|
|
44
|
+
|
|
45
|
+
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.
|
|
46
|
+
|
|
47
|
+
### Upgrade
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npm install @kudzujs/core@^0.7.8
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 0.7.7 - Imported memo collections
|
|
54
|
+
|
|
55
|
+
Kudzu 0.7.7 lets ordinary React migration source filter an imported static catalog through state-dependent `useMemo` while reusing existing keyed-list reconciliation.
|
|
56
|
+
|
|
57
|
+
### New in 0.7.7
|
|
58
|
+
|
|
59
|
+
- Named relative imports of exported JSON-safe `const` arrays can anchor analyzable collection pipelines.
|
|
60
|
+
- Direct local-state reads in collection selectors invalidate the list when their declared state dependencies change.
|
|
61
|
+
- Existing keys retain DOM identity through filtering and restoration; removed keys remount when restored.
|
|
62
|
+
- Compiler-owned static collection state is excluded from development snapshot restoration.
|
|
63
|
+
- Static routes remain JavaScript-free, and interactive routes ship no React runtime or browser memo cache.
|
|
64
|
+
- Focused fixtures verify dependency diagnostics, static output, generated selectors, and browser DOM identity.
|
|
65
|
+
|
|
66
|
+
### Boundary
|
|
67
|
+
|
|
68
|
+
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.
|
|
69
|
+
|
|
70
|
+
### Upgrade
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
npm install @kudzujs/core@^0.7.7
|
|
74
|
+
```
|
|
75
|
+
|
|
3
76
|
## 0.7.6 - Zustand-shaped shared stores
|
|
4
77
|
|
|
5
78
|
Kudzu 0.7.6 lets reduced React migration source retain a Zustand `create(set => ...)` store across an explicitly configured shared-layout navigation group.
|
package/framework/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
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
|
|
|
@@ -31,9 +31,9 @@ Page `metadata` can emit description, canonical, favicon, manifest, Open Graph,
|
|
|
31
31
|
|
|
32
32
|
Inline SVG rendering normalizes an explicit set of common React presentation aliases before static serialization and binding descriptor creation. Reactive aliases therefore use the existing generic `setAttribute` path; static SVG adds no JavaScript and reactive SVG adds no SVG-specific runtime.
|
|
33
33
|
|
|
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.
|
|
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. Missing destructured string, finite-number, boolean, or `null` props use their literal defaults during specialization. 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.
|
|
35
35
|
|
|
36
|
-
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.
|
|
37
37
|
|
|
38
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.
|
|
39
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
|
})
|
|
@@ -1879,7 +1881,7 @@ function normalizeZustandMigrationSyntax(sourceFile, factory, context) {
|
|
|
1879
1881
|
return factory.updateSourceFile(normalized, statements)
|
|
1880
1882
|
}
|
|
1881
1883
|
|
|
1882
|
-
function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
1884
|
+
function normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections = new Set()) {
|
|
1883
1885
|
const supported = new Set(["createContext", "useContext", "useEffect", "useReducer", "useRef", "useState"])
|
|
1884
1886
|
const erased = new Set(["memo", "useCallback", "useMemo"])
|
|
1885
1887
|
const aliases = new Map()
|
|
@@ -2013,14 +2015,15 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
|
2013
2015
|
const dependencies = new Set(node.arguments[1].elements.map(unwrapExpression).filter(ts.isIdentifier).map(entry => entry.text))
|
|
2014
2016
|
const owner = nearestFunction(node)
|
|
2015
2017
|
const states = owner ? ownerStateNames(owner) : new Set()
|
|
2016
|
-
const
|
|
2017
|
-
if (!expression || !
|
|
2018
|
-
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) {
|
|
2019
2021
|
const unsupported = [...reactMemoReferenceNames(expression)].find(reference => !states.has(reference))
|
|
2020
2022
|
if (unsupported) throw sourceNodeError(expression, sourceFile, `React useMemo() pure expressions may only reference direct local state; found ${JSON.stringify(unsupported)}`)
|
|
2021
2023
|
}
|
|
2022
|
-
const
|
|
2023
|
-
|
|
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))
|
|
2024
2027
|
: [...states].find(state => referenceIdentifiers(expression, state).length && !dependencies.has(state))
|
|
2025
2028
|
if (stale) throw sourceNodeError(node.arguments[1], sourceFile, `React useMemo() must list captured state ${JSON.stringify(stale)} as a dependency`)
|
|
2026
2029
|
return ts.visitNode(expression, visitor)
|
|
@@ -2106,10 +2109,10 @@ function lowerReactMemoCollectionExpression(expression, factory) {
|
|
|
2106
2109
|
return visit(expression)
|
|
2107
2110
|
}
|
|
2108
2111
|
|
|
2109
|
-
function
|
|
2112
|
+
function reactMemoCollection(expression, states, importedCollections, sourceFile) {
|
|
2110
2113
|
const setters = new Map([...states].map(state => [state, state]))
|
|
2111
2114
|
const fail = (node, message) => { throw sourceNodeError(node, sourceFile, message) }
|
|
2112
|
-
return renderedCollectionSource(expression, setters, undefined, fail, new Set())
|
|
2115
|
+
return renderedCollectionSource(expression, setters, undefined, fail, new Set(), importedCollections, states)
|
|
2113
2116
|
}
|
|
2114
2117
|
|
|
2115
2118
|
function reactMemoComponentExpression(identifier, sourceFile, factory, context) {
|
|
@@ -2150,9 +2153,10 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2150
2153
|
return context => sourceFile => {
|
|
2151
2154
|
const factory = context.factory
|
|
2152
2155
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
2156
|
+
const importedCollections = importedSerializableCollectionNames(sourceFile, file, sourceFiles, sourceIndex)
|
|
2153
2157
|
sourceFile = normalizeClsxSyntax(sourceFile, factory, context)
|
|
2154
2158
|
ts.setParentRecursive(sourceFile, false)
|
|
2155
|
-
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context)
|
|
2159
|
+
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context, importedCollections)
|
|
2156
2160
|
ts.setParentRecursive(sourceFile, false)
|
|
2157
2161
|
sourceFile = normalizeZustandMigrationSyntax(sourceFile, factory, context)
|
|
2158
2162
|
ts.setParentRecursive(sourceFile, false)
|
|
@@ -2167,7 +2171,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2167
2171
|
if (!imported) {
|
|
2168
2172
|
imported = normalizeClsxSyntax(parseSourceFile(target, sourceIndex.get(target)), factory, context)
|
|
2169
2173
|
ts.setParentRecursive(imported, false)
|
|
2170
|
-
imported = normalizeReactMigrationSyntax(imported, factory, context)
|
|
2174
|
+
imported = normalizeReactMigrationSyntax(imported, factory, context, importedSerializableCollectionNames(imported, target, sourceFiles, sourceIndex))
|
|
2171
2175
|
ts.setParentRecursive(imported, false)
|
|
2172
2176
|
imported = normalizeZustandMigrationSyntax(imported, factory, context)
|
|
2173
2177
|
ts.setParentRecursive(imported, false)
|
|
@@ -2314,7 +2318,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2314
2318
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
2315
2319
|
for (const [name, entries] of declarations) {
|
|
2316
2320
|
for (const declaration of entries) {
|
|
2317
|
-
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)
|
|
2318
2322
|
if (!parts) continue
|
|
2319
2323
|
const uses = []
|
|
2320
2324
|
const collectUses = node => {
|
|
@@ -2508,7 +2512,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2508
2512
|
return
|
|
2509
2513
|
}
|
|
2510
2514
|
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
2511
|
-
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)
|
|
2512
2516
|
if (parts) {
|
|
2513
2517
|
for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.add(declaration)
|
|
2514
2518
|
rawRenderedLists.push({ node, parts })
|
|
@@ -2824,14 +2828,16 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2824
2828
|
if (listParts) {
|
|
2825
2829
|
usesBehavior = true
|
|
2826
2830
|
usesList = true
|
|
2827
|
-
|
|
2831
|
+
const arguments_ = [
|
|
2828
2832
|
listParts.state,
|
|
2829
2833
|
listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
|
|
2830
2834
|
ts.visitNode(listParts.callback, visitor),
|
|
2831
2835
|
factory.createStringLiteral(listParts.ownerField ?? ""),
|
|
2832
2836
|
jsonExpression(listParts.selector ?? [], factory),
|
|
2833
2837
|
listParts.indexed ? factory.createTrue() : factory.createFalse()
|
|
2834
|
-
]
|
|
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_))
|
|
2835
2841
|
}
|
|
2836
2842
|
const conditional = conditionalParts(node.expression)
|
|
2837
2843
|
if (conditional) {
|
|
@@ -3017,11 +3023,11 @@ function containsRenderControl(root, knownLocals) {
|
|
|
3017
3023
|
return found
|
|
3018
3024
|
}
|
|
3019
3025
|
|
|
3020
|
-
function keyedListParts(expression, setters, declarations, fail, aliases = new Set()) {
|
|
3026
|
+
function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set()) {
|
|
3021
3027
|
const value = unwrapExpression(expression)
|
|
3022
3028
|
const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
|
|
3023
3029
|
if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
|
|
3024
|
-
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()))
|
|
3025
3031
|
if (!collection?.state) return undefined
|
|
3026
3032
|
if (directFrom) collection.selector.push(["from", undefined])
|
|
3027
3033
|
const callback = directFrom ? value.arguments[1] : value.arguments[0]
|
|
@@ -3055,16 +3061,17 @@ function nestedKeyedListParts(expression, parentItem, fail) {
|
|
|
3055
3061
|
return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
|
|
3056
3062
|
}
|
|
3057
3063
|
|
|
3058
|
-
function renderedCollectionSource(expression, setters, declarations, fail, aliases) {
|
|
3064
|
+
function renderedCollectionSource(expression, setters, declarations, fail, aliases, importedCollections = new Set(), stateNames = new Set()) {
|
|
3059
3065
|
const value = unwrapExpression(expression)
|
|
3060
3066
|
if (ts.isIdentifier(value)) {
|
|
3061
|
-
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() }
|
|
3062
3069
|
const entries = declarations?.get(value.text)
|
|
3063
3070
|
if (!entries) return undefined
|
|
3064
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`)
|
|
3065
3072
|
if (identifierReferenceCount(nearestFunction(entries[0].node).body, value.text) !== 1) fail(value, `Rendered collection alias "${value.text}" may only be rendered once`)
|
|
3066
3073
|
aliases.add(value.text)
|
|
3067
|
-
const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases)
|
|
3074
|
+
const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
3068
3075
|
aliases.delete(value.text)
|
|
3069
3076
|
return source && { ...source, aliasDeclarations: [...(source.aliasDeclarations ?? []), entries[0].node] }
|
|
3070
3077
|
}
|
|
@@ -3073,14 +3080,15 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
3073
3080
|
const method = value.expression.name.text
|
|
3074
3081
|
if (method === "filter") {
|
|
3075
3082
|
if (value.arguments.length !== 1) fail(value, "Rendered collection filter() requires one inline predicate")
|
|
3076
|
-
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases)
|
|
3083
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
3077
3084
|
if (!source) return undefined
|
|
3078
3085
|
const parameters = collectionParameters(value.arguments[0], "Rendered collection filter()", fail)
|
|
3079
|
-
|
|
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 }
|
|
3080
3088
|
}
|
|
3081
3089
|
if (method === "flatMap") {
|
|
3082
3090
|
if (value.arguments.length !== 1) fail(value, "Rendered collection flatMap() requires one inline projector")
|
|
3083
|
-
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases)
|
|
3091
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
3084
3092
|
if (!source) return undefined
|
|
3085
3093
|
const parameters = collectionParameters(value.arguments[0], "Rendered collection flatMap()", fail)
|
|
3086
3094
|
const field = directProperty(value.arguments[0].body, parameters.item)
|
|
@@ -3091,12 +3099,14 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
3091
3099
|
}
|
|
3092
3100
|
if (isArrayFromCall(value)) {
|
|
3093
3101
|
if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered Array.from() requires an anchor and optional inline mapper")
|
|
3094
|
-
const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases)
|
|
3102
|
+
const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases, importedCollections, stateNames)
|
|
3095
3103
|
if (!source) return undefined
|
|
3096
3104
|
let mapper
|
|
3097
3105
|
if (value.arguments[1]) {
|
|
3098
3106
|
const parameters = collectionParameters(value.arguments[1], "Rendered Array.from() mapper", fail)
|
|
3099
|
-
|
|
3107
|
+
const selectorStates = new Set(source.selectorStates)
|
|
3108
|
+
mapper = collectionExpression(unwrapExpression(value.arguments[1].body), parameters, fail, stateNames, selectorStates)
|
|
3109
|
+
source.selectorStates = selectorStates
|
|
3100
3110
|
}
|
|
3101
3111
|
return { ...source, selector: [...source.selector, ["from", mapper]] }
|
|
3102
3112
|
}
|
|
@@ -3111,7 +3121,7 @@ function collectionParameters(callback, label, fail) {
|
|
|
3111
3121
|
return { item: callback.parameters[0].name.text, index: callback.parameters[1]?.name.text }
|
|
3112
3122
|
}
|
|
3113
3123
|
|
|
3114
|
-
function collectionExpression(expression, parameters, fail) {
|
|
3124
|
+
function collectionExpression(expression, parameters, fail, stateNames = new Set(), selectorStates = new Set()) {
|
|
3115
3125
|
const encode = node => {
|
|
3116
3126
|
node = unwrapExpression(node)
|
|
3117
3127
|
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return ["value", ts.isNumericLiteral(node) ? Number(node.text) : node.text]
|
|
@@ -3122,6 +3132,10 @@ function collectionExpression(expression, parameters, fail) {
|
|
|
3122
3132
|
if (node.text === parameters.item) return ["item"]
|
|
3123
3133
|
if (node.text === parameters.index) return ["index"]
|
|
3124
3134
|
if (node.text === "undefined") return ["undefined"]
|
|
3135
|
+
if (stateNames.has(node.text)) {
|
|
3136
|
+
selectorStates.add(node.text)
|
|
3137
|
+
return ["state", node.text]
|
|
3138
|
+
}
|
|
3125
3139
|
fail(node, `Rendered collection expression identifier "${node.text}" is not allowed`)
|
|
3126
3140
|
}
|
|
3127
3141
|
if (ts.isPropertyAccessExpression(node)) {
|
|
@@ -3368,7 +3382,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
3368
3382
|
const substitutions = new Map()
|
|
3369
3383
|
const acceptedProps = new Set()
|
|
3370
3384
|
for (const element of component.parameters[0].name.elements) {
|
|
3371
|
-
if (element.dotDotDotToken || !ts.isIdentifier(element.name)
|
|
3385
|
+
if (element.dotDotDotToken || !ts.isIdentifier(element.name)) fail(element, `${label} component props cannot use rest or nested destructuring`)
|
|
3372
3386
|
if (element.initializer && !isPrimitiveDefaultLiteral(element.initializer)) fail(element.initializer, `${label} component prop defaults must be primitive literals`)
|
|
3373
3387
|
const prop = (element.propertyName ?? element.name).text
|
|
3374
3388
|
acceptedProps.add(prop)
|
|
@@ -4160,6 +4174,20 @@ function clientImportBindings(sourceFile, file, sourceFiles) {
|
|
|
4160
4174
|
return bindings
|
|
4161
4175
|
}
|
|
4162
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
|
+
|
|
4163
4191
|
function resolveComponentExport(file, exportName, getSource, sourceFiles, trail = []) {
|
|
4164
4192
|
const key = `${file}:${exportName}`
|
|
4165
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(" -> ")}`)
|
|
@@ -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")
|
|
@@ -130,6 +131,14 @@ function createSignal(id, value) {
|
|
|
130
131
|
}
|
|
131
132
|
}
|
|
132
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
|
+
|
|
133
142
|
export function useEffect(callback, dependencies, module, handler, states, scope, source, cleanup, itemDependencies = []) {
|
|
134
143
|
if (!renderContext) throw new Error("useEffect() can only run while rendering a Kudzu component")
|
|
135
144
|
if (typeof callback !== "function" || !Array.isArray(dependencies) || !module || !handler) throw new Error("useEffect() must be compiled with a literal dependency array")
|
|
@@ -247,8 +256,11 @@ export function stateConditional(kind, state, truthy, falsy) {
|
|
|
247
256
|
return { [conditionalMarker]: true, kind, value: state.value, truthy, falsy, state: state.id }
|
|
248
257
|
}
|
|
249
258
|
|
|
250
|
-
export function list(items, keyField, render, ownerField, selector = [], indexed = false) {
|
|
251
|
-
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`)
|
|
252
264
|
let values = items.value
|
|
253
265
|
if (ownerField) {
|
|
254
266
|
const owner = renderContext?.listRoot ?? renderContext?.listRowRoot
|
|
@@ -257,7 +269,7 @@ export function list(items, keyField, render, ownerField, selector = [], indexed
|
|
|
257
269
|
values = renderContext.listTemplate ? [] : owner.item?.[ownerField]
|
|
258
270
|
if (!Array.isArray(values) && values != null) throw new Error(`Nested keyed list property "${ownerField}" must remain an array`)
|
|
259
271
|
}
|
|
260
|
-
values = selectCollection(values, selector)
|
|
272
|
+
values = selectCollection(values, selector, name => selectorStateMap.get(name)?.value)
|
|
261
273
|
const keys = new Set()
|
|
262
274
|
for (const [index, item] of values.entries()) {
|
|
263
275
|
const key = keyField === null ? index : item?.[keyField]
|
|
@@ -268,7 +280,7 @@ export function list(items, keyField, render, ownerField, selector = [], indexed
|
|
|
268
280
|
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
269
281
|
keys.add(token)
|
|
270
282
|
}
|
|
271
|
-
return { [listMarker]: true, items, values, keyField, render, ownerField, selector, indexed }
|
|
283
|
+
return { [listMarker]: true, items, values, keyField, render, ownerField, selector, indexed, selectorStates }
|
|
272
284
|
}
|
|
273
285
|
|
|
274
286
|
export function listField(read, field) {
|
|
@@ -801,7 +813,7 @@ async function renderList(node, namespace, selectValue) {
|
|
|
801
813
|
const ownerTemplate = Boolean(node.ownerField && renderContext.listTemplate)
|
|
802
814
|
const rowList = node.ownerField ? nextRowList() : undefined
|
|
803
815
|
const id = rowList?.id ?? nextRenderId("l")
|
|
804
|
-
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 } : {}) }
|
|
805
817
|
if (ownerTemplate) {
|
|
806
818
|
ownerRoot.descriptor.children ??= []
|
|
807
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
|
|