@kudzujs/core 0.6.9 → 0.6.11
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 +22 -2
- package/framework/README.md +4 -0
- package/framework/build.mjs +85 -16
- package/framework/core.d.ts +3 -0
- package/framework/core.mjs +36 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
HTML-first TSX framework with synchronous state semantics and no virtual DOM.
|
|
8
8
|
|
|
9
|
-
Kudzu keeps the familiar function-component, props, children, event-handler, `useState`, and mount-effect shape. Static components compile to HTML. Simple interactions compile to small behavior commands, while normal sync or async JavaScript handlers and mount effects compile to external ESM.
|
|
9
|
+
Kudzu keeps the familiar function-component, props, children, event-handler, `useState`, `useReducer`, and mount-effect shape. Static components compile to HTML. Simple interactions compile to small behavior commands, while normal sync or async JavaScript handlers and mount effects compile to external ESM.
|
|
10
10
|
|
|
11
11
|
> Experimental `0.6.x`: the compiler API and supported TSX surface may change.
|
|
12
12
|
|
|
@@ -171,6 +171,17 @@ return <p>{weather.temperature}° {weather.label}</p>
|
|
|
171
171
|
|
|
172
172
|
Derived text uses comment-bounded text nodes rather than wrapper elements, so table cells, options, SVG text, layout, and element selectors keep their authored structure.
|
|
173
173
|
|
|
174
|
+
Reducer state uses the same immediate logical updates and batched DOM commit:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
import todoReducer from "../todoReducer"
|
|
178
|
+
|
|
179
|
+
const [todos, dispatch] = useReducer(todoReducer, [])
|
|
180
|
+
dispatch({ type: "add", title: "Ship" })
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The reduced migration form requires `[state, dispatch]`, exactly two hook arguments, and a synchronous two-parameter reducer exported as a default or named value from a relative TypeScript module. Dispatches in compiled handlers lower to functional state updates; lazy initializers, package, namespace, local, async, and generator reducers are not supported. Reducer dispatch functions cannot be passed through props or context.
|
|
184
|
+
|
|
174
185
|
## Reactive Attributes
|
|
175
186
|
|
|
176
187
|
`className`, `disabled`, controlled `value`, and controlled `checked` accept normal state-dependent TSX expressions. The same `value` binding works for inputs and selects:
|
|
@@ -197,6 +208,14 @@ Regular attributes use the same expressions without an allowlist:
|
|
|
197
208
|
|
|
198
209
|
Kudzu compiles derived expressions to external ESM and patches only the bound DOM attribute or property. `aria-*` and `data-*` boolean values serialize as `"true"` or `"false"`; ordinary false values remove the attribute. Object `style` values use React-shaped camelCase properties, add `px` to nonzero dimensional numbers, and preserve unitless properties and CSS custom properties. Reactive `dangerouslySetInnerHTML` remains unsupported.
|
|
199
210
|
|
|
211
|
+
Inline SVG accepts React-shaped presentation props for static and reactive values. Kudzu preserves native camelCase SVG names such as `viewBox` while mapping common aliases such as `fillRule`, `clipRule`, `strokeWidth`, `strokeLinecap`, `strokeLinejoin`, opacity/color props, `textAnchor`, and `vectorEffect` to their SVG attribute names:
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
<svg viewBox="0 0 24 24">
|
|
215
|
+
<path fillRule="evenodd" strokeWidth={active ? 2 : 1} strokeLinecap="round" />
|
|
216
|
+
</svg>
|
|
217
|
+
```
|
|
218
|
+
|
|
200
219
|
## DOM Refs
|
|
201
220
|
|
|
202
221
|
Use an object ref to access an element from a normal event handler:
|
|
@@ -523,7 +542,7 @@ Supported:
|
|
|
523
542
|
- Runtime bracket parameters with static fallback documents and host rewrite metadata
|
|
524
543
|
- Static trusted `dangerouslySetInnerHTML`
|
|
525
544
|
- Base-path deployments, multiple CSS files, and `afterBuild`
|
|
526
|
-
-
|
|
545
|
+
- `useState` and relative-imported `useReducer` bindings
|
|
527
546
|
- Mount-only `useEffect(fn, [])` compiled to route-specific ESM
|
|
528
547
|
- Relative TypeScript module Workers owned by inline effects
|
|
529
548
|
- Conditional and keyed-row effect ownership with cleanup on DOM removal
|
|
@@ -533,6 +552,7 @@ Supported:
|
|
|
533
552
|
- Direct text DOM patches
|
|
534
553
|
- Reactive standard, `aria-*`, and `data-*` attributes
|
|
535
554
|
- Reactive object `style` attributes
|
|
555
|
+
- Static and reactive React-shaped SVG presentation attributes
|
|
536
556
|
- Object DOM refs in native event handlers
|
|
537
557
|
- Default, nested, and reactive context providers
|
|
538
558
|
- Controlled `value` and `checked` form properties
|
package/framework/README.md
CHANGED
|
@@ -21,8 +21,12 @@ Exact relative `.worker.ts` constructors in inline effects are validated and bun
|
|
|
21
21
|
|
|
22
22
|
Page `metadata` can emit description, canonical, favicon, manifest, Open Graph, and Twitter Card tags without a client runtime. Source CSS and global `kudzu.config` styles are emitted in document heads before `afterBuild()` runs; static stylesheet links in component JSX fail compilation instead of loading from the body.
|
|
23
23
|
|
|
24
|
+
Inline SVG rendering normalizes an explicit set of common React presentation aliases before static serialization and binding descriptor creation. Reactive aliases therefore use the existing generic `setAttribute` path; static SVG adds no JavaScript and reactive SVG adds no SVG-specific runtime.
|
|
25
|
+
|
|
24
26
|
Same-file and relative-imported components receiving a direct local-state array prop are specialized to intrinsic JSX before keyed-list analysis, so their component function is not retained in the browser. Handler modules are emitted only when a rendered descriptor references them, preventing specialized imported components from adding dead browser assets. Direct JSON-safe primitive keyed-item dependencies subscribe each row record to its owning list commit and compare selected fields after `list-runtime.js` synchronously refreshes the row marker. Only changed rows rerun with the complete latest item; reorder compares equal, unrelated fields do nothing, and key changes remain remove plus mount. Builds without item dependencies emit no item reader or list-state subscription code.
|
|
25
27
|
|
|
28
|
+
The reduced `useReducer` form reuses ordinary state slots. A direct dispatch in a compiled handler becomes a functional `set` whose reducer is bundled from a relative TypeScript module into that handler graph. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
|
|
29
|
+
|
|
26
30
|
`kudzu.config` may opt one emitted shared-layout group into same-document navigation with legacy `navigation: { routes: ["/product", "/items/[id]"] }`, or multiple groups with `navigation: { groups: [{ routes: [...] }, { routes: [...] }] }`. The forms are mutually exclusive. Identities are globally unique emitted exact paths or `runtimeParams` patterns; each group uses one page-exported layout function identity. Runtime records securely match concrete pathnames under `base`, and their cache-safe parameter initializer runs before route DOM/effects mount on every transition. Each group receives a deterministic route-hashed asset specialized to only its records, pattern decoder, and effect/parameter lifecycle needs. Cross-group and ungrouped anchors remain native and are not prefetched; overlapping path domains across groups fail the build. Route effect entries export cache-safe layout and route mount functions: layout effects, including conditional/keyed DOM-owned effects, persist for the group session; route effects receive a fresh owner registry after each route insertion; and non-persisted page disposal cleans route before layout. Direct primitive state, runtime parameter, and keyed-item property dependencies and cleanup are supported. Fragment payloads and coordinated View Transitions are not implemented.
|
|
27
31
|
|
|
28
32
|
The current matched commerce profile emits 35,355 deploy bytes and loads 7,334 B gzip of product-route JavaScript, including 2,425 B for navigation. These sizes are unchanged because its top-level-only navigation effects retain the smaller specialized path. Validated prefetch reduced the original 128.7 ms product-to-cart navigation to 5.6 ms in the current run. Seven interleaved artifact-clean builds after warm-up measured Kudzu at 486.8 ms and React at 545.4 ms, making Kudzu 10.7% faster. Cache-disabled output is byte-for-byte identical.
|
package/framework/build.mjs
CHANGED
|
@@ -1611,6 +1611,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1611
1611
|
return imported
|
|
1612
1612
|
}
|
|
1613
1613
|
const settersByFunction = new Map()
|
|
1614
|
+
const reducersByFunction = new Map()
|
|
1614
1615
|
const functions = new Map()
|
|
1615
1616
|
const components = new Map()
|
|
1616
1617
|
const contexts = new Set()
|
|
@@ -1630,15 +1631,41 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1630
1631
|
let usesListItem = false
|
|
1631
1632
|
|
|
1632
1633
|
const collect = node => {
|
|
1633
|
-
if (ts.isVariableDeclaration(node) &&
|
|
1634
|
+
if (ts.isVariableDeclaration(node) && node.initializer && ts.isCallExpression(node.initializer)) {
|
|
1634
1635
|
const callName = ts.isIdentifier(node.initializer.expression) ? node.initializer.expression.text : ""
|
|
1635
|
-
|
|
1636
|
-
|
|
1636
|
+
if (callName === "useReducer") {
|
|
1637
|
+
if (!ts.isArrayBindingPattern(node.name)) throw sourceNodeError(node.name, sourceFile, "useReducer() must use [state, dispatch] identifier destructuring")
|
|
1638
|
+
const [stateElement, dispatchElement] = node.name.elements
|
|
1639
|
+
if (node.name.elements.length !== 2 || !stateElement || !dispatchElement || !ts.isBindingElement(stateElement) || !ts.isBindingElement(dispatchElement) || !ts.isIdentifier(stateElement.name) || !ts.isIdentifier(dispatchElement.name)) throw sourceNodeError(node.name, sourceFile, "useReducer() must use [state, dispatch] identifier destructuring")
|
|
1640
|
+
if (node.initializer.arguments.length !== 2) throw sourceNodeError(node.initializer, sourceFile, "useReducer() requires exactly a reducer and initial value")
|
|
1641
|
+
const reducer = node.initializer.arguments[0]
|
|
1642
|
+
if (!ts.isIdentifier(reducer) || !importBindings.has(reducer.text) || importBindings.get(reducer.text).kind === "namespace") throw sourceNodeError(reducer, sourceFile, "useReducer() reducers must be default or named imports from relative TypeScript modules")
|
|
1643
|
+
const reducerImport = importBindings.get(reducer.text)
|
|
1644
|
+
let reducerDeclaration
|
|
1645
|
+
try {
|
|
1646
|
+
reducerDeclaration = resolveComponentExport(reducerImport.target, reducerImport.kind === "default" ? "default" : reducerImport.imported, importedSource, sourceFiles)
|
|
1647
|
+
} catch {
|
|
1648
|
+
throw sourceNodeError(reducer, sourceFile, "useReducer() imports must resolve to a statically analyzable reducer function")
|
|
1649
|
+
}
|
|
1650
|
+
if (reducerDeclaration.parameters.length !== 2 || reducerDeclaration.asteriskToken || reducerDeclaration.modifiers?.some(modifier => modifier.kind === ts.SyntaxKind.AsyncKeyword)) throw sourceNodeError(reducer, sourceFile, "useReducer() reducers must be synchronous functions with exactly state and action parameters")
|
|
1637
1651
|
const owner = nearestFunction(node)
|
|
1638
|
-
if (owner)
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1652
|
+
if (!owner) throw sourceNodeError(node, sourceFile, "useReducer() cannot be used outside a Kudzu component")
|
|
1653
|
+
const setters = settersByFunction.get(owner) ?? new Map()
|
|
1654
|
+
setters.set(dispatchElement.name.text, stateElement.name.text)
|
|
1655
|
+
settersByFunction.set(owner, setters)
|
|
1656
|
+
const reducers = reducersByFunction.get(owner) ?? new Map()
|
|
1657
|
+
reducers.set(dispatchElement.name.text, { state: stateElement.name.text, reducer: reducer.text, import: reducerImport })
|
|
1658
|
+
reducersByFunction.set(owner, reducers)
|
|
1659
|
+
}
|
|
1660
|
+
if (ts.isArrayBindingPattern(node.name)) {
|
|
1661
|
+
const [stateElement, setterElement] = node.name.elements
|
|
1662
|
+
if (callName === "useState" && stateElement && setterElement && ts.isBindingElement(stateElement) && ts.isBindingElement(setterElement) && ts.isIdentifier(stateElement.name) && ts.isIdentifier(setterElement.name)) {
|
|
1663
|
+
const owner = nearestFunction(node)
|
|
1664
|
+
if (owner) {
|
|
1665
|
+
const setters = settersByFunction.get(owner) ?? new Map()
|
|
1666
|
+
setters.set(setterElement.name.text, stateElement.name.text)
|
|
1667
|
+
settersByFunction.set(owner, setters)
|
|
1668
|
+
}
|
|
1642
1669
|
}
|
|
1643
1670
|
}
|
|
1644
1671
|
}
|
|
@@ -1931,7 +1958,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1931
1958
|
} else {
|
|
1932
1959
|
compiledCallback = rewriteEffectWorkers(callback, callbackFile, callbackSource, sourceFiles, workerReferences, factory, context)
|
|
1933
1960
|
}
|
|
1934
|
-
const descriptor = compileNativeCallback(compiledCallback, setters, factory, effectHandlers, listEffect?.imports ?? importBindings, clientImports, "effect", dependencyItem, true, returns.cleanup)
|
|
1961
|
+
const descriptor = compileNativeCallback(compiledCallback, setters, reducersForNode(node, reducersByFunction), factory, effectHandlers, listEffect?.imports ?? importBindings, clientImports, "effect", dependencyItem, true, returns.cleanup)
|
|
1935
1962
|
for (const reference of workerReferences.slice(workerStart)) Object.assign(reference, { module: handlerUrl, handler: descriptor.exportName })
|
|
1936
1963
|
usesListItem ||= Boolean(itemDependencies.length && !listEffect)
|
|
1937
1964
|
usesBehavior = true
|
|
@@ -1948,7 +1975,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1948
1975
|
])
|
|
1949
1976
|
}
|
|
1950
1977
|
|
|
1951
|
-
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && node.initializer.expression.text === "useState" && node.initializer.arguments.length === 1) {
|
|
1978
|
+
if (ts.isVariableDeclaration(node) && ts.isArrayBindingPattern(node.name) && node.initializer && ts.isCallExpression(node.initializer) && ts.isIdentifier(node.initializer.expression) && (node.initializer.expression.text === "useState" && node.initializer.arguments.length === 1 || node.initializer.expression.text === "useReducer" && node.initializer.arguments.length === 2)) {
|
|
1952
1979
|
const stateElement = node.name.elements[0]
|
|
1953
1980
|
if (!stateElement || !ts.isBindingElement(stateElement) || !ts.isIdentifier(stateElement.name)) return node
|
|
1954
1981
|
const initializer = factory.updateCallExpression(node.initializer, node.initializer.expression, node.initializer.typeArguments, [
|
|
@@ -2030,7 +2057,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
2030
2057
|
|
|
2031
2058
|
if (ts.isJsxAttribute(node) && node.initializer && ts.isJsxExpression(node.initializer) && node.initializer.expression && /^on[A-Z]/.test(node.name.text)) {
|
|
2032
2059
|
const setters = settersForNode(node, settersByFunction)
|
|
2033
|
-
const event = compileEvent(node.initializer.expression, setters, functions, factory, nativeHandlers, handlerUrl, listEventItems.get(node), importBindings, clientImports)
|
|
2060
|
+
const event = compileEvent(node.initializer.expression, setters, reducersForNode(node, reducersByFunction), functions, factory, nativeHandlers, handlerUrl, listEventItems.get(node), importBindings, clientImports)
|
|
2034
2061
|
if (event) {
|
|
2035
2062
|
usesBehavior = true
|
|
2036
2063
|
return factory.updateJsxAttribute(node, node.name, factory.createJsxExpression(undefined, event))
|
|
@@ -2656,15 +2683,15 @@ function factoryNull() {
|
|
|
2656
2683
|
return ts.factory.createNull()
|
|
2657
2684
|
}
|
|
2658
2685
|
|
|
2659
|
-
function compileEvent(expression, setters, functions, factory, nativeHandlers, handlerUrl, listItem, importBindings, clientImports) {
|
|
2686
|
+
function compileEvent(expression, setters, reducers, functions, factory, nativeHandlers, handlerUrl, listItem, importBindings, clientImports) {
|
|
2660
2687
|
if (ts.isIdentifier(expression)) expression = functions.get(expression.text)
|
|
2661
2688
|
if (!expression || (!ts.isArrowFunction(expression) && !ts.isFunctionExpression(expression) && !ts.isFunctionDeclaration(expression))) return undefined
|
|
2662
2689
|
|
|
2663
|
-
const optimized = compileOptimizedEvent(expression, setters, factory)
|
|
2690
|
+
const optimized = referencedReducerDispatches(expression.body, reducers, expression).size ? undefined : compileOptimizedEvent(expression, setters, factory)
|
|
2664
2691
|
if (optimized) return optimized
|
|
2665
2692
|
|
|
2666
2693
|
rejectWorkerConstructions(expression, expression.getSourceFile(), "Relative TypeScript Worker construction is only supported directly inside an inline useEffect() callback")
|
|
2667
|
-
const descriptor = compileNativeCallback(expression, setters, factory, nativeHandlers, importBindings, clientImports, "handler", listItem)
|
|
2694
|
+
const descriptor = compileNativeCallback(expression, setters, reducers, factory, nativeHandlers, importBindings, clientImports, "handler", listItem)
|
|
2668
2695
|
return factory.createCallExpression(factory.createIdentifier("__kNativeBehavior"), undefined, [
|
|
2669
2696
|
factory.createStringLiteral(handlerUrl),
|
|
2670
2697
|
factory.createStringLiteral(descriptor.exportName),
|
|
@@ -2673,14 +2700,16 @@ function compileEvent(expression, setters, functions, factory, nativeHandlers, h
|
|
|
2673
2700
|
])
|
|
2674
2701
|
}
|
|
2675
2702
|
|
|
2676
|
-
function compileNativeCallback(expression, setters, factory, entries, importBindings, clientImports, prefix, listItem, deferValues = false, snapshotNested = false) {
|
|
2703
|
+
function compileNativeCallback(expression, setters, reducers, factory, entries, importBindings, clientImports, prefix, listItem, deferValues = false, snapshotNested = false) {
|
|
2677
2704
|
const allCaptures = nativeCaptureNames(expression, setters)
|
|
2705
|
+
const usedReducers = referencedReducerDispatches(expression.body, reducers, expression)
|
|
2678
2706
|
const imports = [...referencedImportedBindings(expression, importBindings)].map(name => importBindings.get(name))
|
|
2707
|
+
imports.push(...[...usedReducers].map(name => reducers.get(name).import))
|
|
2679
2708
|
const captures = new Set([...allCaptures].filter(name => !importBindings.has(name)))
|
|
2680
2709
|
for (const entry of imports) clientImports.add(entry.target)
|
|
2681
2710
|
const usedStates = nativeStateNames(expression, setters)
|
|
2682
2711
|
const exportName = `${prefix}${entries.length}`
|
|
2683
|
-
entries.push({ exportName, expression, captures, imports, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), snapshotNested })
|
|
2712
|
+
entries.push({ exportName, expression, captures, imports, setters: new Map([...setters].filter(([, state]) => usedStates.has(state))), reducers: new Map([...reducers].filter(([name]) => usedReducers.has(name))), snapshotNested })
|
|
2684
2713
|
const value = name => deferValues
|
|
2685
2714
|
? factory.createArrowFunction(undefined, undefined, [], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), factory.createIdentifier(name))
|
|
2686
2715
|
: factory.createIdentifier(name)
|
|
@@ -2697,6 +2726,16 @@ function compileNativeCallback(expression, setters, factory, entries, importBind
|
|
|
2697
2726
|
}
|
|
2698
2727
|
}
|
|
2699
2728
|
|
|
2729
|
+
function referencedReducerDispatches(root, reducers, scopeRoot = root) {
|
|
2730
|
+
const used = new Set()
|
|
2731
|
+
const visit = node => {
|
|
2732
|
+
if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, scopeRoot)) used.add(node.text)
|
|
2733
|
+
ts.forEachChild(node, visit)
|
|
2734
|
+
}
|
|
2735
|
+
visit(root)
|
|
2736
|
+
return used
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2700
2739
|
function nativeStateNames(expression, setters) {
|
|
2701
2740
|
return referencedStateNames(expression.body, setters, expression)
|
|
2702
2741
|
}
|
|
@@ -2945,6 +2984,15 @@ function settersForNode(node, settersByFunction) {
|
|
|
2945
2984
|
return new Map()
|
|
2946
2985
|
}
|
|
2947
2986
|
|
|
2987
|
+
function reducersForNode(node, reducersByFunction) {
|
|
2988
|
+
for (let current = node.parent; current; current = current.parent) {
|
|
2989
|
+
if (!ts.isFunctionDeclaration(current) && !ts.isFunctionExpression(current) && !ts.isArrowFunction(current)) continue
|
|
2990
|
+
const reducers = reducersByFunction.get(current)
|
|
2991
|
+
if (reducers) return reducers
|
|
2992
|
+
}
|
|
2993
|
+
return new Map()
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2948
2996
|
function clientImportBindings(sourceFile, file, sourceFiles) {
|
|
2949
2997
|
const bindings = new Map()
|
|
2950
2998
|
for (const node of sourceFile.statements) {
|
|
@@ -3242,7 +3290,7 @@ function relativeModulePath(from, to) {
|
|
|
3242
3290
|
return path.startsWith(".") ? path : `./${path}`
|
|
3243
3291
|
}
|
|
3244
3292
|
|
|
3245
|
-
function printNativeHandler({ exportName, expression, captures, setters, snapshotNested }) {
|
|
3293
|
+
function printNativeHandler({ exportName, expression, captures, setters, reducers = new Map(), snapshotNested }) {
|
|
3246
3294
|
const factory = ts.factory
|
|
3247
3295
|
const stateNames = new Set(setters.values())
|
|
3248
3296
|
const snapshotNames = snapshotNested ? nestedStateNames(expression, setters) : new Set()
|
|
@@ -3251,6 +3299,16 @@ function printNativeHandler({ exportName, expression, captures, setters, snapsho
|
|
|
3251
3299
|
const captureSnapshots = new Map([...captureSnapshotNames].map(name => [name, factory.createUniqueName("__kEffectCapture")]))
|
|
3252
3300
|
const transformer = context => root => {
|
|
3253
3301
|
const visitor = node => {
|
|
3302
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && reducers.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
|
|
3303
|
+
if (node.arguments.length !== 1) throw sourceNodeError(node, expression.getSourceFile(), "Reducer dispatches require exactly one action")
|
|
3304
|
+
return reducerDispatch(factory, reducers.get(node.expression.text), ts.visitNode(node.arguments[0], visitor))
|
|
3305
|
+
}
|
|
3306
|
+
if (ts.isShorthandPropertyAssignment(node) && reducers.has(node.name.text) && !isShadowedIdentifier(node.name, expression)) {
|
|
3307
|
+
return factory.createPropertyAssignment(node.name, reducerReference(factory, reducers.get(node.name.text)))
|
|
3308
|
+
}
|
|
3309
|
+
if (ts.isIdentifier(node) && reducers.has(node.text) && isReferenceIdentifier(node) && !isShadowedIdentifier(node, expression)) {
|
|
3310
|
+
return reducerReference(factory, reducers.get(node.text))
|
|
3311
|
+
}
|
|
3254
3312
|
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && setters.has(node.expression.text) && !isShadowedIdentifier(node.expression, expression)) {
|
|
3255
3313
|
return factory.createCallExpression(
|
|
3256
3314
|
factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"),
|
|
@@ -3357,6 +3415,17 @@ function setterReference(factory, stateName) {
|
|
|
3357
3415
|
)
|
|
3358
3416
|
}
|
|
3359
3417
|
|
|
3418
|
+
function reducerReference(factory, reducer) {
|
|
3419
|
+
const action = factory.createUniqueName("__kAction")
|
|
3420
|
+
return factory.createArrowFunction(undefined, undefined, [factory.createParameterDeclaration(undefined, undefined, action)], undefined, factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), reducerDispatch(factory, reducer, action))
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
function reducerDispatch(factory, reducer, action) {
|
|
3424
|
+
const previous = factory.createUniqueName("__kPrevious")
|
|
3425
|
+
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]))
|
|
3426
|
+
return factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("__k"), "set"), undefined, [factory.createStringLiteral(reducer.state), update])
|
|
3427
|
+
}
|
|
3428
|
+
|
|
3360
3429
|
function printReactiveBinding({ exportName, expression, captures, states }) {
|
|
3361
3430
|
const factory = ts.factory
|
|
3362
3431
|
const transformer = context => root => {
|
package/framework/core.d.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
export type StateSetter<T> = (value: T | ((previous: T) => T)) => void
|
|
2
|
+
export type Reducer<State, Action> = (state: State, action: Action) => State
|
|
3
|
+
export type Dispatch<Action> = (action: Action) => void
|
|
2
4
|
export type EffectCleanup = () => void | Promise<void>
|
|
3
5
|
export type EffectDependency = string | number | boolean | null
|
|
4
6
|
|
|
5
7
|
export function useState<T>(initialValue: T): [T, StateSetter<T>]
|
|
8
|
+
export function useReducer<State, Action>(reducer: Reducer<State, Action>, initialValue: State): [State, Dispatch<Action>]
|
|
6
9
|
export function useEffect(effect: () => void | EffectCleanup | Promise<void>, dependencies: readonly EffectDependency[]): void
|
|
7
10
|
export function useParams<Params extends Record<string, string> = Record<string, string>>(): Readonly<Params>
|
|
8
11
|
|
package/framework/core.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { serializeStyle } from "./style.js"
|
|
|
2
2
|
|
|
3
3
|
const signalMarker = Symbol("kudzu.signal")
|
|
4
4
|
const setterMarker = Symbol("kudzu.setter")
|
|
5
|
+
const reducerDispatchMarker = Symbol("kudzu.reducerDispatch")
|
|
5
6
|
const behaviorMarker = Symbol("kudzu.behavior")
|
|
6
7
|
const nativeBehaviorMarker = Symbol("kudzu.nativeBehavior")
|
|
7
8
|
const bindingMarker = Symbol("kudzu.binding")
|
|
@@ -16,6 +17,28 @@ const contextMarker = Symbol("kudzu.context")
|
|
|
16
17
|
const contextProviderMarker = Symbol("kudzu.contextProvider")
|
|
17
18
|
const routeScopeMarker = Symbol("kudzu.routeScope")
|
|
18
19
|
const noSelectValue = Symbol("kudzu.no-select-value")
|
|
20
|
+
const svgAttributeAliases = {
|
|
21
|
+
clipRule: "clip-rule",
|
|
22
|
+
colorInterpolation: "color-interpolation",
|
|
23
|
+
colorInterpolationFilters: "color-interpolation-filters",
|
|
24
|
+
dominantBaseline: "dominant-baseline",
|
|
25
|
+
fillOpacity: "fill-opacity",
|
|
26
|
+
fillRule: "fill-rule",
|
|
27
|
+
floodColor: "flood-color",
|
|
28
|
+
floodOpacity: "flood-opacity",
|
|
29
|
+
shapeRendering: "shape-rendering",
|
|
30
|
+
stopColor: "stop-color",
|
|
31
|
+
stopOpacity: "stop-opacity",
|
|
32
|
+
strokeDasharray: "stroke-dasharray",
|
|
33
|
+
strokeDashoffset: "stroke-dashoffset",
|
|
34
|
+
strokeLinecap: "stroke-linecap",
|
|
35
|
+
strokeLinejoin: "stroke-linejoin",
|
|
36
|
+
strokeMiterlimit: "stroke-miterlimit",
|
|
37
|
+
strokeOpacity: "stroke-opacity",
|
|
38
|
+
strokeWidth: "stroke-width",
|
|
39
|
+
textAnchor: "text-anchor",
|
|
40
|
+
vectorEffect: "vector-effect"
|
|
41
|
+
}
|
|
19
42
|
|
|
20
43
|
let renderContext
|
|
21
44
|
|
|
@@ -35,6 +58,16 @@ export function useState(initialValue, name) {
|
|
|
35
58
|
return [signal, setter]
|
|
36
59
|
}
|
|
37
60
|
|
|
61
|
+
export function useReducer(reducer, initialValue, name) {
|
|
62
|
+
if (typeof reducer !== "function") throw new Error("useReducer() requires a reducer function")
|
|
63
|
+
const [state] = useState(initialValue, name)
|
|
64
|
+
const dispatch = () => {
|
|
65
|
+
throw new Error("Reducer dispatches are compiled into browser handlers")
|
|
66
|
+
}
|
|
67
|
+
Object.defineProperty(dispatch, reducerDispatchMarker, { value: state.id })
|
|
68
|
+
return [state, dispatch]
|
|
69
|
+
}
|
|
70
|
+
|
|
38
71
|
export function useParams() {
|
|
39
72
|
if (renderContext?.renderScope === "layout") throw new Error("useParams() is only supported in route scope")
|
|
40
73
|
if (!renderContext?.runtimeParamNames?.length) throw new Error("useParams() requires export const runtimeParams = true on a bracket page")
|
|
@@ -268,6 +301,7 @@ function serializeCapture(name, value, seen) {
|
|
|
268
301
|
if (value?.[listItemMarker]) return { type: "list-item" }
|
|
269
302
|
if (value?.[refMarker]) return { type: "ref", id: value.id }
|
|
270
303
|
if (value?.[signalMarker]) return { type: "state", id: value.id }
|
|
304
|
+
if (typeof value === "function" && value[reducerDispatchMarker]) throw new Error(`Native capture "${name}" cannot contain a reducer dispatch`)
|
|
271
305
|
if (typeof value === "function" && value[setterMarker]) return { type: "setter", id: value[setterMarker] }
|
|
272
306
|
if (value === null || typeof value === "string" || typeof value === "boolean") return value
|
|
273
307
|
if (typeof value === "number") {
|
|
@@ -564,6 +598,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
564
598
|
const childNamespace = tag === "svg" || tag === "math"
|
|
565
599
|
? tag
|
|
566
600
|
: namespace === "svg" && tag === "foreignObject" ? undefined : namespace
|
|
601
|
+
const svg = tag === "svg" || namespace === "svg"
|
|
567
602
|
let attributes = ""
|
|
568
603
|
const attributeBindings = []
|
|
569
604
|
const listAttributes = []
|
|
@@ -624,7 +659,7 @@ async function renderNode(node, namespace, selectValue = noSelectValue) {
|
|
|
624
659
|
continue
|
|
625
660
|
}
|
|
626
661
|
|
|
627
|
-
const name = rawName === "className" ? "class" : rawName === "htmlFor" ? "for" : rawName
|
|
662
|
+
const name = rawName === "className" ? "class" : rawName === "htmlFor" ? "for" : svg ? svgAttributeAliases[rawName] ?? rawName : rawName
|
|
628
663
|
const propertyTarget = name === "class" || name === "disabled" || name === "value" || name === "checked" || name === "style"
|
|
629
664
|
if (value?.[listFieldMarker]) {
|
|
630
665
|
attributes += renderAttribute(name, value.value)
|