From a29763192b25ab5d75d469fe99cf477cbc70d6da Mon Sep 17 00:00:00 2001 From: Gideon Wald Date: Tue, 28 Oct 2025 11:58:59 -0700 Subject: [PATCH 1/5] Fix map closure destructured alias handling and add regression fixtures --- .../src/closures/transformer.ts | 48 ++++++++++-- .../map-destructured-alias.expected.tsx | 64 ++++++++++++++++ .../closures/map-destructured-alias.input.tsx | 19 +++++ ...p-destructured-computed-alias.expected.tsx | 74 +++++++++++++++++++ .../map-destructured-computed-alias.input.tsx | 25 +++++++ ...ap-destructured-numeric-alias.expected.tsx | 54 ++++++++++++++ .../map-destructured-numeric-alias.input.tsx | 18 +++++ ...map-destructured-string-alias.expected.tsx | 54 ++++++++++++++ .../map-destructured-string-alias.input.tsx | 18 +++++ 9 files changed, 368 insertions(+), 6 deletions(-) create mode 100644 packages/ts-transformers/test/fixtures/closures/map-destructured-alias.expected.tsx create mode 100644 packages/ts-transformers/test/fixtures/closures/map-destructured-alias.input.tsx create mode 100644 packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx create mode 100644 packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.input.tsx create mode 100644 packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.expected.tsx create mode 100644 packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.input.tsx create mode 100644 packages/ts-transformers/test/fixtures/closures/map-destructured-string-alias.expected.tsx create mode 100644 packages/ts-transformers/test/fixtures/closures/map-destructured-string-alias.input.tsx diff --git a/packages/ts-transformers/src/closures/transformer.ts b/packages/ts-transformers/src/closures/transformer.ts index 71e36bdf5..bee3e4b38 100644 --- a/packages/ts-transformers/src/closures/transformer.ts +++ b/packages/ts-transformers/src/closures/transformer.ts @@ -898,11 +898,49 @@ function transformDestructuredProperties( const elemName = elemParam?.name; // Collect destructured property names if the param is an object destructuring pattern - const destructuredProps = new Set(); + const destructuredProps = new Map ts.Expression>(); if (elemName && ts.isObjectBindingPattern(elemName)) { for (const element of elemName.elements) { if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) { - destructuredProps.add(element.name.text); + const alias = element.name.text; + const propertyName = element.propertyName; + + destructuredProps.set(alias, () => { + const target = factory.createIdentifier("element"); + + if (!propertyName) { + return factory.createPropertyAccessExpression( + target, + factory.createIdentifier(alias), + ); + } + + if (ts.isIdentifier(propertyName)) { + return factory.createPropertyAccessExpression( + target, + factory.createIdentifier(propertyName.text), + ); + } + + if ( + ts.isStringLiteral(propertyName) || + ts.isNumericLiteral(propertyName) + ) { + return factory.createElementAccessExpression(target, propertyName); + } + + if (ts.isComputedPropertyName(propertyName)) { + return factory.createElementAccessExpression( + target, + propertyName.expression, + ); + } + + return factory.createPropertyAccessExpression( + target, + factory.createIdentifier(alias), + ); + }); } } } @@ -930,10 +968,8 @@ function transformDestructuredProperties( !(ts.isPropertyAccessExpression(node.parent) && node.parent.name === node) ) { - return factory.createPropertyAccessExpression( - factory.createIdentifier("element"), - factory.createIdentifier(node.text), - ); + const accessFactory = destructuredProps.get(node.text)!; + return accessFactory(); } } return visitEachChildWithJsx(node, visitor, undefined); diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-alias.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-alias.expected.tsx new file mode 100644 index 000000000..1bdc9609d --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-alias.expected.tsx @@ -0,0 +1,64 @@ +import * as __ctHelpers from "commontools"; +import { recipe, UI } from "commontools"; +interface State { + items: Array<{ + price: number; + }>; + discount: number; +} +export default recipe({ + type: "object", + properties: { + items: { + type: "array", + items: { + type: "object", + properties: { + price: { + type: "number" + } + }, + required: ["price"] + } + }, + discount: { + type: "number" + } + }, + required: ["items", "discount"] +} as const satisfies __ctHelpers.JSONSchema, (state) => { + return { + [UI]: (
+ {state.items.mapWithPattern(__ctHelpers.recipe({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + element: { + type: "object", + properties: { + price: { + type: "number" + } + }, + required: ["price"] + }, + params: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + } + }, + required: ["discount"] + } + }, + required: ["element", "params"] + } as const satisfies __ctHelpers.JSONSchema, ({ element, params: { discount } }) => ({__ctHelpers.derive({ element_price: element.price, discount }, ({ element_price: _v1, discount: discount }) => _v1 * discount)})), { discount: state.discount })} +
), + }; +}); +// @ts-ignore: Internals +function h(...args: any[]) { return __ctHelpers.h.apply(null, args); } +// @ts-ignore: Internals +h.fragment = __ctHelpers.h.fragment; diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-alias.input.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-alias.input.tsx new file mode 100644 index 000000000..7661c4285 --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-alias.input.tsx @@ -0,0 +1,19 @@ +/// +import { recipe, UI } from "commontools"; + +interface State { + items: Array<{ price: number }>; + discount: number; +} + +export default recipe("MapDestructuredAlias", (state) => { + return { + [UI]: ( +
+ {state.items.map(({ price: cost }) => ( + {cost * state.discount} + ))} +
+ ), + }; +}); diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx new file mode 100644 index 000000000..5b3006404 --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx @@ -0,0 +1,74 @@ +import * as __ctHelpers from "commontools"; +import { recipe, UI } from "commontools"; +const dynamicKey = "value" as const; +interface Item { + value: number; + other: number; +} +interface State { + items: Item[]; +} +export default recipe({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + items: { + type: "array", + items: { + $ref: "#/$defs/Item" + } + } + }, + required: ["items"], + $defs: { + Item: { + type: "object", + properties: { + value: { + type: "number" + }, + other: { + type: "number" + } + }, + required: ["value", "other"] + } + } +} as const satisfies __ctHelpers.JSONSchema, (state) => { + return { + [UI]: (
+ {state.items.mapWithPattern(__ctHelpers.recipe({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + element: { + $ref: "#/$defs/Item" + }, + params: { + type: "object", + properties: {} + } + }, + required: ["element", "params"], + $defs: { + Item: { + type: "object", + properties: { + value: { + type: "number" + }, + other: { + type: "number" + } + }, + required: ["value", "other"] + } + } + } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => ({__ctHelpers.derive(element, element => element[dynamicKey])})), {})} +
), + }; +}); +// @ts-ignore: Internals +function h(...args: any[]) { return __ctHelpers.h.apply(null, args); } +// @ts-ignore: Internals +h.fragment = __ctHelpers.h.fragment; diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.input.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.input.tsx new file mode 100644 index 000000000..631fe4e41 --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.input.tsx @@ -0,0 +1,25 @@ +/// +import { recipe, UI } from "commontools"; + +const dynamicKey = "value" as const; + +interface Item { + value: number; + other: number; +} + +interface State { + items: Item[]; +} + +export default recipe("MapDestructuredComputedAlias", (state) => { + return { + [UI]: ( +
+ {state.items.map(({ [dynamicKey]: val }) => ( + {val} + ))} +
+ ), + }; +}); diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.expected.tsx new file mode 100644 index 000000000..cbe9f06f4 --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.expected.tsx @@ -0,0 +1,54 @@ +import * as __ctHelpers from "commontools"; +import { recipe, UI } from "commontools"; +interface State { + entries: Array<{ + zero: number; + }>; +} +export default recipe({ + type: "object", + properties: { + entries: { + type: "array", + items: { + type: "object", + properties: { + zero: { + type: "number" + } + }, + required: ["zero"] + } + } + }, + required: ["entries"] +} as const satisfies __ctHelpers.JSONSchema, (state) => { + return { + [UI]: (
+ {state.entries.mapWithPattern(__ctHelpers.recipe({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + element: { + type: "object", + properties: { + zero: { + type: "number" + } + }, + required: ["zero"] + }, + params: { + type: "object", + properties: {} + } + }, + required: ["element", "params"] + } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => ({element.zero})), {})} +
), + }; +}); +// @ts-ignore: Internals +function h(...args: any[]) { return __ctHelpers.h.apply(null, args); } +// @ts-ignore: Internals +h.fragment = __ctHelpers.h.fragment; diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.input.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.input.tsx new file mode 100644 index 000000000..5359b79aa --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.input.tsx @@ -0,0 +1,18 @@ +/// +import { recipe, UI } from "commontools"; + +interface State { + entries: Array<{ zero: number }>; +} + +export default recipe("MapDestructuredNumericAlias", (state) => { + return { + [UI]: ( +
+ {state.entries.map(({ zero: first }) => ( + {first} + ))} +
+ ), + }; +}); diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-string-alias.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-string-alias.expected.tsx new file mode 100644 index 000000000..34f6ae688 --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-string-alias.expected.tsx @@ -0,0 +1,54 @@ +import * as __ctHelpers from "commontools"; +import { recipe, UI } from "commontools"; +interface State { + items: Array<{ + couponCode: string; + }>; +} +export default recipe({ + type: "object", + properties: { + items: { + type: "array", + items: { + type: "object", + properties: { + couponCode: { + type: "string" + } + }, + required: ["couponCode"] + } + } + }, + required: ["items"] +} as const satisfies __ctHelpers.JSONSchema, (state) => { + return { + [UI]: (
+ {state.items.mapWithPattern(__ctHelpers.recipe({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + element: { + type: "object", + properties: { + couponCode: { + type: "string" + } + }, + required: ["couponCode"] + }, + params: { + type: "object", + properties: {} + } + }, + required: ["element", "params"] + } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => ({element.couponCode})), {})} +
), + }; +}); +// @ts-ignore: Internals +function h(...args: any[]) { return __ctHelpers.h.apply(null, args); } +// @ts-ignore: Internals +h.fragment = __ctHelpers.h.fragment; diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-string-alias.input.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-string-alias.input.tsx new file mode 100644 index 000000000..9150f861f --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-string-alias.input.tsx @@ -0,0 +1,18 @@ +/// +import { recipe, UI } from "commontools"; + +interface State { + items: Array<{ couponCode: string }>; +} + +export default recipe("MapDestructuredStringAlias", (state) => { + return { + [UI]: ( +
+ {state.items.map(({ couponCode: code }) => ( + {code} + ))} +
+ ), + }; +}); From 5dda47407ee63f5f2c456032ee1c500d8fa0ae73 Mon Sep 17 00:00:00 2001 From: Gideon Wald Date: Tue, 28 Oct 2025 12:20:06 -0700 Subject: [PATCH 2/5] Fix map closure capture name collisions and add regression fixture --- .../src/closures/transformer.ts | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/packages/ts-transformers/src/closures/transformer.ts b/packages/ts-transformers/src/closures/transformer.ts index bee3e4b38..2165a9e9b 100644 --- a/packages/ts-transformers/src/closures/transformer.ts +++ b/packages/ts-transformers/src/closures/transformer.ts @@ -1191,12 +1191,30 @@ function transformMapCallback( const captureExpressions = collectCaptures(callback, checker); // Build map of capture name -> expression - const captures = new Map(); + const captureEntries: Array<{ name: string; expr: ts.Expression }> = []; + const usedNames = new Set(["element", "index", "array", "params"]); + for (const expr of captureExpressions) { - const name = getCaptureName(expr); - if (name && !captures.has(name)) { - captures.set(name, expr); + const baseName = getCaptureName(expr); + if (!baseName) continue; + + // Skip if an existing entry captures an equivalent expression + const existing = captureEntries.find((entry) => expressionsMatch(expr, entry.expr)); + if (existing) continue; + + let candidate = baseName; + let counter = 2; + while (usedNames.has(candidate)) { + candidate = `${baseName}_${counter++}`; } + + usedNames.add(candidate); + captureEntries.push({ name: candidate, expr }); + } + + const captures = new Map(); + for (const entry of captureEntries) { + captures.set(entry.name, entry.expr); } // Build set of captured variable names From 5f8f83bf5d170a949a71bf98ebbe41adb440f81c Mon Sep 17 00:00:00 2001 From: Gideon Wald Date: Tue, 28 Oct 2025 14:04:02 -0700 Subject: [PATCH 3/5] Add fixture for map capture name collisions --- ...map-multiple-similar-captures.expected.tsx | 90 +++++++++++++++++++ .../map-multiple-similar-captures.input.tsx | 22 +++++ 2 files changed, 112 insertions(+) create mode 100644 packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.expected.tsx create mode 100644 packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.input.tsx diff --git a/packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.expected.tsx new file mode 100644 index 000000000..8fda24c4f --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.expected.tsx @@ -0,0 +1,90 @@ +import * as __ctHelpers from "commontools"; +import { recipe, UI } from "commontools"; +interface State { + items: Array<{ + price: number; + }>; + checkout: { + discount: number; + }; + upsell: { + discount: number; + }; +} +export default recipe({ + type: "object", + properties: { + items: { + type: "array", + items: { + type: "object", + properties: { + price: { + type: "number" + } + }, + required: ["price"] + } + }, + checkout: { + type: "object", + properties: { + discount: { + type: "number" + } + }, + required: ["discount"] + }, + upsell: { + type: "object", + properties: { + discount: { + type: "number" + } + }, + required: ["discount"] + } + }, + required: ["items", "checkout", "upsell"] +} as const satisfies __ctHelpers.JSONSchema, (state) => { + return { + [UI]: (
+ {state.items.mapWithPattern(__ctHelpers.recipe({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + element: { + type: "object", + properties: { + price: { + type: "number" + } + }, + required: ["price"] + }, + params: { + type: "object", + properties: { + discount: { + type: "number", + asOpaque: true + }, + discount_2: { + type: "number", + asOpaque: true + } + }, + required: ["discount", "discount_2"] + } + }, + required: ["element", "params"] + } as const satisfies __ctHelpers.JSONSchema, ({ element, params: { discount, discount_2 } }) => ( + {__ctHelpers.derive({ element_price: element.price, discount, discount_2 }, ({ element_price: _v1, discount: discount, discount_2: discount_2 }) => _v1 * discount * discount_2)} + )), { discount: state.checkout.discount, discount_2: state.upsell.discount })} +
), + }; +}); +// @ts-ignore: Internals +function h(...args: any[]) { return __ctHelpers.h.apply(null, args); } +// @ts-ignore: Internals +h.fragment = __ctHelpers.h.fragment; diff --git a/packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.input.tsx b/packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.input.tsx new file mode 100644 index 000000000..1ba648613 --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-multiple-similar-captures.input.tsx @@ -0,0 +1,22 @@ +/// +import { recipe, UI } from "commontools"; + +interface State { + items: Array<{ price: number }>; + checkout: { discount: number }; + upsell: { discount: number }; +} + +export default recipe("MultipleSimilarCaptures", (state) => { + return { + [UI]: ( +
+ {state.items.map((item) => ( + + {item.price * state.checkout.discount * state.upsell.discount} + + ))} +
+ ), + }; +}); From 013947bb73a1661eb35d86e87475ed0594b7545d Mon Sep 17 00:00:00 2001 From: Gideon Wald Date: Tue, 28 Oct 2025 14:04:17 -0700 Subject: [PATCH 4/5] fmt --- packages/ts-transformers/src/closures/transformer.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ts-transformers/src/closures/transformer.ts b/packages/ts-transformers/src/closures/transformer.ts index 2165a9e9b..ddd8644a2 100644 --- a/packages/ts-transformers/src/closures/transformer.ts +++ b/packages/ts-transformers/src/closures/transformer.ts @@ -1199,7 +1199,9 @@ function transformMapCallback( if (!baseName) continue; // Skip if an existing entry captures an equivalent expression - const existing = captureEntries.find((entry) => expressionsMatch(expr, entry.expr)); + const existing = captureEntries.find((entry) => + expressionsMatch(expr, entry.expr) + ); if (existing) continue; let candidate = baseName; From 839a59ebde303fafaa3cc1fe3ce61d4abec2b9eb Mon Sep 17 00:00:00 2001 From: Gideon Wald Date: Tue, 28 Oct 2025 14:44:03 -0700 Subject: [PATCH 5/5] Avoid recomputing computed destructured aliases and add fixture --- .../src/closures/transformer.ts | 80 ++++++++++++++++--- ...ap-computed-alias-side-effect.expected.tsx | 55 +++++++++++++ .../map-computed-alias-side-effect.input.tsx | 23 ++++++ ...p-destructured-computed-alias.expected.tsx | 5 +- ...ap-destructured-numeric-alias.expected.tsx | 12 +-- .../map-destructured-numeric-alias.input.tsx | 4 +- 6 files changed, 159 insertions(+), 20 deletions(-) create mode 100644 packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.expected.tsx create mode 100644 packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.input.tsx diff --git a/packages/ts-transformers/src/closures/transformer.ts b/packages/ts-transformers/src/closures/transformer.ts index ddd8644a2..bdc108fd7 100644 --- a/packages/ts-transformers/src/closures/transformer.ts +++ b/packages/ts-transformers/src/closures/transformer.ts @@ -897,13 +897,53 @@ function transformDestructuredProperties( ): ts.ConciseBody { const elemName = elemParam?.name; - // Collect destructured property names if the param is an object destructuring pattern + let transformedBody: ts.ConciseBody = body; + + const prependStatements = ( + statements: readonly ts.Statement[], + currentBody: ts.ConciseBody, + ): ts.ConciseBody => { + if (statements.length === 0) return currentBody; + + if (ts.isBlock(currentBody)) { + return factory.updateBlock( + currentBody, + factory.createNodeArray([ + ...statements, + ...currentBody.statements, + ]), + ); + } + + return factory.createBlock( + [ + ...statements, + factory.createReturnStatement(currentBody as ts.Expression), + ], + true, + ); + }; + const destructuredProps = new Map ts.Expression>(); + const computedInitializers: ts.VariableStatement[] = []; + const usedTempNames = new Set(); + + const registerTempName = (base: string): string => { + let candidate = `__ct_${base || "prop"}_key`; + let counter = 1; + while (usedTempNames.has(candidate)) { + candidate = `__ct_${base || "prop"}_key_${counter++}`; + } + usedTempNames.add(candidate); + return candidate; + }; + if (elemName && ts.isObjectBindingPattern(elemName)) { for (const element of elemName.elements) { if (ts.isBindingElement(element) && ts.isIdentifier(element.name)) { const alias = element.name.text; const propertyName = element.propertyName; + usedTempNames.add(alias); destructuredProps.set(alias, () => { const target = factory.createIdentifier("element"); @@ -930,9 +970,29 @@ function transformDestructuredProperties( } if (ts.isComputedPropertyName(propertyName)) { + const tempName = registerTempName(alias); + const tempIdentifier = factory.createIdentifier(tempName); + + computedInitializers.push( + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + tempIdentifier, + undefined, + undefined, + propertyName.expression, + ), + ], + ts.NodeFlags.Const, + ), + ), + ); + return factory.createElementAccessExpression( target, - propertyName.expression, + tempIdentifier, ); } @@ -945,7 +1005,6 @@ function transformDestructuredProperties( } } - // Collect array destructured identifiers: [date, pizza] -> {date: 0, pizza: 1} const arrayDestructuredVars = new Map(); if (elemName && ts.isArrayBindingPattern(elemName)) { let index = 0; @@ -957,12 +1016,9 @@ function transformDestructuredProperties( } } - // If param was object-destructured, replace property references with element.prop if (destructuredProps.size > 0) { const visitor: ts.Visitor = (node) => { if (ts.isIdentifier(node) && destructuredProps.has(node.text)) { - // Check if this identifier is not part of a property access already - // (e.g., don't transform the 'x' in 'something.x') if ( !node.parent || !(ts.isPropertyAccessExpression(node.parent) && @@ -974,16 +1030,14 @@ function transformDestructuredProperties( } return visitEachChildWithJsx(node, visitor, undefined); }; - return ts.visitNode(body, visitor) as ts.ConciseBody; + transformedBody = ts.visitNode(transformedBody, visitor) as ts.ConciseBody; } - // If param was array-destructured, replace variable references with element[index] if (arrayDestructuredVars.size > 0) { const visitor: ts.Visitor = (node) => { if (ts.isIdentifier(node)) { const index = arrayDestructuredVars.get(node.text); if (index !== undefined) { - // Check if this identifier is not part of a property access already if ( !node.parent || !(ts.isPropertyAccessExpression(node.parent) && @@ -998,10 +1052,14 @@ function transformDestructuredProperties( } return visitEachChildWithJsx(node, visitor, undefined); }; - return ts.visitNode(body, visitor) as ts.ConciseBody; + transformedBody = ts.visitNode(transformedBody, visitor) as ts.ConciseBody; } - return body; + if (computedInitializers.length > 0) { + transformedBody = prependStatements(computedInitializers, transformedBody); + } + + return transformedBody; } /** diff --git a/packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.expected.tsx new file mode 100644 index 000000000..8170239e3 --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.expected.tsx @@ -0,0 +1,55 @@ +import * as __ctHelpers from "commontools"; +import { recipe, UI } from "commontools"; +let keyCounter = 0; +function nextKey() { + return `value-${keyCounter++}`; +} +interface State { + items: Array>; +} +export default recipe({ + type: "object", + properties: { + items: { + type: "array", + items: { + type: "object", + properties: {}, + additionalProperties: { + type: "number" + } + } + } + }, + required: ["items"] +} as const satisfies __ctHelpers.JSONSchema, (state) => { + return { + [UI]: (
+ {state.items.mapWithPattern(__ctHelpers.recipe({ + $schema: "https://json-schema.org/draft/2020-12/schema", + type: "object", + properties: { + element: { + type: "object", + properties: {}, + additionalProperties: { + type: "number" + } + }, + params: { + type: "object", + properties: {} + } + }, + required: ["element", "params"] + } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => { + const __ct_amount_key = nextKey(); + return ({__ctHelpers.derive({ element, __ct_amount_key }, ({ element: element, __ct_amount_key: __ct_amount_key }) => element[__ct_amount_key])}); + }), {})} +
), + }; +}); +// @ts-ignore: Internals +function h(...args: any[]) { return __ctHelpers.h.apply(null, args); } +// @ts-ignore: Internals +h.fragment = __ctHelpers.h.fragment; diff --git a/packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.input.tsx b/packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.input.tsx new file mode 100644 index 000000000..93169e621 --- /dev/null +++ b/packages/ts-transformers/test/fixtures/closures/map-computed-alias-side-effect.input.tsx @@ -0,0 +1,23 @@ +/// +import { recipe, UI } from "commontools"; + +let keyCounter = 0; +function nextKey() { + return `value-${keyCounter++}`; +} + +interface State { + items: Array>; +} + +export default recipe("ComputedAliasSideEffect", (state) => { + return { + [UI]: ( +
+ {state.items.map(({ [nextKey()]: amount }) => ( + {amount} + ))} +
+ ), + }; +}); diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx index 5b3006404..16927be2d 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-computed-alias.expected.tsx @@ -64,7 +64,10 @@ export default recipe({ required: ["value", "other"] } } - } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => ({__ctHelpers.derive(element, element => element[dynamicKey])})), {})} + } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => { + const __ct_val_key = dynamicKey; + return ({__ctHelpers.derive({ element, __ct_val_key }, ({ element: element, __ct_val_key: __ct_val_key }) => element[__ct_val_key])}); + }), {})} ), }; }); diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.expected.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.expected.tsx index cbe9f06f4..61bd72420 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.expected.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.expected.tsx @@ -2,7 +2,7 @@ import * as __ctHelpers from "commontools"; import { recipe, UI } from "commontools"; interface State { entries: Array<{ - zero: number; + 0: number; }>; } export default recipe({ @@ -13,11 +13,11 @@ export default recipe({ items: { type: "object", properties: { - zero: { + 0: { type: "number" } }, - required: ["zero"] + required: ["0"] } } }, @@ -32,11 +32,11 @@ export default recipe({ element: { type: "object", properties: { - zero: { + 0: { type: "number" } }, - required: ["zero"] + required: ["0"] }, params: { type: "object", @@ -44,7 +44,7 @@ export default recipe({ } }, required: ["element", "params"] - } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => ({element.zero})), {})} + } as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => ({element[0]})), {})} ), }; }); diff --git a/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.input.tsx b/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.input.tsx index 5359b79aa..b9722c92f 100644 --- a/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.input.tsx +++ b/packages/ts-transformers/test/fixtures/closures/map-destructured-numeric-alias.input.tsx @@ -2,14 +2,14 @@ import { recipe, UI } from "commontools"; interface State { - entries: Array<{ zero: number }>; + entries: Array<{ 0: number }>; } export default recipe("MapDestructuredNumericAlias", (state) => { return { [UI]: (
- {state.entries.map(({ zero: first }) => ( + {state.entries.map(({ 0: first }) => ( {first} ))}