@kudzujs/core 0.8.62 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/MIGRATION_ROADMAP.md +36 -1
- package/PERFORMANCE.md +79 -1
- package/README.md +2 -2
- package/RELEASES.md +54 -0
- package/bin/kudzu.mjs +10 -1
- package/docs/next-architecture/0.9-baseline.md +1199 -0
- package/docs/next-architecture/0.9-benchmark-contracts.md +507 -0
- package/docs/next-architecture/0.9-component-property-contract.md +89 -0
- package/docs/next-architecture/0.9-compression-ledger.md +227 -0
- package/docs/next-architecture/0.9-final-proof-audit.md +176 -0
- package/docs/next-architecture/0.9-implementation-plan.md +1819 -0
- package/docs/next-architecture/0.9-resource-lifecycle.md +118 -0
- package/docs/next-architecture/0.9-semantic-compression.md +384 -0
- package/docs/next-architecture/README.md +16 -12
- package/docs/next-architecture/compiler-current-architecture.md +7 -7
- package/docs/next-architecture/large-application-ai-native-roadmap.md +5 -3
- package/docs/next-architecture/versioning.md +1 -1
- package/framework/README.md +2 -0
- package/framework/binding-runtime.js +4 -4
- package/framework/build.mjs +135 -30
- package/framework/compiler/ast-helpers.mjs +5 -0
- package/framework/compiler/browser-signal-passes.mjs +2 -7
- package/framework/compiler/collection-analysis.mjs +4 -0
- package/framework/compiler/descriptor-session.mjs +36 -12
- package/framework/compiler/effect-analysis.mjs +28 -8
- package/framework/compiler/effect-codegen.mjs +79 -36
- package/framework/compiler/effect-private-ref-pass.mjs +4 -8
- package/framework/compiler/handler-lowering.mjs +12 -7
- package/framework/compiler/ir/module-ir.mjs +26 -4
- package/framework/compiler/list-runtime-codegen.mjs +4 -2
- package/framework/compiler/optimize/command-specialization.mjs +4 -7
- package/framework/compiler/route-artifact-report.mjs +4 -3
- package/framework/compiler/route-build-record.mjs +12 -0
- package/framework/compiler/route-capability-planner.mjs +3 -3
- package/framework/compiler/route-ir.mjs +27 -11
- package/framework/compiler/runtime-codegen.mjs +2 -2
- package/framework/compiler/source-compiler.mjs +359 -78
- package/framework/core.d.ts +1 -0
- package/framework/core.mjs +22 -7
- package/framework/dependency-runtime.js +1 -1
- package/framework/effect-runtime.js +2 -2
- package/framework/list-runtime.js +70 -26
- package/framework/native-runtime.js +12 -9
- package/framework/runtime.js +1 -1
- package/framework/serialization.js +13 -6
- package/framework/shared-runtime.js +14 -12
- package/package.json +1 -1
|
@@ -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 :
|
|
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
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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" ?
|
|
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
|
|
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
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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 =>
|
|
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 =>
|
|
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 => !
|
|
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.
|
|
19
|
-
const
|
|
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).
|
|
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).
|
|
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 ?? [])
|
|
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)
|
|
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
|
-
["
|
|
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(
|
|
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 =>
|
|
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
|
|
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,
|
|
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:
|
|
62
|
+
routes: family.records.map(record => record.route).sort(),
|
|
62
63
|
manifest: family.capability,
|
|
63
64
|
requirements: familyRuntimeRequirements(family, base)
|
|
64
65
|
})),
|