@kudzujs/core 0.6.14 → 0.6.16
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 +9 -1
- package/framework/README.md +1 -1
- package/framework/build.mjs +47 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -190,7 +190,7 @@ function Controls({ dispatch }: { dispatch: Dispatch<TodoAction> }) {
|
|
|
190
190
|
return <Controls dispatch={dispatch} />
|
|
191
191
|
```
|
|
192
192
|
|
|
193
|
-
Kudzu specializes that call at build time; no function prop or child component survives in the browser.
|
|
193
|
+
Kudzu specializes that call at build time; no function prop or child component survives in the browser. The direct child may also be a keyed row such as `todos.map(todo => <Item key={todo.id} todo={todo} dispatch={dispatch} />)`. Its inline or simple `const` event handler receives the latest keyed item, while relative TypeScript constants and helpers used inside the handler are renamed for call-site safety and bundled into the parent handler graph. Lazy initializers, package, namespace, local, async, and generator reducers, package imports or child imports used outside event handlers, further dispatch forwarding, reducer-dispatch keyed-row effects or local state, and reducer dispatch through context remain unsupported.
|
|
194
194
|
|
|
195
195
|
That specialized component may pass one inline or simple `const` callback containing dispatch to one relative-imported synchronous child with an intrinsic root:
|
|
196
196
|
|
|
@@ -201,6 +201,14 @@ return <Input onSubmit={add} />
|
|
|
201
201
|
|
|
202
202
|
Kudzu substitutes the callback into the child's compiled event handler at build time. This is not general function-prop serialization: only one nested specialized callback boundary is supported, and `useCallback`, further forwarding, effects, component roots, and callback use outside event handlers are rejected.
|
|
203
203
|
|
|
204
|
+
Reducer dispatch and callback components may use destructured string, finite-number, boolean, or `null` defaults. A missing prop is replaced during specialization; object, array, computed, and function-call defaults remain unsupported:
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
function Input({ onSubmit, editing = false }) {
|
|
208
|
+
// ...
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
204
212
|
## Reactive Attributes
|
|
205
213
|
|
|
206
214
|
`className`, `disabled`, controlled `value`, and controlled `checked` accept normal state-dependent TSX expressions. The same `value` binding works for inputs and selects:
|
package/framework/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Inline SVG rendering normalizes an explicit set of common React presentation ali
|
|
|
25
25
|
|
|
26
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.
|
|
27
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. One direct dispatch prop into a same-file or relative-imported synchronous component is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
|
|
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. One direct dispatch prop into a same-file or relative-imported synchronous component, including a direct keyed row, is specialized to intrinsic JSX at the call site, so its handler retains the parent reducer scope and no dispatch capture or child handler asset is emitted. A keyed-row handler reads the latest item through the existing list scope. Relative TypeScript imports referenced inside that child handler receive collision-free call-site aliases and join the parent handler graph. One nested relative-imported intrinsic child may receive an inline or simple `const` callback containing dispatch; the compiler recursively substitutes that callback once and omits the nested child handler asset. Missing primitive literal defaults in these reducer specializations are substituted at the same call site. Reducer-free routes and shared runtimes are unchanged; no reducer runtime or browser component instance exists.
|
|
29
29
|
|
|
30
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.
|
|
31
31
|
|
package/framework/build.mjs
CHANGED
|
@@ -1735,6 +1735,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1735
1735
|
throw sourceNodeError(node, sourceFile, message)
|
|
1736
1736
|
}
|
|
1737
1737
|
const componentSpecializations = new WeakMap()
|
|
1738
|
+
const reducerComponentCalls = new WeakSet()
|
|
1738
1739
|
const specializedDeclarations = new WeakSet()
|
|
1739
1740
|
const stateBackedComponentFunctions = new WeakSet()
|
|
1740
1741
|
const stateBackedComponentRoots = []
|
|
@@ -1838,6 +1839,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1838
1839
|
if (specialization.effects.length) fail(call, "Reducer-dispatch components cannot declare effects")
|
|
1839
1840
|
specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
|
|
1840
1841
|
componentSpecializations.set(call, specialization)
|
|
1842
|
+
reducerComponentCalls.add(call)
|
|
1841
1843
|
}
|
|
1842
1844
|
specializedDeclarations.add(component.declaration)
|
|
1843
1845
|
}
|
|
@@ -1862,6 +1864,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1862
1864
|
specialization.root = mergeSpecializedImports(specialization.root, componentSource, call)
|
|
1863
1865
|
synthesizeTree(specialization.root)
|
|
1864
1866
|
componentSpecializations.set(call, specialization)
|
|
1867
|
+
reducerComponentCalls.add(call)
|
|
1865
1868
|
}
|
|
1866
1869
|
}
|
|
1867
1870
|
const rawRenderedLists = []
|
|
@@ -1910,7 +1913,9 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1910
1913
|
...stateBackedComponentRoots.flatMap(root => jsxTagUses(root, name))
|
|
1911
1914
|
]
|
|
1912
1915
|
for (const call of calls) {
|
|
1913
|
-
const specialization =
|
|
1916
|
+
const specialization = reducerComponentCalls.has(call)
|
|
1917
|
+
? componentSpecializations.get(call)
|
|
1918
|
+
: specializeComponentCall(call, component.function, sourceFile, factory, context, fail)
|
|
1914
1919
|
if (specialization.effects.length && !keyedComponentCalls.has(call)) fail(call, "Effectful keyed row components may only be used directly as keyed map rows")
|
|
1915
1920
|
componentSpecializations.set(call, specialization)
|
|
1916
1921
|
}
|
|
@@ -2473,10 +2478,11 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2473
2478
|
const substitutions = new Map()
|
|
2474
2479
|
const acceptedProps = new Set()
|
|
2475
2480
|
for (const element of component.parameters[0].name.elements) {
|
|
2476
|
-
if (element.dotDotDotToken ||
|
|
2477
|
-
|
|
2481
|
+
if (element.dotDotDotToken || !ts.isIdentifier(element.name) || (element.initializer && label === "Keyed list")) fail(element, `${label} component props cannot use rest, defaults, or nested destructuring`)
|
|
2482
|
+
if (element.initializer && !isPrimitiveDefaultLiteral(element.initializer)) fail(element.initializer, `${label} component prop defaults must be primitive literals`)
|
|
2483
|
+
const prop = (element.propertyName ?? element.name).text
|
|
2478
2484
|
acceptedProps.add(prop)
|
|
2479
|
-
substitutions.set(element.name.text, props.get(prop) ?? factory.createIdentifier("undefined"))
|
|
2485
|
+
substitutions.set(element.name.text, props.has(prop) ? props.get(prop) : element.initializer ?? factory.createIdentifier("undefined"))
|
|
2480
2486
|
}
|
|
2481
2487
|
for (const prop of props.keys()) if (!acceptedProps.has(prop)) fail(call, `Unknown ${label.toLowerCase()} component prop "${prop}"`)
|
|
2482
2488
|
|
|
@@ -2498,7 +2504,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2498
2504
|
const declaration = statement.declarationList.declarations[0]
|
|
2499
2505
|
if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
|
|
2500
2506
|
const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
|
|
2501
|
-
calculations.push(calculation)
|
|
2507
|
+
calculations.push({ name: declaration.name.text, expression: calculation })
|
|
2502
2508
|
substitutions.set(declaration.name.text, calculation)
|
|
2503
2509
|
}
|
|
2504
2510
|
returned = last.expression
|
|
@@ -2513,7 +2519,42 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2513
2519
|
ts.setParentRecursive(root, false)
|
|
2514
2520
|
root.parent = call.parent
|
|
2515
2521
|
const effects = effectCalls.map(source => ({ source, call: substituteClone(source, substitutions, factory, context) }))
|
|
2516
|
-
return {
|
|
2522
|
+
return {
|
|
2523
|
+
root,
|
|
2524
|
+
calculations: calculations
|
|
2525
|
+
.filter(calculation => label !== "Reducer-dispatch" || !isFunctionLike(calculation.expression) || !isEventOnlyComponentLocal(returned, calculation.name))
|
|
2526
|
+
.map(calculation => calculation.expression),
|
|
2527
|
+
effects
|
|
2528
|
+
}
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2531
|
+
function isPrimitiveDefaultLiteral(node) {
|
|
2532
|
+
return ts.isStringLiteral(node) || ts.isNumericLiteral(node) ||
|
|
2533
|
+
(ts.isPrefixUnaryExpression(node) && (node.operator === ts.SyntaxKind.PlusToken || node.operator === ts.SyntaxKind.MinusToken) && ts.isNumericLiteral(node.operand)) ||
|
|
2534
|
+
node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
function isEventOnlyComponentLocal(root, name) {
|
|
2538
|
+
let found = false
|
|
2539
|
+
let eventOnly = true
|
|
2540
|
+
const visit = node => {
|
|
2541
|
+
if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node)) {
|
|
2542
|
+
found = true
|
|
2543
|
+
let parent = node.parent
|
|
2544
|
+
while (parent && parent !== root) {
|
|
2545
|
+
if (ts.isJsxAttribute(parent)) {
|
|
2546
|
+
if (!/^on[A-Z]/.test(parent.name.text)) eventOnly = false
|
|
2547
|
+
return
|
|
2548
|
+
}
|
|
2549
|
+
parent = parent.parent
|
|
2550
|
+
}
|
|
2551
|
+
eventOnly = false
|
|
2552
|
+
return
|
|
2553
|
+
}
|
|
2554
|
+
ts.forEachChild(node, visit)
|
|
2555
|
+
}
|
|
2556
|
+
visit(root)
|
|
2557
|
+
return found && eventOnly
|
|
2517
2558
|
}
|
|
2518
2559
|
|
|
2519
2560
|
function substituteClone(root, substitutions, factory, context) {
|