@kudzujs/core 0.6.15 → 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 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. 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.
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
 
@@ -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. 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.
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
 
@@ -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 = specializeComponentCall(call, component.function, sourceFile, factory, context, fail)
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
  }
@@ -2499,7 +2504,7 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2499
2504
  const declaration = statement.declarationList.declarations[0]
2500
2505
  if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, `${label} component locals must be initialized identifiers`)
2501
2506
  const calculation = substituteClone(declaration.initializer, substitutions, factory, context)
2502
- calculations.push(calculation)
2507
+ calculations.push({ name: declaration.name.text, expression: calculation })
2503
2508
  substitutions.set(declaration.name.text, calculation)
2504
2509
  }
2505
2510
  returned = last.expression
@@ -2514,7 +2519,13 @@ function specializeComponentCall(call, component, sourceFile, factory, context,
2514
2519
  ts.setParentRecursive(root, false)
2515
2520
  root.parent = call.parent
2516
2521
  const effects = effectCalls.map(source => ({ source, call: substituteClone(source, substitutions, factory, context) }))
2517
- return { root, calculations, effects }
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
+ }
2518
2529
  }
2519
2530
 
2520
2531
  function isPrimitiveDefaultLiteral(node) {
@@ -2523,6 +2534,29 @@ function isPrimitiveDefaultLiteral(node) {
2523
2534
  node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword || node.kind === ts.SyntaxKind.NullKeyword
2524
2535
  }
2525
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
2558
+ }
2559
+
2526
2560
  function substituteClone(root, substitutions, factory, context) {
2527
2561
  const visit = (node, shadowed = new Set()) => {
2528
2562
  if (ts.isTypeNode(node)) return cloneAst(node, factory, context)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.6.15",
3
+ "version": "0.6.16",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",