@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 +1 -1
- package/RELEASES.md +50 -0
- package/framework/build.mjs +157 -6
- package/package.json +1 -1
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.
|
|
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.
|
package/framework/build.mjs
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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
|
-
|
|
4267
|
+
let collection = renderedCollectionSource(value.expression.expression, new Map(), undefined, fail, new Set())
|
|
4161
4268
|
if (!collection?.ownerField || collection.parentItem !== parentItem) return undefined
|
|
4162
|
-
|
|
4269
|
+
let callback = value.arguments[0]
|
|
4163
4270
|
const parameters = collectionParameters(callback, "Nested keyed list map", fail)
|
|
4164
|
-
|
|
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
|
}
|