@kudzujs/core 0.8.8 → 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 +24 -0
- package/framework/build.mjs +127 -2
- 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,29 @@
|
|
|
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
|
+
|
|
3
27
|
## 0.8.8 - Conditional keyed map roots
|
|
4
28
|
|
|
5
29
|
Kudzu 0.8.8 compiles ordinary expression-bodied conditional keyed maps through the existing pure collection selector path.
|
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
|
}
|
|
@@ -5251,9 +5356,13 @@ function compileNativeCallback(expression, setters, reducers, factory, entries,
|
|
|
5251
5356
|
const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
|
|
5252
5357
|
const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
|
|
5253
5358
|
imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
|
|
5254
|
-
const captures = new Set([...allCaptures].filter(name => !importBindings.has(name)))
|
|
5359
|
+
const captures = new Set([...allCaptures].filter(name => !importBindings.has(name) && !usedReducers.has(name)))
|
|
5255
5360
|
for (const entry of imports) if (!entry.package) clientImports.add(entry.target)
|
|
5256
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
|
+
}
|
|
5257
5366
|
const exportName = `${prefix}${entries.length}`
|
|
5258
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 })
|
|
5259
5368
|
const value = name => deferValues
|
|
@@ -5572,6 +5681,14 @@ function clientImportBindings(sourceFile, file, sourceFiles) {
|
|
|
5572
5681
|
return bindings
|
|
5573
5682
|
}
|
|
5574
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
|
+
|
|
5575
5692
|
function packageImportBindings(sourceFile) {
|
|
5576
5693
|
const bindings = new Map()
|
|
5577
5694
|
const rejectDynamic = node => {
|
|
@@ -6136,15 +6253,23 @@ function printNativeHandler({ exportName, expression, captures, setters, reducer
|
|
|
6136
6253
|
const visitor = node => {
|
|
6137
6254
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
|
|
6138
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
|
+
}
|
|
6139
6262
|
if (reducer.store) return zustandActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)))
|
|
6140
6263
|
if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
|
|
6141
6264
|
return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
|
|
6142
6265
|
}
|
|
6143
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")
|
|
6144
6268
|
if (reducers.get(node.name.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
|
|
6145
6269
|
return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
|
|
6146
6270
|
}
|
|
6147
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")
|
|
6148
6273
|
if (reducers.get(node.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
|
|
6149
6274
|
return reducerReference(factory, reducers.get(node.text))
|
|
6150
6275
|
}
|