@kudzujs/core 0.16.47 → 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 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.16.x`: the compiler API and supported TSX surface may change.
15
+ > Experimental `0.17.x`: the compiler API and supported TSX surface may change.
16
16
 
17
- **Current release: [0.16.47](https://github.com/kudzujs/kudzu/releases/tag/v0.16.47).** The opt-in `create-kudzu@0.1.160` AI check inspects only its own Linux process subtree, including worker-thread children, while preserving sibling work and reporting inspection failures. Repository browser checks now reject disconnected commands instead of waiting indefinitely. Compiler behavior and browser output remain unchanged. No reliable AI-token saving is established; the AI-cost/1.0 gates remain blocked.
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.
@@ -71,9 +71,11 @@ function mountBindings(root) {
71
71
  mountedBindings.add(node)
72
72
  const registrations = []
73
73
  bindingRegistrations.set(node, registrations)
74
- const descriptors = globalThis.__KUDZU_PROPERTY_BINDINGS__ !== false ? bindingTypes.flatMap(target => node.hasAttribute(`data-k-bind-${target}`)
75
- ? [[target, JSON.parse(node.dataset[`kBind${capitalize(target)}`])]]
76
- : []) : []
74
+ const descriptors = []
75
+ if (globalThis.__KUDZU_PROPERTY_BINDINGS__ !== false) for (const target of bindingTypes) {
76
+ const encoded = node.getAttribute(`data-k-bind-${target}`)
77
+ if (encoded !== null) descriptors.push([target, JSON.parse(encoded)])
78
+ }
77
79
  if (globalThis.__KUDZU_ATTRIBUTE_BINDINGS__ !== false && node.dataset.kBindAttrs) descriptors.push(...JSON.parse(node.dataset.kBindAttrs).map(({ target, ...descriptor }) => [target, descriptor]))
78
80
  for (const [target, descriptor] of descriptors) {
79
81
  if (descriptor.state) {
@@ -340,10 +342,6 @@ function patchText(start, value) {
340
342
  if (next) end.before(start.ownerDocument.createTextNode(next))
341
343
  }
342
344
 
343
- function capitalize(value) {
344
- return value[0].toUpperCase() + value.slice(1)
345
- }
346
-
347
345
  function isStringBooleanAttribute(name) {
348
346
  return name.startsWith("aria-") || name.startsWith("data-")
349
347
  }
@@ -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: effects.captures ? replaceRequired(source, '"./serialization.js"', '"./kudzu-serialization.js"', "serialization import", "effect-runtime.js") : replaceRequired(source, /^import[^\n]+\n/, "", "serialization import", "effect-runtime.js"),
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; aliases, property paths, and composed arguments are not supported")
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 || call.arguments.some(argument => !ts.isIdentifier(unwrapExpression(argument)) || !states.has(unwrapExpression(argument).text))) effectFail(call, "useEffect() selected calculations require at least two direct primitive state arguments")
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)]))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.16.47",
3
+ "version": "0.17.0",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",