@kudzujs/core 0.8.7 → 0.8.9

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
@@ -10,7 +10,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
10
10
 
11
11
  > Experimental `0.8.x`: the compiler API and supported TSX surface may change.
12
12
 
13
- **Latest release: 0.8.7 - Reactive keyed row selection.** Flat keyed rows may combine their current item or index with direct primitive parent state in pure text and attribute expressions, enabling selected classes and ARIA state without remounting rows. Read the [release notes](./RELEASES.md#087---reactive-keyed-row-selection) or open the [release page](https://kudzujs.cloud/releases/0.8.7).
13
+ **Latest release: 0.8.9 - Context-backed CRUD actions.** A relative custom hook may directly return `useContext(Context)` for one analyzable Provider whose state-backed actions compile into existing route handler ESM, without a browser Context tree. Read the [release notes](./RELEASES.md#089---context-backed-crud-actions) or open the [release page](https://kudzujs.cloud/releases/0.8.9).
14
14
 
15
15
  - [Documentation](https://kudzujs.cloud/docs)
16
16
  - [Installation guide](https://kudzujs.cloud/docs#install)
package/RELEASES.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.9 - Context-backed CRUD actions
4
+
5
+ Kudzu 0.8.9 specializes state-backed actions exposed through one conventional Context Provider and relative custom hook.
6
+
7
+ ### New in 0.8.9
8
+
9
+ - A relative zero-argument custom hook may directly return `useContext(ContextIdentifier)` from one local or named relative Context module.
10
+ - One Provider may expose direct shorthand `useState` pairs and synchronous actions that capture only those exposed pairs.
11
+ - Consumers may select state and actions across ordinary component boundaries; direct action calls inline into existing route handler ESM and concrete state operations.
12
+ - CRUD actions over object arrays compose with keyed rows, reactive selection, conditional editors, and guarded local storage effects in the React Notes migration.
13
+ - Unsupported private captures, hidden state dependencies, indirect action references, dynamic Provider values, multiple Providers, and consumer binding collisions fail with source diagnostics.
14
+ - Static sibling routes remain JavaScript-free. No action function, Context runtime, Provider tree, callback registry, VDOM, or hydration is emitted.
15
+ - The complete suite passes 139/139 tests, including Chrome coverage for create, rename, select, and delete behavior.
16
+
17
+ ### Boundary
18
+
19
+ The hook must directly return `useContext(ContextIdentifier)`. The Context and exactly one Provider must be declared together in one local or named relative module, and its value must be one direct shorthand object. Actions must be synchronous, called directly in intrinsic handlers, and capture only exposed Provider state pairs.
20
+
21
+ ### Upgrade
22
+
23
+ ```bash
24
+ npm install @kudzujs/core@^0.8.9
25
+ ```
26
+
27
+ ## 0.8.8 - Conditional keyed map roots
28
+
29
+ Kudzu 0.8.8 compiles ordinary expression-bodied conditional keyed maps through the existing pure collection selector path.
30
+
31
+ ### New in 0.8.8
32
+
33
+ - One-parameter keyed maps may return `condition && <Row />` or `condition ? <Row /> : null`.
34
+ - Top-level conditions may combine the current item with direct primitive parent state; nested maps support item-only conditions.
35
+ - Omitted rows own no DOM or hooks. True-to-false transitions release row state, effects, and refs; re-entry creates fresh ownership.
36
+ - Retained siblings preserve keyed DOM identity through condition changes, insertion, and reorder.
37
+ - Same-file and relative row components remain compiler-specialized before the existing list runtime receives the normalized filter.
38
+ - Imported build-known item-only conditions still fold to complete zero-JavaScript HTML.
39
+ - The React Notes migration restores its ordinary `notes.map(note => activeId === note.id && <Editor />)` source shape.
40
+ - Map indexes, alternate JSX fallbacks, block-bodied conditional maps, arbitrary captures, and impure predicates remain diagnosed.
41
+ - The complete suite passes 134/134 tests.
42
+
43
+ ### Boundary
44
+
45
+ Conditional map callbacks must be synchronous expression arrows with exactly one item parameter. Indexes are rejected because implicit filtering changes their meaning. The feature adds no runtime capability, VDOM, hydration, or component rerenderer.
46
+
47
+ ### Upgrade
48
+
49
+ ```bash
50
+ npm install @kudzujs/core@^0.8.8
51
+ ```
52
+
3
53
  ## 0.8.7 - Reactive keyed row selection
4
54
 
5
55
  Kudzu 0.8.7 lets a flat keyed row combine its current item or index with direct primitive parent state in pure text and attribute expressions.
@@ -2843,6 +2843,78 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2843
2843
  let usesComponentRef = false
2844
2844
  let usesComponentEffects = false
2845
2845
 
2846
+ const resolveContextHook = (returned, hookSource) => {
2847
+ if (!hasFrameworkImport(hookSource, "useContext")) throw sourceNodeError(returned.expression, hookSource, "Relative Context hooks must call useContext imported from react or @kudzujs/core")
2848
+ if (returned.arguments.length !== 1 || !ts.isIdentifier(returned.arguments[0])) throw sourceNodeError(returned, hookSource, "Relative Context hooks must directly return useContext(ContextIdentifier)")
2849
+ const contextName = returned.arguments[0].text
2850
+ let providerSource = hookSource
2851
+ let providerContextName = contextName
2852
+ const hookImports = clientImportBindings(hookSource, hookSource.fileName, sourceFiles)
2853
+ if (hookImports.has(contextName)) {
2854
+ const binding = hookImports.get(contextName)
2855
+ if (binding.kind === "namespace" || binding.kind === "default") throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require a named Context import")
2856
+ providerSource = importedSource(binding.target)
2857
+ providerContextName = binding.imported
2858
+ }
2859
+ const hasContext = hasFrameworkImport(providerSource, "createContext") && providerSource.statements.some(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => ts.isIdentifier(declaration.name) && declaration.name.text === providerContextName && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.text === "createContext"))
2860
+ if (!hasContext) throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require a local or named relative createContext() declaration")
2861
+
2862
+ const providers = []
2863
+ const findProviders = node => {
2864
+ if (ts.isJsxAttribute(node) && node.name.text === "value") {
2865
+ const element = node.parent?.parent
2866
+ const tag = ts.isJsxOpeningElement(element) || ts.isJsxSelfClosingElement(element) ? element.tagName : undefined
2867
+ if (ts.isPropertyAccessExpression(tag) && tag.name.text === "Provider" && ts.isIdentifier(tag.expression) && tag.expression.text === providerContextName) providers.push(node)
2868
+ }
2869
+ ts.forEachChild(node, findProviders)
2870
+ }
2871
+ findProviders(providerSource)
2872
+ if (providers.length !== 1) throw sourceNodeError(returned.arguments[0], hookSource, "Relative Context hooks require exactly one Provider value in the Context module")
2873
+ const provider = providers[0]
2874
+ const value = provider.initializer && ts.isJsxExpression(provider.initializer) && provider.initializer.expression ? unwrapExpression(provider.initializer.expression) : undefined
2875
+ if (!value || !ts.isObjectLiteralExpression(value)) throw sourceNodeError(provider, providerSource, "Context Provider value must be one direct object literal")
2876
+ const owner = nearestFunction(provider)
2877
+ if (!owner) throw sourceNodeError(provider, providerSource, "Context Provider value must be returned by a component")
2878
+
2879
+ const states = new Map()
2880
+ const callbacks = new Map()
2881
+ const hasUseState = hasFrameworkImport(providerSource, "useState")
2882
+ const collectProviderBindings = node => {
2883
+ if (ts.isVariableDeclaration(node) && nearestFunction(node) === owner) {
2884
+ if (hasUseState && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "useState") {
2885
+ const [state, setter] = node.name.elements
2886
+ if (node.name.elements.length === 2 && state && setter && ts.isBindingElement(state) && ts.isBindingElement(setter) && ts.isIdentifier(state.name) && ts.isIdentifier(setter.name)) states.set(setter.name.text, state.name.text)
2887
+ }
2888
+ if (ts.isIdentifier(node.name) && node.initializer && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer))) callbacks.set(node.name.text, node.initializer)
2889
+ }
2890
+ ts.forEachChild(node, collectProviderBindings)
2891
+ }
2892
+ collectProviderBindings(owner.body)
2893
+
2894
+ const fields = new Set()
2895
+ const stateFields = new Set([...states].flat())
2896
+ for (const property of value.properties) {
2897
+ if (!ts.isShorthandPropertyAssignment(property)) throw sourceNodeError(property, providerSource, "Context Provider values must use direct shorthand state, setter, or action fields")
2898
+ const name = property.name.text
2899
+ if (!stateFields.has(name) && !callbacks.has(name)) throw sourceNodeError(property, providerSource, `Context Provider field ${JSON.stringify(name)} must be a direct provider-owned state, setter, or action`)
2900
+ fields.add(name)
2901
+ }
2902
+ for (const [setter, state] of states) {
2903
+ if (fields.has(setter) !== fields.has(state)) throw sourceNodeError(value, providerSource, `Context Provider state ${JSON.stringify(state)} and setter ${JSON.stringify(setter)} must be exposed together`)
2904
+ }
2905
+ for (const [name, callback] of callbacks) {
2906
+ if (!fields.has(name)) continue
2907
+ if (callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} must be synchronous`)
2908
+ const capture = nativeCaptureNames(callback, states).values().next().value
2909
+ if (capture) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} cannot capture private binding ${JSON.stringify(capture)}`)
2910
+ for (const state of referencedStateNames(callback.body, states, callback)) {
2911
+ const setter = [...states].find(([, candidate]) => candidate === state)?.[0]
2912
+ if (!setter || !fields.has(state) || !fields.has(setter)) throw sourceNodeError(callback, providerSource, `Context action ${JSON.stringify(name)} requires exposed state and setter fields for ${JSON.stringify(state)}`)
2913
+ }
2914
+ }
2915
+ return { callbacks: new Map([...callbacks].filter(([name]) => fields.has(name))), context: true, fields, privateStates: new Set(), states }
2916
+ }
2917
+
2846
2918
  const resolveCustomHook = (binding, call) => {
2847
2919
  const exportName = binding.kind === "default" ? "default" : binding.imported
2848
2920
  const key = `${binding.target}:${exportName}`
@@ -2852,7 +2924,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2852
2924
  if (hook.parameters.length || hook.asteriskToken || hook.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword) || !ts.isBlock(hook.body)) throw sourceNodeError(hook, hookSource, "Relative custom hooks must be synchronous zero-argument functions with a block body")
