@kudzujs/core 0.8.1 → 0.8.2
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/RELEASES.md +25 -0
- package/framework/README.md +1 -1
- package/framework/binding-runtime.js +1 -1
- package/framework/build.mjs +68 -23
- package/framework/core.d.ts +1 -1
- package/framework/core.mjs +9 -4
- package/framework/list-runtime.js +49 -18
- package/framework/navigation-runtime.js +4 -3
- package/package.json +1 -1
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Kudzu Releases
|
|
2
2
|
|
|
3
|
+
## 0.8.2 - Calculated keyed collections
|
|
4
|
+
|
|
5
|
+
Kudzu 0.8.2 lets one direct array field from a synchronous relative calculation result drive an intrinsic keyed list, preserving static HTML and existing DOM ownership while improving list and navigation hot paths.
|
|
6
|
+
|
|
7
|
+
### New in 0.8.2
|
|
8
|
+
|
|
9
|
+
- A top-level `const result = calculate(state)` may feed `result.items.map(...)` when the relative synchronous helper returns a JSON-safe array field.
|
|
10
|
+
- Route-specific evaluator ESM refreshes a compiler-owned array anchor after source-state commits; the existing keyed reconciler retains keys, current handlers, and SVG namespaces.
|
|
11
|
+
- Initial structural rows hydrate list bookkeeping without repeating DOM writes, retained direct attributes skip redundant DOM reads, and stable-list fallback avoids duplicate validation.
|
|
12
|
+
- Immutable local filter sources with unused ordinary `useState` setters use the existing static-filter restoration path while indirect custom-hook and reducer mutation paths remain dynamic.
|
|
13
|
+
- Required route modules emit `modulepreload` hints, and navigation prefetch resolves validated destination capabilities before commit while preserving complete-document fallback and cleanup ordering.
|
|
14
|
+
- Navigation prefetch safely ignores focus and pointer targets outside anchors.
|
|
15
|
+
- Calculated collection fixtures verify insert, reorder, removal, retained identity, latest handlers, scalar bindings, SVG namespace, and zero JavaScript on unaffected static routes.
|
|
16
|
+
- The complete suite passes 133/133 tests, and the FIRE migration retains all chart nodes and acceptance behavior.
|
|
17
|
+
|
|
18
|
+
### Boundary
|
|
19
|
+
|
|
20
|
+
The calculation must be one top-level immutable result from a synchronous relative TypeScript function, and the consumed field must remain a JSON-safe array. Indirect result aliases, dynamic fields, nested calculated SVG lists, package calculations, and asynchronous helpers remain unsupported. Static-filter specialization applies only when an ordinary local setter is provably unused; lifecycle-bearing and indirectly mutable collections retain the general path.
|
|
21
|
+
|
|
22
|
+
### Upgrade
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @kudzujs/core@^0.8.2
|
|
26
|
+
```
|
|
27
|
+
|
|
3
28
|
## 0.8.1 - Custom-hook reset actions
|
|
4
29
|
|
|
5
30
|
Kudzu 0.8.1 compiles direct multi-state reset callbacks returned by relative custom hooks into one existing behavior command, keeping conventional hook ownership without adding a callback runtime.
|
package/framework/README.md
CHANGED
|
@@ -4,7 +4,7 @@ Kudzu specializes ordinary common React-shaped TSX so migrations need minimal so
|
|
|
4
4
|
|
|
5
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`, direct intrinsic `forwardRef`, top-level `const` identifiers initialized by `useId()`, and default, namespace, or named `Fragment`. A named or default zero-argument custom hook imported from a relative TypeScript module may return direct shorthand state/setter pairs and callbacks that capture those states, including a callback that resets multiple states to direct primitive literals; callers use one top-level `const` object destructuring without aliases, defaults, or rest. Kudzu's JSX declarations accept ReactNode-shaped component returns and contextually type common intrinsic DOM events, so strict React component props do not need migration-only `unknown` or explicit event annotations. `forwardRef()` accepts one inline synchronous `(props, ref)` function and requires the object ref exactly once on its direct intrinsic root; the compiler removes `ref` from props/rest and erases the wrapper. `useId()` becomes a deterministic build-time HTML ID and emits no browser capability; keyed rows reject it because cloned row templates cannot safely duplicate HTML IDs. Collection memos may start from local array state or a named relative import of an exported JSON-safe `const` array, including type-only `as const` and `satisfies` wrappers, 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
|
-
Compilation begins from page entries and follows relative runtime imports, re-exports, and validated Worker references; unreachable TypeScript migration files are not transformed. Direct maps over imported immutable JSON-safe arrays fold to literals for zero-JavaScript static rows. Synchronous relative calculation functions may return objects whose direct static fields feed reactive JSX bindings; build rendering uses current signal values and route-specific binding ESM reevaluates the same helper after state commits. Package imports have a separate narrow boundary: direct references inside intrinsic JSX event callbacks are erased from build modules and bundled into route handler ESM, while render-time, effect, helper-indirect, and mixed package use fails.
|
|
7
|
+
Compilation begins from page entries and follows relative runtime imports, re-exports, and validated Worker references; unreachable TypeScript migration files are not transformed. Direct maps over imported immutable JSON-safe arrays fold to literals for zero-JavaScript static rows. Synchronous relative calculation functions may return objects whose direct static fields feed reactive JSX bindings; build rendering uses current signal values and route-specific binding ESM reevaluates the same helper after state commits. One direct array field may instead feed a keyed intrinsic map: its evaluator refreshes a compiler-owned array anchor before the existing list reconciler runs, preserving keyed DOM and SVG identity without a calculation runtime. That field must remain a JSON-safe array after every source-state commit. Package imports have a separate narrow boundary: direct references inside intrinsic JSX event callbacks are erased from build modules and bundled into route handler ESM, while render-time, effect, helper-indirect, and mixed package use fails.
|
|
8
8
|
|
|
9
9
|
A named or aliased `Link` import from `react-router-dom` may render directly with one static root-relative `to` plus native anchor props. The compiler prefixes the configured `base`, changes the element to `<a href>`, and erases the import, so native navigation remains the default and configured navigation groups see an ordinary eligible anchor. Dynamic or relative destinations, `NavLink`, router-only props, spreads, default/namespace imports, and non-JSX uses fail with source diagnostics. No React Router package code or router runtime is emitted.
|
|
10
10
|
|
|
@@ -268,7 +268,7 @@ function register(targets, id, entry) {
|
|
|
268
268
|
targets.set(id, entries)
|
|
269
269
|
}
|
|
270
270
|
|
|
271
|
-
async function loadEvaluator(descriptor) {
|
|
271
|
+
export async function loadEvaluator(descriptor) {
|
|
272
272
|
let modulePromise = imports.get(descriptor.module)
|
|
273
273
|
if (!modulePromise) {
|
|
274
274
|
modulePromise = import(descriptor.module)
|
package/framework/build.mjs
CHANGED
|
@@ -209,13 +209,14 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
209
209
|
const hasListEvents = plans.some(plan => plan.lists.some(list => list.events))
|
|
210
210
|
const hasListExpressions = plans.some(plan => plan.lists.some(list => list.expressions))
|
|
211
211
|
const hasListExpressionAttributes = plans.some(plan => plan.lists.some(list => list.expressionAttributes))
|
|
212
|
-
const hasListSeeds = plans.some(plan => plan.lists.some(list => list.seed))
|
|
212
|
+
const hasListSeeds = plans.some(plan => plan.lists.some(list => list.seed || list.valueSeed))
|
|
213
213
|
const hasListEffects = plans.some(plan => plan.lists.some(list => list.effects))
|
|
214
214
|
const hasListRowHooks = plans.some(plan => plan.lists.some(list => list.rowStates?.length || list.rowRefs?.length))
|
|
215
215
|
const hasListRowRefs = plans.some(plan => plan.lists.some(list => list.rowRefs?.length))
|
|
216
216
|
const hasComplexListRowState = plans.some(plan => plan.lists.some(list => list.rowStates?.some(state => state.initialValue !== null && typeof state.initialValue === "object")))
|
|
217
217
|
const hasNestedLists = plans.some(plan => plan.lists.some(list => list.ownerField))
|
|
218
218
|
const hasCollectionSelectors = plans.some(plan => plan.lists.some(list => list.selector))
|
|
219
|
+
const hasCalculatedCollections = plans.some(plan => plan.lists.some(list => list.source))
|
|
219
220
|
const hasDerivedEffectDependencies = plans.some(plan => plan.effects.some(effect => effect.dependencyExpressions?.length))
|
|
220
221
|
const hasStaticCollections = plans.some(plan => plan.lists.some(list => list.static))
|
|
221
222
|
const hasListIndexes = plans.some(plan => plan.lists.some(list => list.indexed))
|
|
@@ -237,7 +238,7 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
237
238
|
for (const entry of pageEntries) {
|
|
238
239
|
const routeDirectory = join(outputDirectory, entry.route)
|
|
239
240
|
await mkdir(routeDirectory, { recursive: true })
|
|
240
|
-
const html = entry.html.replace(runtimePlaceholder, escapeAttribute(assetPath(base, `assets/${runtimeName(entry.usesDependencyRuntime)}`)))
|
|
241
|
+
const html = preloadModules(entry.html.replace(runtimePlaceholder, escapeAttribute(assetPath(base, `assets/${runtimeName(entry.usesDependencyRuntime)}`))))
|
|
241
242
|
await writeFile(join(routeDirectory, "index.html"), html)
|
|
242
243
|
}
|
|
243
244
|
if (navigationRoutes.length || behaviorCount && (hasSharedRuntime || regularBehaviorCount)) {
|
|
@@ -284,6 +285,9 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
284
285
|
if (hasCollectionSelectors && !hasDerivedEffectDependencies) await writeJavaScript(join(assetsDirectory, "kudzu-collection-selector.js"), await readFile(new URL("./collection-selector.js", import.meta.url), "utf8"), minify)
|
|
285
286
|
let listRuntime = (await readFile(new URL("./list-runtime.js", import.meta.url), "utf8"))
|
|
286
287
|
.replace('"./shared-runtime.js"', '"./kudzu.js"')
|
|
288
|
+
listRuntime = hasCalculatedCollections
|
|
289
|
+
? listRuntime.replace('"./binding-runtime.js"', '"./kudzu-binding.js"')
|
|
290
|
+
: listRuntime.replace(/^const loadListEvaluator[^\n]+\n/m, "")
|
|
287
291
|
listRuntime = hasCollectionSelectors
|
|
288
292
|
? listRuntime.replace('"./collection-selector.js"', '"./kudzu-collection-selector.js"')
|
|
289
293
|
: listRuntime.replace(/^import \{ selectCollection \}[^\n]+\n/m, "")
|
|
@@ -297,8 +301,8 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
297
301
|
.replace("function addListRoot(list, { item, index = list.roots.size, key, token, value })", "function addListRoot(list, { item, key, token, value })")
|
|
298
302
|
.replace("fillListParts(root, listItemParts(root), listItems.get(owner), 0, __KUDZU_LIST_INDEXES__ ? listIndexes.get(owner) ?? 0 : 0)", "fillListParts(root, listItemParts(root), listItems.get(owner), 0)")
|
|
299
303
|
.replace("function fillListItem(root, item, nested = false, index = 0)", "function fillListItem(root, item, nested = false)")
|
|
300
|
-
.replace("fillListParts(root, parts, item, revision, index)", "fillListParts(root, parts, item, revision)")
|
|
301
|
-
.replace("function fillListParts(root, parts, item, revision, index = 0)", "function fillListParts(root, parts, item, revision)")
|
|
304
|
+
.replace("fillListParts(root, parts, item, revision, index, previous)", "fillListParts(root, parts, item, revision, previous)")
|
|
305
|
+
.replace("function fillListParts(root, parts, item, revision, index = 0, previous)", "function fillListParts(root, parts, item, revision, previous)")
|
|
302
306
|
.replaceAll('value?.type === "list-item" ? serializeItem(item) : value?.type === "list-index" ? index : value', 'value?.type === "list-item" ? serializeItem(item) : value')
|
|
303
307
|
.replaceAll("evaluate(descriptor, item, index)", "evaluate(descriptor, item)")
|
|
304
308
|
.replaceAll("evaluate({ module, handler }, item, index)", "evaluate({ module, handler }, item)")
|
|
@@ -459,6 +463,13 @@ export async function build({ quiet = false, minify = true } = {}) {
|
|
|
459
463
|
if (!quiet) console.log(`Built ${plans.length} page(s), ${behaviorCount} interactive page(s) into dist/`)
|
|
460
464
|
}
|
|
461
465
|
|
|
466
|
+
function preloadModules(html) {
|
|
467
|
+
const scripts = [...html.matchAll(/<script type="module"[^>]* src="([^"]+)"[^>]*><\/script>/g)]
|
|
468
|
+
if (!scripts.length) return html
|
|
469
|
+
const links = [...new Set(scripts.map(match => match[1]))].map(href => `<link rel="modulepreload" href="${href}">`).join("")
|
|
470
|
+
return html.replace(scripts[0][0], `${links}${scripts[0][0]}`)
|
|
471
|
+
}
|
|
472
|
+
|
|
462
473
|
function specializeEvents(source, events) {
|
|
463
474
|
return source.replace(/const eventNames = \[[^\n]+\]/, `const eventNames = ${JSON.stringify(events)}`)
|
|
464
475
|
}
|
|
@@ -492,10 +503,11 @@ async function mountInitial() {
|
|
|
492
503
|
}
|
|
493
504
|
`, "")
|
|
494
505
|
.replace(" await ready\n", "")
|
|
495
|
-
.replace(" const capabilities =
|
|
506
|
+
.replace(" const { incoming, parsed, capabilities } = documentResult\n", " const { incoming, parsed } = documentResult\n")
|
|
496
507
|
.replace(" await routeDispose()\n if (current !== revision) return\n", "")
|
|
497
508
|
.replace(" commit(incoming, parsed.nodes, capabilities.params, url.pathname, url.search)\n", " commit(incoming, parsed.nodes)\n")
|
|
498
509
|
.replace(" routeDispose = await capabilities.effects?.mountRouteEffects?.() ?? noDispose\n", "")
|
|
510
|
+
.replace(" return { incoming, parsed, capabilities: await loadCapabilities(parsed), record }\n", " await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))\n return { incoming, parsed, record }\n")
|
|
499
511
|
.replace(`
|
|
500
512
|
async function loadCapabilities(parsed) {
|
|
501
513
|
const modules = await Promise.all(parsed.assets.filter(path => path !== navigationAsset).map(path => import(path)))
|
|
@@ -1508,7 +1520,7 @@ async function writeBundledJavaScript(file, source, minify, define) {
|
|
|
1508
1520
|
stdin: { contents: source, resolveDir: dirname(file), sourcefile: file },
|
|
1509
1521
|
bundle: true,
|
|
1510
1522
|
write: false,
|
|
1511
|
-
external: ["./kudzu.js", "./kudzu-serialization.js", "./kudzu-style.js"],
|
|
1523
|
+
external: ["./kudzu.js", "./kudzu-binding.js", "./kudzu-serialization.js", "./kudzu-style.js"],
|
|
1512
1524
|
define,
|
|
1513
1525
|
format: "esm",
|
|
1514
1526
|
target: "es2022",
|
|
@@ -2655,6 +2667,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2655
2667
|
} catch {}
|
|
2656
2668
|
}
|
|
2657
2669
|
const settersByFunction = new Map()
|
|
2670
|
+
const localStateSettersByFunction = new Map()
|
|
2658
2671
|
const reducersByFunction = new Map()
|
|
2659
2672
|
const zustandStores = new Map()
|
|
2660
2673
|
const resolvedZustandStore = entry => {
|
|
@@ -2813,6 +2826,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2813
2826
|
const setters = settersByFunction.get(owner) ?? new Map()
|
|
2814
2827
|
setters.set(setterElement.name.text, stateElement.name.text)
|
|
2815
2828
|
settersByFunction.set(owner, setters)
|
|
2829
|
+
const localSetters = localStateSettersByFunction.get(owner) ?? new Set()
|
|
2830
|
+
localSetters.add(setterElement.name.text)
|
|
2831
|
+
localStateSettersByFunction.set(owner, localSetters)
|
|
2816
2832
|
}
|
|
2817
2833
|
}
|
|
2818
2834
|
}
|
|
@@ -3327,7 +3343,23 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3327
3343
|
return
|
|
3328
3344
|
}
|
|
3329
3345
|
if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
|
|
3330
|
-
const
|
|
3346
|
+
const owner = nearestFunction(node)
|
|
3347
|
+
const setters = settersForNode(node, settersByFunction)
|
|
3348
|
+
const staticCollection = state => [...(localStateSettersByFunction.get(owner) ?? [])].some(setter => setters.get(setter) === state && !referenceIdentifiers(owner.body, setter).length)
|
|
3349
|
+
const calculatedCollection = expression => {
|
|
3350
|
+
const value = unwrapExpression(expression)
|
|
3351
|
+
if (!ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.expression)) return undefined
|
|
3352
|
+
const entries = jsxLocalDeclarations.get(nearestFunction(node))?.get(value.expression.text)
|
|
3353
|
+
if (!entries?.length) return undefined
|
|
3354
|
+
const initializer = entries.length === 1 ? unwrapExpression(entries[0].initializer) : undefined
|
|
3355
|
+
if (!initializer || !ts.isCallExpression(initializer) || !ts.isIdentifier(initializer.expression) || !importBindings.has(initializer.expression.text)) return undefined
|
|
3356
|
+
if (entries[0].node.parent?.parent?.parent !== nearestFunction(entries[0].node)?.body) fail(value.expression, `Calculated collection result "${value.expression.text}" must be one top-level immutable local`)
|
|
3357
|
+
validateImportedCalculation(initializer, value.name.text)
|
|
3358
|
+
const expanded = resolveReactiveJsxExpression(value, nearestFunction(node), setters)
|
|
3359
|
+
if (expanded === value || !referencedStateNames(expanded, setters).size) fail(value, "Calculated collection fields must directly depend on local state")
|
|
3360
|
+
return expanded
|
|
3361
|
+
}
|
|
3362
|
+
const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, setters, jsxLocalDeclarations.get(owner), fail, new Set(), importedCollections, factory, context, importedCollectionTransforms, calculatedCollection, staticCollection)
|
|
3331
3363
|
if (parts) {
|
|
3332
3364
|
for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.add(declaration)
|
|
3333
3365
|
rawRenderedLists.push({ node, parts })
|
|
@@ -3731,15 +3763,21 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
3731
3763
|
if (listParts) {
|
|
3732
3764
|
usesBehavior = true
|
|
3733
3765
|
usesList = true
|
|
3766
|
+
let listSource = listParts.state
|
|
3767
|
+
if (listParts.calculation) {
|
|
3768
|
+
usesBinding = true
|
|
3769
|
+
listSource = compileReactiveBinding(listParts.calculation, settersForNode(node, settersByFunction), factory, context, reactiveBindings, handlerUrl, importBindings, clientImports)
|
|
3770
|
+
}
|
|
3734
3771
|
const arguments_ = [
|
|
3735
|
-
|
|
3772
|
+
listSource,
|
|
3736
3773
|
listParts.keyField === null ? factory.createNull() : factory.createStringLiteral(listParts.keyField),
|
|
3737
3774
|
ts.visitNode(listParts.callback, visitor),
|
|
3738
3775
|
factory.createStringLiteral(listParts.ownerField ?? ""),
|
|
3739
3776
|
jsonExpression(listParts.selector ?? [], factory),
|
|
3740
3777
|
listParts.indexed ? factory.createTrue() : factory.createFalse()
|
|
3741
3778
|
]
|
|
3742
|
-
if (listParts.selectorStates?.size) arguments_.push(factory.createArrayLiteralExpression([...listParts.selectorStates].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
|
|
3779
|
+
if (listParts.selectorStates?.size || listParts.static) arguments_.push(factory.createArrayLiteralExpression([...(listParts.selectorStates ?? [])].map(name => factory.createArrayLiteralExpression([factory.createStringLiteral(name), factory.createIdentifier(name)]))))
|
|
3780
|
+
if (listParts.static) arguments_.push(factory.createTrue())
|
|
3743
3781
|
return factory.updateJsxExpression(node, factory.createCallExpression(factory.createIdentifier("__kList"), undefined, arguments_))
|
|
3744
3782
|
}
|
|
3745
3783
|
const conditional = conditionalParts(node.expression)
|
|
@@ -3932,12 +3970,12 @@ function containsRenderControl(root, knownLocals) {
|
|
|
3932
3970
|
return found
|
|
3933
3971
|
}
|
|
3934
3972
|
|
|
3935
|
-
function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set(), factory = ts.factory, context, importedCollectionTransforms = new Map()) {
|
|
3973
|
+
function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set(), factory = ts.factory, context, importedCollectionTransforms = new Map(), calculatedCollection, staticCollection) {
|
|
3936
3974
|
const value = unwrapExpression(expression)
|
|
3937
3975
|
const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
|
|
3938
3976
|
if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
|
|
3939
|
-
const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()), importedCollectionTransforms, factory, context)
|
|
3940
|
-
if (!collection?.state) return undefined
|
|
3977
|
+
const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()), importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
|
|
3978
|
+
if (!collection?.state && !collection?.calculation) return undefined
|
|
3941
3979
|
if (directFrom) collection.selector.push(["from", undefined])
|
|
3942
3980
|
let callback = directFrom ? value.arguments[1] : value.arguments[0]
|
|
3943
3981
|
const parameters = collectionParameters(callback, "Keyed list map", fail)
|
|
@@ -3962,7 +4000,7 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
|
|
|
3962
4000
|
const field = keyExpression && directProperty(keyExpression, parameters.item)
|
|
3963
4001
|
const positional = Boolean(keyExpression && parameters.index && ts.isIdentifier(unwrapExpression(keyExpression)) && unwrapExpression(keyExpression).text === parameters.index)
|
|
3964
4002
|
if (!field && !positional) fail(key ?? root, `Keyed list root must have key={${parameters.item}.<field>} or key={${parameters.index ?? "index"}}`)
|
|
3965
|
-
return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : field }
|
|
4003
|
+
return { ...collection, static: collection.static && (!collection.localStatic || collection.selector.length > 0), callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : field }
|
|
3966
4004
|
}
|
|
3967
4005
|
|
|
3968
4006
|
function nestedKeyedListParts(expression, parentItem, fail) {
|
|
@@ -3983,20 +4021,27 @@ function nestedKeyedListParts(expression, parentItem, fail) {
|
|
|
3983
4021
|
return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
|
|
3984
4022
|
}
|
|
3985
4023
|
|
|
3986
|
-
function renderedCollectionSource(expression, setters, declarations, fail, aliases, importedCollections = new Set(), stateNames = new Set(), importedCollectionTransforms = new Map(), factory = ts.factory, context) {
|
|
4024
|
+
function renderedCollectionSource(expression, setters, declarations, fail, aliases, importedCollections = new Set(), stateNames = new Set(), importedCollectionTransforms = new Map(), factory = ts.factory, context, calculatedCollection, staticCollection) {
|
|
3987
4025
|
const value = unwrapExpression(expression)
|
|
3988
4026
|
if (ts.isIdentifier(value)) {
|
|
3989
|
-
if ([...setters.values()].includes(value.text))
|
|
4027
|
+
if ([...setters.values()].includes(value.text)) {
|
|
4028
|
+
const localStatic = staticCollection?.(value.text)
|
|
4029
|
+
return { state: value, static: localStatic, localStatic, selector: [], selectorStates: new Set() }
|
|
4030
|
+
}
|
|
3990
4031
|
if (importedCollections.has(value.text)) return { state: value, static: true, selector: [], selectorStates: new Set() }
|
|
3991
4032
|
const entries = declarations?.get(value.text)
|
|
3992
4033
|
if (!entries) return undefined
|
|
3993
4034
|
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`)
|
|
3994
4035
|
aliases.add(value.text)
|
|
3995
|
-
const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
4036
|
+
const source = renderedCollectionSource(entries[0].initializer, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
|
|
3996
4037
|
aliases.delete(value.text)
|
|
3997
4038
|
return source && { ...source, aliasDeclarations: [...(source.aliasDeclarations ?? []), entries[0].node], aliasUses: [...(source.aliasUses ?? []), value] }
|
|
3998
4039
|
}
|
|
3999
|
-
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression))
|
|
4040
|
+
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression)) {
|
|
4041
|
+
const calculation = calculatedCollection?.(value)
|
|
4042
|
+
if (calculation) return { calculation, selector: [], selectorStates: new Set() }
|
|
4043
|
+
return { state: undefined, ownerField: value.name.text, selector: [], parentItem: value.expression.text }
|
|
4044
|
+
}
|
|
4000
4045
|
if (ts.isCallExpression(value) && ts.isIdentifier(value.expression) && importedCollectionTransforms.has(value.expression.text)) {
|
|
4001
4046
|
const transform = importedCollectionTransforms.get(value.expression.text)
|
|
4002
4047
|
const parameter = transform.parameters[0]
|
|
@@ -4007,7 +4052,7 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
4007
4052
|
if (!returned) fail(value, `Imported collection transform "${value.expression.text}" must contain only one returned collection expression`)
|
|
4008
4053
|
const transformSource = renderedCollectionSource(returned, new Map([[parameter.name.text, parameter.name.text]]), undefined, fail, new Set(), new Set(), new Set([parameter.name.text]))
|
|
4009
4054
|
if (!transformSource?.state || transformSource.state.text !== parameter.name.text || transformSource.selectorStates.size) fail(value, `Imported collection transform "${value.expression.text}" must return a supported pure pipeline rooted only in its parameter`)
|
|
4010
|
-
const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
4055
|
+
const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
|
|
4011
4056
|
if (!source) fail(value.arguments[0], `Imported collection transform "${value.expression.text}" requires a supported collection argument`)
|
|
4012
4057
|
return { ...source, selector: [...source.selector, ...transformSource.selector] }
|
|
4013
4058
|
}
|
|
@@ -4015,7 +4060,7 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
4015
4060
|
const method = value.expression.name.text
|
|
4016
4061
|
if (method === "filter") {
|
|
4017
4062
|
if (value.arguments.length !== 1) fail(value, "Rendered collection filter() requires one inline predicate")
|
|
4018
|
-
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
4063
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
|
|
4019
4064
|
if (!source) return undefined
|
|
4020
4065
|
const parameters = collectionParameters(value.arguments[0], "Rendered collection filter()", fail)
|
|
4021
4066
|
const selectorStates = new Set(source.selectorStates)
|
|
@@ -4023,7 +4068,7 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
4023
4068
|
}
|
|
4024
4069
|
if (method === "flatMap") {
|
|
4025
4070
|
if (value.arguments.length !== 1) fail(value, "Rendered collection flatMap() requires one inline projector")
|
|
4026
|
-
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
4071
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
|
|
4027
4072
|
if (!source) return undefined
|
|
4028
4073
|
const parameters = collectionParameters(value.arguments[0], "Rendered collection flatMap()", fail)
|
|
4029
4074
|
const field = directProperty(value.arguments[0].body, parameters.item)
|
|
@@ -4033,7 +4078,7 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
4033
4078
|
}
|
|
4034
4079
|
if (method === "slice") {
|
|
4035
4080
|
if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered collection slice() requires a start and optional end")
|
|
4036
|
-
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
4081
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
|
|
4037
4082
|
if (!source) return undefined
|
|
4038
4083
|
const selectorStates = new Set(source.selectorStates)
|
|
4039
4084
|
const start = collectionExpression(value.arguments[0], {}, fail, stateNames, selectorStates)
|
|
@@ -4042,7 +4087,7 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
4042
4087
|
}
|
|
4043
4088
|
if (method === "toSorted") {
|
|
4044
4089
|
if (value.arguments.length !== 1) fail(value, "Rendered collection toSorted() requires one inline comparator")
|
|
4045
|
-
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
4090
|
+
const source = renderedCollectionSource(value.expression.expression, setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
|
|
4046
4091
|
if (!source) return undefined
|
|
4047
4092
|
const comparator = value.arguments[0]
|
|
4048
4093
|
const parameters = collectionParameters(comparator, "Rendered collection toSorted()", fail)
|
|
@@ -4055,7 +4100,7 @@ function renderedCollectionSource(expression, setters, declarations, fail, alias
|
|
|
4055
4100
|
}
|
|
4056
4101
|
if (isArrayFromCall(value)) {
|
|
4057
4102
|
if (value.arguments.length < 1 || value.arguments.length > 2) fail(value, "Rendered Array.from() requires an anchor and optional inline mapper")
|
|
4058
|
-
const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context)
|
|
4103
|
+
const source = renderedCollectionSource(value.arguments[0], setters, declarations, fail, aliases, importedCollections, stateNames, importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
|
|
4059
4104
|
if (!source) return undefined
|
|
4060
4105
|
let mapper
|
|
4061
4106
|
if (value.arguments[1]) {
|
package/framework/core.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function nativeBehavior(module: string, handler: string, states: Array<[s
|
|
|
36
36
|
export function binding(value: unknown, module: string, handler: string, states: Array<[string, unknown]>, scope: Array<[string, unknown]>): unknown
|
|
37
37
|
export function bindingValue(value: unknown): unknown
|
|
38
38
|
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
|
|
39
|
-
export function list(items: unknown, keyField: string | null, render: (item: unknown, index: number) => unknown, ownerField?: string, selector?: unknown[], indexed?: boolean, selectorStates?: Array<[string, unknown]
|
|
39
|
+
export function list(items: unknown, keyField: string | null, render: (item: unknown, index: number) => unknown, ownerField?: string, selector?: unknown[], indexed?: boolean, selectorStates?: Array<[string, unknown]>, staticCollection?: boolean): unknown
|
|
40
40
|
export function listField(read: () => unknown, field: string): unknown
|
|
41
41
|
export function listExpression(read: () => unknown, module: string, handler: string): unknown
|
|
42
42
|
export function listItem(): unknown
|
package/framework/core.mjs
CHANGED
|
@@ -291,7 +291,9 @@ export function stateConditional(kind, state, truthy, falsy) {
|
|
|
291
291
|
return { [conditionalMarker]: true, kind, value: state.value, truthy, falsy, state: state.id }
|
|
292
292
|
}
|
|
293
293
|
|
|
294
|
-
export function list(items, keyField, render, ownerField, selector = [], indexed = false, selectorStates = []) {
|
|
294
|
+
export function list(items, keyField, render, ownerField, selector = [], indexed = false, selectorStates = [], staticCollection = false) {
|
|
295
|
+
const source = items?.[bindingMarker] ? bindingDescriptor(items) : undefined
|
|
296
|
+
if (source) items = createInternalState(items.value)
|
|
295
297
|
if (Array.isArray(items)) items = createInternalState(items)
|
|
296
298
|
if (!items?.[signalMarker] || !Array.isArray(items.value)) throw new Error("A keyed list must use local array state or a supported imported static array")
|
|
297
299
|
const selectorStateMap = new Map(selectorStates)
|
|
@@ -315,7 +317,7 @@ export function list(items, keyField, render, ownerField, selector = [], indexed
|
|
|
315
317
|
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
316
318
|
keys.add(token)
|
|
317
319
|
}
|
|
318
|
-
return { [listMarker]: true, items, values, keyField, render, ownerField, selector, indexed, selectorStates }
|
|
320
|
+
return { [listMarker]: true, items, values, keyField, render, ownerField, selector, indexed, static: staticCollection, selectorStates, source }
|
|
319
321
|
}
|
|
320
322
|
|
|
321
323
|
export function listField(read, field) {
|
|
@@ -870,7 +872,8 @@ async function renderList(node, namespace, selectValue) {
|
|
|
870
872
|
const ownerTemplate = Boolean(node.ownerField && renderContext.listTemplate)
|
|
871
873
|
const rowList = node.ownerField ? nextRowList() : undefined
|
|
872
874
|
const id = rowList?.id ?? nextRenderId("l")
|
|
873
|
-
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.values.map((item, index) => node.keyField === null ? index : item[node.keyField]), ...(namespace === "svg" ? { svg: true } : {}), ...(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 } : {}) }
|
|
875
|
+
const descriptor = { id, state: node.items.id, key: node.keyField, keys: node.values.map((item, index) => node.keyField === null ? index : item[node.keyField]), ...(namespace === "svg" ? { svg: true } : {}), ...(node.static || node.items[internalStateMarker] && !node.source ? { static: true } : {}), ...(node.source ? { source: node.source } : {}), ...(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 } : {}) }
|
|
876
|
+
if (node.source) renderContext.hasBindings = true
|
|
874
877
|
if (ownerTemplate) {
|
|
875
878
|
ownerRoot.descriptor.children ??= []
|
|
876
879
|
ownerRoot.descriptor.children.push({ id, field: node.ownerField, key: node.keyField, ...(node.selector.length ? { selector: node.selector } : {}) })
|
|
@@ -919,8 +922,10 @@ async function renderList(node, namespace, selectValue) {
|
|
|
919
922
|
descriptor.mount = true
|
|
920
923
|
}
|
|
921
924
|
if (descriptor.rowStates && !descriptor.effects && !descriptor.nested && !template.includes("data-k-text")) descriptor.fastRelease = true
|
|
922
|
-
const
|
|
925
|
+
const valueSeed = node.ownerField || node.keyField === null ? undefined : listSeed(node.values, renderContext.listFields)
|
|
926
|
+
const seed = node.selector.length ? undefined : valueSeed
|
|
923
927
|
if (seed) descriptor.seed = seed
|
|
928
|
+
else if (valueSeed) descriptor.valueSeed = valueSeed
|
|
924
929
|
let current = ""
|
|
925
930
|
renderContext.listTemplate = false
|
|
926
931
|
renderContext.listInitialMarkers = Boolean(descriptor.conditions)
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { browserState, mountDom, notifyListItem, registerCommitter, registerMountHook, registerUnmountHook, releaseState, unmountDom } from "./shared-runtime.js"
|
|
2
2
|
import { selectCollection } from "./collection-selector.js"
|
|
3
|
+
const loadListEvaluator = descriptor => import("./binding-runtime.js").then(module => module.loadEvaluator(descriptor))
|
|
3
4
|
|
|
4
5
|
const listTargets = new Map()
|
|
5
6
|
const listRegistrations = new WeakMap()
|
|
7
|
+
const listLoads = new WeakMap()
|
|
6
8
|
const mountedLists = new WeakSet()
|
|
7
9
|
const imports = __KUDZU_LIST_ASYNC_PARTS__ ? new Map() : undefined
|
|
8
10
|
const revisions = __KUDZU_LIST_ASYNC_PARTS__ ? new WeakMap() : undefined
|
|
@@ -68,7 +70,8 @@ function mountLists(root) {
|
|
|
68
70
|
parts,
|
|
69
71
|
...(__KUDZU_STATIC_COLLECTIONS__ && staticRows ? { staticRows } : {}),
|
|
70
72
|
...(__KUDZU_STATIC_COLLECTIONS__ && staticEntries ? { staticEntries, staticPositions: staticEntries.positions } : {}),
|
|
71
|
-
seedFields: __KUDZU_LIST_SEEDS__ && descriptor.seed && Object.keys(descriptor.seed),
|
|
73
|
+
seedFields: __KUDZU_LIST_SEEDS__ && (descriptor.seed || descriptor.valueSeed) && Object.keys(descriptor.seed ?? descriptor.valueSeed),
|
|
74
|
+
valueSeed: __KUDZU_LIST_SEEDS__ && (descriptor.seed ?? descriptor.valueSeed),
|
|
72
75
|
roots: new Map(roots.map((node, index) => [keyToken(descriptor.keys[index]), node])),
|
|
73
76
|
...(__KUDZU_LIST_STABLE_FAST_PATHS__ ? { orderedRoots: roots } : {}),
|
|
74
77
|
values: new Map(),
|
|
@@ -84,6 +87,17 @@ function mountLists(root) {
|
|
|
84
87
|
lists.set(descriptor.id, list)
|
|
85
88
|
ownedLists.set(list.owner, lists)
|
|
86
89
|
listRegistrations.set(start, { list, owner: list.owner })
|
|
90
|
+
} else if (descriptor.source) {
|
|
91
|
+
const load = {}
|
|
92
|
+
listLoads.set(start, load)
|
|
93
|
+
loadListEvaluator(descriptor.source).then(evaluator => {
|
|
94
|
+
if (listLoads.get(start) !== load || !start.isConnected) return
|
|
95
|
+
list.sourceEvaluator = evaluator
|
|
96
|
+
const states = [...new Set([...evaluator.stateIds, ...(__KUDZU_COLLECTION_SELECTORS__ ? Object.values(descriptor.selectorStates ?? {}) : [])])]
|
|
97
|
+
for (const state of states) register(listTargets, state, list)
|
|
98
|
+
listRegistrations.set(start, { states, list })
|
|
99
|
+
updateList(list)
|
|
100
|
+
}).catch(error => console.error(error))
|
|
87
101
|
} else {
|
|
88
102
|
if (__KUDZU_COLLECTION_SELECTORS__ && descriptor.selectorStates) {
|
|
89
103
|
const states = [...new Set([descriptor.state, ...Object.values(descriptor.selectorStates)])]
|
|
@@ -94,7 +108,7 @@ function mountLists(root) {
|
|
|
94
108
|
listRegistrations.set(start, { state: descriptor.state, list })
|
|
95
109
|
}
|
|
96
110
|
}
|
|
97
|
-
updateList(list)
|
|
111
|
+
if (!descriptor.source) updateList(list)
|
|
98
112
|
}
|
|
99
113
|
}
|
|
100
114
|
|
|
@@ -103,6 +117,7 @@ function unmountLists(root) {
|
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
function unregisterList(start) {
|
|
120
|
+
listLoads.delete(start)
|
|
106
121
|
const registration = listRegistrations.get(start)
|
|
107
122
|
if (registration) {
|
|
108
123
|
if (__KUDZU_NESTED_LISTS__ && registration.owner) {
|
|
@@ -110,7 +125,7 @@ function unregisterList(start) {
|
|
|
110
125
|
if (lists?.get(registration.list.descriptor.id) === registration.list) lists.delete(registration.list.descriptor.id)
|
|
111
126
|
if (!lists?.size) ownedLists.delete(registration.owner)
|
|
112
127
|
} else {
|
|
113
|
-
if (__KUDZU_COLLECTION_SELECTORS__ && registration.states) {
|
|
128
|
+
if ((__KUDZU_COLLECTION_SELECTORS__ || registration.list.descriptor.source) && registration.states) {
|
|
114
129
|
for (const state of registration.states) {
|
|
115
130
|
const lists = listTargets.get(state)
|
|
116
131
|
lists?.delete(registration.list)
|
|
@@ -129,6 +144,7 @@ function unregisterList(start) {
|
|
|
129
144
|
}
|
|
130
145
|
|
|
131
146
|
function updateList(list) {
|
|
147
|
+
if (list.sourceEvaluator) browserState.set(list.descriptor.state, list.sourceEvaluator.read())
|
|
132
148
|
let items = __KUDZU_NESTED_LISTS__ && list.descriptor.ownerField
|
|
133
149
|
? listItems.get(list.owner)?.[list.descriptor.ownerField]
|
|
134
150
|
: browserState.get(list.descriptor.state)
|
|
@@ -148,13 +164,26 @@ function updateList(list) {
|
|
|
148
164
|
const key = list.descriptor.key === null ? index : item?.[list.descriptor.key]
|
|
149
165
|
if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
|
|
150
166
|
assertListItem(item)
|
|
151
|
-
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.
|
|
167
|
+
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.valueSeed, !referenceOnly) : undefined
|
|
152
168
|
if (seededValue === undefined) assertListValue(item, seen, true)
|
|
153
169
|
const token = keyToken(key)
|
|
154
170
|
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
155
171
|
keys.add(token)
|
|
156
172
|
entries.push({ item, index, key, token, value: referenceOnly ? item : seededValue === undefined ? JSON.stringify(item) : seededValue })
|
|
157
173
|
}
|
|
174
|
+
if (list.items === undefined && list.parts.directFill && entries.length === list.roots.size) {
|
|
175
|
+
const roots = __KUDZU_LIST_STABLE_FAST_PATHS__ ? list.orderedRoots : [...list.roots.values()]
|
|
176
|
+
if (entries.every((entry, index) => list.roots.get(entry.token) === roots[index])) {
|
|
177
|
+
for (let index = 0; index < entries.length; index++) {
|
|
178
|
+
const entry = entries[index]
|
|
179
|
+
listItems.set(roots[index], entry.item)
|
|
180
|
+
if (__KUDZU_LIST_INDEXES__) listIndexes.set(roots[index], index)
|
|
181
|
+
if (!referenceOnly) list.values.set(entry.token, entry.value)
|
|
182
|
+
}
|
|
183
|
+
list.items = items
|
|
184
|
+
return
|
|
185
|
+
}
|
|
186
|
+
}
|
|
158
187
|
if (list.descriptor.key !== null && !list.descriptor.indexed && !list.descriptor.selector && (referenceOnly || list.values.size) && entries.every(entry => {
|
|
159
188
|
const root = list.roots.get(entry.token)
|
|
160
189
|
return !root || (referenceOnly ? listItems.get(root) === entry.item : list.values.get(entry.token) === entry.value)
|
|
@@ -349,19 +378,18 @@ function updateStableList(list, items) {
|
|
|
349
378
|
const seen = new Set()
|
|
350
379
|
const keyField = list.descriptor.key
|
|
351
380
|
const fragment = list.start.ownerDocument.createDocumentFragment()
|
|
352
|
-
let stable = true
|
|
353
381
|
for (let index = 0; index < items.length; index++) {
|
|
354
382
|
const item = items[index]
|
|
355
383
|
assertListItem(item)
|
|
356
|
-
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.
|
|
384
|
+
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.valueSeed, !referenceOnly) : undefined
|
|
357
385
|
if (seededValue === undefined) assertListValue(item, seen, true)
|
|
358
386
|
const key = item[keyField]
|
|
359
387
|
if (!validListKey(key)) throw new Error(`Keyed list key "${keyField}" must be a string or finite number`)
|
|
360
388
|
const token = keyToken(key)
|
|
361
389
|
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
362
390
|
keys.add(token)
|
|
363
|
-
if (index < previous.length && (key !== previous[index][keyField] || appending && item !== previous[index]))
|
|
364
|
-
if (
|
|
391
|
+
if (index < previous.length && (key !== previous[index][keyField] || appending && item !== previous[index])) return false
|
|
392
|
+
if (items.length === previous.length + 1 || index < previous.length) continue
|
|
365
393
|
let node = list.templateRoot?.cloneNode(true)
|
|
366
394
|
if (node?.dataset.kListRoot !== list.descriptor.id) node = undefined
|
|
367
395
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
@@ -371,13 +399,12 @@ function updateStableList(list, items) {
|
|
|
371
399
|
else fillListItem(node, item, list.descriptor.nested, index, mapListItemParts(list.parts, node, list.descriptor.nested))
|
|
372
400
|
fragment.append(node)
|
|
373
401
|
}
|
|
374
|
-
if (!stable) return false
|
|
375
402
|
if (appending) {
|
|
376
403
|
if (items.length === previous.length + 1) {
|
|
377
404
|
const item = items.at(-1)
|
|
378
405
|
const key = item[keyField]
|
|
379
406
|
const token = keyToken(key)
|
|
380
|
-
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.
|
|
407
|
+
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.valueSeed, !referenceOnly) : undefined
|
|
381
408
|
addListRoot(list, { item, index: previous.length, key, token, value: referenceOnly ? item : seededValue === undefined ? JSON.stringify(item) : seededValue })
|
|
382
409
|
if (referenceOnly) list.values.clear()
|
|
383
410
|
list.items = items
|
|
@@ -391,7 +418,7 @@ function updateStableList(list, items) {
|
|
|
391
418
|
list.roots.set(token, node)
|
|
392
419
|
list.orderedRoots.push(node)
|
|
393
420
|
if (!referenceOnly) {
|
|
394
|
-
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.
|
|
421
|
+
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.valueSeed) : undefined
|
|
395
422
|
list.values.set(token, seededValue === undefined ? JSON.stringify(item) : seededValue)
|
|
396
423
|
}
|
|
397
424
|
node = node.nextSibling
|
|
@@ -414,7 +441,7 @@ function updateStableList(list, items) {
|
|
|
414
441
|
const item = items[index]
|
|
415
442
|
if (item === previous[index]) continue
|
|
416
443
|
const token = keyToken(item[keyField])
|
|
417
|
-
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.
|
|
444
|
+
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.valueSeed) : undefined
|
|
418
445
|
const value = seededValue === undefined ? JSON.stringify(item) : seededValue
|
|
419
446
|
if (list.values.get(token) !== value) {
|
|
420
447
|
const node = list.orderedRoots[index]
|
|
@@ -535,7 +562,7 @@ function updateReducerList(list, items) {
|
|
|
535
562
|
if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
|
|
536
563
|
assertListItem(item)
|
|
537
564
|
const seen = new Set()
|
|
538
|
-
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.
|
|
565
|
+
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.valueSeed) : undefined
|
|
539
566
|
if (seededValue === undefined) assertListValue(item, seen, true)
|
|
540
567
|
const token = keyToken(key)
|
|
541
568
|
if (list.roots.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
@@ -574,11 +601,12 @@ function removeListRoot(list, token) {
|
|
|
574
601
|
}
|
|
575
602
|
|
|
576
603
|
function fillListItem(root, item, nested = false, index = 0, parts = listItemParts(root, nested)) {
|
|
604
|
+
const previous = listItems.get(root)
|
|
577
605
|
listItems.set(root, item)
|
|
578
606
|
if (__KUDZU_LIST_INDEXES__) listIndexes.set(root, index)
|
|
579
607
|
const revision = __KUDZU_LIST_ASYNC_PARTS__ ? (revisions.get(root) ?? 0) + 1 : 0
|
|
580
608
|
if (__KUDZU_LIST_ASYNC_PARTS__) revisions.set(root, revision)
|
|
581
|
-
fillListParts(root, parts, item, revision, index)
|
|
609
|
+
fillListParts(root, parts, item, revision, index, previous)
|
|
582
610
|
if (__KUDZU_NESTED_LISTS__) {
|
|
583
611
|
const children = ownedLists.get(root)
|
|
584
612
|
if (children) for (const child of children.values()) updateList(child)
|
|
@@ -586,7 +614,7 @@ function fillListItem(root, item, nested = false, index = 0, parts = listItemPar
|
|
|
586
614
|
if (__KUDZU_LIST_ROW_HOOKS__) replaceRowIds(root, rowReplacements.get(root))
|
|
587
615
|
}
|
|
588
616
|
|
|
589
|
-
function fillListParts(root, parts, item, revision, index = 0) {
|
|
617
|
+
function fillListParts(root, parts, item, revision, index = 0, previous) {
|
|
590
618
|
if (__KUDZU_LIST_EFFECTS__) for (const node of parts.effects) node.dataset.kEffectItem = JSON.stringify(item)
|
|
591
619
|
for (const [node, field] of parts.directTexts) {
|
|
592
620
|
const text = item?.[field]
|
|
@@ -603,7 +631,10 @@ function fillListParts(root, parts, item, revision, index = 0) {
|
|
|
603
631
|
}
|
|
604
632
|
if (__KUDZU_LIST_ATTRIBUTES__) {
|
|
605
633
|
for (const [node, attributes] of parts.attributes) {
|
|
606
|
-
for (const [target, field] of attributes)
|
|
634
|
+
for (const [target, field] of attributes) {
|
|
635
|
+
if (previous && Object.is(previous[field], item?.[field])) continue
|
|
636
|
+
patchBinding(node, target, item?.[field], true)
|
|
637
|
+
}
|
|
607
638
|
}
|
|
608
639
|
}
|
|
609
640
|
if (__KUDZU_LIST_EVENTS__) {
|
|
@@ -978,7 +1009,7 @@ function serializeItem(value) {
|
|
|
978
1009
|
return { type: "object", nullPrototype: false, value: Object.entries(value).map(([key, entry]) => [key, serializeItem(entry)]) }
|
|
979
1010
|
}
|
|
980
1011
|
|
|
981
|
-
function patchBinding(node, target, value) {
|
|
1012
|
+
function patchBinding(node, target, value, write = false) {
|
|
982
1013
|
/* list-style */
|
|
983
1014
|
if (target === "disabled") {
|
|
984
1015
|
node.toggleAttribute("disabled", Boolean(value))
|
|
@@ -995,7 +1026,7 @@ function patchBinding(node, target, value) {
|
|
|
995
1026
|
node.removeAttribute(target)
|
|
996
1027
|
} else {
|
|
997
1028
|
const next = value === true && !isStringBooleanAttribute(target) ? "" : String(value)
|
|
998
|
-
if (node.getAttribute(target) !== next) node.setAttribute(target, next)
|
|
1029
|
+
if (write || node.getAttribute(target) !== next) node.setAttribute(target, next)
|
|
999
1030
|
}
|
|
1000
1031
|
}
|
|
1001
1032
|
|
|
@@ -63,6 +63,7 @@ function eligibleClick(event, anchor) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
function eligibleAnchor(anchor) {
|
|
66
|
+
if (!anchor) return false
|
|
66
67
|
if (anchor.hasAttribute("download") || anchor.hasAttribute("data-k-native") || !["", "_self"].includes(anchor.target)) return false
|
|
67
68
|
if (anchor.relList?.contains("external")) return false
|
|
68
69
|
const url = new URL(anchor.href)
|
|
@@ -125,8 +126,7 @@ async function navigate(url, push) {
|
|
|
125
126
|
catch { documentResult = await fetchDocument(url, record, request.signal) }
|
|
126
127
|
} else documentResult = await fetchDocument(url, record, request.signal)
|
|
127
128
|
documents.set(url.href, Promise.resolve(documentResult))
|
|
128
|
-
const { incoming, parsed } = documentResult
|
|
129
|
-
const capabilities = await loadCapabilities(parsed)
|
|
129
|
+
const { incoming, parsed, capabilities } = documentResult
|
|
130
130
|
if (current !== revision) return
|
|
131
131
|
await routeDispose()
|
|
132
132
|
if (current !== revision) return
|
|
@@ -157,7 +157,8 @@ async function fetchDocument(url, record, signal) {
|
|
|
157
157
|
const response = await fetch(url, { signal, redirect: "manual", headers: { accept: "text/html" } })
|
|
158
158
|
if (!response.ok || response.redirected || response.type === "opaqueredirect" || !response.headers.get("content-type")?.toLowerCase().includes("text/html")) throw new Error("Navigation response is not successful nonredirected HTML")
|
|
159
159
|
const incoming = new DOMParser().parseFromString(await response.text(), "text/html")
|
|
160
|
-
|
|
160
|
+
const parsed = validate(incoming, record)
|
|
161
|
+
return { incoming, parsed, capabilities: await loadCapabilities(parsed), record }
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
function validate(incoming, record) {
|