@kudzujs/core 0.7.5 → 0.7.6

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 is designed so ordinary common React-shaped TSX can migrate with minimal s
10
10
 
11
11
  > Experimental `0.7.x`: the compiler API and supported TSX surface may change.
12
12
 
13
- **0.7.5:** Class composition migration. Direct `clsx` calls compile to ordinary reactive class expressions without shipping the package, and mixed React type imports erase cleanly. See [release notes](./RELEASES.md#075---class-composition-migration).
13
+ **0.7.6:** Zustand-shaped shared stores. Reduced `create(set => ...)` stores compile to persistent application-layout state and direct handler updates without shipping React or Zustand. See [release notes](./RELEASES.md#076---zustand-shaped-shared-stores).
14
14
 
15
15
  Documentation: [kudzujs.cloud/docs](https://kudzujs.cloud/docs)
16
16
 
@@ -78,6 +78,8 @@ Kudzu rewrites supported React imports to its compile-time APIs before evaluatin
78
78
 
79
79
  Direct default or named `clsx` imports compile away for string/number literals, literal arrays, literal object conditions, and conditional expressions. Kudzu lowers those calls to ordinary class expressions, so reactive classes reuse existing bindings without shipping `clsx`; spreads, computed object keys, arbitrary calls, and indirect references remain unsupported.
80
80
 
81
+ Migration source may also retain a reduced Zustand store declared as one exported `const` initialized by a named `create` import. The initializer accepts `set`, returns exactly one directly serializable data property plus synchronous actions, and components select one direct property with `state => state.property`. A shared layout must select the store before its routes use it; Kudzu then owns the data as layout state, inlines action updates into existing handler ESM, and ships neither React nor Zustand. Derived selectors, multiple data properties, middleware, `get`, subscriptions, equality functions, persist/devtools wrappers, async actions, helper captures, replacement updates, and indirect action forwarding remain unsupported.
82
+
81
83
  Create `src/pages/index.tsx`:
82
84
 
83
85
  ```tsx
package/RELEASES.md CHANGED
@@ -1,5 +1,28 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.7.6 - Zustand-shaped shared stores
4
+
5
+ Kudzu 0.7.6 lets reduced React migration source retain a Zustand `create(set => ...)` store across an explicitly configured shared-layout navigation group.
6
+
7
+ ### New in 0.7.6
8
+
9
+ - One exported store with one directly serializable data property and synchronous capture-free actions compiles to one ordinary layout-lifetime Kudzu state slot.
10
+ - Components select the data or an action with direct forms such as `state => state.quantities` and `state => state.add`.
11
+ - Selected actions inline through existing functional state updates, so repeated same-turn calls observe current logical state and DOM writes still batch.
12
+ - Same-group navigation retains the store and layout DOM while incoming route bindings mount against the current value.
13
+ - Neither React, Zustand, a subscription runtime, nor a generic external-store capability enters the deploy output.
14
+ - The shopping fixture verifies two same-turn additions, product-to-cart retention, removal, layout DOM identity, package erasure, and source diagnostics in Chrome.
15
+
16
+ ### Boundary
17
+
18
+ The shared layout must initialize the store before route consumers. Derived selectors, multiple data properties, middleware, `get`, subscriptions, equality functions, persist/devtools wrappers, async actions, helper captures, replacement updates, keyed-row initialization, and indirect action forwarding remain unsupported.
19
+
20
+ ### Upgrade
21
+
22
+ ```bash
23
+ npm install @kudzujs/core@^0.7.6
24
+ ```
25
+
3
26
  ## 0.7.5 - Class composition migration
4
27
 
5
28
  Kudzu 0.7.5 lets ordinary React source retain common direct `clsx` calls while compiling them to existing static and reactive class paths.
@@ -6,6 +6,8 @@ Migration source may retain conventional `react` imports for supported named or
6
6
 
7
7
  Direct `clsx` calls over literal strings, numbers, arrays, object conditions, and conditional expressions are similarly lowered to ordinary concatenation and conditional expressions. The package import is erased, and dynamic classes continue through the existing binding compiler without serializing or shipping the `clsx` function.
8
8
 
9
+ Reduced Zustand migration stores lower to one ordinary layout-lifetime state slot. The compiler accepts one exported `create(set => ({ data, ...actions }))` store with one serializable data property, direct property selectors, and synchronous capture-free actions using one-argument merge-form `set`; selected actions reuse the reducer-style functional update compiler, so same-turn calls observe current logical state and DOM writes still batch. The shared layout must initialize the store before route consumers, outside keyed rows. No Zustand import, store subscription runtime, React hook, or generic external-store capability is emitted.
10
+
9
11
  - `build.mjs`: TSX compilation, static, `getStaticPaths`, and runtime-fallback routes, base paths, CSS collection, post-build hooks, behavior extraction, static HTML output, and the development server.
10
12
  - `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
11
13
  - `jsx-runtime.mjs`: automatic JSX runtime used by TypeScript.
@@ -1791,6 +1791,94 @@ function normalizeClsxSyntax(sourceFile, factory, context) {
1791
1791
  return ts.visitNode(sourceFile, visitor)
1792
1792
  }
1793
1793
 
1794
+ function analyzeZustandStores(sourceFile) {
1795
+ const createNames = new Set()
1796
+ for (const statement of sourceFile.statements) {
1797
+ if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "zustand") continue
1798
+ const bindings = statement.importClause?.namedBindings
1799
+ if (statement.importClause?.name || !bindings || !ts.isNamedImports(bindings)) throw sourceNodeError(statement, sourceFile, "Zustand migration input requires a named create import")
1800
+ for (const entry of bindings.elements) {
1801
+ if (entry.isTypeOnly) continue
1802
+ if ((entry.propertyName ?? entry.name).text !== "create") throw sourceNodeError(entry, sourceFile, "Only Zustand create is supported")
1803
+ createNames.add(entry.name.text)
1804
+ }
1805
+ }
1806
+ const stores = new Map()
1807
+ if (!createNames.size) return stores
1808
+ for (const statement of sourceFile.statements) {
1809
+ if (!ts.isVariableStatement(statement) || !statement.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.ExportKeyword)) continue
1810
+ for (const declaration of statement.declarationList.declarations) {
1811
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer || !ts.isCallExpression(declaration.initializer) || !ts.isIdentifier(declaration.initializer.expression) || !createNames.has(declaration.initializer.expression.text)) continue
1812
+ const callback = declaration.initializer.arguments[0]
1813
+ if (declaration.initializer.arguments.length !== 1 || !callback || (!ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback)) || callback.parameters.length !== 1 || !ts.isIdentifier(callback.parameters[0].name) || callback.asteriskToken || callback.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(declaration.initializer, sourceFile, "Zustand create() requires one synchronous initializer with one set parameter")
1814
+ const body = unwrapExpression(callback.body)
1815
+ if (!ts.isObjectLiteralExpression(body)) throw sourceNodeError(callback.body, sourceFile, "Zustand create() initializer must return one object literal")
1816
+ const data = []
1817
+ const actions = new Map()
1818
+ for (const property of body.properties) {
1819
+ if (!ts.isPropertyAssignment(property) || !property.name || !(ts.isIdentifier(property.name) || ts.isStringLiteral(property.name))) throw sourceNodeError(property, sourceFile, "Zustand store entries must be ordinary properties")
1820
+ const name = property.name.text
1821
+ const value = unwrapExpression(property.initializer)
1822
+ if (ts.isArrowFunction(value) || ts.isFunctionExpression(value)) actions.set(name, value)
1823
+ else data.push({ name, value })
1824
+ }
1825
+ if (data.length !== 1 || !isSerializableStateLiteral(data[0].value)) throw sourceNodeError(body, sourceFile, "Zustand migration stores require exactly one directly serializable data property")
1826
+ if (!actions.size) throw sourceNodeError(body, sourceFile, "Zustand migration stores require at least one action")
1827
+ for (const [name, action] of actions) {
1828
+ if (action.asteriskToken || action.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(action, sourceFile, `Zustand action ${JSON.stringify(name)} must be synchronous`)
1829
+ const capture = [...nativeCaptureNames(action, new Map())].find(entry => entry !== callback.parameters[0].name.text)
1830
+ if (capture) throw sourceNodeError(action, sourceFile, `Zustand action ${JSON.stringify(name)} cannot capture ${JSON.stringify(capture)}`)
1831
+ const validateAction = node => {
1832
+ if (ts.isAwaitExpression(node) || ts.isYieldExpression(node) || ts.isNewExpression(node)) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} must be synchronous`)
1833
+ if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ["then", "catch", "finally"].includes(node.expression.name.text)) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} cannot schedule asynchronous updates`)
1834
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === callback.parameters[0].name.text && !isShadowedIdentifier(node.expression, action)) {
1835
+ if (nearestFunction(node) !== action) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} must call set directly`)
1836
+ if (node.arguments.length !== 1) throw sourceNodeError(node, sourceFile, `Zustand action ${JSON.stringify(name)} set() requires exactly one partial update`)
1837
+ }
1838
+ ts.forEachChild(node, validateAction)
1839
+ }
1840
+ validateAction(action.body)
1841
+ }
1842
+ stores.set(declaration.name.text, { name: declaration.name.text, setName: callback.parameters[0].name.text, field: data[0].name, initialValue: data[0].value, actions, declaration })
1843
+ }
1844
+ }
1845
+ const visit = node => {
1846
+ const recognized = ts.isIdentifier(node) && ts.isCallExpression(node.parent) && node.parent.expression === node && [...stores.values()].some(store => store.declaration.initializer === node.parent)
1847
+ if (ts.isIdentifier(node) && createNames.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !recognized) throw sourceNodeError(node, sourceFile, "Zustand create must directly initialize an exported const store")
1848
+ ts.forEachChild(node, visit)
1849
+ }
1850
+ visit(sourceFile)
1851
+ return stores
1852
+ }
1853
+
1854
+ function normalizeZustandMigrationSyntax(sourceFile, factory, context) {
1855
+ const stores = analyzeZustandStores(sourceFile)
1856
+ if (!stores.size) {
1857
+ const declaration = sourceFile.statements.find(statement => ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) && statement.moduleSpecifier.text === "zustand" && !statement.importClause?.isTypeOnly)
1858
+ if (declaration) throw sourceNodeError(declaration, sourceFile, "Zustand create must directly initialize an exported const store")
1859
+ return sourceFile
1860
+ }
1861
+ const identity = name => `${relative(sourceDirectory, sourceFile.fileName).replaceAll(sep, "/")}#${name}`
1862
+ const visitor = node => {
1863
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && stores.has(node.name.text)) {
1864
+ const store = stores.get(node.name.text)
1865
+ return factory.updateVariableDeclaration(node, node.name, node.exclamationToken, node.type, factory.createCallExpression(factory.createIdentifier("__kCreateStore"), undefined, [
1866
+ factory.createStringLiteral(identity(store.name)),
1867
+ factory.createStringLiteral(store.field),
1868
+ store.initialValue,
1869
+ factory.createArrayLiteralExpression([...store.actions.keys()].map(name => factory.createStringLiteral(name)))
1870
+ ]))
1871
+ }
1872
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "zustand") return undefined
1873
+ return ts.visitEachChild(node, visitor, context)
1874
+ }
1875
+ const normalized = ts.visitNode(sourceFile, visitor)
1876
+ const declaration = factory.createImportDeclaration(undefined, factory.createImportClause(false, undefined, factory.createNamedImports([factory.createImportSpecifier(false, undefined, factory.createIdentifier("__kCreateStore"))])), factory.createStringLiteral("@kudzujs/core"))
1877
+ const statements = [...normalized.statements]
1878
+ statements.splice(statements.findLastIndex(statement => ts.isImportDeclaration(statement)) + 1, 0, declaration)
1879
+ return factory.updateSourceFile(normalized, statements)
1880
+ }
1881
+
1794
1882
  function normalizeReactMigrationSyntax(sourceFile, factory, context) {
1795
1883
  const supported = new Set(["createContext", "useContext", "useEffect", "useReducer", "useRef", "useState"])
1796
1884
  const erased = new Set(["memo", "useCallback", "useMemo"])
@@ -2066,6 +2154,8 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2066
2154
  ts.setParentRecursive(sourceFile, false)
2067
2155
  sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context)