2853
2925
  const returns = hook.body.statements.filter(ts.isReturnStatement)
2854
2926
  const returned = returns.length === 1 && returns[0] === hook.body.statements.at(-1) && returns[0].expression ? unwrapExpression(returns[0].expression) : undefined
2855
- if (!returned || !ts.isObjectLiteralExpression(returned)) throw sourceNodeError(hook.body, hookSource, "Relative custom hooks must end with one direct object return")
2927
+ if (returned && ts.isCallExpression(returned) && ts.isIdentifier(returned.expression) && returned.expression.text === "useContext") {
2928
+ const analysis = resolveContextHook(returned, hookSource)
2929
+ customHooks.set(key, analysis)
2930
+ return analysis
2931
+ }
2932
+ if (!returned || !ts.isObjectLiteralExpression(returned)) throw sourceNodeError(hook.body, hookSource, "Relative custom hooks must end with one direct object return or direct useContext(ContextIdentifier)")
2856
2933
 
2857
2934
  const states = new Map()
2858
2935
  const callbacks = new Map()
@@ -2897,7 +2974,30 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2897
2974
  const owner = nearestFunction(node)
2898
2975
  if (!owner) throw sourceNodeError(node, sourceFile, "Relative custom hooks cannot be used outside a Kudzu component")
