@kudzujs/core 0.7.19 → 0.7.20

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
@@ -10,7 +10,7 @@ Kudzu compiles ordinary React-shaped TypeScript and TSX into complete static HTM
10
10
 
11
11
  > Experimental `0.7.x`: the compiler API and supported TSX surface may change.
12
12
 
13
- **Latest release: 0.7.19 - Reusable collection aliases.** One immutable local collection alias may feed multiple keyed lists when every reference remains statically analyzable; setter callbacks and object refs also have a proven one-boundary component contract. Read the [release notes](./RELEASES.md#0719---reusable-collection-aliases) or open the [release page](https://kudzujs.cloud/releases/0.7.19).
13
+ **Latest release: 0.7.20 - Computed child collections.** A block-bodied keyed `map` callback may compute one direct child collection through the supported pure selector pipeline and use it as a nested keyed list source. Read the [release notes](./RELEASES.md#0720---computed-child-collections) or open the [release page](https://kudzujs.cloud/releases/0.7.20).
14
14
 
15
15
  - [Documentation](https://kudzujs.cloud/docs)
16
16
  - [Installation guide](https://kudzujs.cloud/docs#install)
package/RELEASES.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.7.20 - Computed child collections
4
+
5
+ Kudzu 0.7.20 accepts a common block-bodied keyed `map` callback that computes one direct child collection before returning JSX, without executing arbitrary callback code or adding a browser runtime path.
6
+
7
+ ### New in 0.7.20
8
+
9
+ - A keyed `map` callback may contain one top-level `const` declaration followed by its final JSX return.
10
+ - The `const` must start from a direct parent-item child property and may use the existing pure `filter`, direct-property `flatMap`, or `Array.from` selector pipeline.
11
+ - The computed alias feeds exactly one nested keyed list source.
12
+ - The compiler substitutes the proven calculation into the returned JSX before nested-list analysis, then reuses existing selector encoding and keyed ownership.
13
+ - Child insertion, reorder, removal, indexes, and DOM identity retain the existing nested-list behavior.
14
+ - Additional statements, multiple or mixed alias reads, parent capture, mutation, arbitrary calls, and asynchronous callbacks fail during compilation.
15
+ - The complete suite passes 107/107 tests with browser coverage for dynamic computed-child insertion and source-diagnostic coverage for mixed alias use.
16
+
17
+ ### Boundary
18
+
19
+ This release supports one direct-child collection `const` in a block-bodied keyed-map callback. Multiple calculations, aliases used outside one nested keyed list, imported transforms, parent captures, mutation, and async work remain unsupported.
20
+
21
+ ### Upgrade
22
+
23
+ ```bash
24
+ npm install @kudzujs/core@^0.7.20
25
+ ```
26
+
3
27
  ## 0.7.19 - Reusable collection aliases
4
28
 
5
29
  Kudzu 0.7.19 lets one immutable local collection alias feed multiple keyed list sites while preserving independent DOM identity, and formalizes setter callback and object-ref ownership across one ordinary component boundary.
@@ -33,7 +33,7 @@ Page `metadata` can emit description, canonical, favicon, manifest, Open Graph,
33
33
 
34
34
  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.
35
35
 
36
- Same-file, directly exported same-file, and relative-imported component chains receiving a direct local-state array or keyed item are recursively specialized to intrinsic JSX before keyed-list analysis, so their component functions are not retained in the browser. A directly exported row may be reused across static and keyed JSX sites; export-list/default aliases and non-JSX references remain rejected. Missing destructured props use directly serializable primitive, plain-object, or array literal defaults during specialization. One final identifier rest binding may be expanded exactly once at the direct intrinsic root. Rows may own multiple direct-property child maps recursively, nested conditions, latest-item handlers, multiple directly serializable state slots, effects, and `null`-initialized object refs. Structural list sites and ancestor key paths scope hooks across updates and reorder and release them on removal. Handler modules are emitted only when a rendered descriptor references them. 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 and unrelated fields do nothing. Builds without item dependencies emit no item reader or list-state subscription code.
36
+ Same-file, directly exported same-file, and relative-imported component chains receiving a direct local-state array or keyed item are recursively specialized to intrinsic JSX before keyed-list analysis, so their component functions are not retained in the browser. A directly exported row may be reused across static and keyed JSX sites; export-list/default aliases and non-JSX references remain rejected. Missing destructured props use directly serializable primitive, plain-object, or array literal defaults during specialization. One final identifier rest binding may be expanded exactly once at the direct intrinsic root. Rows may own multiple direct-property child maps recursively, nested conditions, latest-item handlers, multiple directly serializable state slots, effects, and `null`-initialized object refs. A block-bodied keyed `map` callback may declare one top-level `const` computed from a direct child collection through the supported pure selector pipeline and then return JSX; the alias must feed exactly one nested keyed list source. Structural list sites and ancestor key paths scope hooks across updates and reorder and release them on removal. Handler modules are emitted only when a rendered descriptor references them. 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 and unrelated fields do nothing. Builds without item dependencies emit no item reader or list-state subscription code.
37
37
 
38
38
  Rendered collection selectors compile immutable local aliases and inline `(item)` or `(item, index)` pipelines over local array state or supported static named imports. One alias may feed multiple keyed list sites when every reference is a statically analyzable collection source; mixed non-collection reads fail during compilation. Supported selectors are pure `filter` with direct local-state reads, direct-property `flatMap`, and `Array.from` before a final keyed `map`; dependency commits re-evaluate the selector against the immutable build-time collection while field keys retain item identity and `key={index}` retains positional identity. Compiler-owned static filters over structural keyed rows validate source references and keys once, retain removed rows as detached prototypes, clone fresh restoration nodes, and insert only new runs without moving retained DOM. Specialized collection wrappers and keyed rows inline direct object-literal or calling-component `const` object prop spreads in source order and forward JSX children into intrinsic output. Anonymous zero-argument lazy state initializers that return a directly serializable literal lower to the same ownership path. This route-specific path is compiled out elsewhere. Compiler-owned collection state is excluded from development snapshot restoration. Dynamic/computed prop spreads, arbitrary callbacks, mutation, asynchronous selectors, imported callback functions, prototype-sensitive reads, dynamic row state initializers, non-`null` or callback refs, and recursive component cycles fail during compilation.
39
39
 
@@ -2446,7 +2446,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2446
2446
  const setters = settersByFunction.get(owner) ?? new Map()
2447
2447
  for (const [name, entries] of declarations) {
2448
2448
  for (const declaration of entries) {
2449
- const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) }, new Set(), importedCollections)
2449
+ const parts = keyedListParts(declaration.initializer, setters, declarations, (target, message) => { throw sourceNodeError(target, sourceFile, message) }, new Set(), importedCollections, factory, context)
2450
2450
  if (!parts) continue
2451
2451
  const uses = []
2452
2452
  const collectUses = node => {
@@ -2640,7 +2640,7 @@ function createKudzuTransformer(nativeHandlers, effectHandlers, reactiveBindings
2640
2640
  return
2641
2641
  }
2642
2642
  if (ts.isJsxExpression(node) && node.initializer === undefined && node.expression && (ts.isJsxElement(node.parent) || ts.isJsxFragment(node.parent))) {
2643
- const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction), jsxLocalDeclarations.get(nearestFunction(node)), fail, new Set(), importedCollections)
2643
+ const parts = listLocalUses.get(node) ?? keyedListParts(node.expression, settersForNode(node, settersByFunction), jsxLocalDeclarations.get(nearestFunction(node)), fail, new Set(), importedCollections, factory, context)
2644
2644
  if (parts) {
2645
2645
  for (const declaration of parts.aliasDeclarations ?? []) listLocalDeclarations.add(declaration)
2646
2646
  rawRenderedLists.push({ node, parts })
@@ -3157,16 +3157,29 @@ function containsRenderControl(root, knownLocals) {
3157
3157
  return found
3158
3158
  }
3159
3159
 
3160
- function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set()) {
3160
+ function keyedListParts(expression, setters, declarations, fail, aliases = new Set(), importedCollections = new Set(), factory = ts.factory, context) {
3161
3161
  const value = unwrapExpression(expression)
3162
3162
  const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
3163
3163
  if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
3164
3164
  const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()))
3165
3165
  if (!collection?.state) return undefined
3166
3166
  if (directFrom) collection.selector.push(["from", undefined])
3167
- const callback = directFrom ? value.arguments[1] : value.arguments[0]
3167
+ let callback = directFrom ? value.arguments[1] : value.arguments[0]
3168
3168
  const parameters = collectionParameters(callback, "Keyed list map", fail)
3169
- const root = unwrapExpression(callback.body)
3169
+ let root = unwrapExpression(callback.body)
3170
+ if (ts.isBlock(root)) {
3171
+ if (!context || root.statements.length !== 2 || !ts.isVariableStatement(root.statements[0]) || (root.statements[0].declarationList.flags & ts.NodeFlags.Const) === 0 || root.statements[0].declarationList.declarations.length !== 1 || !ts.isReturnStatement(root.statements[1]) || !root.statements[1].expression) fail(root, "Block-bodied keyed list map callbacks require one computed child collection const and a final JSX return")
3172
+ const declaration = root.statements[0].declarationList.declarations[0]
3173
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer) fail(declaration, "Computed child collections must initialize one const identifier")
3174
+ const computed = renderedCollectionSource(declaration.initializer, new Map(), undefined, fail, new Set())
3175
+ if (!computed?.ownerField || computed.parentItem !== parameters.item) fail(declaration.initializer, `Computed child collections must start from ${parameters.item}.<field>`)
3176
+ const returned = root.statements[1].expression
3177
+ if (identifierReferenceCount(returned, declaration.name.text) !== 1) fail(declaration.name, `Computed child collection alias "${declaration.name.text}" must be used exactly once`)
3178
+ root = unwrapExpression(substituteClone(returned, new Map([[declaration.name.text, declaration.initializer]]), factory, context))
3179
+ callback = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, root)
3180
+ ts.setParentRecursive(callback, false)
3181
+ callback.parent = value
3182
+ }
3170
3183
  if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Keyed list map callback must return one JSX element")
3171
3184
  const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
3172
3185
  const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.7.19",
3
+ "version": "0.7.20",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",