@kudzujs/core 0.8.7 → 0.8.8

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.8.x`: the compiler API and supported TSX surface may change.
12
12
 
13
- **Latest release: 0.8.7 - Reactive keyed row selection.** Flat keyed rows may combine their current item or index with direct primitive parent state in pure text and attribute expressions, enabling selected classes and ARIA state without remounting rows. Read the [release notes](./RELEASES.md#087---reactive-keyed-row-selection) or open the [release page](https://kudzujs.cloud/releases/0.8.7).
13
+ **Latest release: 0.8.8 - Conditional keyed map roots.** Expression-bodied keyed maps may return `condition && <Row />` or `condition ? <Row /> : null`; Kudzu lowers the condition to its existing filter path so omitted rows clean up and re-entry starts fresh. Read the [release notes](./RELEASES.md#088---conditional-keyed-map-roots) or open the [release page](https://kudzujs.cloud/releases/0.8.8).
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,31 @@
1
1
  # Kudzu Releases
2
2
 
3
+ ## 0.8.8 - Conditional keyed map roots
4
+
5
+ Kudzu 0.8.8 compiles ordinary expression-bodied conditional keyed maps through the existing pure collection selector path.
6
+
7
+ ### New in 0.8.8
8
+
9
+ - One-parameter keyed maps may return `condition && <Row />` or `condition ? <Row /> : null`.
10
+ - Top-level conditions may combine the current item with direct primitive parent state; nested maps support item-only conditions.
11
+ - Omitted rows own no DOM or hooks. True-to-false transitions release row state, effects, and refs; re-entry creates fresh ownership.
12
+ - Retained siblings preserve keyed DOM identity through condition changes, insertion, and reorder.
13
+ - Same-file and relative row components remain compiler-specialized before the existing list runtime receives the normalized filter.
14
+ - Imported build-known item-only conditions still fold to complete zero-JavaScript HTML.
15
+ - The React Notes migration restores its ordinary `notes.map(note => activeId === note.id && <Editor />)` source shape.
16
+ - Map indexes, alternate JSX fallbacks, block-bodied conditional maps, arbitrary captures, and impure predicates remain diagnosed.
17
+ - The complete suite passes 134/134 tests.
18
+
19
+ ### Boundary
20
+
21
+ Conditional map callbacks must be synchronous expression arrows with exactly one item parameter. Indexes are rejected because implicit filtering changes their meaning. The feature adds no runtime capability, VDOM, hydration, or component rerenderer.
22
+
23
+ ### Upgrade
24
+
25
+ ```bash
26
+ npm install @kudzujs/core@^0.8.8
27
+ ```
28
+
3
29
  ## 0.8.7 - Reactive keyed row selection
4
30
 
5
31
  Kudzu 0.8.7 lets a flat keyed row combine its current item or index with direct primitive parent state in pure text and attribute expressions.
@@ -4125,7 +4125,7 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4125
4125
  const value = unwrapExpression(expression)
4126
4126
  const directFrom = isArrayFromCall(value) && value.arguments.length === 2 && containsJsx(value.arguments[1])
4127
4127
  if (!directFrom && (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map")) return undefined
4128
- const collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()), importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4128
+ let collection = renderedCollectionSource(directFrom ? value.arguments[0] : value.expression.expression, setters, declarations, fail, aliases, importedCollections, new Set(setters.values()), importedCollectionTransforms, factory, context, calculatedCollection, staticCollection)
4129
4129
  if (!collection?.state && !collection?.calculation) return undefined
4130
4130
  if (directFrom) collection.selector.push(["from", undefined])
4131
4131
  let callback = directFrom ? value.arguments[1] : value.arguments[0]
@@ -4144,6 +4144,8 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4144
4144
  ts.setParentRecursive(callback, false)
4145
4145
  callback.parent = value
4146
4146
  }
4147
+ const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(setters.values()), factory, value)
4148
+ if (conditional) ({ callback, root, collection } = conditional)
4147
4149
  if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Keyed list map callback must return one JSX element")
4148
4150
  const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
4149
4151
  const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
@@ -4157,11 +4159,13 @@ function keyedListParts(expression, setters, declarations, fail, aliases = new S
4157
4159
  function nestedKeyedListParts(expression, parentItem, fail) {
4158
4160
  const value = unwrapExpression(expression)
4159
4161
  if (!ts.isCallExpression(value) || value.arguments.length !== 1 || !ts.isPropertyAccessExpression(value.expression) || value.expression.name.text !== "map") return undefined
4160
- const collection = renderedCollectionSource(value.expression.expression, new Map(), undefined, fail, new Set())
4162
+ let collection = renderedCollectionSource(value.expression.expression, new Map(), undefined, fail, new Set())
4161
4163
  if (!collection?.ownerField || collection.parentItem !== parentItem) return undefined
4162
- const callback = value.arguments[0]
4164
+ let callback = value.arguments[0]
4163
4165
  const parameters = collectionParameters(callback, "Nested keyed list map", fail)
4164
- const root = unwrapExpression(callback.body)
4166
+ let root = unwrapExpression(callback.body)
4167
+ const conditional = conditionalKeyedMapRoot(callback, root, parameters, collection, fail, new Set(), ts.factory, value)
4168
+ if (conditional) ({ callback, root, collection } = conditional)
4165
4169
  if (!ts.isJsxElement(root) && !ts.isJsxSelfClosingElement(root)) fail(callback.body, "Nested keyed list map callback must return one JSX element")
4166
4170
  const attributes = ts.isJsxElement(root) ? root.openingElement.attributes : root.attributes
4167
4171
  const key = attributes.properties.find(attribute => ts.isJsxAttribute(attribute) && ts.isIdentifier(attribute.name) && attribute.name.text === "key")
@@ -4172,6 +4176,28 @@ function nestedKeyedListParts(expression, parentItem, fail) {
4172
4176
  return { ...collection, callback, root, item: parameters.item, index: parameters.index, indexed: Boolean(parameters.index), keyField: positional ? null : keyField }
4173
4177
  }
4174
4178
 
4179
+ function conditionalKeyedMapRoot(callback, root, parameters, collection, fail, stateNames, factory, parent) {
4180
+ let condition
4181
+ let rendered
4182
+ if (ts.isBinaryExpression(root) && root.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken && (ts.isJsxElement(unwrapExpression(root.right)) || ts.isJsxSelfClosingElement(unwrapExpression(root.right)))) {
4183
+ condition = root.left
4184
+ rendered = unwrapExpression(root.right)
4185
+ } else if (ts.isConditionalExpression(root) && (ts.isJsxElement(unwrapExpression(root.whenTrue)) || ts.isJsxSelfClosingElement(unwrapExpression(root.whenTrue)))) {
4186
+ if (unwrapExpression(root.whenFalse).kind !== ts.SyntaxKind.NullKeyword) fail(root.whenFalse, "Conditional keyed map callbacks require condition ? <Element> : null")
4187
+ condition = root.condition
4188
+ rendered = unwrapExpression(root.whenTrue)
4189
+ } else {
4190
+ return undefined
4191
+ }
4192
+ if (parameters.index) fail(callback.parameters[1], "Conditional keyed map callbacks cannot use a map index because filtering changes index semantics; use an explicit filter(...).map((item, index) => ...) when a filtered index is intended")
4193
+ const selectorStates = new Set(collection.selectorStates)
4194
+ const selector = collectionExpression(condition, parameters, fail, stateNames, selectorStates)
4195
+ const normalized = factory.updateArrowFunction(callback, callback.modifiers, callback.typeParameters, callback.parameters, callback.type, callback.equalsGreaterThanToken, rendered)
4196
+ ts.setParentRecursive(normalized, false)
4197
+ normalized.parent = parent
4198
+ return { callback: normalized, root: rendered, collection: { ...collection, selector: [...collection.selector, ["filter", selector]], selectorStates } }
4199
+ }
4200
+
4175
4201
  function renderedCollectionSource(expression, setters, declarations, fail, aliases, importedCollections = new Set(), stateNames = new Set(), importedCollectionTransforms = new Map(), factory = ts.factory, context, calculatedCollection, staticCollection) {
4176
4202
  const value = unwrapExpression(expression)
4177
4203
  if (ts.isIdentifier(value)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kudzujs/core",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "description": "HTML-first TSX framework with synchronous state semantics and no virtual DOM",
5
5
  "type": "module",
6
6
  "license": "MIT",