2899
2976
  const setters = settersByFunction.get(owner) ?? new Map()
2977
+ const requiredContextStates = new Set()
2978
+ if (hook.context) {
2979
+ for (const name of names) {
2980
+ const callback = hook.callbacks.get(name)
2981
+ if (callback) for (const state of referencedStateNames(callback.body, hook.states, callback)) requiredContextStates.add(state)
2982
+ }
2983
+ }
2900
2984
  for (const [setter, state] of hook.states) {
2985
+ if (hook.context) {
2986
+ if (names.has(setter) && !names.has(state)) throw sourceNodeError(node.name, sourceFile, `Relative Context setter ${JSON.stringify(setter)} requires state ${JSON.stringify(state)} to be destructured`)
2987
+ if (!names.has(state) && !requiredContextStates.has(state)) continue
2988
+ setters.set(names.has(setter) || requiredContextStates.has(state) ? setter : `__kContextState_${state}`, state)
2989
+ if (requiredContextStates.has(state)) {
2990
+ const fields = customHookPrivateFields.get(node) ?? []
2991
+ for (const field of [state, setter]) {
2992
+ if (names.has(field) || fields.includes(field)) continue
2993
+ const conflict = owner.parameters.some(parameter => bindingNames(parameter.name).includes(field)) || owner.body.statements.some(statement => statement !== node.parent.parent && statementDeclaresName(statement, field))
2994
+ if (conflict) throw sourceNodeError(node.name, sourceFile, `Context action state field ${JSON.stringify(field)} conflicts with a consumer binding`)
2995
+ fields.push(field)
2996
+ }
2997
+ customHookPrivateFields.set(node, fields)
2998
+ }
2999
+ continue
3000
+ }
2901
3001
  if (hook.privateStates.has(state)) {
2902
3002
  setters.set(setter, state)
2903
3003
  const fields = customHookPrivateFields.get(node) ?? []
@@ -2914,6 +3014,11 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2914
3014
  const callbacks = customHookFunctionsByOwner.get(owner) ?? new Map()
2915
3015
  callbacks.set(name, hook.callbacks.get(name))
2916
3016
  customHookFunctionsByOwner.set(owner, callbacks)
3017
+ if (hook.context) {
3018
+ const reducers = reducersByFunction.get(owner) ?? new Map()
3019
+ reducers.set(name, { contextAction: hook.callbacks.get(name), states: hook.states })
3020
+ reducersByFunction.set(owner, reducers)
3021
+ }
2917
3022
  }
2918
3023
  else if (![...hook.states].some(([setter, state]) => name === setter || name === state)) throw sourceNodeError(node.name, sourceFile, `Relative custom hook result ${JSON.stringify(name)} must be a direct useState value, setter, or callback`)
2919
3024
  }
