@kudzujs/core 0.8.62 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION_ROADMAP.md +36 -1
- package/PERFORMANCE.md +79 -1
- package/README.md +2 -2
- package/RELEASES.md +54 -0
- package/bin/kudzu.mjs +10 -1
- package/docs/next-architecture/0.9-baseline.md +1199 -0
- package/docs/next-architecture/0.9-benchmark-contracts.md +507 -0
- package/docs/next-architecture/0.9-component-property-contract.md +89 -0
- package/docs/next-architecture/0.9-compression-ledger.md +227 -0
- package/docs/next-architecture/0.9-final-proof-audit.md +176 -0
- package/docs/next-architecture/0.9-implementation-plan.md +1819 -0
- package/docs/next-architecture/0.9-resource-lifecycle.md +118 -0
- package/docs/next-architecture/0.9-semantic-compression.md +384 -0
- package/docs/next-architecture/README.md +16 -12
- package/docs/next-architecture/compiler-current-architecture.md +7 -7
- package/docs/next-architecture/large-application-ai-native-roadmap.md +5 -3
- package/docs/next-architecture/versioning.md +1 -1
- package/framework/README.md +2 -0
- package/framework/binding-runtime.js +4 -4
- package/framework/build.mjs +135 -30
- package/framework/compiler/ast-helpers.mjs +5 -0
- package/framework/compiler/browser-signal-passes.mjs +2 -7
- package/framework/compiler/collection-analysis.mjs +4 -0
- package/framework/compiler/descriptor-session.mjs +36 -12
- package/framework/compiler/effect-analysis.mjs +28 -8
- package/framework/compiler/effect-codegen.mjs +79 -36
- package/framework/compiler/effect-private-ref-pass.mjs +4 -8
- package/framework/compiler/handler-lowering.mjs +12 -7
- package/framework/compiler/ir/module-ir.mjs +26 -4
- package/framework/compiler/list-runtime-codegen.mjs +4 -2
- package/framework/compiler/optimize/command-specialization.mjs +4 -7
- package/framework/compiler/route-artifact-report.mjs +4 -3
- package/framework/compiler/route-build-record.mjs +12 -0
- package/framework/compiler/route-capability-planner.mjs +3 -3
- package/framework/compiler/route-ir.mjs +27 -11
- package/framework/compiler/runtime-codegen.mjs +2 -2
- package/framework/compiler/source-compiler.mjs +359 -78
- package/framework/core.d.ts +1 -0
- package/framework/core.mjs +22 -7
- package/framework/dependency-runtime.js +1 -1
- package/framework/effect-runtime.js +2 -2
- package/framework/list-runtime.js +70 -26
- package/framework/native-runtime.js +12 -9
- package/framework/runtime.js +1 -1
- package/framework/serialization.js +13 -6
- package/framework/shared-runtime.js +14 -12
- package/package.json +1 -1
package/framework/core.d.ts
CHANGED
|
@@ -154,6 +154,7 @@ export interface RouteIR {
|
|
|
154
154
|
dependencies?: string[]
|
|
155
155
|
dependencyExpressions?: unknown[]
|
|
156
156
|
dependencyStates?: Record<string, string>
|
|
157
|
+
dependencyEvaluators?: Array<{ module: string; handler: string; states: Record<string, string>; scope: Record<string, unknown>; field: string }>
|
|
157
158
|
itemDependencies?: string[]
|
|
158
159
|
listState?: string
|
|
159
160
|
cleanup?: true
|
package/framework/core.mjs
CHANGED
|
@@ -170,7 +170,7 @@ function createInternalState(initialValue) {
|
|
|
170
170
|
return signal
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
export function useEffect(callback, dependencies, module, handler, states, scope, source, cleanup, itemDependencies = [], dependencyExpressions = [], dependencyStates = []) {
|
|
173
|
+
export function useEffect(callback, dependencies, module, handler, states, scope, source, cleanup, itemDependencies = [], dependencyExpressions = [], dependencyStates = [], dependencyEvaluators = []) {
|
|
174
174
|
if (!renderContext) throw new Error("useEffect() can only run while rendering a Kudzu component")
|
|
175
175
|
if (typeof callback !== "function" || !Array.isArray(dependencies) || !module || !handler) throw new Error("useEffect() must be compiled with a literal dependency array")
|
|
176
176
|
if (itemDependencies.length && !renderContext.listDepth) throw new Error(`${source} useEffect() item-property dependencies are only supported in direct keyed row components`)
|
|
@@ -183,6 +183,12 @@ export function useEffect(callback, dependencies, module, handler, states, scope
|
|
|
183
183
|
if (!dependency?.[signalMarker]) throw new Error(`${source} useEffect() derived dependency state ${JSON.stringify(name)} must be Kudzu state`)
|
|
184
184
|
return [name, dependency.id]
|
|
185
185
|
}))
|
|
186
|
+
const evaluators = dependencyEvaluators.map(evaluator => {
|
|
187
|
+
if (!evaluator || typeof evaluator.field !== "string" || ["__proto__", "constructor", "prototype"].includes(evaluator.field)) throw new Error(`${source} useEffect() calculation dependency requires a static safe field`)
|
|
188
|
+
const descriptor = reactiveDescriptor(evaluator.module, evaluator.handler, evaluator.states, evaluator.scope)
|
|
189
|
+
retainHandlerReference(descriptor.module, descriptor.handler)
|
|
190
|
+
return { ...descriptor, field: evaluator.field }
|
|
191
|
+
})
|
|
186
192
|
let owner
|
|
187
193
|
let list = false
|
|
188
194
|
if (renderContext.listDepth) {
|
|
@@ -209,7 +215,7 @@ export function useEffect(callback, dependencies, module, handler, states, scope
|
|
|
209
215
|
owners.push(owner)
|
|
210
216
|
}
|
|
211
217
|
if (!renderContext.listDepth || list) {
|
|
212
|
-
renderContext.effects.push({ module, handler, states, scope, source, renderScope: renderContext.renderScope, ...(dependencyIds.length ? { dependencies: dependencyIds } : {}), ...(dependencyExpressions.length ? { dependencyExpressions, dependencyStates: dependencyStateIds } : {}), ...(itemDependencies.length ? { itemDependencies, listState: renderContext.listRoot.state } : {}), ...(cleanup ? { cleanup: true } : {}), ...(owner ? { owner } : {}), ...(list ? { list: true } : {}) })
|
|
218
|
+
renderContext.effects.push({ module, handler, states, scope, source, renderScope: renderContext.renderScope, ...(dependencyIds.length ? { dependencies: dependencyIds } : {}), ...(dependencyExpressions.length ? { dependencyExpressions, dependencyStates: dependencyStateIds } : {}), ...(evaluators.length ? { dependencyEvaluators: evaluators } : {}), ...(itemDependencies.length ? { itemDependencies, listState: renderContext.listRoot.state } : {}), ...(cleanup ? { cleanup: true } : {}), ...(owner ? { owner } : {}), ...(list ? { list: true } : {}) })
|
|
213
219
|
retainHandlerReference(module, handler)
|
|
214
220
|
}
|
|
215
221
|
renderContext.hasBehaviors = true
|
|
@@ -313,7 +319,7 @@ export function list(items, keyField, render, ownerField, selector = [], indexed
|
|
|
313
319
|
for (const [index, item] of values.entries()) {
|
|
314
320
|
const key = keyField === null ? index : item?.[keyField]
|
|
315
321
|
if (!validListKey(key)) throw new Error(`Keyed list key "${keyField}" must be a string or finite number`)
|
|
316
|
-
assertListItem(item)
|
|
322
|
+
assertListItem(item, keyField === null)
|
|
317
323
|
assertListValue(item, new Set())
|
|
318
324
|
const token = `${typeof key}:${key}`
|
|
319
325
|
if (keys.has(token)) throw new Error(`Duplicate keyed list key: ${String(key)}`)
|
|
@@ -354,7 +360,8 @@ function validListKey(key) {
|
|
|
354
360
|
return typeof key === "string" || typeof key === "number" && Number.isFinite(key)
|
|
355
361
|
}
|
|
356
362
|
|
|
357
|
-
function assertListItem(item) {
|
|
363
|
+
function assertListItem(item, allowPrimitive = false) {
|
|
364
|
+
if (allowPrimitive && (item === null || typeof item !== "object")) return
|
|
358
365
|
const prototype = item && typeof item === "object" ? Object.getPrototypeOf(item) : undefined
|
|
359
366
|
if (!item || Array.isArray(item) || prototype !== Object.prototype) throw new Error("Keyed list items must be ordinary plain objects")
|
|
360
367
|
}
|
|
@@ -434,11 +441,15 @@ function serializeCapture(name, value, seen) {
|
|
|
434
441
|
if (value === undefined) return { type: "undefined" }
|
|
435
442
|
if (typeof value !== "object") throw new Error(`Native capture "${name}" is not serializable: ${typeof value}`)
|
|
436
443
|
if (seen.has(value)) throw new Error(`Native capture "${name}" is not serializable: cycle`)
|
|
444
|
+
const cached = renderContext?.captureCache.get(value)
|
|
445
|
+
if (cached) return cached
|
|
437
446
|
|
|
438
447
|
seen.add(value)
|
|
439
448
|
try {
|
|
440
449
|
if (Array.isArray(value)) {
|
|
441
|
-
|
|
450
|
+
const serialized = { type: "array", value: Array.from(value, entry => serializeCapture(name, entry, seen)) }
|
|
451
|
+
renderContext?.captureCache.set(value, serialized)
|
|
452
|
+
return serialized
|
|
442
453
|
}
|
|
443
454
|
const prototype = Object.getPrototypeOf(value)
|
|
444
455
|
if (prototype !== Object.prototype && prototype !== null) {
|
|
@@ -451,14 +462,16 @@ function serializeCapture(name, value, seen) {
|
|
|
451
462
|
if (!("value" in descriptor)) throw new Error(`Native capture "${name}" is not serializable: accessor`)
|
|
452
463
|
entries.push([key, serializeCapture(name, descriptor.value, seen)])
|
|
453
464
|
}
|
|
454
|
-
|
|
465
|
+
const serialized = { type: "object", nullPrototype: prototype === null, value: entries }
|
|
466
|
+
renderContext?.captureCache.set(value, serialized)
|
|
467
|
+
return serialized
|
|
455
468
|
} finally {
|
|
456
469
|
seen.delete(value)
|
|
457
470
|
}
|
|
458
471
|
}
|
|
459
472
|
|
|
460
473
|
export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
461
|
-
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, nextId: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], sharedStates: new Map(), states: {}, textStates: new Set(), conditionStates: new Set(), conditionOwnedStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerReferences: new Map(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, searchParams: new Map(), searchParamEntries: [], searchParamsWritable: false, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
474
|
+
renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0, i: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, nextId: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], captureCache: new WeakMap(), sharedStates: new Map(), states: {}, textStates: new Set(), conditionStates: new Set(), conditionOwnedStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerReferences: new Map(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, searchParams: new Map(), searchParamEntries: [], searchParamsWritable: false, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
|
|
462
475
|
|
|
463
476
|
try {
|
|
464
477
|
const page = { [routeScopeMarker]: true, component, props }
|
|
@@ -471,6 +484,7 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
|
471
484
|
handler: effect.handler,
|
|
472
485
|
...(effect.dependencies ? { dependencies: effect.dependencies } : {}),
|
|
473
486
|
...(effect.dependencyExpressions ? { dependencyExpressions: effect.dependencyExpressions, dependencyStates: effect.dependencyStates } : {}),
|
|
487
|
+
...(effect.dependencyEvaluators ? { dependencyEvaluators: effect.dependencyEvaluators } : {}),
|
|
474
488
|
...(effect.itemDependencies ? { itemDependencies: effect.itemDependencies, listState: effect.listState } : {}),
|
|
475
489
|
...(effect.cleanup ? { cleanup: true } : {}),
|
|
476
490
|
...(effect.owner ? { owner: effect.owner } : {}),
|
|
@@ -1090,6 +1104,7 @@ function escapeJsonAttribute(value) {
|
|
|
1090
1104
|
|
|
1091
1105
|
function compactListState(value) {
|
|
1092
1106
|
if (!Array.isArray(value) || !value.length) return undefined
|
|
1107
|
+
if (!value[0] || typeof value[0] !== "object" || Array.isArray(value[0]) || Object.getPrototypeOf(value[0]) !== Object.prototype) return undefined
|
|
1093
1108
|
const fields = Object.keys(value[0])
|
|
1094
1109
|
if (!fields.length || !value.every(item => Object.keys(item).length === fields.length && fields.every(field => Object.hasOwn(item, field)))) return undefined
|
|
1095
1110
|
return [fields, value.map(item => fields.map(field => item[field]))]
|
|
@@ -4,7 +4,7 @@ export function applyCommands(state, commands, commit, log = console.log) {
|
|
|
4
4
|
const current = state.get(id)
|
|
5
5
|
if (operation === "log") log(operand, current)
|
|
6
6
|
else {
|
|
7
|
-
state.set(id, operation === "add" ? current + operand : operand)
|
|
7
|
+
state.set(id, operation === "add" ? current + operand : operation === "toggle" ? !current : operand)
|
|
8
8
|
changed.add(id)
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { deserialize } from "./serialization.js"
|
|
2
2
|
|
|
3
|
-
export function createEffectContext(state, stateIds, commit, serializedScope = {}, active = () => true) {
|
|
3
|
+
export function createEffectContext(state, stateIds, commit, serializedScope = {}, active = () => true, resolveRef) {
|
|
4
4
|
const changed = new Set()
|
|
5
5
|
let scheduled = false
|
|
6
6
|
|
|
@@ -24,7 +24,7 @@ export function createEffectContext(state, stateIds, commit, serializedScope = {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const scope = globalThis.__KUDZU_EFFECT_CAPTURES__
|
|
27
|
-
? Object.fromEntries(Object.entries(serializedScope).map(([name, value]) => [name, deserialize(value, id => state.get(id), globalThis.__KUDZU_CAPTURE_SETTER__ ? setId : undefined)]))
|
|
27
|
+
? Object.fromEntries(Object.entries(serializedScope).map(([name, value]) => [name, deserialize(value, id => state.get(id), globalThis.__KUDZU_CAPTURE_SETTER__ ? setId : undefined, undefined, resolveRef)]))
|
|
28
28
|
: undefined
|
|
29
29
|
|
|
30
30
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { browserState, mountDom, notifyListItem, registerCommitter, registerMountHook, registerUnmountHook, releaseState, unmountDom } from "./shared-runtime.js"
|
|
1
|
+
import { browserState, listItems, listRowPaths, mountDom, notifyListItem, registerCommitter, registerMountHook, registerUnmountHook, releaseState, unmountDom } from "./shared-runtime.js"
|
|
2
2
|
import { selectCollection } from "./collection-selector.js"
|
|
3
3
|
const loadListEvaluator = descriptor => import("./binding-runtime.js").then(module => module.loadEvaluator(descriptor))
|
|
4
4
|
|
|
@@ -9,10 +9,10 @@ const mountedLists = new WeakSet()
|
|
|
9
9
|
const imports = __KUDZU_LIST_ASYNC_PARTS__ ? new Map() : undefined
|
|
10
10
|
const revisions = __KUDZU_LIST_ASYNC_PARTS__ ? new WeakMap() : undefined
|
|
11
11
|
const itemParts = new WeakMap()
|
|
12
|
-
const listItems = new WeakMap()
|
|
13
12
|
const listIndexes = __KUDZU_LIST_INDEXES__ ? new WeakMap() : undefined
|
|
14
13
|
const ownershipPaths = __KUDZU_LIST_ROW_HOOKS__ ? new WeakMap() : undefined
|
|
15
14
|
const rowReplacements = __KUDZU_LIST_ROW_HOOKS__ ? new WeakMap() : undefined
|
|
15
|
+
const directRowReplacements = __KUDZU_LIST_ROW_HOOKS__ ? new WeakSet() : undefined
|
|
16
16
|
const ownedLists = __KUDZU_NESTED_LISTS__ ? new WeakMap() : undefined
|
|
17
17
|
const conditionOwners = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
|
|
18
18
|
const conditionTemplates = __KUDZU_LIST_CONDITIONS__ ? new WeakMap() : undefined
|
|
@@ -33,8 +33,8 @@ function commitLists(id) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
registerCommitter(commitLists)
|
|
36
|
-
registerMountHook(mountLists)
|
|
37
|
-
registerUnmountHook(unmountLists)
|
|
36
|
+
registerMountHook(mountLists, "lists")
|
|
37
|
+
registerUnmountHook(unmountLists, "lists")
|
|
38
38
|
|
|
39
39
|
if (typeof document !== "undefined") mountDom(document)
|
|
40
40
|
|
|
@@ -50,8 +50,9 @@ function mountLists(root) {
|
|
|
50
50
|
const roots = listRoots(start, end)
|
|
51
51
|
const nested = __KUDZU_NESTED_LISTS__ ? mountNestedPrototype(start, descriptor, roots) : undefined
|
|
52
52
|
const templateRoot = __KUDZU_NESTED_LISTS__ ? nested.templateRoot : listTemplateRoot(start, descriptor)
|
|
53
|
+
const lifecycle = listLifecycle(descriptor, templateRoot)
|
|
53
54
|
if (__KUDZU_LIST_ROW_HOOKS__) for (let index = 0; index < roots.length; index++) initializeGeneralRowHooks(descriptor, descriptor.keys[index], roots[index], nested?.owner)
|
|
54
|
-
const parts = listItemPartPlan(templateRoot, descriptor.nested)
|
|
55
|
+
const parts = listItemPartPlan(templateRoot, descriptor.nested, descriptor)
|
|
55
56
|
const staticRows = __KUDZU_STATIC_COLLECTIONS__ && descriptor.static && parts.directFill ? new Map() : undefined
|
|
56
57
|
for (const root of roots) {
|
|
57
58
|
if (__KUDZU_LIST_CONDITIONS__ && descriptor.conditions) {
|
|
@@ -68,6 +69,8 @@ function mountLists(root) {
|
|
|
68
69
|
const list = {
|
|
69
70
|
start,
|
|
70
71
|
descriptor,
|
|
72
|
+
lifecycle,
|
|
73
|
+
unmountLifecycle: descriptor.rowStates?.length ? lifecycle.filter(capability => capability !== "bindings") : lifecycle,
|
|
71
74
|
templateRoot,
|
|
72
75
|
...(__KUDZU_NESTED_LISTS__ && nested.childPrototypes?.size ? { childPrototypes: nested.childPrototypes } : {}),
|
|
73
76
|
parts,
|
|
@@ -167,7 +170,7 @@ function updateList(list) {
|
|
|
167
170
|
for (const [index, item] of items.entries()) {
|
|
168
171
|
const key = list.descriptor.key === null ? index : item?.[list.descriptor.key]
|
|
169
172
|
if (!validListKey(key)) throw new Error(`Keyed list key "${list.descriptor.key}" must be a string or finite number`)
|
|
170
|
-
assertListItem(item)
|
|
173
|
+
assertListItem(item, list.descriptor.key === null)
|
|
171
174
|
const seededValue = __KUDZU_LIST_SEEDS__ ? list.seedFields && seededListValue(item, list.seedFields, list.valueSeed, !referenceOnly) : undefined
|
|
172
175
|
if (seededValue === undefined) assertListValue(item, seen, true)
|
|
173
176
|
const token = keyToken(key)
|
|
@@ -237,7 +240,7 @@ function updateList(list) {
|
|
|
237
240
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
238
241
|
node.removeAttribute("data-k-list-root")
|
|
239
242
|
if (__KUDZU_NESTED_LISTS__ && list.childPrototypes) childPrototypes.set(node, list.childPrototypes)
|
|
240
|
-
if (__KUDZU_LIST_ROW_HOOKS__) initializeGeneralRowHooks(list.descriptor, key, node, list.owner, item)
|
|
243
|
+
if (__KUDZU_LIST_ROW_HOOKS__ && hasRowHooks(list.descriptor)) initializeGeneralRowHooks(list.descriptor, key, node, list.owner, item)
|
|
241
244
|
if (staticRoot) listItems.set(node, item)
|
|
242
245
|
else if (list.parts.directFill) {
|
|
243
246
|
listItems.set(node, item)
|
|
@@ -256,7 +259,7 @@ function updateList(list) {
|
|
|
256
259
|
if (keys.has(token)) continue
|
|
257
260
|
if (list.descriptor.fastRelease) node.remove()
|
|
258
261
|
else if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) {
|
|
259
|
-
unmountDom(node)
|
|
262
|
+
unmountDom(node, list.unmountLifecycle)
|
|
260
263
|
node.remove()
|
|
261
264
|
} else node.remove()
|
|
262
265
|
if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, ownershipPaths.get(node))
|
|
@@ -282,8 +285,8 @@ function updateList(list) {
|
|
|
282
285
|
if (run.firstChild) parent.insertBefore(run, list.boundary)
|
|
283
286
|
ordered = true
|
|
284
287
|
} else parent.insertBefore(additions, list.boundary)
|
|
285
|
-
if (addedNodes?.length > 32 && addedNodes.length * 2 > next.length && addedNodes.length * 2 > parent.children.length && !list.descriptor.children && !list.descriptor.ownerField) mountDom(parent)
|
|
286
|
-
else if (addedNodes) for (const node of addedNodes) mountDom(node)
|
|
288
|
+
if (addedNodes?.length > 32 && addedNodes.length * 2 > next.length && addedNodes.length * 2 > parent.children.length && !list.descriptor.children && !list.descriptor.ownerField) mountDom(parent, list.lifecycle)
|
|
289
|
+
else if (addedNodes) for (const node of addedNodes) mountDom(node, list.lifecycle)
|
|
287
290
|
list.container ??= parent
|
|
288
291
|
}
|
|
289
292
|
let anchor = list.boundary
|
|
@@ -433,7 +436,7 @@ function updateStableList(list, items) {
|
|
|
433
436
|
const firstAdded = fragment.firstChild
|
|
434
437
|
parent.insertBefore(fragment, list.boundary)
|
|
435
438
|
if (referenceOnly) list.values.clear()
|
|
436
|
-
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) for (let node = firstAdded; node !== list.boundary; node = node.nextSibling) mountDom(node)
|
|
439
|
+
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) for (let node = firstAdded; node !== list.boundary; node = node.nextSibling) mountDom(node, list.lifecycle)
|
|
437
440
|
list.container ??= parent
|
|
438
441
|
list.items = items
|
|
439
442
|
return true
|
|
@@ -578,12 +581,12 @@ function addListRoot(list, { item, index = list.roots.size, key, token, value })
|
|
|
578
581
|
if (!node) throw new Error("Keyed list template has no root element")
|
|
579
582
|
node.removeAttribute("data-k-list-root")
|
|
580
583
|
if (__KUDZU_NESTED_LISTS__ && list.childPrototypes) childPrototypes.set(node, list.childPrototypes)
|
|
581
|
-
if (__KUDZU_LIST_ROW_HOOKS__) initializeGeneralRowHooks(list.descriptor, key, node, list.owner, item)
|
|
584
|
+
if (__KUDZU_LIST_ROW_HOOKS__ && hasRowHooks(list.descriptor)) initializeGeneralRowHooks(list.descriptor, key, node, list.owner, item)
|
|
582
585
|
mapListItemParts(list.parts, node, list.descriptor.nested)
|
|
583
586
|
fillListItem(node, item, list.descriptor.nested, index)
|
|
584
587
|
const parent = list.container ?? list.start.parentNode
|
|
585
588
|
parent.insertBefore(node, list.boundary)
|
|
586
|
-
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) mountDom(node)
|
|
589
|
+
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount) mountDom(node, list.lifecycle)
|
|
587
590
|
list.roots.set(token, node)
|
|
588
591
|
if (__KUDZU_LIST_STABLE_FAST_PATHS__) list.orderedRoots.push(node)
|
|
589
592
|
if (!usesItemReferences(list)) list.values.set(token, value)
|
|
@@ -592,7 +595,7 @@ function addListRoot(list, { item, index = list.roots.size, key, token, value })
|
|
|
592
595
|
|
|
593
596
|
function removeListRoot(list, token) {
|
|
594
597
|
const node = list.roots.get(token)
|
|
595
|
-
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount && !list.descriptor.fastRelease) unmountDom(node)
|
|
598
|
+
if (__KUDZU_LIST_MOUNTS__ && list.descriptor.mount && !list.descriptor.fastRelease) unmountDom(node, list.unmountLifecycle)
|
|
596
599
|
node.remove()
|
|
597
600
|
if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) deleteRowStates(list.descriptor, ownershipPaths.get(node))
|
|
598
601
|
list.roots.delete(token)
|
|
@@ -610,7 +613,7 @@ function fillListItem(root, item, nested = false, index = 0, parts = listItemPar
|
|
|
610
613
|
const children = ownedLists.get(root)
|
|
611
614
|
if (children) for (const child of children.values()) updateList(child)
|
|
612
615
|
}
|
|
613
|
-
if (__KUDZU_LIST_ROW_HOOKS__)
|
|
616
|
+
if (__KUDZU_LIST_ROW_HOOKS__) replaceOwnedRowIds(root)
|
|
614
617
|
}
|
|
615
618
|
|
|
616
619
|
function fillListParts(root, parts, item, revision, index = 0, previous) {
|
|
@@ -720,7 +723,7 @@ function hydrateListItemPartValues(planned, initial, missing) {
|
|
|
720
723
|
if (index !== planned.length) throw new Error("Keyed list item markers do not match its template")
|
|
721
724
|
}
|
|
722
725
|
|
|
723
|
-
function listItemPartPlan(template, nested = false) {
|
|
726
|
+
function listItemPartPlan(template, nested = false, descriptor) {
|
|
724
727
|
const source = nested ? ownedElements(template) : [template, ...template.querySelectorAll("*")]
|
|
725
728
|
const indexes = new Map(source.map((node, index) => [node, index]))
|
|
726
729
|
const parts = listItemParts(template, nested)
|
|
@@ -728,7 +731,7 @@ function listItemPartPlan(template, nested = false) {
|
|
|
728
731
|
const location = structural ? node => elementPath(template, node) : node => indexes.get(node)
|
|
729
732
|
return {
|
|
730
733
|
structural,
|
|
731
|
-
directFill: structural && !parts.events.length && !__KUDZU_LIST_ASYNC_PARTS__ && !__KUDZU_NESTED_LISTS__ && !__KUDZU_LIST_INDEXES__
|
|
734
|
+
directFill: structural && !parts.events.length && !__KUDZU_LIST_ASYNC_PARTS__ && !__KUDZU_NESTED_LISTS__ && !__KUDZU_LIST_INDEXES__,
|
|
732
735
|
directTexts: parts.directTexts.map(([node, field]) => [location(node), field]),
|
|
733
736
|
texts: __KUDZU_LIST_TEXT_RANGES__ ? parts.texts.map(([node, field]) => [location(node), field]) : [],
|
|
734
737
|
attributes: __KUDZU_LIST_ATTRIBUTES__ ? parts.attributes.map(([node, attributes]) => [location(node), attributes]) : [],
|
|
@@ -736,7 +739,11 @@ function listItemPartPlan(template, nested = false) {
|
|
|
736
739
|
expressions: __KUDZU_LIST_EXPRESSIONS__ ? parts.expressions.map(([node, descriptor]) => [location(node), descriptor]) : [],
|
|
737
740
|
expressionAttributes: __KUDZU_LIST_EXPRESSION_ATTRIBUTES__ ? parts.expressionAttributes.map(([node, attributes]) => [location(node), attributes]) : [],
|
|
738
741
|
conditions: __KUDZU_LIST_CONDITIONS__ ? parts.conditions.map(([node, descriptor]) => [location(node), descriptor, node]) : [],
|
|
739
|
-
effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(location) : []
|
|
742
|
+
effects: __KUDZU_LIST_EFFECTS__ ? parts.effects.map(location) : [],
|
|
743
|
+
rowIds: structural && __KUDZU_GENERAL_ROW_HOOKS__ && hasRowHooks(descriptor) ? source.flatMap(node => {
|
|
744
|
+
const attributes = [...node.attributes].filter(attribute => attribute.name.startsWith("data-k-") && attribute.value.includes("$k")).map(attribute => attribute.name)
|
|
745
|
+
return attributes.length ? [[location(node), attributes]] : []
|
|
746
|
+
}) : []
|
|
740
747
|
}
|
|
741
748
|
}
|
|
742
749
|
|
|
@@ -795,6 +802,14 @@ function fillStructuralListParts(parts, root, item) {
|
|
|
795
802
|
const attributes = part[1]
|
|
796
803
|
for (let index = 0; index < attributes.length; index++) patchBinding(node, attributes[index][0], item[attributes[index][1]])
|
|
797
804
|
}
|
|
805
|
+
if (__KUDZU_GENERAL_ROW_HOOKS__ && directRowReplacements.delete(root)) {
|
|
806
|
+
const replacement = listRowPaths.get(root)
|
|
807
|
+
for (const [path, attributes] of parts.rowIds) {
|
|
808
|
+
let node = root
|
|
809
|
+
for (let index = 0; index < path.length; index++) node = node.children[path[index]]
|
|
810
|
+
for (const attribute of attributes) node.setAttribute(attribute, node.getAttribute(attribute).replaceAll("$k", replacement))
|
|
811
|
+
}
|
|
812
|
+
}
|
|
798
813
|
}
|
|
799
814
|
|
|
800
815
|
function elementPath(root, node) {
|
|
@@ -1051,20 +1066,48 @@ function initializeGeneralRowHooks(descriptor, key, root, owner, item) {
|
|
|
1051
1066
|
const path = [...(ownershipPaths.get(owner) ?? []), `${descriptor.id}=${token}`]
|
|
1052
1067
|
ownershipPaths.set(root, path)
|
|
1053
1068
|
const statePath = descriptor.ownerField ? path : [token]
|
|
1054
|
-
root
|
|
1055
|
-
const
|
|
1069
|
+
listRowPaths.set(root, encodeURIComponent(statePath.join("/")))
|
|
1070
|
+
const direct = !descriptor.nested && !descriptor.children && !descriptor.ownerField
|
|
1071
|
+
if (direct) directRowReplacements.add(root)
|
|
1072
|
+
const replacements = direct ? undefined : new Map()
|
|
1056
1073
|
for (const state of descriptor.rowStates ?? []) {
|
|
1057
1074
|
const id = rowStateId(state.id, statePath)
|
|
1058
1075
|
if (!browserState.has(id)) browserState.set(id, state.initializer === "list-item" ? structuredClone(item) : __KUDZU_COMPLEX_LIST_ROW_STATE__ && state.initialValue !== null && typeof state.initialValue === "object" ? structuredClone(state.initialValue) : state.initialValue)
|
|
1059
|
-
replacements
|
|
1076
|
+
replacements?.set(state.id, id)
|
|
1077
|
+
}
|
|
1078
|
+
if (__KUDZU_LIST_ROW_REFS__) for (const ref of descriptor.rowRefs ?? []) replacements?.set(ref, rowStateId(ref, statePath))
|
|
1079
|
+
for (const marker of descriptor.rowConditions ?? []) replacements?.set(marker, rowStateId(marker, statePath))
|
|
1080
|
+
if (replacements) rowReplacements.set(root, replacements)
|
|
1081
|
+
if (descriptor.conditions || descriptor.expressions || descriptor.expressionAttributes) replaceOwnedRowIds(root)
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function replaceOwnedRowIds(root) {
|
|
1085
|
+
if (!directRowReplacements.delete(root)) return replaceRowIds(root, rowReplacements.get(root))
|
|
1086
|
+
const path = listRowPaths.get(root)
|
|
1087
|
+
const replace = node => {
|
|
1088
|
+
for (const attribute of [...node.attributes]) if (attribute.name.startsWith("data-k-") && attribute.value.includes("$k")) attribute.value = attribute.value.replaceAll("$k", path)
|
|
1089
|
+
for (const child of node.children) replace(child)
|
|
1090
|
+
for (const child of node.content?.children ?? []) replace(child)
|
|
1060
1091
|
}
|
|
1061
|
-
|
|
1062
|
-
for (const marker of descriptor.rowConditions ?? []) replacements.set(marker, rowStateId(marker, statePath))
|
|
1063
|
-
rowReplacements.set(root, replacements)
|
|
1064
|
-
replaceRowIds(root, replacements)
|
|
1092
|
+
replace(root)
|
|
1065
1093
|
}
|
|
1066
1094
|
/* general-row-hooks-end */
|
|
1067
1095
|
|
|
1096
|
+
function hasRowHooks(descriptor) {
|
|
1097
|
+
return Boolean(descriptor.rowStates?.length || descriptor.rowRefs?.length || descriptor.rowConditions?.length)
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
function listLifecycle(descriptor, template) {
|
|
1101
|
+
const capabilities = []
|
|
1102
|
+
if (template.matches?.("[data-k-text]") || template.querySelector?.("[data-k-text]")) capabilities.push("text")
|
|
1103
|
+
if (descriptor.attributes || descriptor.expressions || descriptor.expressionAttributes || descriptor.rowStates?.length) capabilities.push("bindings")
|
|
1104
|
+
if (descriptor.conditions || template.querySelector?.("[data-k-if]")) capabilities.push("conditions")
|
|
1105
|
+
if (descriptor.nested || descriptor.children || template.querySelector?.("template[data-k-list]")) capabilities.push("lists")
|
|
1106
|
+
if (descriptor.events || template.querySelector?.("[data-k-native-click],[data-k-native-input],[data-k-native-change],[data-k-native-submit],[data-k-native-keydown],[data-k-native-keyup]")) capabilities.push("native")
|
|
1107
|
+
if (descriptor.effects) capabilities.push("effects")
|
|
1108
|
+
return capabilities
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1068
1111
|
function initializeRowStates(descriptor, key, root) {
|
|
1069
1112
|
const token = keyToken(key)
|
|
1070
1113
|
const replacements = new Map()
|
|
@@ -1112,7 +1155,8 @@ function validListKey(key) {
|
|
|
1112
1155
|
return typeof key === "string" || typeof key === "number" && Number.isFinite(key)
|
|
1113
1156
|
}
|
|
1114
1157
|
|
|
1115
|
-
function assertListItem(item) {
|
|
1158
|
+
function assertListItem(item, allowPrimitive = false) {
|
|
1159
|
+
if (allowPrimitive && (item === null || typeof item !== "object")) return
|
|
1116
1160
|
const prototype = item && typeof item === "object" ? Object.getPrototypeOf(item) : undefined
|
|
1117
1161
|
if (!item || Array.isArray(item) || prototype !== Object.prototype) throw new Error("Keyed list items must be ordinary plain objects")
|
|
1118
1162
|
}
|
|
@@ -60,18 +60,21 @@ export function createNativeContext(state, stateIds, commit, serializedScope = {
|
|
|
60
60
|
|
|
61
61
|
if (typeof document !== "undefined") {
|
|
62
62
|
const eventNames = ["click", "input", "change", "submit", "keydown", "keyup"]
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
const selector = eventNames.map(eventName => `[data-k-native-${eventName}]`).join(",")
|
|
64
|
+
const mount = root => mountNative(root, eventNames, selector, modules)
|
|
65
|
+
const unmount = root => unmountNative(root, selector)
|
|
66
|
+
registerMountHook(mount, "native")
|
|
67
|
+
registerUnmountHook(unmount, "native")
|
|
66
68
|
mount(document)
|
|
67
69
|
addEventListener("pagehide", event => {
|
|
68
|
-
if (!event.persisted)
|
|
70
|
+
if (!event.persisted) unmount(document)
|
|
69
71
|
})
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
function mountNative(root, eventNames, modules) {
|
|
73
|
-
for (const
|
|
74
|
-
for (const
|
|
74
|
+
function mountNative(root, eventNames, selector, modules) {
|
|
75
|
+
for (const node of matching(root, selector)) {
|
|
76
|
+
for (const eventName of eventNames) {
|
|
77
|
+
if (!node.hasAttribute(`data-k-native-${eventName}`)) continue
|
|
75
78
|
const listeners = registrations.get(node) ?? new Map()
|
|
76
79
|
if (listeners.has(eventName)) continue
|
|
77
80
|
let encoded = node.dataset[`kNative${capitalize(eventName)}`]
|
|
@@ -104,8 +107,8 @@ function mountNative(root, eventNames, modules) {
|
|
|
104
107
|
}
|
|
105
108
|
}
|
|
106
109
|
|
|
107
|
-
function unmountNative(root) {
|
|
108
|
-
for (const node of matching(root,
|
|
110
|
+
function unmountNative(root, selector) {
|
|
111
|
+
for (const node of matching(root, selector)) {
|
|
109
112
|
for (const [eventName, registration] of registrations.get(node) ?? []) {
|
|
110
113
|
registration.active = false
|
|
111
114
|
node.removeEventListener(eventName, registration.listener)
|
package/framework/runtime.js
CHANGED
|
@@ -4,7 +4,7 @@ export function applyCommands(state, commands, commit, log = console.log) {
|
|
|
4
4
|
const current=state.get(id)
|
|
5
5
|
if(operation==="log")log(value,current)
|
|
6
6
|
else {
|
|
7
|
-
state.set(id,operation==="add"?current+value:value)
|
|
7
|
+
state.set(id,operation==="add"?current+value:operation==="toggle"?!current:value)
|
|
8
8
|
changed.add(id)
|
|
9
9
|
}
|
|
10
10
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
export function deserialize(value, getState, setState, active) {
|
|
1
|
+
export function deserialize(value, getState, setState, active, resolveRef) {
|
|
2
2
|
if (!value || typeof value !== "object") return value
|
|
3
3
|
if (value.type === "undefined") return undefined
|
|
4
4
|
if (value.type === "number") return value.value === "NaN" ? NaN : value.value === "Infinity" ? Infinity : value.value === "-Infinity" ? -Infinity : -0
|
|
5
|
-
if (value.type === "ref")
|
|
5
|
+
if (value.type === "ref") {
|
|
6
|
+
let current
|
|
7
|
+
return { get current() {
|
|
8
|
+
if (typeof document === "undefined" || active?.() === false) return null
|
|
9
|
+
if (!current?.isConnected) current = resolveRef ? resolveRef(value.id) : document.querySelector(`[data-k-ref="${value.id}"]`)
|
|
10
|
+
return current
|
|
11
|
+
} }
|
|
12
|
+
}
|
|
6
13
|
if (globalThis.__KUDZU_CAPTURE_STATE__ && value.type === "state") return getState?.(value.id)
|
|
7
14
|
if (globalThis.__KUDZU_CAPTURE_SETTER__ && value.type === "setter") return next => {
|
|
8
15
|
if (!setState) throw new Error("Captured state setter is not available in this context")
|
|
@@ -10,20 +17,20 @@ export function deserialize(value, getState, setState, active) {
|
|
|
10
17
|
}
|
|
11
18
|
if (value.type === "array") {
|
|
12
19
|
const array = []
|
|
13
|
-
for (const [index, entry] of value.value.entries()) defineCapture(array, String(index), entry, getState, setState, active)
|
|
20
|
+
for (const [index, entry] of value.value.entries()) defineCapture(array, String(index), entry, getState, setState, active, resolveRef)
|
|
14
21
|
return array
|
|
15
22
|
}
|
|
16
23
|
if (value.type === "object") {
|
|
17
24
|
const object = value.nullPrototype ? Object.create(null) : {}
|
|
18
|
-
for (const [key, entry] of value.value) defineCapture(object, key, entry, getState, setState, active)
|
|
25
|
+
for (const [key, entry] of value.value) defineCapture(object, key, entry, getState, setState, active, resolveRef)
|
|
19
26
|
return object
|
|
20
27
|
}
|
|
21
28
|
return value
|
|
22
29
|
}
|
|
23
30
|
|
|
24
|
-
function defineCapture(target, key, entry, getState, setState, active) {
|
|
31
|
+
function defineCapture(target, key, entry, getState, setState, active, resolveRef) {
|
|
25
32
|
const descriptor = globalThis.__KUDZU_CAPTURE_STATE__ && entry?.type === "state" && getState
|
|
26
33
|
? { get: () => getState(entry.id) }
|
|
27
|
-
: { value: deserialize(entry, getState, setState, active), writable: true }
|
|
34
|
+
: { value: deserialize(entry, getState, setState, active, resolveRef), writable: true }
|
|
28
35
|
Object.defineProperty(target, key, { ...descriptor, enumerable: true, configurable: true })
|
|
29
36
|
}
|
|
@@ -2,7 +2,7 @@ export function applyCommands(state, commands, commit, log = console.log) {
|
|
|
2
2
|
if (commands.length === 1 && commands[0][0] !== "log") {
|
|
3
3
|
const [operation, id, operand] = commands[0]
|
|
4
4
|
const current = state.get(id)
|
|
5
|
-
const value = operation === "add" ? current + operand : operand
|
|
5
|
+
const value = operation === "add" ? current + operand : operation === "toggle" ? !current : operand
|
|
6
6
|
state.set(id, value)
|
|
7
7
|
commit(id, value)
|
|
8
8
|
return
|
|
@@ -15,7 +15,7 @@ export function applyCommands(state, commands, commit, log = console.log) {
|
|
|
15
15
|
log(operand, current)
|
|
16
16
|
continue
|
|
17
17
|
}
|
|
18
|
-
state.set(id, operation === "add" ? current + operand : operand)
|
|
18
|
+
state.set(id, operation === "add" ? current + operand : operation === "toggle" ? !current : operand)
|
|
19
19
|
changed.add(id)
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -23,6 +23,8 @@ export function applyCommands(state, commands, commit, log = console.log) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export const browserState = new Map()
|
|
26
|
+
export const listItems = new WeakMap()
|
|
27
|
+
export const listRowPaths = new WeakMap()
|
|
26
28
|
const committers = []
|
|
27
29
|
const mountHooks = []
|
|
28
30
|
const unmountHooks = []
|
|
@@ -52,12 +54,12 @@ export function registerCommitter(commit) {
|
|
|
52
54
|
committers.push(commit)
|
|
53
55
|
}
|
|
54
56
|
|
|
55
|
-
export function registerMountHook(mount) {
|
|
56
|
-
mountHooks.push(mount)
|
|
57
|
+
export function registerMountHook(mount, capability) {
|
|
58
|
+
mountHooks.push({ mount, capability })
|
|
57
59
|
}
|
|
58
60
|
|
|
59
|
-
export function registerUnmountHook(unmount) {
|
|
60
|
-
unmountHooks.push(unmount)
|
|
61
|
+
export function registerUnmountHook(unmount, capability) {
|
|
62
|
+
unmountHooks.push({ unmount, capability })
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
export function registerStateReleaseHook(release) {
|
|
@@ -100,14 +102,14 @@ function unmountText(root) {
|
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
104
|
|
|
103
|
-
export function mountDom(root) {
|
|
104
|
-
mountText(root)
|
|
105
|
-
for (const
|
|
105
|
+
export function mountDom(root, capabilities) {
|
|
106
|
+
if (!capabilities || capabilities.includes("text")) mountText(root)
|
|
107
|
+
for (const entry of mountHooks) if (!capabilities || !entry.capability || capabilities.includes(entry.capability)) entry.mount(root)
|
|
106
108
|
}
|
|
107
109
|
|
|
108
|
-
export function unmountDom(root) {
|
|
109
|
-
for (const
|
|
110
|
-
unmountText(root)
|
|
110
|
+
export function unmountDom(root, capabilities) {
|
|
111
|
+
for (const entry of unmountHooks) if (!capabilities || !entry.capability || capabilities.includes(entry.capability)) entry.unmount(root)
|
|
112
|
+
if (!capabilities || capabilities.includes("text")) unmountText(root)
|
|
111
113
|
}
|
|
112
114
|
|
|
113
115
|
if (typeof document !== "undefined") {
|