@kudzujs/core 0.6.12 → 0.6.14
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 +10 -1
- package/framework/README.md +1 -1
- package/framework/build.mjs +97 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -190,7 +190,16 @@ 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. Lazy initializers, package, namespace, local, async, and generator reducers,
|
|
193
|
+
Kudzu specializes that call at build time; no function prop or child component survives in the browser. Relative TypeScript constants and helpers used inside the child 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, forwarding across another component, and reducer dispatch through context remain unsupported.
|
|
194
|
+
|
|
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
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
const add = (title: string) => dispatch({ type: "add", title })
|
|
199
|
+
return <Input onSubmit={add} />
|
|
200
|
+
```
|
|
201
|
+
|
|
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.
|
|
194
203
|
|
|
195
204
|
## Reactive Attributes
|
|
196
205
|
|
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. 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 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.
|
|
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
|
@@ -1738,6 +1738,57 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1738
1738
|
const specializedDeclarations = new WeakSet()
|
|
1739
1739
|
const stateBackedComponentFunctions = new WeakSet()
|
|
1740
1740
|
const stateBackedComponentRoots = []
|
|
1741
|
+
let specializedImportIndex = 0
|
|
1742
|
+
const mergeSpecializedImports = (root, componentSource, call) => {
|
|
1743
|
+
const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
|
|
1744
|
+
for (const name of runtimeImportNames(componentSource, false)) if (referenceIdentifiers(root, name).length) fail(call, "Imported specialized component handlers may only use relative TypeScript runtime imports")
|
|
1745
|
+
const substitutions = new Map()
|
|
1746
|
+
for (const [name, entry] of componentImports) {
|
|
1747
|
+
const references = referenceIdentifiers(root, name)
|
|
1748
|
+
if (!references.length) continue
|
|
1749
|
+
if (references.some(reference => !insideJsxEventHandler(reference, root))) fail(call, `Imported specialized component runtime import "${name}" may only be used inside event handlers`)
|
|
1750
|
+
let local
|
|
1751
|
+
do local = `__kDispatchImport${specializedImportIndex++}`
|
|
1752
|
+
while (importBindings.has(local))
|
|
1753
|
+
substitutions.set(name, factory.createIdentifier(local))
|
|
1754
|
+
importBindings.set(local, { ...entry, local })
|
|
1755
|
+
}
|
|
1756
|
+
if (!substitutions.size) return root
|
|
1757
|
+
const merged = substituteClone(root, substitutions, factory, context)
|
|
1758
|
+
ts.setParentRecursive(merged, false)
|
|
1759
|
+
merged.parent = root.parent
|
|
1760
|
+
return merged
|
|
1761
|
+
}
|
|
1762
|
+
const expandReducerCallbacks = (root, componentSource, call) => {
|
|
1763
|
+
const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
|
|
1764
|
+
const replacements = new WeakMap()
|
|
1765
|
+
let count = 0
|
|
1766
|
+
for (const [name, entry] of componentImports) {
|
|
1767
|
+
if (entry.kind === "namespace") continue
|
|
1768
|
+
const nestedCalls = jsxTagUses(root, name).filter(nestedCall => jsxCallHasReducerCallbackProp(nestedCall, reducersForNode(nestedCall, reducersByFunction)))
|
|
1769
|
+
if (!nestedCalls.length) continue
|
|
1770
|
+
const imported = entry.kind === "default" ? "default" : entry.imported
|
|
1771
|
+
let nestedComponent
|
|
1772
|
+
try {
|
|
1773
|
+
nestedComponent = resolveComponentExport(entry.target, imported, importedSource, sourceFiles)
|
|
1774
|
+
} catch {
|
|
1775
|
+
fail(nestedCalls[0], "Reducer callback props require a component imported from a relative TypeScript module")
|
|
1776
|
+
}
|
|
1777
|
+
for (const nestedCall of nestedCalls) {
|
|
1778
|
+
const nested = specializeComponentCall(nestedCall, nestedComponent, sourceFile, factory, context, fail, "Reducer-callback")
|
|
1779
|
+
if (nested.effects.length) fail(nestedCall, "Reducer-callback components cannot declare effects")
|
|
1780
|
+
nested.root = mergeSpecializedImports(nested.root, nestedComponent.getSourceFile(), nestedCall)
|
|
1781
|
+
synthesizeTree(nested.root)
|
|
1782
|
+
replacements.set(nestedCall, nested.root)
|
|
1783
|
+
count++
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (!count) return root
|
|
1787
|
+
const expanded = replaceSpecializedCalls(root, replacements, context)
|
|
1788
|
+
ts.setParentRecursive(expanded, false)
|
|
1789
|
+
expanded.parent = root.parent
|
|
1790
|
+
return expanded
|
|
1791
|
+
}
|
|
1741
1792
|
for (const [name, component] of components) {
|
|
1742
1793
|
const calls = jsxTagUses(sourceFile, name)
|
|
1743
1794
|
const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component.function, settersByFunction.get(nearestFunction(call)) ?? new Map()))
|
|
@@ -1785,6 +1836,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1785
1836
|
if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
|
|
1786
1837
|
const specialization = specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Reducer-dispatch")
|
|
1787
1838
|
if (specialization.effects.length) fail(call, "Reducer-dispatch components cannot declare effects")
|
|
1839
|
+
specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
|
|
1788
1840
|
componentSpecializations.set(call, specialization)
|
|
1789
1841
|
}
|
|
1790
1842
|
specializedDeclarations.add(component.declaration)
|
|
@@ -1802,11 +1854,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
|
|
|
1802
1854
|
fail(dispatchCalls[0], `Reducer dispatch props require a component imported from a relative TypeScript module`)
|
|
1803
1855
|
}
|
|
1804
1856
|
const componentSource = component.getSourceFile()
|
|
1805
|
-
if (clientImportBindings(componentSource, componentSource.fileName, sourceFiles).size) fail(dispatchCalls[0], "Imported reducer-dispatch components cannot use runtime imports")
|
|
1806
1857
|
for (const call of dispatchCalls) {
|
|
1807
1858
|
if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
|
|
1808
1859
|
const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Reducer-dispatch")
|
|
1809
1860
|
if (specialization.effects.length) fail(call, "Reducer-dispatch components cannot declare effects")
|
|
1861
|
+
specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
|
|
1862
|
+
specialization.root = mergeSpecializedImports(specialization.root, componentSource, call)
|
|
1810
1863
|
synthesizeTree(specialization.root)
|
|
1811
1864
|
componentSpecializations.set(call, specialization)
|
|
1812
1865
|
}
|
|
@@ -2304,6 +2357,43 @@ function jsxCallHasDirectReducerProp(call, reducers) {
|
|
|
2304
2357
|
})
|
|
2305
2358
|
}
|
|
2306
2359
|
|
|
2360
|
+
function jsxCallHasReducerCallbackProp(call, reducers) {
|
|
2361
|
+
const attributes = ts.isJsxElement(call) ? call.openingElement.attributes : call.attributes
|
|
2362
|
+
return attributes.properties.some(attribute => {
|
|
2363
|
+
const value = ts.isJsxAttribute(attribute) && attribute.initializer && ts.isJsxExpression(attribute.initializer) ? unwrapExpression(attribute.initializer.expression) : undefined
|
|
2364
|
+
return value && referencedReducerDispatches(value, reducers, value).size
|
|
2365
|
+
})
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function runtimeImportNames(sourceFile, relative) {
|
|
2369
|
+
const names = new Set()
|
|
2370
|
+
for (const statement of sourceFile.statements) {
|
|
2371
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause || statement.importClause.isTypeOnly || !ts.isStringLiteral(statement.moduleSpecifier) || statement.moduleSpecifier.text.startsWith(".") !== relative) continue
|
|
2372
|
+
const clause = statement.importClause
|
|
2373
|
+
if (clause.name) names.add(clause.name.text)
|
|
2374
|
+
if (clause.namedBindings && ts.isNamespaceImport(clause.namedBindings)) names.add(clause.namedBindings.name.text)
|
|
2375
|
+
if (clause.namedBindings && ts.isNamedImports(clause.namedBindings)) for (const entry of clause.namedBindings.elements) if (!entry.isTypeOnly) names.add(entry.name.text)
|
|
2376
|
+
}
|
|
2377
|
+
return names
|
|
2378
|
+
}
|
|
2379
|
+
|
|
2380
|
+
function referenceIdentifiers(root, name) {
|
|
2381
|
+
const references = []
|
|
2382
|
+
const visit = node => {
|
|
2383
|
+
if (ts.isIdentifier(node) && node.text === name && isReferenceIdentifier(node) && !isShadowedIdentifier(node, root)) references.push(node)
|
|
2384
|
+
ts.forEachChild(node, visit)
|
|
2385
|
+
}
|
|
2386
|
+
visit(root)
|
|
2387
|
+
return references
|
|
2388
|
+
}
|
|
2389
|
+
|
|
2390
|
+
function insideJsxEventHandler(node, root) {
|
|
2391
|
+
for (let current = node.parent; current && current !== root.parent; current = current.parent) {
|
|
2392
|
+
if (ts.isJsxAttribute(current) && /^on[A-Z]/.test(current.name.text)) return true
|
|
2393
|
+
}
|
|
2394
|
+
return false
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2307
2397
|
function validateKeyedList(parts, sourceFile, listValues, listEventItems, listConditions) {
|
|
2308
2398
|
const fail = (node, message) => {
|
|
2309
2399
|
throw sourceNodeError(node, sourceFile, message)
|
|
@@ -2368,7 +2458,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
|
|
|
2368
2458
|
let key
|
|
2369
2459
|
for (const attribute of callAttributes.properties) {
|
|
2370
2460
|
if (ts.isJsxSpreadAttribute(attribute)) fail(attribute, `${label} component prop spreads are not supported`)
|
|
2371
|
-
const name = attribute.name.
|
|
2461
|
+
const name = attribute.name.text
|
|
2372
2462
|
if (props.has(name) || name === "key" && key) fail(attribute, `Duplicate ${label.toLowerCase()} component prop "${name}"`)
|
|
2373
2463
|
const value = !attribute.initializer
|
|
2374
2464
|
? factory.createTrue()
|
|
@@ -2446,6 +2536,11 @@ function substituteClone(root, substitutions, factory, context) {
|
|
|
2446
2536
|
return visit(root)
|
|
2447
2537
|
}
|
|
2448
2538
|
|
|
2539
|
+
function replaceSpecializedCalls(root, replacements, context) {
|
|
2540
|
+
const visit = node => replacements.get(node) ?? ts.visitEachChild(node, visit, context)
|
|
2541
|
+
return ts.visitNode(root, visit)
|
|
2542
|
+
}
|
|
2543
|
+
|
|
2449
2544
|
function cloneAst(root, factory, context) {
|
|
2450
2545
|
const visit = node => {
|
|
2451
2546
|
const clone = factory.cloneNode(node)
|