@@ -4125,7 +4230,7 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4125
4230
  const value = unwrapExpression(expression)
4126
4231
  const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
4127
4232
  if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
4128
- const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()), importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4233
+ let collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()), importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4129
4234
  if (!collection?.state && !collection?.calculation) return undefined
4130
4235
  if (directFrom) collection.selector.push(["from", undefined])
4131
4236
  let callback = directFrom ? value.arguments[1] : value.arguments[0]
@@ -4144,6 +4249,8 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4144
4249
  ts.setParentRecursive(callback, false)
4145
4250
  callback.parent = value
4146
4251
  }
4252
+ const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(setters.values()), factory, value)
4253
+ if (conditional) ({ callback, root, collection } = conditional)
4147
4254
  if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Keyed list map callback must return one JSX element")
4148
4255
  const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
4149
4256
  const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
@@ -4157,11 +4264,13 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4157
4264
  function nestedKeyedListParts(expression, parentItem, fail) {
4158
4265
  const value = unwrapExpression(expression)
4159
4266
  if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map") return undefined
4160
- const collection = renderedCollectionSource(value.expression.expression, new Map(), undefined, fail, new Set())
4267
+ let collection = renderedCollectionSource(value.expression.expression, new Map(), undefined, fail, new Set())
4161
4268
  if (!collection?.ownerField || collection.parentItem !== parentItem) return undefined
4162
- const callback = value.arguments[0]
4269
+ let callback = value.arguments[0]
4163
4270
  const parameters = collectionParameters(callback, "Nested keyed list map", fail)
4164
- const root = unwrapExpression(callback.body)
4271
+ let root = unwrapExpression(callback.body)
4272
+ const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(), ts.factory, value)
4273
+ if (conditional) ({ callback, root, collection } = conditional)
4165
4274
  if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Nested keyed list map callback must return one JSX element")
4166
4275
  const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
4167
4276
  const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
@@ -4172,6 +4281,28 @@ function nestedKeyedListParts(expression, parentItem, fail) {
4172
4281
  return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
4173
4282
  }
4174
4283
 
