@kudzujs/core 0.7.4 → 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 +5 -1
- package/RELEASES.md +45 -0
- package/framework/README.md +4 -0
- package/framework/build.mjs +214 -3
- package/framework/core.mjs +27 -1
- package/package.json +1 -1
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.
|
|
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
|
|
|
@@ -76,6 +76,10 @@ export default function Header() {
|
|
|
76
76
|
|
|
77
77
|
Kudzu rewrites supported React imports to its compile-time APIs before evaluating the module; neither the React package nor a compatibility runtime enters the deploy output. Named or aliased `useState`, `useReducer`, `useEffect`, `useRef`, `createContext`, and `useContext` imports compile to their canonical forms. Default and namespace imports may call those APIs as direct members such as `React.useState`, and default, namespace, or named `Fragment` also works. `memo(Component)` is erased to a same-file component. Inline `useCallback(function, literalDependencies)` is erased to its function, while inline synchronous `useMemo` callbacks may return one expression over primitive literals/direct local state or an analyzable `filter`, `map`, `flatMap`, and `Array.from` collection pipeline. Scalar expressions inline into existing bindings; collection pipelines lower to existing keyed-list selectors and preserve row identity. Both hooks require inert literal dependency arrays and complete captured-state dependencies; memo locals cannot be duplicated or captured by nested functions. React classes and side-effect or dynamic React imports remain unsupported. A static route using these forms still emits zero JavaScript.
|
|
78
78
|
|
|
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
|
+
|
|
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
|
+
|
|
79
83
|
Create `src/pages/index.tsx`:
|
|
80
84
|
|
|
81
85
|
```tsx
|
package/RELEASES.md
CHANGED
|
@@ -1,5 +1,50 @@
|
|
|
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
|
+
|
|
26
|
+
## 0.7.5 - Class composition migration
|
|
27
|
+
|
|
28
|
+
Kudzu 0.7.5 lets ordinary React source retain common direct `clsx` calls while compiling them to existing static and reactive class paths.
|
|
29
|
+
|
|
30
|
+
### New in 0.7.5
|
|
31
|
+
|
|
32
|
+
- Default and named `clsx` imports lower at build time for string and number literals, literal arrays, literal object conditions, and conditional expressions.
|
|
33
|
+
- Dynamic object conditions reuse existing reactive class bindings without serializing or shipping the `clsx` function.
|
|
34
|
+
- Static uses add no browser JavaScript, and the package import is erased from compiled modules.
|
|
35
|
+
- Mixed React imports such as `import { useState, type ReactNode } from "react"` now erase type-only specifiers before runtime module rewriting.
|
|
36
|
+
- The React/Vite fixture verifies initial class output, state-driven class updates in Chrome, package erasure, and mixed type imports.
|
|
37
|
+
|
|
38
|
+
### Boundary
|
|
39
|
+
|
|
40
|
+
`clsx` spreads, computed object keys, arbitrary calls, and indirect references remain unsupported. This is source lowering for a proven migration pattern, not package execution or a general React ecosystem runtime.
|
|
41
|
+
|
|
42
|
+
### Upgrade
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @kudzujs/core@^0.7.5
|
|
46
|
+
```
|
|
47
|
+
|
|
3
48
|
## 0.7.4 - Memoized collection pipelines
|
|
4
49
|
|
|
5
50
|
Kudzu 0.7.4 lets ordinary React/Vite source retain analyzable collection work inside `useMemo` while reusing the existing keyed-list selector and DOM identity model.
|
package/framework/README.md
CHANGED
|
@@ -4,6 +4,10 @@ Kudzu specializes ordinary common React-shaped TSX so migrations need minimal so
|
|
|
4
4
|
|
|
5
5
|
Migration source may retain conventional `react` imports for supported named or aliased hooks, direct members such as `React.useState`, same-file `memo`, inline `useCallback`, direct-state expression or analyzable collection-pipeline `useMemo`, and default, namespace, or named `Fragment`. `build.mjs` canonicalizes those forms and rewrites module references to `@kudzujs/core` before build-time evaluation. Memo wrappers are erased or inlined into existing bindings and keyed-list selectors because no browser component rerender or memo cache exists. Static routes remain JavaScript-free and emitted modules are checked for surviving React imports.
|
|
6
6
|
|
|
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
|
+
|
|
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
|
+
|
|
7
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.
|
|
8
12
|
- `core.mjs`: server-side JSX rendering, state slots, context providers, behavior metadata, and serializable capture validation.
|
|
9
13
|
- `jsx-runtime.mjs`: automatic JSX runtime used by TypeScript.
|
package/framework/build.mjs
CHANGED
|
@@ -1744,6 +1744,141 @@ function hasReactModuleReference(source, file) {
|
|
|
1744
1744
|
return found
|
|
1745
1745
|
}
|
|
1746
1746
|
|
|
1747
|
+
function normalizeClsxSyntax(sourceFile, factory, context) {
|
|
1748
|
+
const names = new Set()
|
|
1749
|
+
for (const statement of sourceFile.statements) {
|
|
1750
|
+
if (!ts.isImportDeclaration(statement) || statement.importClause?.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text !== "clsx") continue
|
|
1751
|
+
if (statement.importClause?.name) names.add(statement.importClause.name.text)
|
|
1752
|
+
const bindings = statement.importClause?.namedBindings
|
|
1753
|
+
if (bindings && ts.isNamedImports(bindings)) for (const entry of bindings.elements) if (!entry.isTypeOnly && (entry.propertyName ?? entry.name).text === "clsx") names.add(entry.name.text)
|
|
1754
|
+
}
|
|
1755
|
+
if (!names.size) return sourceFile
|
|
1756
|
+
|
|
1757
|
+
const lower = node => {
|
|
1758
|
+
node = unwrapExpression(node)
|
|
1759
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return node
|
|
1760
|
+
if (node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword) return factory.createStringLiteral("")
|
|
1761
|
+
if (ts.isConditionalExpression(node)) return factory.updateConditionalExpression(node, node.condition, node.questionToken, lower(node.whenTrue), node.colonToken, lower(node.whenFalse))
|
|
1762
|
+
if (ts.isArrayLiteralExpression(node)) return combine(node.elements.map(lower))
|
|
1763
|
+
if (ts.isObjectLiteralExpression(node)) return combine(node.properties.map(property => {
|
|
1764
|
+
if (!ts.isPropertyAssignment(property) || property.name && ts.isComputedPropertyName(property.name)) throw sourceNodeError(property, sourceFile, "clsx() object arguments require ordinary key/value properties")
|
|
1765
|
+
const name = property.name
|
|
1766
|
+
const value = name && (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) ? name.text : undefined
|
|
1767
|
+
if (value === undefined) throw sourceNodeError(property, sourceFile, "clsx() object keys must be identifiers or literals")
|
|
1768
|
+
return factory.createConditionalExpression(property.initializer, undefined, factory.createStringLiteral(value), undefined, factory.createStringLiteral(""))
|
|
1769
|
+
}))
|
|
1770
|
+
throw sourceNodeError(node, sourceFile, "clsx() arguments must be string/number literals, literal arrays, literal objects, or conditionals")
|
|
1771
|
+
}
|
|
1772
|
+
const combine = entries => entries.length ? entries.reduce((result, entry) => factory.createBinaryExpression(factory.createBinaryExpression(result, factory.createToken(ts.SyntaxKind.PlusToken), factory.createStringLiteral(" ")), factory.createToken(ts.SyntaxKind.PlusToken), entry)) : factory.createStringLiteral("")
|
|
1773
|
+
|
|
1774
|
+
const visitor = node => {
|
|
1775
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && names.has(node.expression.text) && !isShadowedIdentifier(node.expression, sourceFile)) return combine(node.arguments.map(lower))
|
|
1776
|
+
if (ts.isIdentifier(node) && names.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, sourceFile) && !(ts.isCallExpression(node.parent) && node.parent.expression === node)) throw sourceNodeError(node, sourceFile, "clsx imports may only be called directly")
|
|
1777
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && node.moduleSpecifier.text === "clsx") {
|
|
1778
|
+
const clause = node.importClause
|
|
1779
|
+
if (!clause || clause.isTypeOnly) return node
|
|
1780
|
+
let bindings = clause.namedBindings
|
|
1781
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
1782
|
+
const elements = bindings.elements.filter(entry => entry.isTypeOnly || (entry.propertyName ?? entry.name).text !== "clsx")
|
|
1783
|
+
bindings = elements.length ? factory.updateNamedImports(bindings, elements) : undefined
|
|
1784
|
+
}
|
|
1785
|
+
const defaultName = clause.name && names.has(clause.name.text) ? undefined : clause.name
|
|
1786
|
+
if (!defaultName && !bindings) return undefined
|
|
1787
|
+
return factory.updateImportDeclaration(node, node.modifiers, factory.updateImportClause(clause, clause.isTypeOnly, defaultName, bindings), node.moduleSpecifier, node.attributes)
|
|
1788
|
+
}
|
|
1789
|
+
return ts.visitEachChild(node, visitor, context)
|
|
1790
|
+
}
|
|
1791
|
+
return ts.visitNode(sourceFile, visitor)
|
|
1792
|
+
}
|
|
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
|
+
|
|
1747
1882
|
function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
1748
1883
|
const supported = new Set(["createContext", "useContext", "useEffect", "useReducer", "useRef", "useState"])
|
|
1749
1884
|
const erased = new Set(["memo", "useCallback", "useMemo"])
|
|
@@ -1903,6 +2038,7 @@ function normalizeReactMigrationSyntax(sourceFile, factory, context) {
|
|
|
1903
2038
|
const entries = []
|
|
1904
2039
|
for (const entry of bindings.elements) {
|
|
1905
2040
|
const name = (entry.propertyName ?? entry.name).text
|
|
2041
|
+
if (entry.isTypeOnly) continue
|
|
1906
2042
|
if (!entry.isTypeOnly && erased.has(name)) continue
|
|
1907
2043
|
if (!entry.isTypeOnly && supported.has(name)) {
|
|
1908
2044
|
if (imported.has(name)) continue
|
|
@@ -2014,8 +2150,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2014
2150
|
return context => sourceFile => {
|
|
2015
2151
|
const factory = context.factory
|
|
2016
2152
|
const hasLinkElements = /<link/i.test(sourceFile.text)
|
|
2153
|
+
sourceFile = normalizeClsxSyntax(sourceFile, factory, context)
|
|
2154
|
+
ts.setParentRecursive(sourceFile, false)
|
|
2017
2155
|
sourceFile = normalizeReactMigrationSyntax(sourceFile, factory, context)
|
|
2018
2156
|
ts.setParentRecursive(sourceFile, false)
|
|
2157
|
+
sourceFile = normalizeZustandMigrationSyntax(sourceFile, factory, context)
|
|
2158
|
+
ts.setParentRecursive(sourceFile, false)
|
|
2019
2159
|
sourceFile = normalizeRenderControlFlow(sourceFile, factory, context)
|
|
2020
2160
|
ts.setParentRecursive(sourceFile, false)
|
|
2021
2161
|
rejectOrdinaryWorkerImports(sourceFile, file, sourceFiles)
|
|
@@ -2025,7 +2165,11 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2025
2165
|
const importedSource = target => {
|
|
2026
2166
|
let imported = importedSources.get(target)
|
|
2027
2167
|
if (!imported) {
|
|
2028
|
-
imported =
|
|
2168
|
+
imported = normalizeClsxSyntax(parseSourceFile(target, sourceIndex.get(target)), factory, context)
|
|
2169
|
+
ts.setParentRecursive(imported, false)
|
|
2170
|
+
imported = normalizeReactMigrationSyntax(imported, factory, context)
|
|
2171
|
+
ts.setParentRecursive(imported, false)
|
|
2172
|
+
imported = normalizeZustandMigrationSyntax(imported, factory, context)
|
|
2029
2173
|
ts.setParentRecursive(imported, false)
|
|
2030
2174
|
imported = normalizeRenderControlFlow(imported, factory, context)
|
|
2031
2175
|
ts.setParentRecursive(imported, false)
|
|
@@ -2035,6 +2179,16 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2035
2179
|
}
|
|
2036
2180
|
const settersByFunction = new Map()
|
|
2037
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
|
+
}
|
|
2038
2192
|
const functions = new Map()
|
|
2039
2193
|
const components = new Map()
|
|
2040
2194
|
const contexts = new Set()
|
|
@@ -2059,6 +2213,26 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2059
2213
|
const collect = node => {
|
|
2060
2214
|
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
2061
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
|
+
}
|
|
2062
2236
|
if (callName === "useReducer") {
|
|
2063
2237
|
if (!ts.isArrayBindingPattern(node.name)) throw sourceNodeError(node.name, sourceFile, "useReducer() must use [state, dispatch] identifier destructuring")
|
|
2064
2238
|
const [stateElement, dispatchElement] = node.name.elements
|
|
@@ -3665,7 +3839,7 @@ function compileNativeCallback(expression, setters, reducers, factory, entries,
|
|
|
3665
3839
|
const allCaptures = nativeCaptureNames(expression, setters)
|
|
3666
3840
|
const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
|
|
3667
3841
|
const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
|
|
3668
|
-
imports.push(...[...usedReducers].map(name => reducers.get(name).import))
|
|
3842
|
+
imports.push(...[...usedReducers].map(name => reducers.get(name).import).filter(Boolean))
|
|
3669
3843
|
const captures = new Set([...allCaptures].filter(name => !importBindings.has(name)))
|
|
3670
3844
|
for (const entry of imports) clientImports.add(entry.target)
|
|
3671
3845
|
const usedStates = nativeStateNames(expression, setters)
|
|
@@ -4492,13 +4666,17 @@ function printNativeHandler({ exportName, expression, captures, setters, reducer
|
|
|
4492
4666
|
const transformer = context => root => {
|
|
4493
4667
|
const visitor = node => {
|
|
4494
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)))
|
|
4495
4671
|
if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
|
|
4496
|
-
return reducerDispatch(factory,
|
|
4672
|
+
return reducerDispatch(factory, reducer, ts.visitNode(node.arguments[0], visitor))
|
|
4497
4673
|
}
|
|
4498
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")
|
|
4499
4676
|
return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
|
|
4500
4677
|
}
|
|
4501
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")
|
|
4502
4680
|
return reducerReference(factory, reducers.get(node.text))
|
|
4503
4681
|
}
|
|
4504
4682
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
|
|
@@ -4613,11 +4791,44 @@ function reducerReference(factory, reducer) {
|
|
|
4613
4791
|
}
|
|
4614
4792
|
|
|
4615
4793
|
function reducerDispatch(factory, reducer, action) {
|
|
4794
|
+
if (reducer.store) return zustandActionDispatch(factory, reducer, [action])
|
|
4616
4795
|
const previous = factory.createUniqueName("__kPrevious")
|
|
4617
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]))
|
|
4618
4797
|
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
|
|
4619
4798
|
}
|
|
4620
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
|
+
|
|
4621
4832
|
function printReactiveBinding({ exportName, expression, captures, states }) {
|
|
4622
4833
|
const factory = ts.factory
|
|
4623
4834
|
const transformer = context => root => {
|
package/framework/core.mjs
CHANGED
|
@@ -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 }
|