2068
2156
  ts.setParentRecursive(sourceFile, false)
2157
+ sourceFile = normalizeZustandMigrationSyntax(sourceFile, factory, context)
2158
+ ts.setParentRecursive(sourceFile, false)
2069
2159
  sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
2070
2160
  ts.setParentRecursive(sourceFile, false)
2071
2161
  rejectOrdinaryWorkerImports(sourceFile, file, sourceFiles)
@@ -2079,6 +2169,8 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2079
2169
  ts.setParentRecursive(imported, false)
2080
2170
  imported = normalizeReactMigrationSyntax(imported, factory, context)
2081
2171
  ts.setParentRecursive(imported, false)
2172
+ imported = normalizeZustandMigrationSyntax(imported, factory, context)
2173
+ ts.setParentRecursive(imported, false)
2082
2174
  imported = normalizeRenderControlFlow(imported, factory, context)
2083
2175
  ts.setParentRecursive(imported, false)
2084
2176
  importedSources.set(target, imported)
@@ -2087,6 +2179,16 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2087
2179
  }
2088
2180
  const settersByFunction = new Map()
2089
2181
  const reducersByFunction = new Map()
2182
+ const zustandStores = new Map()
2183
+ const resolvedZustandStore = entry => {
2184
+ const exportName = entry.kind === "default" ? "default" : entry.imported
2185
+ const key = `${entry.target}:${exportName}`
2186
+ if (zustandStores.has(key)) return zustandStores.get(key)
2187
+ const targetSource = parseSourceFile(entry.target, sourceIndex.get(entry.target))
2188
+ const store = analyzeZustandStores(targetSource).get(exportName)
2189
+ zustandStores.set(key, store)
2190
+ return store
2191
+ }
2090
2192
  const functions = new Map()