4284
+ function conditionalKeyedMapRoot(callback, root, parameters, collection, fail, stateNames, factory, parent) {
4285
+ let condition
4286
+ let rendered
4287
+ if (ts.isBinaryExpression(root) && root.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && (ts.isJsxElement(unwrapExpression(root.right)) || ts.isJsxSelfClosingElement(unwrapExpression(root.right)))) {
4288
+ condition = root.left
4289
+ rendered = unwrapExpression(root.right)
4290
+ } else if (ts.isConditionalExpression(root) && (ts.isJsxElement(unwrapExpression(root.whenTrue)) || ts.isJsxSelfClosingElement(unwrapExpression(root.whenTrue)))) {
4291
+ if (unwrapExpression(root.whenFalse).kind !== ts.SyntaxKind.NullKeyword) fail(root.whenFalse, "Conditional keyed map callbacks require condition ? <Element> : null")
4292
+ condition = root.condition
4293
+ rendered = unwrapExpression(root.whenTrue)
4294
+ } else {
4295
+ return undefined
4296
+ }
4297
+ if (parameters.index) fail(callback.parameters[1], "Conditional keyed map callbacks cannot use a map index because filtering changes index semantics; use an explicit filter(...).map((item, index) => ...) when a filtered index is intended")
4298
+ const selectorStates = new Set(collection.selectorStates)
4299
+ const selector = collectionExpression(condition, parameters, fail, stateNames, selectorStates)
4300
+ const normalized = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, rendered)
4301
+ ts.setParentRecursive(normalized, false)
4302
+ normalized.parent = parent
4303
+ return { callback: normalized, root: rendered, collection: { ...collection, selector: [...collection.selector, ["filter", selector]], selectorStates } }
4304
+ }
4305
+
4175
4306
  function renderedCollectionSource(expression, setters, declarations, fail, aliases, importedCollections = new Set(), stateNames = new Set(), importedCollectionTransforms = new Map(), factory = ts.factory, context, calculatedCollection, staticCollection) {
4176
4307
  const value = unwrapExpression(expression)
4177
4308
  if (ts.isIdentifier(value)) {
@@ -5225,9 +5356,13 @@ function compileNativeCallback(expression, setters, reducers, factory, entries,
5225
5356
  const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
5226
5357
  const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
5227
5358
  imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
5228
- const captures = new Set([...allCaptures].filter(name => !importBindings.has(name)))
5359
+ const captures = new Set([...allCaptures].filter(name => !importBindings.has(name) && !usedReducers.has(name)))
5229
5360
  for (const entry of imports) if (!entry.package) clientImports.add(entry.target)
5230
5361
  const usedStates = nativeStateNames(expression, setters)
5362
+ for (const name of usedReducers) {
5363
+ const reducer = reducers.get(name)
5364
+ if (reducer.contextAction) for (const state of referencedStateNames(reducer.contextAction.body, reducer.states, reducer.contextAction)) usedStates.add(state)
5365
+ }
5231
5366
  const exportName = `${prefix}${entries.length}`
5232
5367
  entries.push({ exportName, expression, captures, imports, liveStates, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested })
5233
5368
  const value = name => deferValues
@@ -5546,6 +5681,14 @@ function clientImportBindings(sourceFile, file, sourceFiles) {
5546
5681
  return bindings
5547
5682
  }
5548
5683
 
5684
+ function hasFrameworkImport(sourceFile, name) {
5685
+ return sourceFile.statements.some(node => {
5686
+ if (!ts.isImportDeclaration(node) || node.importClause?.isTypeOnly || !ts.isStringLiteral(node.moduleSpecifier) || !["react", "@kudzujs/core"].includes(node.moduleSpecifier.text)) return false
5687
+ const bindings = node.importClause?.namedBindings
5688
+ return bindings && ts.isNamedImports(bindings) && bindings.elements.some(entry => !entry.isTypeOnly && entry.name.text === name && (entry.propertyName ?? entry.name).text === name)
5689
+ })
5690
+ }
5691
+
5549
5692
  function packageImportBindings(sourceFile) {
5550
5693
  const bindings = new Map()
5551
5694
  const rejectDynamic = node => {
@@ -6110,15 +6253,23 @@ function printNativeHandler({ exportName, expression, captures, setters, reducer
6110
6253
  const visitor = node => {
6111
6254
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
6112
6255
  const reducer = reducers.get(node.expression.text)
6256
+ if (reducer.contextAction) {
6257
+ const action = synthesizeTree(cloneAst(reducer.contextAction, factory, context))
6258
+ const call = factory.createCallExpression(action, undefined, node.arguments)
6259
+ ts.setParentRecursive(call, false)
6260
+ return ts.visitNode(call, visitor)
6261
+ }
6113
6262
  if (reducer.store) return zustandActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)))
6114
6263
  if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
6115
6264
  return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
6116
6265
  }
6117
6266
  if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
6267
+ if (reducers.get(node.name.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
6118
6268
  if (reducers.get(node.name.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
6119
6269
  return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
6120
6270
  }
6121
6271
  if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
6272
+ if (reducers.get(node.text).contextAction) throw sourceNodeError(node, expression.getSourceFile(), "Context actions must be called directly inside an event handler")
6122
6273
  if (reducers.get(node.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
6123
6274
  return reducerReference(factory, reducers.get(node.text))
6124
6275
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.7",
3
+ "version": "0.8.9",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",