@kudzujs/core 0.6.13 → 0.6.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -192,6 +192,23 @@ return <Controls dispatch={dispatch} />
192
192
 
193
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
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.
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
+
195
212
  ## Reactive Attributes
196
213
 
197
214
  `className`, `disabled`, controlled `value`, and controlled `checked` accept normal state-dependent TSX expressions. The same `value` binding works for inputs and selects:
@@ -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. 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. 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
 
@@ -1739,6 +1739,56 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1739
1739
  const stateBackedComponentFunctions = new WeakSet()
1740
1740
  const stateBackedComponentRoots = []
1741
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
+ }
1742
1792
  for (const [name, component] of components) {
1743
1793
  const calls = jsxTagUses(sourceFile, name)
1744
1794
  const stateBackedCalls = calls.filter(call => isStateBackedListComponentCall(call, component.function, settersByFunction.get(nearestFunction(call)) ?? new Map()))
@@ -1786,6 +1836,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1786
1836
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1787
1837
  const specialization = specializeComponentCall(call, component.function, sourceFile, factory, context, fail, "Reducer-dispatch")
1788
1838
  if (specialization.effects.length) fail(call, "Reducer-dispatch components cannot declare effects")
1839
+ specialization.root = expandReducerCallbacks(specialization.root, component.function.getSourceFile(), call)
1789
1840
  componentSpecializations.set(call, specialization)
1790
1841
  }
1791
1842
  specializedDeclarations.add(component.declaration)
@@ -1803,29 +1854,12 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
1803
1854
  fail(dispatchCalls[0], `Reducer dispatch props require a component imported from a relative TypeScript module`)
1804
1855
  }
1805
1856
  const componentSource = component.getSourceFile()
1806
- const componentImports = clientImportBindings(componentSource, componentSource.fileName, sourceFiles)
1807
- const packageImports = runtimeImportNames(componentSource, false)
1808
1857
  for (const call of dispatchCalls) {
1809
1858
  if (componentSpecializations.has(call)) fail(call, "Reducer dispatch props cannot be combined with another component specialization")
1810
1859
  const specialization = specializeComponentCall(call, component, sourceFile, factory, context, fail, "Reducer-dispatch")
1811
1860
  if (specialization.effects.length) fail(call, "Reducer-dispatch components cannot declare effects")
1812
- for (const name of packageImports) if (referenceIdentifiers(specialization.root, name).length) fail(call, "Imported reducer-dispatch component handlers may only use relative TypeScript runtime imports")
1813
- const substitutions = new Map()
1814
- for (const [name, entry] of componentImports) {
1815
- const references = referenceIdentifiers(specialization.root, name)
1816
- if (!references.length) continue
1817
- if (references.some(reference => !insideJsxEventHandler(reference, specialization.root))) fail(call, `Imported reducer-dispatch component runtime import "${name}" may only be used inside event handlers`)
1818
- let local
1819
- do local = `__kDispatchImport${specializedImportIndex++}`
1820
- while (importBindings.has(local))
1821
- substitutions.set(name, factory.createIdentifier(local))
1822
- importBindings.set(local, { ...entry, local })
1823
- }
1824
- if (substitutions.size) {
1825
- specialization.root = substituteClone(specialization.root, substitutions, factory, context)
1826
- ts.setParentRecursive(specialization.root, false)
1827
- specialization.root.parent = call.parent
1828
- }
1861
+ specialization.root = expandReducerCallbacks(specialization.root, componentSource, call)
1862
+ specialization.root = mergeSpecializedImports(specialization.root, componentSource, call)
1829
1863
  synthesizeTree(specialization.root)
1830
1864
  componentSpecializations.set(call, specialization)
1831
1865
  }
@@ -2323,6 +2357,14 @@ function jsxCallHasDirectReducerProp(call, reducers) {
2323
2357
  })
2324
2358
  }
2325
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
+
2326
2368
  function runtimeImportNames(sourceFile, relative) {
2327
2369
  const names = new Set()
2328
2370
  for (const statement of sourceFile.statements) {
@@ -2416,7 +2458,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2416
2458
  let key
2417
2459
  for (const attribute of callAttributes.properties) {
2418
2460
  if (ts.isJsxSpreadAttribute(attribute)) fail(attribute, `${label} component prop spreads are not supported`)
2419
- const name = attribute.name.getText()
2461
+ const name = attribute.name.text
2420
2462
  if (props.has(name) || name === "key" && key) fail(attribute, `Duplicate ${label.toLowerCase()} component prop "${name}"`)
2421
2463
  const value = !attribute.initializer
2422
2464
  ? factory.createTrue()
@@ -2431,10 +2473,11 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2431
2473
  const substitutions = new Map()
2432
2474
  const acceptedProps = new Set()
2433
2475
  for (const element of component.parameters[0].name.elements) {
2434
- if (element.dotDotDotToken || element.initializer || !ts.isIdentifier(element.name)) fail(element, `${label} component props cannot use rest, defaults, or nested destructuring`)
2435
- const prop = (element.propertyName ?? element.name).getText()
2476
+ if (element.dotDotDotToken || !ts.isIdentifier(element.name) || (element.initializer && label === "Keyed list")) fail(element, `${label} component props cannot use rest, defaults, or nested destructuring`)
2477
+ if (element.initializer && !isPrimitiveDefaultLiteral(element.initializer)) fail(element.initializer, `${label} component prop defaults must be primitive literals`)
2478
+ const prop = (element.propertyName ?? element.name).text
2436
2479
  acceptedProps.add(prop)
2437
- substitutions.set(element.name.text, props.get(prop) ?? factory.createIdentifier("undefined"))
2480
+ substitutions.set(element.name.text, props.has(prop) ? props.get(prop) : element.initializer ?? factory.createIdentifier("undefined"))
2438
2481
  }
2439
2482
  for (const prop of props.keys()) if (!acceptedProps.has(prop)) fail(call, `Unknown ${label.toLowerCase()} component prop "${prop}"`)
2440
2483
 
@@ -2474,6 +2517,12 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2474
2517
  return { root, calculations, effects }
2475
2518
  }
2476
2519
 
2520
+ function isPrimitiveDefaultLiteral(node) {
2521
+ return ts.isStringLiteral(node) || ts.isNumericLiteral(node) ||
2522
+ (ts.isPrefixUnaryExpression(node) && (node.operator === ts.SyntaxKind.PlusToken || node.operator === ts.SyntaxKind.MinusToken) && ts.isNumericLiteral(node.operand)) ||
2523
+ node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
2524
+ }
2525
+
2477
2526
  function substituteClone(root, substitutions, factory, context) {
2478
2527
  const visit = (node, shadowed = new Set()) => {
2479
2528
  if (ts.isTypeNode(node)) return cloneAst(node, factory, context)
@@ -2494,6 +2543,11 @@ function substituteClone(root, substitutions, factory, context) {
2494
2543
  return visit(root)
2495
2544
  }
2496
2545
 
2546
+ function replaceSpecializedCalls(root, replacements, context) {
2547
+ const visit = node => replacements.get(node) ?? ts.visitEachChild(node, visit, context)
2548
+ return ts.visitNode(root, visit)
2549
+ }
2550
+
2497
2551
  function cloneAst(root, factory, context) {
2498
2552
  const visit = node => {
2499
2553
  const clone = factory.cloneNode(node)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.6.13",
3
+ "version": "0.6.15",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",