@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.
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 +54 -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 +22 -7
  40. package/framework/dependency-runtime.js +1 -1
  41. package/framework/effect-runtime.js +2 -2
  42. package/framework/list-runtime.js +70 -26
  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
@@ -1,14 +1,15 @@
1
1
  import ts from "typescript"
2
- import { nearestFunction, referencesIdentifier, unwrapExpression } from "./ast-helpers.mjs"
2
+ import { isNodeWithin, nearestFunction, referencesIdentifier, unwrapExpression } from "./ast-helpers.mjs"
3
3
  import { collectionExpression } from "./collection-analysis.mjs"
4
4
  import { referencedStateNames } from "./descriptor-session.mjs"
5
5
 
6
- export function analyzeEffectDependencies({ dependencies, node, listEffect, keyedItem, setters, localDeclarations, factory, fail, bindingIndex }) {
6
+ export function analyzeEffectDependencies({ dependencies, node, listEffect, keyedItem, setters, localDeclarations, factory, fail, bindingIndex, resolveCalculation }) {
7
7
  const itemDependencies = []
8
8
  const ordinaryDependencies = []
9
9
  let dependencyItem = listEffect ? keyedItem : undefined
10
10
  for (const dependency of dependencies.elements) {
11
11
  const value = unwrapExpression(dependency)
12
+ if (ts.isElementAccessExpression(value) && ts.isIdentifier(unwrapExpression(value.expression)) && isDestructuredParameter(unwrapExpression(value.expression), nearestFunction(node)) && !ts.isStringLiteral(value.argumentExpression) && !ts.isNumericLiteral(value.argumentExpression)) fail(dependency, "useEffect() object property dependencies require a direct static property path")
12
13
  if (!dependencyItem && ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.expression) && isDestructuredParameter(value.expression, nearestFunction(node))) dependencyItem = value.expression.text
13
14
  const field = dependencyItem && directProperty(dependency, dependencyItem)
14
15
  if (field) {
@@ -29,7 +30,19 @@ export function analyzeEffectDependencies({ dependencies, node, listEffect, keye
29
30
  let hasDerived = false
30
31
  const stateNames = new Set(setters.values())
31
32
  for (const dependency of ordinaryDependencies) {
33
+ const calculation = resolveCalculation?.(dependency)
34
+ if (calculation) {
35
+ entries.push({ kind: "calculation", ...calculation })
36
+ for (const name of calculation.states) {
37
+ subscriptions.push(factory.createIdentifier(name))
38
+ dependencyStates.set(name, factory.createIdentifier(name))
39
+ }
40
+ substitutions.set(calculation.name, calculation.call)
41
+ hasDerived = true
42
+ continue
43
+ }
32
44
  const direct = ts.isIdentifier(dependency)
45
+ if (!direct && dynamicStatePropertyDependency(dependency, stateNames)) fail(dependency, "useEffect() object property dependencies require a direct static property path")
33
46
  if (!direct && !statePropertyDependency(dependency, stateNames)) fail(dependency, "useEffect() dependencies must be direct state or runtime parameter identifiers or property reads")
34
47
  const declarations = direct ? localDeclarations?.get(dependency.text) : undefined
35
48
  const initializer = declarations?.length === 1 ? declarations[0].initializer : undefined
@@ -42,7 +55,7 @@ export function analyzeEffectDependencies({ dependencies, node, listEffect, keye
42
55
  const usedStates = new Set()
43
56
  const expression = collectionExpression(expressionSource, { fail, stateNames, selectorStates: usedStates })
44
57
  if (!usedStates.size) fail(dependency, `useEffect() derived dependency must read direct state`)
45
- entries.push({ kind: "derived", name: direct ? dependency.text : dependency.getText(), expression, states: usedStates, source: expressionSource })
58
+ entries.push({ kind: "derived", name: direct ? dependency.text : "derived", expression, states: usedStates, source: expressionSource })
46
59
  for (const name of usedStates) {
47
60
  subscriptions.push(factory.createIdentifier(name))
48
61
  dependencyStates.set(name, factory.createIdentifier(name))
@@ -56,7 +69,7 @@ export function analyzeEffectDependencies({ dependencies, node, listEffect, keye
56
69
  dependencyStates.set(dependency.text, dependency)
57
70
  }
58
71
  }
59
- const derivedSourceNames = new Set(entries.filter(entry => entry.kind === "derived").flatMap(entry => [...entry.states]))
72
+ const derivedSourceNames = new Set(entries.filter(entry => entry.kind !== "signal").flatMap(entry => [...entry.states]))
60
73
  const ambiguous = entries.find(entry => entry.kind === "signal" && derivedSourceNames.has(entry.name))
61
74
  if (ambiguous) fail(ordinaryDependencies[entries.indexOf(ambiguous)], `useEffect() cannot mix whole-object and property dependencies for state ${JSON.stringify(ambiguous.name)}`)
62
75
  if (!hasDerived) dependencyStates.clear()
@@ -69,10 +82,7 @@ export function validateEffectOwnedBrowserResources(callback, returns, fail, bin
69
82
  const frameAssignments = []
70
83
  const cancellations = new Set()
71
84
  const disconnected = new Set()
72
- const insideCleanup = node => returns.cleanups.some(cleanup => {
73
- for (let current = node; current; current = current.parent) if (current === cleanup) return true
74
- return false
75
- })
85
+ const insideCleanup = node => returns.cleanups.some(cleanup => isNodeWithin(node, cleanup))
76
86
  const isGlobal = (identifier, name) => identifier.text === name && (!bindingIndex || bindingIndex.resolveReference(identifier, callback)?.kind === "global")
77
87
  const resource = identifier => bindingIndex?.resolveReference(identifier, callback)?.declaration ?? identifier.text
78
88
  const visit = node => {
@@ -109,3 +119,13 @@ function statePropertyDependency(expression, stateNames) {
109
119
  }
110
120
  return property && ts.isIdentifier(value) && stateNames.has(value.text)
111
121
  }
122
+
123
+ function dynamicStatePropertyDependency(expression, stateNames) {
124
+ let value = unwrapExpression(expression)
125
+ let dynamic = false
126
+ while (ts.isPropertyAccessExpression(value) || ts.isElementAccessExpression(value)) {
127
+ if (ts.isElementAccessExpression(value) && !ts.isStringLiteral(value.argumentExpression) && !ts.isNumericLiteral(value.argumentExpression)) dynamic = true
128
+ value = unwrapExpression(value.expression)
129
+ }
130
+ return dynamic && ts.isIdentifier(value) && stateNames.has(value.text)
131
+ }
@@ -6,6 +6,7 @@ function printEffectEntry(effects, output, handlerModules, assetsDirectory, runt
6
6
  const hasDependencies = effects.some(effect => effect.dependencies?.length || effect.itemDependencies?.length)
7
7
  const hasOwners = effects.some(effect => effect.owner)
8
8
  const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
9
+ const hasDependencyEvaluators = effects.some(effect => effect.dependencyEvaluators?.length)
9
10
  const moduleUrls = [...new Set(effects.map(effect => effect.module))]
10
11
  const modules = moduleUrls.map(url => {
11
12
  const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
@@ -34,7 +35,7 @@ const dispose = root => {
34
35
  for (const record of records) invokeCleanup(record)
35
36
  }
36
37
 
37
- if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose)
38
+ if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose, "effects")
38
39
  addEventListener("pagehide", event => {
39
40
  if (event.persisted) return
40
41
  if (__kRuntime.unmountDom) __kRuntime.unmountDom(document)
@@ -101,7 +102,7 @@ async function flush() {
101
102
  }
102
103
  }
103
104
  function readDependencies(record) {
104
- ${hasDependencyExpressions ? printDerivedDependencyRead("browserState") : ""}
105
+ ${hasDependencyExpressions || hasDependencyEvaluators ? printDerivedDependencyRead("browserState", hasDependencyExpressions, hasDependencyEvaluators) : ""}
105
106
  return (record.effect.dependencies ?? []).map(id => {
106
107
  const value = browserState.get(id)
107
108
  if (!Array.isArray(value) && value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive or array")
@@ -170,7 +171,7 @@ const dispose = root => {
170
171
  }
171
172
  cleanups.length = 0
172
173
  }
173
- if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose)
174
+ if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose, "effects")
174
175
  addEventListener("pagehide", event => {
175
176
  if (event.persisted) return
176
177
  if (__kRuntime.unmountDom) __kRuntime.unmountDom(document)
@@ -180,6 +181,7 @@ addEventListener("pagehide", event => {
180
181
 
181
182
  function printNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base) {
182
183
  const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
184
+ const hasDependencyEvaluators = effects.some(effect => effect.dependencyEvaluators?.length)
183
185
  const moduleUrls = [...new Set(effects.map(effect => effect.module))]
184
186
  const modules = moduleUrls.map(url => {
185
187
  const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
@@ -257,7 +259,7 @@ function mount(lifetime) {
257
259
  }
258
260
  }
259
261
  function readDependencies(record) {
260
- ${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState") : ""}
262
+ ${hasDependencyExpressions || hasDependencyEvaluators ? printDerivedDependencyRead("__kRuntime.browserState", hasDependencyExpressions, hasDependencyEvaluators) : ""}
261
263
  return (record.effect.dependencies ?? []).map(id => {
262
264
  const value = __kRuntime.browserState.get(id)
263
265
  if (!Array.isArray(value) && value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive or array")
@@ -303,6 +305,7 @@ ${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState
303
305
  function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base) {
304
306
  const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
305
307
  const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
308
+ const hasDependencyEvaluators = effects.some(effect => effect.dependencyEvaluators?.length)
306
309
  const moduleUrls = [...new Set(effects.map(effect => effect.module))]
307
310
  const modules = moduleUrls.map(url => {
308
311
  const module = handlerModules.find(entry => assetPath(base, `assets/${entry.path}`) === url)
@@ -346,8 +349,8 @@ function mount(lifetime) {
346
349
  for (const record of dependencies.get(id) ?? []) if (record.mounted) pending.add(record)
347
350
  schedule()
348
351
  }) : undefined
349
- const unsubscribeMount = __kRuntime.registerMountHook(mountOwned)
350
- const unsubscribeUnmount = __kRuntime.registerUnmountHook(unmountOwned)
352
+ const unsubscribeMount = __kRuntime.registerMountHook(mountOwned, "effects")
353
+ const unsubscribeUnmount = __kRuntime.registerUnmountHook(unmountOwned, "effects")
351
354
  ${hasItemDependencies ? `const unsubscribeItems = [...new Set(selectedEffects.filter(({ effect }) => effect.itemDependencies?.length).map(({ effect }) => effect.listState))].map(listState => __kRuntime.registerListItemHook(listState, root => {
352
355
  if (!active) return
353
356
  for (const record of registrations.get(root) ?? []) if (record.mounted && record.effect.itemDependencies) pending.add(record)
@@ -481,7 +484,7 @@ function mount(lifetime) {
481
484
  }
482
485
  }
483
486
  function readDependencies(record) {
484
- ${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState") : ""}
487
+ ${hasDependencyExpressions || hasDependencyEvaluators ? printDerivedDependencyRead("__kRuntime.browserState", hasDependencyExpressions, hasDependencyEvaluators) : ""}
485
488
  const values = (record.effect.dependencies ?? []).map(id => {
486
489
  const value = __kRuntime.browserState.get(id)
487
490
  if (!Array.isArray(value) && value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive or array")
@@ -554,19 +557,28 @@ ${hasDependencyExpressions ? printDerivedDependencyRead("__kRuntime.browserState
554
557
  }`
555
558
  }
556
559
 
557
- function printDerivedDependencyRead(state) {
558
- return ` if (record.effect.dependencyExpressions) return record.effect.dependencyExpressions.map(expression => {
560
+ function printDerivedDependencyRead(state, hasExpressions, hasEvaluators) {
561
+ return `${hasEvaluators ? ` if (record.effect.dependencyEvaluators) return record.effect.dependencyEvaluators.map(evaluator => {
562
+ const result = modules.get(evaluator.module)[evaluator.handler](createEffectContext(${state}, evaluator.states, () => {}, evaluator.scope))
563
+ const value = result[evaluator.field]
564
+ if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() calculated dependency must remain a JSON-safe primitive")
565
+ return value
566
+ })
567
+ ` : ""}${hasExpressions ? ` if (record.effect.dependencyExpressions) return record.effect.dependencyExpressions.map(expression => {
559
568
  const value = __kEvaluateDependency(expression, undefined, undefined, name => ${state}.get(record.effect.dependencyStates[name]))
560
- if (value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() derived dependency must remain a JSON-safe primitive")
569
+ if (value !== null && !Array.isArray(value) && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() derived dependency must remain a JSON-safe primitive or array")
561
570
  return value
562
- })`
571
+ })` : ""}`
563
572
  }
564
573
 
565
574
  function printOwnedEffectEntry(imports, effects, entries) {
566
575
  const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
567
576
  const hasOrdinaryDependencies = effects.some(effect => effect.dependencies?.length)
568
577
  const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
569
- const hasRowState = effects.some(effect => JSON.stringify([effect.dependencies, effect.dependencyStates, effect.states, effect.scope]).includes("$k"))
578
+ const hasDependencyEvaluators = effects.some(effect => effect.dependencyEvaluators?.length)
579
+ const hasRowState = effects.some(effect => JSON.stringify([effect.dependencies, effect.dependencyStates, effect.states]).includes("$k") || Object.values(effect.scope).some(hasRowStateCapture))
580
+ const hasRowRef = effects.some(effect => Object.values(effect.scope).some(hasRowRefCapture))
581
+ const initialDependencyCheck = [hasOrdinaryDependencies && "record.effect.dependencies?.length", hasItemDependencies && "record.effect.itemDependencies?.length", hasDependencyExpressions && "record.effect.dependencyExpressions?.length", hasDependencyEvaluators && "record.effect.dependencyEvaluators?.length"].filter(Boolean).join(" || ")
570
582
  return `${imports.join("\n")}
571
583
  const effects = ${inlineJson(effects)}
572
584
  const modules = new Map([${entries}])
@@ -574,6 +586,7 @@ ${hasItemDependencies ? "let order = 0\n" : ""}const records = effects.map((effe
574
586
  const listTemplates = new Map(effects.map((effect, index) => effect.list ? [effect.owner, { effect, index }] : undefined).filter(Boolean))
575
587
  const owners = new Map(records.filter(record => record.effect.owner).map(record => [record.effect.owner, record]))
576
588
  const listRegistrations = new WeakMap()
589
+ const listOwnerSets = new Map()
577
590
  const mountedRecords = new Set(records.filter(record => record.mounted))
578
591
  const dependencies = new Map()
579
592
  const pending = new Set()
@@ -585,7 +598,7 @@ function createRecord(effect, index${hasRowState ? ", marker" : ""}) {
585
598
  return { effect: ${hasRowState ? "marker ? specializeRowEffect(effect, marker) : effect" : "effect"}, index, ${hasItemDependencies ? "order: order++, " : ""}mounted: !effect.owner, marker: undefined, version: 0, values: undefined, cleanup: undefined, disposal: undefined, token: undefined }
586
599
  }
587
600
  ${hasRowState ? `function specializeRowEffect(effect, marker) {
588
- const path = marker.dataset.kRowPath
601
+ const path = __kRuntime.listRowPaths.get(marker)
589
602
  const id = value => typeof value === "string" ? value.replace("$k", path) : value
590
603
  const capture = value => value?.type === "state" || value?.type === "setter" || value?.type === "ref" ? { ...value, id: id(value.id) } : value?.type === "array" ? { ...value, value: value.value.map(capture) } : value?.type === "object" ? { ...value, value: value.value.map(([key, entry]) => [key, capture(entry)]) } : value
591
604
  return { ...effect, dependencies: effect.dependencies?.map(id), dependencyStates: effect.dependencyStates && Object.fromEntries(Object.entries(effect.dependencyStates).map(([name, value]) => [name, id(value)])), states: Object.fromEntries(Object.entries(effect.states).map(([name, value]) => [name, id(value)])), scope: Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, capture(value)])) }
@@ -620,12 +633,18 @@ ${hasItemDependencies ? `for (const listState of new Set(effects.filter(effect =
620
633
  for (const marker of matching(root)) {
621
634
  if (marker.dataset.kEffects) {
622
635
  if (listRegistrations.has(marker)) continue
623
- const rowRecords = JSON.parse(marker.dataset.kEffects).map(owner => {
636
+ const encoded = marker.dataset.kEffects
637
+ let effectOwners = listOwnerSets.get(encoded)
638
+ if (!effectOwners) {
639
+ effectOwners = JSON.parse(encoded)
640
+ listOwnerSets.set(encoded, effectOwners)
641
+ }
642
+ const rowRecords = effectOwners.map(owner => {
624
643
  const template = listTemplates.get(owner)
625
644
  if (!template) throw new Error("Keyed row effect template was not emitted")
626
645
  const record = createRecord(template.effect, template.index${hasRowState ? ", marker" : ""})
627
- registerDependencies(record)
628
- mount(record, marker)
646
+ if (record.effect.dependencies?.length) registerDependencies(record)
647
+ mount(record, marker, true)
629
648
  return record
630
649
  })
631
650
  listRegistrations.set(marker, rowRecords)
@@ -634,7 +653,7 @@ ${hasItemDependencies ? `for (const listState of new Set(effects.filter(effect =
634
653
  const record = owners.get(marker.dataset.kEffect)
635
654
  if (!record?.mounted) mount(record, marker)
636
655
  }
637
- })
656
+ }, "effects")
638
657
  __kRuntime.registerUnmountHook(root => {
639
658
  if (root === document) {
640
659
  if (!active) return
@@ -653,7 +672,7 @@ __kRuntime.registerUnmountHook(root => {
653
672
  const record = owners.get(marker.dataset.kEffect)
654
673
  if (record?.marker === marker) unmount(record)
655
674
  }
656
- })
675
+ }, "effects")
657
676
  for (const record of records) if (record.mounted) start(record)
658
677
  __kRuntime.mountDom(document)
659
678
  addEventListener("pagehide", event => {
@@ -663,17 +682,16 @@ function matching(root) {
663
682
  const selector = "template[data-k-effect],[data-k-effects]"
664
683
  return [...(root.matches?.(selector) ? [root] : []), ...(root.querySelectorAll?.(selector) ?? [])]
665
684
  }
666
- function mount(record, marker) {
685
+ function mount(record, marker, fresh = false) {
667
686
  record.mounted = true
668
687
  record.marker = marker
669
688
  mountedRecords.add(record)
670
689
  const version = ++record.version
671
- const begin = () => {
690
+ if (record.disposal) record.disposal.then(() => {
672
691
  if (!active || !record.mounted || record.version !== version || !marker.isConnected) return
673
692
  start(record)
674
- }
675
- if (record.disposal) record.disposal.then(begin)
676
- else begin()
693
+ })
694
+ else if (fresh || active && marker.isConnected) start(record)
677
695
  }
678
696
  function unmount(record, dynamic = false) {
679
697
  if (!record.mounted) return
@@ -687,7 +705,7 @@ function unmount(record, dynamic = false) {
687
705
  }
688
706
  function start(record) {
689
707
  try {
690
- record.values = readDependencies(record)
708
+ ${initialDependencyCheck ? `if (${initialDependencyCheck}) record.values = readDependencies(record)` : ""}
691
709
  invoke(record)
692
710
  } catch (error) {
693
711
  console.error(error)
@@ -730,7 +748,7 @@ async function flush() {
730
748
  }
731
749
  }
732
750
  function readDependencies(record) {
733
- ${hasDependencyExpressions ? printDerivedDependencyRead("browserState") : ""}
751
+ ${hasDependencyExpressions || hasDependencyEvaluators ? printDerivedDependencyRead("browserState", hasDependencyExpressions, hasDependencyEvaluators) : ""}
734
752
  const values = (record.effect.dependencies ?? []).map(id => {
735
753
  const value = browserState.get(id)
736
754
  if (!Array.isArray(value) && value !== null && typeof value !== "string" && typeof value !== "boolean" && !(typeof value === "number" && Number.isFinite(value) && !Object.is(value, -0))) throw new Error("useEffect() dependency state must remain a JSON-safe primitive or array")
@@ -752,9 +770,16 @@ function invoke(record) {
752
770
  try {
753
771
  const effect = record.effect
754
772
  const scope = effect.list
755
- ? Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, value?.type === "list-item" ? JSON.parse(record.marker.dataset.kEffectItem) : value]))
773
+ ? Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, value?.type === "list-item" ? __kRuntime.listItems.get(record.marker) : value]))
756
774
  : effect.scope
757
- const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, scope, () => active && token.active && record.token === token))
775
+ const marker = record.marker
776
+ ${hasRowRef ? "const rowPath = effect.list ? __kRuntime.listRowPaths.get(marker) : undefined" : ""}
777
+ const resolveRef = id => {
778
+ if (!marker?.isConnected) return null
779
+ const resolved = ${hasRowRef ? 'id.replace("$k", rowPath)' : "id"}
780
+ return marker.dataset.kRef === resolved ? marker : [...marker.querySelectorAll("[data-k-ref]")].find(node => node.dataset.kRef === resolved) ?? null
781
+ }
782
+ const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, scope, () => active && token.active && record.token === token, effect.list ? resolveRef : undefined))
758
783
  if (effect.cleanup && typeof result === "function") record.cleanup = result
759
784
  else if (result && typeof result.then === "function") result.catch(error => console.error(error))
760
785
  } catch (error) {
@@ -767,14 +792,16 @@ function invokeCleanup(record) {
767
792
  if (record.disposal) return record.disposal
768
793
  const cleanup = record.cleanup
769
794
  record.cleanup = undefined
770
- if (!cleanup) return Promise.resolve()
771
- const disposal = (async () => {
772
- try {
773
- await cleanup()
774
- } catch (error) {
775
- console.error(error)
776
- }
777
- })()
795
+ if (!cleanup) return
796
+ let result
797
+ try {
798
+ result = cleanup()
799
+ } catch (error) {
800
+ console.error(error)
801
+ return
802
+ }
803
+ if (!result || typeof result.then !== "function") return
804
+ const disposal = Promise.resolve(result).catch(error => console.error(error))
778
805
  record.disposal = disposal
779
806
  disposal.finally(() => {
780
807
  if (record.disposal === disposal) record.disposal = undefined
@@ -783,6 +810,22 @@ function invokeCleanup(record) {
783
810
  }`
784
811
  }
785
812
 
813
+ function hasRowStateCapture(value) {
814
+ if (!value || typeof value !== "object") return false
815
+ if ((value.type === "state" || value.type === "setter") && value.id.includes("$k")) return true
816
+ if (value.type === "array") return value.value.some(hasRowStateCapture)
817
+ if (value.type === "object") return value.value.some(([, entry]) => hasRowStateCapture(entry))
818
+ return false
819
+ }
820
+
821
+ function hasRowRefCapture(value) {
822
+ if (!value || typeof value !== "object") return false
823
+ if (value.type === "ref" && value.id.includes("$k")) return true
824
+ if (value.type === "array") return value.value.some(hasRowRefCapture)
825
+ if (value.type === "object") return value.value.some(([, entry]) => hasRowRefCapture(entry))
826
+ return false
827
+ }
828
+
786
829
  function printSingleDependencyEffect(imports, effect, hasCleanup) {
787
830
  const disposal = hasCleanup ? `
788
831
  const dispose = root => {
@@ -791,7 +834,7 @@ const dispose = root => {
791
834
  pending = false
792
835
  invokeCleanup()
793
836
  }
794
- if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose)
837
+ if (__kRuntime.registerUnmountHook) __kRuntime.registerUnmountHook(dispose, "effects")
795
838
  addEventListener("pagehide", event => {
796
839
  if (event.persisted) return
797
840
  if (__kRuntime.unmountDom) __kRuntime.unmountDom(document)
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript"
2
- import { effectReturns, importDeclarationNames, isShadowedIdentifier, nearestFunction, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
2
+ import { effectReturns, importDeclarationNames, isNodeWithin, isShadowedIdentifier, nearestFunction, referenceIdentifiers, sourceNodeError, statementDeclaresName, unwrapExpression } from "./ast-helpers.mjs"
3
3
 
4
4
  export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
5
5
  const frameCall = (node, name) => ts.isCallExpression(node) && (
@@ -11,10 +11,6 @@ export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
11
11
  return name && !isShadowedIdentifier(name, owner) && !sourceFile.statements.some(statement => statementDeclaresName(statement, name.text) || ts.isImportDeclaration(statement) && importDeclarationNames(statement).includes(name.text))
12
12
  }
13
13
  const currentAccess = (node, name) => ts.isPropertyAccessExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === name && node.name.text === "current"
14
- const inside = (node, root) => {
15
- for (let current = node; current; current = current.parent) if (current === root) return true
16
- return false
17
- }
18
14
  const directOrGuarded = (statement, body, name, negated) => {
19
15
  if (statement.parent === body) return true
20
16
  let branch = statement
@@ -58,13 +54,13 @@ export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
58
54
  const effectCalls = owner.body.statements.flatMap(statement => hasUseEffectImport && ts.isExpressionStatement(statement) && ts.isCallExpression(statement.expression) && ts.isIdentifier(statement.expression.expression) && statement.expression.expression.text === "useEffect" && !isShadowedIdentifier(statement.expression.expression, sourceFile) ? [statement.expression] : [])
59
55
  const effects = effectCalls.filter(effect => {
60
56
  const callback = effect.arguments[0]
61
- return callback && accesses.every(access => inside(access, callback))
57
+ return callback && accesses.every(access => isNodeWithin(access, callback))
62
58
  })
63
59
  if (!frameAssignments.length) {
64
60
  const callback = effects.length === 1 ? effects[0].arguments[0] : undefined
65
61
  if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) && ts.isBlock(callback.body)) {
66
62
  const cleanups = effectReturns(callback).cleanups
67
- const cleanupWrites = cleanups.length === 1 && accesses.some(access => inside(access, cleanups[0]) && ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && access.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
63
+ const cleanupWrites = cleanups.length === 1 && accesses.some(access => isNodeWithin(access, cleanups[0]) && ts.isBinaryExpression(access.parent) && unwrapExpression(access.parent.left) === access && access.parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && access.parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment)
68
64
  if (!cleanupWrites) throw sourceNodeError(node, sourceFile, "Effect-private refs require one cleanup that directly resets or invalidates ref.current")
69
65
  registerPrivateRef(node, callback)
70
66
  }
@@ -77,7 +73,7 @@ export function normalizeEffectPrivateRefs(sourceFile, factory, context) {
77
73
  const effect = effects[0]
78
74
  const callback = effect.arguments[0]
79
75
  if (!(ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) || !ts.isBlock(callback.body)) throw sourceNodeError(callback, sourceFile, "Animation frame refs require one inline block-bodied effect")
80
- if (accesses.some(access => !inside(access, callback))) throw sourceNodeError(node, sourceFile, "Animation frame refs may only be used inside their owning effect")
76
+ if (accesses.some(access => !isNodeWithin(access, callback))) throw sourceNodeError(node, sourceFile, "Animation frame refs may only be used inside their owning effect")
81
77
  const callbacks = new Map()
82
78
  for (const statement of callback.body.statements) {
83
79
  if (ts.isVariableStatement(statement)) for (const declaration of statement.declarationList.declarations) {
@@ -15,23 +15,22 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
15
15
  const visitor = node => {
16
16
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && matchesExternalReference(node.expression, expression, bindingIndex)) {
17
17
  const reducer = reducers.get(node.expression.text)
18
- if (reducer.contextAction) {
19
- const action = synthesizeTree(cloneAst(reducer.contextAction, factory, context))
20
- const call = factory.createCallExpression(action, undefined, node.arguments)
18
+ if (reducer.directImplementation) {
19
+ const call = factory.createCallExpression(synthesizeTree(cloneAst(reducer.directImplementation, factory, context)), undefined, node.arguments.map(argument => ts.visitNode(argument, visitor)))
21
20
  ts.setParentRecursive(call, false)
22
21
  return ts.visitNode(call, visitor)
23
22
  }
24
- if (reducer.sharedAction) return sharedActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)))
23
+ if (reducer.sharedAction) return sharedActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)), visitor, context)
25
24
  if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
26
25
  return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
27
26
  }
28
27
  if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && matchesExternalReference(node.name, expression, bindingIndex)) {
29
- if (reducers.get(node.name.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
28
+ if (reducers.get(node.name.text).directImplementation) throw sourceNodeError(node, expression.getSourceFile(), `${reducers.get(node.name.text).sourceKind} actions must be called directly inside an event handler`)
30
29
  if (reducers.get(node.name.text).sharedAction) throw sourceNodeError(node, expression.getSourceFile(), `${reducers.get(node.name.text).sourceKind} actions must be called directly inside an event handler`)
31
30
  return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
32
31
  }
33
32
  if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && matchesExternalReference(node, expression, bindingIndex)) {
34
- if (reducers.get(node.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
33
+ if (reducers.get(node.text).directImplementation) throw sourceNodeError(node, expression.getSourceFile(), `${reducers.get(node.text).sourceKind} actions must be called directly inside an event handler`)
35
34
  if (reducers.get(node.text).sharedAction) throw sourceNodeError(node, expression.getSourceFile(), `${reducers.get(node.text).sourceKind} actions must be called directly inside an event handler`)
36
35
  return reducerReference(factory, reducers.get(node.text))
37
36
  }
@@ -162,7 +161,13 @@ export function createHandlerLowering({ cloneAst, synthesizeTree }) {
162
161
  return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
163
162
  }
164
163
 
165
- function sharedActionDispatch(factory, reducer, args) {
164
+ function sharedActionDispatch(factory, reducer, args, visitor, context) {
165
+ const direct = reducer.sharedAction.directImplementation
166
+ if (direct) {
167
+ const call = factory.createCallExpression(synthesizeTree(cloneAst(direct, factory, context)), undefined, args)
168
+ ts.setParentRecursive(call, false)
169
+ return ts.visitNode(call, visitor)
170
+ }
166
171
  const previous = factory.createUniqueName("__kPrevious")
167
172
  const current = factory.createUniqueName("__kStore")
168
173
  const updateValue = factory.createUniqueName("__kUpdate")
@@ -40,7 +40,13 @@ export function assertModuleIRReferences(moduleIR, componentAnalysis) {
40
40
  indexed(`Component specialization ${specialization.slot} ref`, specialization.refs)
41
41
  indexed(`Component specialization ${specialization.slot} ID`, specialization.ids)
42
42
  if (specialization.owner !== undefined) ownerRef(specialization.owner, `Component specialization ${specialization.slot} owner`)
43
- for (const prop of specialization.props ?? []) for (const signal of prop.signals ?? []) slot(moduleIR.signals, signal, `Component specialization ${specialization.slot} prop ${JSON.stringify(prop.name)}`, "SignalIR")
43
+ for (const prop of specialization.props ?? []) {
44
+ for (const signal of prop.signals ?? []) slot(moduleIR.signals, signal, `Component specialization ${specialization.slot} prop ${JSON.stringify(prop.name)}`, "SignalIR")
45
+ for (const property of prop.properties ?? []) {
46
+ slot(moduleIR.signals, property.signal, `Component specialization ${specialization.slot} prop ${JSON.stringify(prop.name)} property`, "SignalIR")
47
+ if (!(prop.signals ?? []).includes(property.signal) || !Array.isArray(property.path) || property.path.length !== 1 || property.path.some(segment => typeof segment !== "string" || !segment || ["__proto__", "constructor", "prototype"].includes(segment)) || !Array.isArray(property.consumers) || !property.consumers.length || property.consumers.some(consumer => !["binding", "effect", "list"].includes(consumer)) || property.equality !== "object-is") throw new Error(`Component specialization ${specialization.slot} prop ${JSON.stringify(prop.name)} has an invalid property link`)
48
+ }
49
+ }
44
50
  }
45
51
  }
46
52
  function moduleSymbol(symbol, label) {
@@ -86,17 +92,32 @@ export function assertModuleIRReferences(moduleIR, componentAnalysis) {
86
92
  for (const [index, signal] of (binding.signals ?? []).entries()) slot(moduleIR.signals, signal, `BindingIR ${binding.slot} signal ${index}`, "SignalIR")
87
93
  for (const [index, imported] of (binding.imports ?? []).entries()) slot(moduleIR.imports, imported, `BindingIR ${binding.slot} import ${index}`, "ImportIR")
88
94
  for (const [index, capture] of (binding.captures ?? []).entries()) if (capture.symbol !== undefined) slot(moduleIR.symbols, capture.symbol, `BindingIR ${binding.slot} capture ${index}`, "SymbolRef")
95
+ if (binding.derived) {
96
+ const derived = slot(moduleIR.derived, binding.derived.derived, `BindingIR ${binding.slot} derived value`, "DerivedIR")
97
+ if (derived.kind !== "calculation" || !Array.isArray(binding.derived.fields) || !binding.derived.fields.length || binding.derived.fields.some(field => !derived.calculation.fields.includes(field))) throw new Error(`BindingIR ${binding.slot} has invalid calculation fields`)
98
+ }
89
99
  if (binding.keyedBlock !== undefined) slot(moduleIR.keyedBlocks, binding.keyedBlock, `BindingIR ${binding.slot} keyed block`, "KeyedBlockIR")
90
100
  }
91
- for (const derived of moduleIR.derived) for (const [index, signal] of (derived.signals ?? []).entries()) slot(moduleIR.signals, signal, `DerivedIR ${derived.slot} signal ${index}`, "SignalIR")
101
+ for (const derived of moduleIR.derived) {
102
+ for (const [index, signal] of (derived.signals ?? []).entries()) slot(moduleIR.signals, signal, `DerivedIR ${derived.slot} signal ${index}`, "SignalIR")
103
+ if (derived.kind === "calculation") {
104
+ const binding = slot(moduleIR.bindings, derived.calculation?.binding, `DerivedIR ${derived.slot} calculation`, "BindingIR")
105
+ if (binding.kind !== "module-export") throw new Error(`DerivedIR ${derived.slot} calculation must use a module-export BindingIR`)
106
+ if (!Array.isArray(derived.calculation?.fields) || !derived.calculation.fields.length || derived.calculation.fields.some(field => typeof field !== "string" || !field || ["__proto__", "constructor", "prototype"].includes(field))) throw new Error(`DerivedIR ${derived.slot} calculation has invalid fields`)
107
+ }
108
+ }
92
109
  for (const effect of moduleIR.effects) {
93
110
  const handler = slot(moduleIR.handlers, effect.setup?.handler, `EffectIR ${effect.slot} setup`, "HandlerIR")
94
111
  if (handler.kind !== "module-export" || handler.role !== "effect") throw new Error(`EffectIR ${effect.slot} setup HandlerIR ${handler.slot} must have role "effect"`)
95
112
  for (const [index, dependency] of (effect.dependencies ?? []).entries()) {
96
113
  if (dependency.kind === "signal") slot(moduleIR.signals, dependency.signal, `EffectIR ${effect.slot} dependency ${index}`, "SignalIR")
97
114
  else if (dependency.kind === "derived") {
98
- slot(moduleIR.derived, dependency.derived, `EffectIR ${effect.slot} dependency ${index}`, "DerivedIR")
115
+ const derived = slot(moduleIR.derived, dependency.derived, `EffectIR ${effect.slot} dependency ${index}`, "DerivedIR")
99
116
  for (const [sourceIndex, signal] of (dependency.sources ?? []).entries()) slot(moduleIR.signals, signal, `EffectIR ${effect.slot} dependency ${index} source ${sourceIndex}`, "SignalIR")
117
+ if (dependency.evaluator !== undefined || dependency.field !== undefined) {
118
+ if (derived.kind !== "calculation" || dependency.evaluator !== derived.calculation.binding) throw new Error(`EffectIR ${effect.slot} dependency ${index} must use its calculation evaluator`)
119
+ if (!derived.calculation.fields.includes(dependency.field)) throw new Error(`EffectIR ${effect.slot} dependency ${index} references unknown calculation field ${JSON.stringify(dependency.field)}`)
120
+ }
100
121
  } else throw new Error(`EffectIR ${effect.slot} dependency ${index} has invalid kind ${JSON.stringify(dependency.kind)}`)
101
122
  }
102
123
  for (const [index, signal] of (effect.subscriptions ?? []).entries()) slot(moduleIR.signals, signal, `EffectIR ${effect.slot} subscription ${index}`, "SignalIR")
@@ -172,11 +193,12 @@ export function registerSharedAction(moduleIR, descriptor) {
172
193
  return action
173
194
  }
174
195
 
175
- export function registerCommandHandler(moduleIR, commands, source) {
196
+ export function registerCommandHandler(moduleIR, commands, source, actions = []) {
176
197
  const handler = {
177
198
  slot: moduleIR.handlers.length,
178
199
  kind: "commands",
179
200
  commands: commands.map(({ operation, reference, state, value, syntax }) => ({ operation, signal: registerSignal(moduleIR, reference, state).slot, value, ...(syntax ? { syntax } : {}) })),
201
+ ...(actions.length ? { actions } : {}),
180
202
  ...(source ? { source } : {})
181
203
  }
182
204
  moduleIR.handlers.push(handler)
@@ -43,7 +43,8 @@ export function generateListRuntime(source, capabilityIR) {
43
43
  ["if (__KUDZU_LIST_ROW_HOOKS__) initializeRowStates(list.descriptor, key, node, list.owner, item)", "if (__KUDZU_LIST_ROW_HOOKS__ && list.descriptor.rowStates) initializeRowStates(list.descriptor, key, node)", "flat row add", true],
44
44
  ["for (const node of registration.list.roots.values()) deleteRowStates(registration.list.descriptor, ownershipPaths.get(node))", "for (const token of registration.list.roots.keys()) deleteFlatRowStates(registration.list.descriptor, token)", "flat registration cleanup"],
45
45
  ["deleteRowStates(list.descriptor, ownershipPaths.get(node))", "deleteFlatRowStates(list.descriptor, token)", "flat row cleanup", true],
46
- [" if (__KUDZU_LIST_ROW_HOOKS__) replaceRowIds(root, rowReplacements.get(root))\n", "", "row ID replacement"],
46
+ ["const directRowReplacements = __KUDZU_LIST_ROW_HOOKS__ ? new WeakSet() : undefined\n", "", "direct row replacement storage"],
47
+ [" if (__KUDZU_LIST_ROW_HOOKS__) replaceOwnedRowIds(root)\n", "", "row ID replacement"],
47
48
  [" if (!replacements) return\n", "", "row replacement guard"]
48
49
  ])
49
50
  if (!effects.itemDependencies) runtime = replaceRequired(runtime, ", notifyListItem", "", "item notification import")
@@ -67,11 +68,12 @@ export function generateListRuntime(source, capabilityIR) {
67
68
  __KUDZU_LIST_EXPRESSIONS__: String(lists.expressions),
68
69
  __KUDZU_LIST_EXPRESSION_ATTRIBUTES__: String(lists.expressionAttributes),
69
70
  __KUDZU_LIST_SEEDS__: String(lists.seeds),
70
- __KUDZU_LIST_EFFECTS__: String(lists.effects),
71
+ __KUDZU_LIST_EFFECTS__: String(effects.itemDependencies),
71
72
  __KUDZU_LIST_ASYNC_PARTS__: String(lists.asyncParts),
72
73
  __KUDZU_LIST_MOUNTS__: String(lists.mounts),
73
74
  __KUDZU_LIST_ITEM_HOOKS__: String(effects.itemDependencies),
74
75
  __KUDZU_LIST_ROW_HOOKS__: String(lists.rowHooks),
76
+ __KUDZU_GENERAL_ROW_HOOKS__: String(lists.generalRowHooks),
75
77
  __KUDZU_LIST_ROW_REFS__: String(lists.rowRefs),
76
78
  __KUDZU_COMPLEX_LIST_ROW_STATE__: String(lists.complexRowState),
77
79
  __KUDZU_NESTED_LISTS__: String(lists.nested),
@@ -1,5 +1,5 @@
1
1
  import ts from "typescript"
2
- import { sourceNodeError } from "../ast-helpers.mjs"
2
+ import { isNodeWithin, sourceNodeError } from "../ast-helpers.mjs"
3
3
 
4
4
  export function createCommandSpecializer({ isPrimitiveLiteral }) {
5
5
  const specialize = (expression, setters) => {
@@ -12,6 +12,8 @@ export function createCommandSpecializer({ isPrimitiveLiteral }) {
12
12
  const value = expression.arguments[0]
13
13
  if (ts.isBinaryExpression(value) && ts.isIdentifier(value.left) && value.left.text === state) return addCommand(state, value)
14
14
  if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isBinaryExpression(value.body) && ts.isIdentifier(value.body.left) && value.body.left.text === value.parameters[0].name.text) return addCommand(state, value.body)
15
+ if (ts.isPrefixUnaryExpression(value) && value.operator === ts.SyntaxKind.ExclamationToken && ts.isIdentifier(value.operand) && value.operand.text === state) return { operation: "toggle", state, value: false }
16
+ if (ts.isArrowFunction(value) && value.parameters.length === 1 && ts.isIdentifier(value.parameters[0].name) && ts.isPrefixUnaryExpression(value.body) && value.body.operator === ts.SyntaxKind.ExclamationToken && ts.isIdentifier(value.body.operand) && value.body.operand.text === value.parameters[0].name.text) return { operation: "toggle", state, value: false }
15
17
  if (isPrimitiveLiteral(value)) {
16
18
  const literal = primitiveValue(value)
17
19
  return literal ? { operation: "set", state, ...literal } : undefined
@@ -95,7 +97,7 @@ function rejectUnsafe(statements, boundary, setters, bindingIndex) {
95
97
  const helper = helperDeclaration(statement, boundary, setters, bindingIndex)
96
98
  if (!helper) continue
97
99
  const uses = refs(boundary, helper.name, boundary, bindingIndex)
98
- if (uses.some(reference => inside(reference, helper.body))) fail(helper.name, "Semantic state helpers cannot be recursive")
100
+ if (uses.some(reference => isNodeWithin(reference, helper.body))) fail(helper.name, "Semantic state helpers cannot be recursive")
99
101
  if (helper.mutable || uses.some(mutated)) fail(helper.name, "Semantic state helpers must remain immutable")
100
102
  if (uses.some(dynamic)) fail(uses.find(dynamic), "Semantic state helpers do not support dynamic dispatch")
101
103
  if (uses.length !== 1 || !directIdentifierCall(uses[0])) fail(uses.find(reference => !directIdentifierCall(reference)) ?? helper.name, "Semantic state helpers must be called exactly once and cannot escape")
@@ -191,11 +193,6 @@ function mutated(identifier) {
191
193
  return ts.isPrefixUnaryExpression(parent) && [ts.SyntaxKind.PlusPlusToken, ts.SyntaxKind.MinusMinusToken].includes(parent.operator) || ts.isPostfixUnaryExpression(parent) || ts.isBinaryExpression(parent) && parent.left === identifier && parent.operatorToken.kind >= ts.SyntaxKind.FirstAssignment && parent.operatorToken.kind <= ts.SyntaxKind.LastAssignment
192
194
  }
193
195
 
194
- function inside(node, root) {
195
- for (let current = node; current; current = current.parent) if (current === root) return true
196
- return false
197
- }
198
-
199
196
  function fail(node, message) {
200
197
  throw sourceNodeError(node, node.getSourceFile(), message)
201
198
  }
@@ -18,7 +18,8 @@ export function createRouteArtifactReport(records, {
18
18
  if (!runtimeFamilies || !runtimeFamilyByRecord) ({ families: runtimeFamilies, familyByRecord: runtimeFamilyByRecord } = planRuntimeFamilies(records))
19
19
  const handlerGraph = handlerMetafile ? outputGraph(handlerMetafile, outputDirectory, base) : new Map()
20
20
  const routes = records.map(record => {
21
- const capability = planRouteCapabilities([record], { navigationRouteCount: Number(record.capabilities.navigable) })
21
+ const family = runtimeFamilyByRecord.get(record)
22
+ const capability = family && !family.navigation ? family.capability : planRouteCapabilities([record], { navigationRouteCount: Number(record.capabilities.navigable) })
22
23
  const handlerEntries = [...new Set(record.artifacts.handlers.map(reference => reference.module))].sort()
23
24
  const handlerOutputs = closure(handlerEntries, handlerGraph, Boolean(handlerMetafile))
24
25
  const workers = workerReferences
@@ -35,7 +36,7 @@ export function createRouteArtifactReport(records, {
35
36
  signature: capabilitySignature(capability),
36
37
  manifest: capability
37
38
  },
38
- runtime: routeRuntimeEdges(record, capability, runtimeFamilyByRecord.get(record), base, navigationAssets.get(record.route)),
39
+ runtime: routeRuntimeEdges(record, capability, family, base, navigationAssets.get(record.route)),
39
40
  handlers: {
40
41
  entries: handlerEntries,
41
42
  chunks: handlerOutputs.filter(output => !handlerEntries.includes(output))
@@ -58,7 +59,7 @@ export function createRouteArtifactReport(records, {
58
59
  id: family.id,
59
60
  signature: family.signature,
60
61
  navigation: family.navigation,
61
- routes: records.filter(record => runtimeFamilyByRecord.get(record)?.id === family.id).map(record => record.route).sort(),
62
+ routes: family.records.map(record => record.route).sort(),
62
63
  manifest: family.capability,
63
64
  requirements: familyRuntimeRequirements(family, base)
64
65
  })),