@kudzujs/core 0.16.48 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/framework/compiler/effect-codegen.mjs +40 -38
- package/framework/compiler/route-capability-planner.mjs +2 -0
- package/framework/compiler/runtime-codegen.mjs +7 -2
- package/framework/compiler/source-compiler.mjs +10 -2
- package/framework/effect-runtime.js +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,9 +12,9 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
|
|
|
12
12
|
|
|
13
13
|
*Watch: React-shaped TSX to static HTML and route-specific ESM in 22 seconds.*
|
|
14
14
|
|
|
15
|
-
> Experimental `0.
|
|
15
|
+
> Experimental `0.17.x`: the compiler API and supported TSX surface may change.
|
|
16
16
|
|
|
17
|
-
**Current release: [0.
|
|
17
|
+
**Current release: [0.17.0](https://github.com/kudzujs/kudzu/releases/tag/v0.17.0).** Selected calculation effect dependencies accept static primitive configuration alongside at least two direct state inputs, reusing existing normalization and evaluators. Proven read-only effects omit state-write machinery, and ordinary owned effects omit keyed-row machinery. Measured fixture savings range from 253 to 990 raw JavaScript bytes without new runtime concepts. Generator `create-kudzu@0.1.161` targets core `^0.17.0`. Restoration latency and AI-cost/1.0 gates remain unresolved.
|
|
18
18
|
|
|
19
19
|
- [Documentation](https://kudzujs.cloud/docs)
|
|
20
20
|
- [Installation guide](https://kudzujs.cloud/docs#install)
|
|
@@ -177,6 +177,7 @@ ordinary React-shaped TSX
|
|
|
177
177
|
- React Bootstrap `Row` and `Col` with children, static classes, and numeric literal `Col` breakpoints erase to native Bootstrap grid markup; applications retain ownership of Bootstrap CSS.
|
|
178
178
|
- Browser-only query data uses owned effects with application loading/error/data state; URL-derived dependencies, stale-response isolation, keyed results, and recovery require no query package runtime.
|
|
179
179
|
- Only TypeScript modules reachable from pages are compiled. Imported immutable direct maps can fold to static HTML, while direct fields from relative structured calculations reevaluate through route binding ESM.
|
|
180
|
+
- A selected primitive calculation field can be the sole effect dependency with at least two direct state inputs plus static primitive arguments. Strings, finite numbers except negative zero, booleans, null, and const aliases normalized to those literals add no signals or calculation runtime. Object/array configuration, mutable locals, composed arguments, undefined/non-finite values, and parameter defaults/rest remain unsupported.
|
|
180
181
|
- Package imports used directly inside JSX event callbacks are removed from build modules and retained only in bundled route handler ESM.
|
|
181
182
|
- A named or aliased React Router `useNavigate()` top-level binding lowers direct nested-callback calls with safe static root-relative destinations to native `location.assign()` or `location.replace()` navigation.
|
|
182
183
|
- Unsupported nearby patterns fail during the build with a source location and actionable boundary.
|
|
@@ -303,6 +303,7 @@ ${hasDependencyExpressions || hasDependencyEvaluators ? printDerivedDependencyRe
|
|
|
303
303
|
}
|
|
304
304
|
|
|
305
305
|
function printOwnedNavigableEffectEntry(effects, output, handlerModules, assetsDirectory, runtimeDirectory, base) {
|
|
306
|
+
const hasLists = effects.some(effect => effect.list || effect.itemDependencies?.length)
|
|
306
307
|
const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
|
|
307
308
|
const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
|
|
308
309
|
const hasDependencyEvaluators = effects.some(effect => effect.dependencyEvaluators?.length)
|
|
@@ -331,18 +332,18 @@ function mount(lifetime) {
|
|
|
331
332
|
const selectedEffects = effects.map((effect, index) => ({ effect, index })).filter(entry => entry.effect.lifetime === lifetime)
|
|
332
333
|
const records = new Set()
|
|
333
334
|
const owners = new Map()
|
|
334
|
-
const listTemplates = new Map()
|
|
335
|
+
${hasLists ? `const listTemplates = new Map()
|
|
335
336
|
const registrations = new WeakMap()
|
|
336
|
-
const dependencies = new Map()
|
|
337
|
+
` : ""}const dependencies = new Map()
|
|
337
338
|
const pending = new Set()
|
|
338
339
|
const startedCleanups = new Set()
|
|
339
340
|
let scheduled = false
|
|
340
341
|
for (const template of selectedEffects) {
|
|
341
|
-
if (template.effect.list) listTemplates.set(template.effect.owner, template)
|
|
342
|
-
else {
|
|
342
|
+
${hasLists ? `if (template.effect.list) listTemplates.set(template.effect.owner, template)
|
|
343
|
+
else {` : ""}
|
|
343
344
|
const record = createRecord(template, !template.effect.owner)
|
|
344
345
|
if (template.effect.owner) owners.set(template.effect.owner, record)
|
|
345
|
-
}
|
|
346
|
+
${hasLists ? "}" : ""}
|
|
346
347
|
}
|
|
347
348
|
const unsubscribeCommitter = selectedEffects.some(({ effect }) => effect.dependencies?.length) ? __kRuntime.registerCommitter(id => {
|
|
348
349
|
if (!active) return
|
|
@@ -371,18 +372,18 @@ function mount(lifetime) {
|
|
|
371
372
|
dependencies.set(id, subscribers)
|
|
372
373
|
}
|
|
373
374
|
}
|
|
374
|
-
function unregisterDependencies(record) {
|
|
375
|
+
${hasLists ? `function unregisterDependencies(record) {
|
|
375
376
|
for (const id of record.effect.dependencies ?? []) {
|
|
376
377
|
const subscribers = dependencies.get(id)
|
|
377
378
|
subscribers?.delete(record)
|
|
378
379
|
if (!subscribers?.size) dependencies.delete(id)
|
|
379
380
|
}
|
|
380
381
|
}
|
|
381
|
-
function mountOwned(root) {
|
|
382
|
+
` : ""}function mountOwned(root) {
|
|
382
383
|
if (!active) return
|
|
383
384
|
for (const marker of matching(root)) {
|
|
384
385
|
if (!marker.isConnected) continue
|
|
385
|
-
if (marker.dataset.kEffects) {
|
|
386
|
+
${hasLists ? `if (marker.dataset.kEffects) {
|
|
386
387
|
if (registrations.has(marker)) continue
|
|
387
388
|
const rowRecords = JSON.parse(marker.dataset.kEffects).flatMap(owner => {
|
|
388
389
|
const template = listTemplates.get(owner)
|
|
@@ -395,25 +396,25 @@ function mount(lifetime) {
|
|
|
395
396
|
if (rowRecords.length) registrations.set(marker, rowRecords)
|
|
396
397
|
continue
|
|
397
398
|
}
|
|
398
|
-
const record = owners.get(marker.dataset.kEffect)
|
|
399
|
+
` : ""}const record = owners.get(marker.dataset.kEffect)
|
|
399
400
|
if (record && !record.mounted) mountRecord(record, marker)
|
|
400
401
|
}
|
|
401
402
|
}
|
|
402
403
|
function unmountOwned(root) {
|
|
403
404
|
if (!active) return
|
|
404
405
|
for (const marker of matching(root)) {
|
|
405
|
-
const rowRecords = registrations.get(marker)
|
|
406
|
+
${hasLists ? `const rowRecords = registrations.get(marker)
|
|
406
407
|
if (rowRecords) {
|
|
407
408
|
for (const record of rowRecords) unmountRecord(record, true)
|
|
408
409
|
registrations.delete(marker)
|
|
409
410
|
continue
|
|
410
411
|
}
|
|
411
|
-
const record = owners.get(marker.dataset.kEffect)
|
|
412
|
+
` : ""}const record = owners.get(marker.dataset.kEffect)
|
|
412
413
|
if (record?.marker === marker) unmountRecord(record)
|
|
413
414
|
}
|
|
414
415
|
}
|
|
415
416
|
function matching(root) {
|
|
416
|
-
const selector = "template[data-k-effect],[data-k-effects]"
|
|
417
|
+
const selector = ${JSON.stringify(hasLists ? "template[data-k-effect],[data-k-effects]" : "template[data-k-effect]")}
|
|
417
418
|
return [...(root.matches?.(selector) ? [root] : []), ...(root.querySelectorAll?.(selector) ?? [])]
|
|
418
419
|
}
|
|
419
420
|
function mountRecord(record, marker) {
|
|
@@ -426,16 +427,16 @@ function mount(lifetime) {
|
|
|
426
427
|
if (record.disposal) record.disposal.then(begin)
|
|
427
428
|
else begin()
|
|
428
429
|
}
|
|
429
|
-
function unmountRecord(record, dynamic = false) {
|
|
430
|
+
function unmountRecord(record${hasLists ? ", dynamic = false" : ""}) {
|
|
430
431
|
if (!record.mounted) return
|
|
431
432
|
record.mounted = false
|
|
432
433
|
record.marker = undefined
|
|
433
434
|
record.version++
|
|
434
435
|
pending.delete(record)
|
|
435
|
-
if (dynamic) {
|
|
436
|
+
${hasLists ? `if (dynamic) {
|
|
436
437
|
unregisterDependencies(record)
|
|
437
438
|
records.delete(record)
|
|
438
|
-
}
|
|
439
|
+
}` : ""}
|
|
439
440
|
void cleanup(record)
|
|
440
441
|
}
|
|
441
442
|
function start(record) {
|
|
@@ -505,9 +506,9 @@ ${hasDependencyExpressions || hasDependencyEvaluators ? printDerivedDependencyRe
|
|
|
505
506
|
record.token = token
|
|
506
507
|
try {
|
|
507
508
|
const effect = record.effect
|
|
508
|
-
const scope = effect.list
|
|
509
|
+
const scope = ${hasLists ? `effect.list
|
|
509
510
|
? Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, value?.type === "list-item" ? JSON.parse(record.marker.dataset.kEffectItem) : value]))
|
|
510
|
-
: effect.scope
|
|
511
|
+
: effect.scope` : "effect.scope"}
|
|
511
512
|
const result = modules.get(effect.module)[effect.handler](createEffectContext(__kRuntime.browserState, effect.states, __kRuntime.commitDom, scope, () => active && token.active && record.token === token))
|
|
512
513
|
if (effect.cleanup && typeof result === "function") record.cleanup = result
|
|
513
514
|
else if (result && typeof result.then === "function") result.catch(error => console.error(error))
|
|
@@ -572,6 +573,7 @@ function printDerivedDependencyRead(state, hasExpressions, hasEvaluators) {
|
|
|
572
573
|
}
|
|
573
574
|
|
|
574
575
|
function printOwnedEffectEntry(imports, effects, entries) {
|
|
576
|
+
const hasLists = effects.some(effect => effect.list || effect.itemDependencies?.length)
|
|
575
577
|
const hasItemDependencies = effects.some(effect => effect.itemDependencies?.length)
|
|
576
578
|
const hasOrdinaryDependencies = effects.some(effect => effect.dependencies?.length)
|
|
577
579
|
const hasDependencyExpressions = effects.some(effect => effect.dependencyExpressions?.length)
|
|
@@ -582,12 +584,12 @@ function printOwnedEffectEntry(imports, effects, entries) {
|
|
|
582
584
|
return `${imports.join("\n")}
|
|
583
585
|
const effects = ${inlineJson(effects)}
|
|
584
586
|
const modules = new Map([${entries}])
|
|
585
|
-
${hasItemDependencies ? "let order = 0\n" : ""}const records = effects.map((effect, index) => effect.list ? undefined : createRecord(effect, index)).filter(Boolean)
|
|
586
|
-
const listTemplates = new Map(effects.map((effect, index) => effect.list ? [effect.owner, { effect, index }] : undefined).filter(Boolean))
|
|
587
|
-
const owners = new Map(records.filter(record => record.effect.owner).map(record => [record.effect.owner, record]))
|
|
588
|
-
const listRegistrations = new WeakMap()
|
|
587
|
+
${hasItemDependencies ? "let order = 0\n" : ""}const records = ${hasLists ? "effects.map((effect, index) => effect.list ? undefined : createRecord(effect, index)).filter(Boolean)" : "effects.map((effect, index) => createRecord(effect, index))"}
|
|
588
|
+
${hasLists ? `const listTemplates = new Map(effects.map((effect, index) => effect.list ? [effect.owner, { effect, index }] : undefined).filter(Boolean))
|
|
589
|
+
` : ""}const owners = new Map(records.filter(record => record.effect.owner).map(record => [record.effect.owner, record]))
|
|
590
|
+
${hasLists ? `const listRegistrations = new WeakMap()
|
|
589
591
|
const listOwnerSets = new Map()
|
|
590
|
-
const mountedRecords = new Set(records.filter(record => record.mounted))
|
|
592
|
+
` : ""}const mountedRecords = new Set(records.filter(record => record.mounted))
|
|
591
593
|
const dependencies = new Map()
|
|
592
594
|
const pending = new Set()
|
|
593
595
|
let scheduled = false
|
|
@@ -611,14 +613,14 @@ function registerDependencies(record) {
|
|
|
611
613
|
dependencies.set(id, subscribers)
|
|
612
614
|
}
|
|
613
615
|
}
|
|
614
|
-
function unregisterDependencies(record) {
|
|
616
|
+
${hasLists ? `function unregisterDependencies(record) {
|
|
615
617
|
for (const id of record.effect.dependencies ?? []) {
|
|
616
618
|
const subscribers = dependencies.get(id)
|
|
617
619
|
subscribers?.delete(record)
|
|
618
620
|
if (!subscribers?.size) dependencies.delete(id)
|
|
619
621
|
}
|
|
620
622
|
}
|
|
621
|
-
${hasItemDependencies ? `if (${hasOrdinaryDependencies}) ` : ""}__kRuntime.registerCommitter(id => {
|
|
623
|
+
` : ""}${hasItemDependencies ? `if (${hasOrdinaryDependencies}) ` : ""}__kRuntime.registerCommitter(id => {
|
|
622
624
|
if (!active) return
|
|
623
625
|
for (const record of dependencies.get(id) ?? []) if (record.mounted) pending.add(record)
|
|
624
626
|
schedule()
|
|
@@ -631,7 +633,7 @@ ${hasItemDependencies ? `for (const listState of new Set(effects.filter(effect =
|
|
|
631
633
|
` : ""}__kRuntime.registerMountHook(root => {
|
|
632
634
|
if (!active) return
|
|
633
635
|
for (const marker of matching(root)) {
|
|
634
|
-
if (marker.dataset.kEffects) {
|
|
636
|
+
${hasLists ? `if (marker.dataset.kEffects) {
|
|
635
637
|
if (listRegistrations.has(marker)) continue
|
|
636
638
|
const encoded = marker.dataset.kEffects
|
|
637
639
|
let effectOwners = listOwnerSets.get(encoded)
|
|
@@ -650,7 +652,7 @@ ${hasItemDependencies ? `for (const listState of new Set(effects.filter(effect =
|
|
|
650
652
|
listRegistrations.set(marker, rowRecords)
|
|
651
653
|
continue
|
|
652
654
|
}
|
|
653
|
-
const record = owners.get(marker.dataset.kEffect)
|
|
655
|
+
` : ""}const record = owners.get(marker.dataset.kEffect)
|
|
654
656
|
if (!record?.mounted) mount(record, marker)
|
|
655
657
|
}
|
|
656
658
|
}, "effects")
|
|
@@ -659,17 +661,17 @@ __kRuntime.registerUnmountHook(root => {
|
|
|
659
661
|
if (!active) return
|
|
660
662
|
active = false
|
|
661
663
|
pending.clear()
|
|
662
|
-
for (const record of [...mountedRecords]) unmount(record, record.effect.list)
|
|
664
|
+
for (const record of [...mountedRecords]) unmount(record${hasLists ? ", record.effect.list" : ""})
|
|
663
665
|
return
|
|
664
666
|
}
|
|
665
667
|
for (const marker of matching(root)) {
|
|
666
|
-
const rowRecords = listRegistrations.get(marker)
|
|
668
|
+
${hasLists ? `const rowRecords = listRegistrations.get(marker)
|
|
667
669
|
if (rowRecords) {
|
|
668
670
|
for (const record of rowRecords) unmount(record, true)
|
|
669
671
|
listRegistrations.delete(marker)
|
|
670
672
|
continue
|
|
671
673
|
}
|
|
672
|
-
const record = owners.get(marker.dataset.kEffect)
|
|
674
|
+
` : ""}const record = owners.get(marker.dataset.kEffect)
|
|
673
675
|
if (record?.marker === marker) unmount(record)
|
|
674
676
|
}
|
|
675
677
|
}, "effects")
|
|
@@ -679,10 +681,10 @@ addEventListener("pagehide", event => {
|
|
|
679
681
|
if (!event.persisted) __kRuntime.unmountDom(document)
|
|
680
682
|
})
|
|
681
683
|
function matching(root) {
|
|
682
|
-
const selector = "template[data-k-effect],[data-k-effects]"
|
|
684
|
+
const selector = ${JSON.stringify(hasLists ? "template[data-k-effect],[data-k-effects]" : "template[data-k-effect]")}
|
|
683
685
|
return [...(root.matches?.(selector) ? [root] : []), ...(root.querySelectorAll?.(selector) ?? [])]
|
|
684
686
|
}
|
|
685
|
-
function mount(record, marker, fresh = false) {
|
|
687
|
+
function mount(record, marker${hasLists ? ", fresh = false" : ""}) {
|
|
686
688
|
record.mounted = true
|
|
687
689
|
record.marker = marker
|
|
688
690
|
mountedRecords.add(record)
|
|
@@ -691,14 +693,14 @@ function mount(record, marker, fresh = false) {
|
|
|
691
693
|
if (!active || !record.mounted || record.version !== version || !marker.isConnected) return
|
|
692
694
|
start(record)
|
|
693
695
|
})
|
|
694
|
-
else if (fresh || active && marker.isConnected) start(record)
|
|
696
|
+
else if (${hasLists ? "fresh || " : ""}active && marker.isConnected) start(record)
|
|
695
697
|
}
|
|
696
|
-
function unmount(record, dynamic = false) {
|
|
698
|
+
function unmount(record${hasLists ? ", dynamic = false" : ""}) {
|
|
697
699
|
if (!record.mounted) return
|
|
698
700
|
record.mounted = false
|
|
699
701
|
record.marker = undefined
|
|
700
702
|
mountedRecords.delete(record)
|
|
701
|
-
if (dynamic) unregisterDependencies(record)
|
|
703
|
+
${hasLists ? "if (dynamic) unregisterDependencies(record)" : ""}
|
|
702
704
|
record.version++
|
|
703
705
|
pending.delete(record)
|
|
704
706
|
invokeCleanup(record)
|
|
@@ -769,17 +771,17 @@ function invoke(record) {
|
|
|
769
771
|
record.token = token
|
|
770
772
|
try {
|
|
771
773
|
const effect = record.effect
|
|
772
|
-
const scope = effect.list
|
|
774
|
+
const scope = ${hasLists ? `effect.list
|
|
773
775
|
? Object.fromEntries(Object.entries(effect.scope).map(([name, value]) => [name, value?.type === "list-item" ? __kRuntime.listItems.get(record.marker) : value]))
|
|
774
|
-
: effect.scope
|
|
775
|
-
const marker = record.marker
|
|
776
|
+
: effect.scope` : "effect.scope"}
|
|
777
|
+
${hasLists ? `const marker = record.marker
|
|
776
778
|
${hasRowRef ? "const rowPath = effect.list ? __kRuntime.listRowPaths.get(marker) : undefined" : ""}
|
|
777
779
|
const resolveRef = id => {
|
|
778
780
|
if (!marker?.isConnected) return null
|
|
779
781
|
const resolved = ${hasRowRef ? 'id.replace("$k", rowPath)' : "id"}
|
|
780
782
|
return marker.dataset.kRef === resolved ? marker : [...marker.querySelectorAll("[data-k-ref]")].find(node => node.dataset.kRef === resolved) ?? null
|
|
781
783
|
}
|
|
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))
|
|
784
|
+
` : ""}const result = modules.get(effect.module)[effect.handler](createEffectContext(browserState, effect.states, commitDom, scope, () => active && token.active && record.token === token${hasLists ? ", effect.list ? resolveRef : undefined" : ""}))
|
|
783
785
|
if (effect.cleanup && typeof result === "function") record.cleanup = result
|
|
784
786
|
else if (result && typeof result.then === "function") result.catch(error => console.error(error))
|
|
785
787
|
} catch (error) {
|
|
@@ -105,6 +105,7 @@ export function planRouteCapabilities(records, { navigationRouteCount = 0 } = {}
|
|
|
105
105
|
lists.generalRowHooks ||= lists.rowRefs || lists.complexRowState
|
|
106
106
|
lists.asyncParts = lists.expressions || lists.expressionAttributes || lists.conditions
|
|
107
107
|
lists.mounts ||= lists.conditions || lists.nested
|
|
108
|
+
if (effects.any && plans.every(plan => plan.effects.every(effect => !Object.keys(effect.states).length && !hasCaptureType(effect.scope, "setter")))) effects.stateWrites = false
|
|
108
109
|
|
|
109
110
|
const capabilityIR = {
|
|
110
111
|
version: 1,
|
|
@@ -143,6 +144,7 @@ export function assertCapabilityIR(capabilityIR, records, options = {}) {
|
|
|
143
144
|
if (capabilityIR.lists.rowRefs && !capabilityIR.lists.rowHooks || capabilityIR.lists.generalRowHooks && !capabilityIR.lists.rowHooks) throw new Error("CapabilityIR row ownership requires row hooks")
|
|
144
145
|
if (capabilityIR.effects.navigableOwners && (!capabilityIR.effects.navigable || !capabilityIR.effects.any)) throw new Error("CapabilityIR navigable effect owners require navigable effects")
|
|
145
146
|
if ((capabilityIR.bindings.count || capabilityIR.lists.count || capabilityIR.events.native.length || options.navigationRouteCount) && !capabilityIR.runtime.shared) throw new Error("CapabilityIR shared runtime flag is inconsistent")
|
|
147
|
+
if (capabilityIR.effects.stateWrites !== undefined && (capabilityIR.effects.stateWrites !== false || !capabilityIR.effects.any)) throw new Error("Invalid CapabilityIR effect state-write exclusion")
|
|
146
148
|
assertJsonSafe(capabilityIR, "CapabilityIR")
|
|
147
149
|
if (records) {
|
|
148
150
|
const expected = planRouteCapabilities(records, options, false)
|
|
@@ -16,10 +16,15 @@ export function generateCoreRuntime(source, capabilityIR) {
|
|
|
16
16
|
export function generateEffectRuntime(source, capabilityIR) {
|
|
17
17
|
assertCapabilityIR(capabilityIR)
|
|
18
18
|
const { captures, effects } = capabilityIR
|
|
19
|
+
let runtime = effects.captures ? replaceRequired(source, '"./serialization.js"', '"./kudzu-serialization.js"', "serialization import", "effect-runtime.js") : replaceRequired(source, /^import[^\n]+\n/, "", "serialization import", "effect-runtime.js")
|
|
20
|
+
if (effects.stateWrites === false) runtime = replaceSequenceRequired(runtime, [
|
|
21
|
+
[/ \/\* effect-state-writes \*\/[\s\S]*? \/\* effect-state-writes-end \*\/\n/, "", "unused effect state writes"],
|
|
22
|
+
[" set(name, value) {\n setId(stateIds[name], value)\n }\n", "", "unused effect setter"]
|
|
23
|
+
], "effect-runtime.js")
|
|
19
24
|
return {
|
|
20
|
-
source:
|
|
25
|
+
source: runtime,
|
|
21
26
|
define: {
|
|
22
|
-
"globalThis.__KUDZU_CAPTURE_SETTER__": String(captures.setter),
|
|
27
|
+
"globalThis.__KUDZU_CAPTURE_SETTER__": String(effects.stateWrites !== false && captures.setter),
|
|
23
28
|
"globalThis.__KUDZU_EFFECT_CAPTURES__": String(effects.captures)
|
|
24
29
|
}
|
|
25
30
|
}
|
|
@@ -1043,7 +1043,7 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
1043
1043
|
const reject = (target, message) => { throw sourceNodeError(target, calculationSource, message) }
|
|
1044
1044
|
if (calculation.asteriskToken || calculation.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) reject(calculation, "Imported calculations used by useEffect() must be synchronous; move async work into the owned effect")
|
|
1045
1045
|
if (calculation.parameters.some(parameter => !ts.isIdentifier(parameter.name) || parameter.dotDotDotToken || parameter.initializer || parameter.questionToken)) reject(calculation, "Imported calculations used by useEffect() require ordinary identifier parameters without defaults or rest")
|
|
1046
|
-
if (call.arguments.some(argument => !ts.isIdentifier(unwrapExpression(argument)))) fail(call, "useEffect() selected calculation arguments must be direct primitive state identifiers;
|
|
1046
|
+
if (call.arguments.some(argument => !ts.isIdentifier(unwrapExpression(argument)) && !isStaticPrimitiveArgument(argument))) fail(call, "useEffect() selected calculation arguments must be direct primitive state identifiers or static primitive literals; property paths and composed arguments are not supported")
|
|
1047
1047
|
const shapes = returns.map(returned => returned.properties.map(property => {
|
|
1048
1048
|
if (!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property) || ts.isComputedPropertyName(property.name) || ["__proto__", "constructor", "prototype"].includes(property.name.text)) reject(property, "Imported calculations used by useEffect() must return direct safe plain-object fields without spreads, methods, or computed names")
|
|
1049
1049
|
return property.name.text
|
|
@@ -2085,7 +2085,8 @@ function createKudzuTransformer({ semantic, handlerUrl, file, sourceFiles, sourc
|
|
|
2085
2085
|
const call = unwrapExpression(expanded.expression)
|
|
2086
2086
|
if (!ts.isCallExpression(call) || !ts.isIdentifier(call.expression) || !importBindings.has(call.expression.text)) return undefined
|
|
2087
2087
|
const states = referencedStateNames(call, setters, call, bindingIndex)
|
|
2088
|
-
if (states.size < 2
|
|
2088
|
+
if (states.size < 2) effectFail(call, "useEffect() selected calculations require at least two direct primitive state arguments")
|
|
2089
|
+
if (call.arguments.some(argument => !isStaticPrimitiveArgument(argument) && (!ts.isIdentifier(unwrapExpression(argument)) || !states.has(unwrapExpression(argument).text)))) effectFail(call, "useEffect() selected calculation arguments must be direct state identifiers or static primitive literals after const normalization")
|
|
2089
2090
|
return { name: value.expression.text, field: value.name.text, call, states, source: dependency }
|
|
2090
2091
|
}
|
|
2091
2092
|
const dependencyAnalysis = analyzeEffectDependencies({
|
|
@@ -3243,6 +3244,13 @@ function isPrimitiveDefaultLiteral(node) {
|
|
|
3243
3244
|
node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
|
|
3244
3245
|
}
|
|
3245
3246
|
|
|
3247
|
+
function isStaticPrimitiveArgument(node) {
|
|
3248
|
+
const value = unwrapExpression(node)
|
|
3249
|
+
if (!isPrimitiveDefaultLiteral(value) && !ts.isNoSubstitutionTemplateLiteral(value)) return false
|
|
3250
|
+
const number = ts.isNumericLiteral(value) ? Number(value.text) : ts.isPrefixUnaryExpression(value) ? (value.operator === ts.SyntaxKind.MinusToken ? -1 : 1) * Number(value.operand.text) : null
|
|
3251
|
+
return number === null || Number.isFinite(number) && !Object.is(number, -0)
|
|
3252
|
+
}
|
|
3253
|
+
|
|
3246
3254
|
function isEventOnlyComponentLocal(root, name) {
|
|
3247
3255
|
let found = false
|
|
3248
3256
|
let eventOnly = true
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { deserialize } from "./serialization.js"
|
|
2
2
|
|
|
3
3
|
export function createEffectContext(state, stateIds, commit, serializedScope = {}, active = () => true, resolveRef) {
|
|
4
|
+
/* effect-state-writes */
|
|
4
5
|
const changed = new Set()
|
|
5
6
|
let scheduled = false
|
|
6
7
|
|
|
@@ -22,6 +23,7 @@ export function createEffectContext(state, stateIds, commit, serializedScope = {
|
|
|
22
23
|
queueMicrotask(flush)
|
|
23
24
|
}
|
|
24
25
|
}
|
|
26
|
+
/* effect-state-writes-end */
|
|
25
27
|
|
|
26
28
|
const scope = globalThis.__KUDZU_EFFECT_CAPTURES__
|
|
27
29
|
? Object.fromEntries(Object.entries(serializedScope).map(([name, value]) => [name, deserialize(value, id => state.get(id), globalThis.__KUDZU_CAPTURE_SETTER__ ? setId : undefined, undefined, resolveRef)]))
|