2091
2193
  const components = new Map()
2092
2194
  const contexts = new Set()
@@ -2111,6 +2213,26 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2111
2213
  const collect = node => {
2112
2214
  if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
2113
2215
  const callName = ts.isIdentifier(node.initializer.expression) ? node.initializer.expression.text : ""
2216
+ if (ts.isIdentifier(node.name) && callName && importBindings.has(callName) && importBindings.get(callName).kind !== "namespace") {
2217
+ const storeImport = importBindings.get(callName)
2218
+ const store = resolvedZustandStore(storeImport)
2219
+ if (store) {
2220
+ const selector = node.initializer.arguments[0]
2221
+ if (node.initializer.arguments.length !== 1 || !selector || !ts.isArrowFunction(selector) || selector.parameters.length !== 1 || !ts.isIdentifier(selector.parameters[0].name) || !ts.isPropertyAccessExpression(unwrapExpression(selector.body)) || !ts.isIdentifier(unwrapExpression(selector.body).expression) || unwrapExpression(selector.body).expression.text !== selector.parameters[0].name.text) throw sourceNodeError(node.initializer, sourceFile, "Zustand selectors must be direct arrows such as state => state.quantities")
2222
+ const selected = unwrapExpression(selector.body).name.text
2223
+ const owner = nearestFunction(node)
2224
+ if (!owner) throw sourceNodeError(node, sourceFile, "Zustand stores cannot be used outside a Kudzu component")
2225
+ const setters = settersByFunction.get(owner) ?? new Map()
2226
+ if (selected === store.field) setters.set(`__kStoreState_${node.name.text}`, node.name.text)
2227
+ else if (store.actions.has(selected)) {
2228
+ setters.set(node.name.text, node.name.text)
2229
+ const reducers = reducersByFunction.get(owner) ?? new Map()
2230
+ reducers.set(node.name.text, { state: node.name.text, store, action: selected })
2231
+ reducersByFunction.set(owner, reducers)
2232
+ } else throw sourceNodeError(unwrapExpression(selector.body).name, sourceFile, `Zustand store ${JSON.stringify(store.name)} has no supported property ${JSON.stringify(selected)}`)
2233
+ settersByFunction.set(owner, setters)
2234
+ }
2235
+ }
2114
2236
  if (callName === "useReducer") {
2115
2237
  if (!ts.isArrayBindingPattern(node.name)) throw sourceNodeError(node.name, sourceFile, "useReducer() must use [state, dispatch] identifier destructuring")
2116
2238
  const [stateElement, dispatchElement] = node.name.elements
@@ -3717,7 +3839,7 @@ function compileNativeCallback(expression, setters, reducers, factory, entries,
3717
3839
  const allCaptures = nativeCaptureNames(expression, setters)
3718
3840
  const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
3719
3841
  const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
3720
- imports.push(...[...usedReducers].map(name => reducers.get(name).import))
3842
+ imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
3721
3843
  const captures = new Set([...allCaptures].filter(name => !importBindings.has(name)))
3722
3844
  for (const entry of imports) clientImports.add(entry.target)
3723
3845
  const usedStates = nativeStateNames(expression, setters)
@@ -4544,13 +4666,17 @@ function printNativeHandler({ exportName, expression, captures, setters, reducer
4544
4666
  const transformer = context => root => {
4545
4667
  const visitor = node => {
4546
4668
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
4669
+ const reducer = reducers.get(node.expression.text)
4670
+ if (reducer.store) return zustandActionDispatch(factory, reducer, node.arguments.map(argument => ts.visitNode(argument, visitor)))
4547
4671
  if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
4548
- return reducerDispatch(factory, reducers.get(node.expression.text), ts.visitNode(node.arguments[0], visitor))
4672
+ return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
4549
4673
  }
4550
4674
  if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
4675
+ if (reducers.get(node.name.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
4551
4676
  return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
4552
4677
  }
4553
4678
  if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
4679
+ if (reducers.get(node.text).store) throw sourceNodeError(node, expression.getSourceFile(), "Zustand actions must be called directly inside an event handler")
4554
4680
  return reducerReference(factory, reducers.get(node.text))
4555
4681
  }
4556
4682
  if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
@@ -4665,11 +4791,44 @@ function reducerReference(factory, reducer) {
4665
4791
  }
4666
4792
 
4667
4793
  function reducerDispatch(factory, reducer, action) {
4794
+ if (reducer.store) return zustandActionDispatch(factory, reducer, [action])
4668
4795
  const previous = factory.createUniqueName("__kPrevious")
4669
4796
  const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createCallExpression(factory.createIdentifier(reducer.reducer), undefined, [previous, action]))
4670
4797
  return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
4671
4798
  }
4672
4799
 
4800
+ function zustandActionDispatch(factory, reducer, args) {
4801
+ const previous = factory.createUniqueName("__kPrevious")
4802
+ const current = factory.createUniqueName("__kStore")
4803
+ const updateValue = factory.createUniqueName("__kUpdate")
4804
+ const partial = factory.createUniqueName("__kPartial")
4805
+ const action = factory.createUniqueName("__kAction")
4806
+ const set = factory.createIdentifier(reducer.store.setName)
4807
+ const merge = factory.createExpressionStatement(factory.createBinaryExpression(current, factory.createToken(ts.SyntaxKind.EqualsToken), factory.createObjectLiteralExpression([
4808
+ factory.createSpreadAssignment(current),
4809
+ factory.createSpreadAssignment(partial)
4810
+ ])))
4811
+ const setBody = factory.createBlock([
4812
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(partial, undefined, undefined, factory.createConditionalExpression(
4813
+ factory.createBinaryExpression(factory.createTypeOfExpression(updateValue), factory.createToken(ts.SyntaxKind.EqualsEqualsEqualsToken), factory.createStringLiteral("function")),
4814
+ undefined,
4815
+ factory.createCallExpression(updateValue, undefined, [current]),
4816
+ undefined,
4817
+ updateValue
4818
+ ))], ts.NodeFlags.Const)),
4819
+ merge
4820
+ ], true)
4821
+ const body = factory.createBlock([
4822
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(current, undefined, undefined, factory.createObjectLiteralExpression([factory.createPropertyAssignment(reducer.store.field, previous)]))], ts.NodeFlags.Let)),
4823
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(set, undefined, undefined, factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, updateValue)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), setBody))], ts.NodeFlags.Const)),
4824
+ factory.createVariableStatement(undefined, factory.createVariableDeclarationList([factory.createVariableDeclaration(action, undefined, undefined, synthesizeTree(reducer.store.actions.get(reducer.action)))], ts.NodeFlags.Const)),
4825
+ factory.createExpressionStatement(factory.createCallExpression(action, undefined, args)),
4826
+ factory.createReturnStatement(factory.createPropertyAccessExpression(current, reducer.store.field))
4827
+ ], true)
4828
+ const update = factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, previous)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), body)
4829
+ return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
4830
+ }
4831
+
4673
4832
  function printReactiveBinding({ exportName, expression, captures, states }) {
4674
4833
  const factory = ts.factory
4675
4834
  const transformer = context => root => {
@@ -73,6 +73,32 @@ export function useReducer(reducer, initialValue, name) {
73
73
  return [state, dispatch]
74
74
  }
75
75
 
76
+ export function __kCreateStore(name, field, initialValue, actions) {
77
+ return selector => {
78
+ if (!renderContext) throw new Error("Zustand stores can only be read while rendering a Kudzu component")
79
+ let store = renderContext.stores.get(name)
80
+ if (!store) {
81
+ if (renderContext.scoped && renderContext.renderScope !== "layout") throw new Error(`Zustand store ${JSON.stringify(name)} must be initialized by the shared layout before route components use it`)
82
+ if (renderContext.listRoot || renderContext.listRowRoot || renderContext.listTemplate) throw new Error(`Zustand store ${JSON.stringify(name)} cannot be initialized inside a keyed row`)
83
+ const [state] = useState(initialValue, `${name}.${field}`)
84
+ store = { [field]: state }
85
+ for (const action of actions) {
86
+ const marker = () => {
87
+ throw new Error("Zustand actions are compiled into browser handlers")
88
+ }
89
+ Object.defineProperties(marker, {
90
+ [signalMarker]: { value: true },
91
+ id: { value: state.id },
92
+ value: { get: () => state.value }
93
+ })
94
+ store[action] = marker
95
+ }
96
+ renderContext.stores.set(name, store)
97
+ }
98
+ return selector(store)
99
+ }
100
+ }
101
+
76
102
  export function useParams() {
77
103
  if (renderContext?.renderScope === "layout") throw new Error("useParams() is only supported in route scope")
78
104
  if (!renderContext?.runtimeParamNames?.length) throw new Error("useParams() requires export const runtimeParams = true on a bracket page")
@@ -368,7 +394,7 @@ function serializeCapture(name, value, seen) {
368
394
  }
369
395
 
370
396
  export async function renderPage(component, metadata = {}, props = {}, layout) {
371
- renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerModules: new Set(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
397
+ renderContext = { scoped: Boolean(layout), renderScope: layout ? "layout" : "route", counters: { layout: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 }, route: { s: 0, r: 0, c: 0, l: 0, e: 0, p: 0 } }, nextState: 0, nextRef: 0, nextCondition: 0, nextList: 0, nextEffect: 0, nextParam: 0, conditionDepth: 0, listDepth: 0, listRoot: undefined, listRowRoot: undefined, listTemplate: false, listInitialMarkers: false, listConditionalBranch: false, listFields: undefined, listEffectOwners: [], listRowStates: [], listRowRefs: [], listRowConditions: [], listRowLists: [], effectOwners: [], contexts: [], stores: new Map(), states: {}, textStates: new Set(), conditionStates: new Set(), events: [], effects: [], bindings: [], textBindings: [], conditions: [], lists: [], handlerModules: new Set(), runtimeParamNames: metadata.runtimeParams, paramEntries: [], params: undefined, hasBehaviors: false, hasNativeBehaviors: false, hasEffects: false, hasParams: false, hasBindings: false, hasLists: false, hasListStyles: false }
372
398
 
373
399
  try {
374
400
  const page = { [routeScopeMarker]: true, component, props }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.7.5",
3
+ "version": "0.7.6",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",