@kudzujs/core 0.8.62 → 0.9.0
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 +29 -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 +18 -5
- package/framework/dependency-runtime.js +1 -1
- package/framework/effect-runtime.js +2 -2
- package/framework/list-runtime.js +67 -24
- 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
|
|
@@ -434,11 +440,15 @@ function serializeCapture(name, value, seen) {
|
|
|
434
440
|
if (value === undefined) return { type: "undefined" }
|
|
435
441
|
if (typeof value !== "object") throw new Error(`Native capture "${name}" is not serializable: ${typeof value}`)
|
|
436
442
|
if (seen.has(value)) throw new Error(`Native capture "${name}" is not serializable: cycle`)
|
|
443
|
+
const cached = renderContext?.captureCache.get(value)
|
|
444
|
+
if (cached) return cached
|
|
437
445
|
|
|
438
446
|
seen.add(value)
|
|
439
447
|
try {
|
|
440
448
|
if (Array.isArray(value)) {
|
|
441
|
-
|
|
449
|
+
const serialized = { type: "array", value: Array.from(value, entry => serializeCapture(name, entry, seen)) }
|
|
450
|
+
renderContext?.captureCache.set(value, serialized)
|
|
451
|
+
return serialized
|
|
442
452
|
}
|
|
443
453
|
const prototype = Object.getPrototypeOf(value)
|
|
444
454
|
if (prototype !== Object.prototype && prototype !== null) {
|
|
@@ -451,14 +461,16 @@ function serializeCapture(name, value, seen) {
|
|
|
451
461
|
if (!("value" in descriptor)) throw new Error(`Native capture "${name}" is not serializable: accessor`)
|
|
452
462
|
entries.push([key, serializeCapture(name, descriptor.value, seen)])
|
|
453
463
|
}
|
|
454
|
-
|
|
464
|
+
const serialized = { type: "object", nullPrototype: prototype === null, value: entries }
|
|
465
|
+
renderContext?.captureCache.set(value, serialized)
|
|
466
|
+
return serialized
|
|
455
467
|
} finally {
|
|
456
468
|
seen.delete(value)
|
|
457
469
|
}
|
|
458
470
|
}
|
|
459
471
|
|
|
460
472
|
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 }
|
|
473
|
+
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
474
|
|
|
463
475
|
try {
|
|
464
476
|
const page = { [routeScopeMarker]: true, component, props }
|
|
@@ -471,6 +483,7 @@ export async function renderPage(component, metadata = {}, props = {}, layout) {
|
|
|
471
483
|
handler: effect.handler,
|
|
472
484
|
...(effect.dependencies ? { dependencies: effect.dependencies } : {}),
|
|
473
485
|
...(effect.dependencyExpressions ? { dependencyExpressions: effect.dependencyExpressions, dependencyStates: effect.dependencyStates } : {}),
|
|
486
|
+
...(effect.dependencyEvaluators ? { dependencyEvaluators: effect.dependencyEvaluators } : {}),
|
|
474
487
|
...(effect.itemDependencies ? { itemDependencies: effect.itemDependencies, listState: effect.listState } : {}),
|
|
475
488
|
...(effect.cleanup ? { cleanup: true } : {}),
|
|
476
489
|
...(effect.owner ? { owner: effect.owner } : {}),
|
|
@@ -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,
|
|
@@ -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)
|
|
1060
1077
|
}
|
|
1061
|
-
if (__KUDZU_LIST_ROW_REFS__) for (const ref of descriptor.rowRefs ?? []) replacements
|
|
1062
|
-
for (const marker of descriptor.rowConditions ?? []) replacements
|
|
1063
|
-
rowReplacements.set(root, replacements)
|
|
1064
|
-
|
|
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)
|
|
1091
|
+
}
|
|
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()
|
|
@@ -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") {
|