@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.
Files changed (47) hide show
  1. package/MIGRATION_ROADMAP.md +36 -1
  2. package/PERFORMANCE.md +79 -1
  3. package/README.md +2 -2
  4. package/RELEASES.md +29 -0
  5. package/bin/kudzu.mjs +10 -1
  6. package/docs/next-architecture/0.9-baseline.md +1199 -0
  7. package/docs/next-architecture/0.9-benchmark-contracts.md +507 -0
  8. package/docs/next-architecture/0.9-component-property-contract.md +89 -0
  9. package/docs/next-architecture/0.9-compression-ledger.md +227 -0
  10. package/docs/next-architecture/0.9-final-proof-audit.md +176 -0
  11. package/docs/next-architecture/0.9-implementation-plan.md +1819 -0
  12. package/docs/next-architecture/0.9-resource-lifecycle.md +118 -0
  13. package/docs/next-architecture/0.9-semantic-compression.md +384 -0
  14. package/docs/next-architecture/README.md +16 -12
  15. package/docs/next-architecture/compiler-current-architecture.md +7 -7
  16. package/docs/next-architecture/large-application-ai-native-roadmap.md +5 -3
  17. package/docs/next-architecture/versioning.md +1 -1
  18. package/framework/README.md +2 -0
  19. package/framework/binding-runtime.js +4 -4
  20. package/framework/build.mjs +135 -30
  21. package/framework/compiler/ast-helpers.mjs +5 -0
  22. package/framework/compiler/browser-signal-passes.mjs +2 -7
  23. package/framework/compiler/collection-analysis.mjs +4 -0
  24. package/framework/compiler/descriptor-session.mjs +36 -12
  25. package/framework/compiler/effect-analysis.mjs +28 -8
  26. package/framework/compiler/effect-codegen.mjs +79 -36
  27. package/framework/compiler/effect-private-ref-pass.mjs +4 -8
  28. package/framework/compiler/handler-lowering.mjs +12 -7
  29. package/framework/compiler/ir/module-ir.mjs +26 -4
  30. package/framework/compiler/list-runtime-codegen.mjs +4 -2
  31. package/framework/compiler/optimize/command-specialization.mjs +4 -7
  32. package/framework/compiler/route-artifact-report.mjs +4 -3
  33. package/framework/compiler/route-build-record.mjs +12 -0
  34. package/framework/compiler/route-capability-planner.mjs +3 -3
  35. package/framework/compiler/route-ir.mjs +27 -11
  36. package/framework/compiler/runtime-codegen.mjs +2 -2
  37. package/framework/compiler/source-compiler.mjs +359 -78
  38. package/framework/core.d.ts +1 -0
  39. package/framework/core.mjs +18 -5
  40. package/framework/dependency-runtime.js +1 -1
  41. package/framework/effect-runtime.js +2 -2
  42. package/framework/list-runtime.js +67 -24
  43. package/framework/native-runtime.js +12 -9
  44. package/framework/runtime.js +1 -1
  45. package/framework/serialization.js +13 -6
  46. package/framework/shared-runtime.js +14 -12
  47. package/package.json +1 -1
@@ -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
@@ -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
- return { type: "array", value: Array.from(value, entry => serializeCapture(name, entry, seen)) }
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
- return { type: "object", nullPrototype: prototype === null, value: entries }
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__) replaceRowIds(root, rowReplacements.get(root))
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__ && !__KUDZU_LIST_ROW_HOOKS__,
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.dataset.kRowPath = encodeURIComponent(statePath.join("/"))
1055
- const replacements = new Map()
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.set(state.id, id)
1076
+ replacements?.set(state.id, id)
1060
1077
  }
1061
- if (__KUDZU_LIST_ROW_REFS__) for (const ref of descriptor.rowRefs ?? []) replacements.set(ref, rowStateId(ref, statePath))
1062
- for (const marker of descriptor.rowConditions ?? []) replacements.set(marker, rowStateId(marker, statePath))
1063
- rowReplacements.set(root, replacements)
1064
- replaceRowIds(root, replacements)
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 mount = root => mountNative(root, eventNames, modules)
64
- registerMountHook(mount)
65
- registerUnmountHook(unmountNative)
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) unmountNative(document)
70
+ if (!event.persisted) unmount(document)
69
71
  })
70
72
  }
71
73
 
72
- function mountNative(root, eventNames, modules) {
73
- for (const eventName of eventNames) {
74
- for (const node of matching(root, `[data-k-native-${eventName}]`)) {
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)
@@ -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") return { get current() { return typeof document === "undefined" || active?.() === false ? null : document.querySelector(`[data-k-ref="${value.id}"]`) } }
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 mount of mountHooks) mount(root)
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 unmount of unmountHooks) unmount(root)
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") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.62",
3
+ "version": "0.9.0",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",