Skip to content

Commit ee8b951

Browse files
committed
fix for case with both destructured local bindings as well as computed keys
1 parent bd012a7 commit ee8b951

File tree

3 files changed

+239
-0
lines changed

3 files changed

+239
-0
lines changed

packages/ts-transformers/src/closures/transformer.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ interface ElementBindingAnalysis {
479479
readonly bindingName: ts.BindingName;
480480
readonly elementIdentifier: ts.Identifier;
481481
readonly computedAliases: readonly ComputedAliasInfo[];
482+
readonly destructureStatement?: ts.Statement;
482483
}
483484
function collectComputedAliases(
484485
name: ts.BindingName,
@@ -570,6 +571,95 @@ function collectComputedAliases(
570571
}
571572
}
572573

574+
function cloneWithoutComputedProperties(
575+
name: ts.BindingName,
576+
context: TransformationContext,
577+
used: Set<string>,
578+
): ts.BindingName | undefined {
579+
const { factory } = context;
580+
581+
if (ts.isIdentifier(name)) {
582+
return factory.createIdentifier(name.text);
583+
}
584+
585+
if (ts.isObjectBindingPattern(name)) {
586+
const elements: ts.BindingElement[] = [];
587+
588+
for (const element of name.elements) {
589+
if (
590+
element.propertyName &&
591+
ts.isComputedPropertyName(element.propertyName)
592+
) {
593+
continue;
594+
}
595+
596+
let clonedName: ts.BindingName | undefined;
597+
if (ts.isIdentifier(element.name)) {
598+
clonedName = factory.createIdentifier(element.name.text);
599+
} else if (isBindingPattern(element.name)) {
600+
clonedName = cloneWithoutComputedProperties(
601+
element.name,
602+
context,
603+
used,
604+
);
605+
}
606+
607+
if (!clonedName && !element.dotDotDotToken) {
608+
continue;
609+
}
610+
611+
elements.push(
612+
factory.createBindingElement(
613+
element.dotDotDotToken,
614+
element.propertyName,
615+
clonedName ?? element.name,
616+
element.initializer as ts.Expression | undefined,
617+
),
618+
);
619+
}
620+
621+
if (elements.length === 0) {
622+
return undefined;
623+
}
624+
625+
return factory.createObjectBindingPattern(elements);
626+
}
627+
628+
if (ts.isArrayBindingPattern(name)) {
629+
const newElements = name.elements.map((element) => {
630+
if (ts.isOmittedExpression(element)) {
631+
return element;
632+
}
633+
if (ts.isBindingElement(element)) {
634+
let clonedName: ts.BindingName | undefined;
635+
if (ts.isIdentifier(element.name)) {
636+
clonedName = factory.createIdentifier(element.name.text);
637+
} else if (isBindingPattern(element.name)) {
638+
clonedName = cloneWithoutComputedProperties(
639+
element.name,
640+
context,
641+
used,
642+
);
643+
}
644+
if (!clonedName && !element.dotDotDotToken) {
645+
return element;
646+
}
647+
return factory.createBindingElement(
648+
element.dotDotDotToken,
649+
element.propertyName,
650+
clonedName ?? element.name,
651+
element.initializer as ts.Expression | undefined,
652+
);
653+
}
654+
return element;
655+
});
656+
657+
return factory.createArrayBindingPattern(newElements);
658+
}
659+
660+
return undefined;
661+
}
662+
573663
function analyzeElementBinding(
574664
elemParam: ts.ParameterDeclaration | undefined,
575665
captureTree: CaptureTreeMap,
@@ -628,10 +718,40 @@ function analyzeElementBinding(
628718
captureTree.has("element") ? "__ct_element" : "element",
629719
);
630720

721+
const residualPattern = cloneWithoutComputedProperties(
722+
elemParam.name,
723+
context,
724+
used,
725+
);
726+
727+
let destructureStatement: ts.Statement | undefined;
728+
if (residualPattern) {
729+
const normalized = normalizeBindingName(
730+
residualPattern,
731+
factory,
732+
used,
733+
);
734+
destructureStatement = factory.createVariableStatement(
735+
undefined,
736+
factory.createVariableDeclarationList(
737+
[
738+
factory.createVariableDeclaration(
739+
normalized,
740+
undefined,
741+
undefined,
742+
factory.createIdentifier(elementIdentifier.text),
743+
),
744+
],
745+
ts.NodeFlags.Const,
746+
),
747+
);
748+
}
749+
631750
return {
632751
bindingName: elementIdentifier,
633752
elementIdentifier,
634753
computedAliases: aliasBucket,
754+
destructureStatement,
635755
};
636756
}
637757

@@ -739,6 +859,10 @@ function rewriteCallbackBody(
739859

740860
const prologue: ts.Statement[] = [];
741861

862+
if (analysis.destructureStatement) {
863+
prologue.push(analysis.destructureStatement);
864+
}
865+
742866
for (const info of analysis.computedAliases) {
743867
prologue.push(
744868
factory.createVariableStatement(
@@ -1332,6 +1456,7 @@ function createRecipeCallWithParams(
13321456
{
13331457
bindingName: elementAnalysis.bindingName,
13341458
elementIdentifier: elementAnalysis.elementIdentifier,
1459+
destructureStatement: elementAnalysis.destructureStatement,
13351460
computedAliases: visitedAliases,
13361461
},
13371462
context,
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import * as __ctHelpers from "commontools";
2+
import { recipe, UI } from "commontools";
3+
function dynamicKey(): "value" {
4+
return "value";
5+
}
6+
interface Item {
7+
foo: number;
8+
value: number;
9+
}
10+
interface State {
11+
items: Item[];
12+
}
13+
export default recipe({
14+
$schema: "https://json-schema.org/draft/2020-12/schema",
15+
type: "object",
16+
properties: {
17+
items: {
18+
type: "array",
19+
items: {
20+
$ref: "#/$defs/Item"
21+
}
22+
}
23+
},
24+
required: ["items"],
25+
$defs: {
26+
Item: {
27+
type: "object",
28+
properties: {
29+
foo: {
30+
type: "number"
31+
},
32+
value: {
33+
type: "number"
34+
}
35+
},
36+
required: ["foo", "value"]
37+
}
38+
}
39+
} as const satisfies __ctHelpers.JSONSchema, (state) => {
40+
return {
41+
[UI]: (<div>
42+
{state.items.mapWithPattern(__ctHelpers.recipe({
43+
$schema: "https://json-schema.org/draft/2020-12/schema",
44+
type: "object",
45+
properties: {
46+
element: {
47+
$ref: "#/$defs/Item"
48+
},
49+
params: {
50+
type: "object",
51+
properties: {}
52+
}
53+
},
54+
required: ["element", "params"],
55+
$defs: {
56+
Item: {
57+
type: "object",
58+
properties: {
59+
foo: {
60+
type: "number"
61+
},
62+
value: {
63+
type: "number"
64+
}
65+
},
66+
required: ["foo", "value"]
67+
}
68+
}
69+
} as const satisfies __ctHelpers.JSONSchema, ({ element, params: {} }) => {
70+
const __ct_val_key = dynamicKey();
71+
const { foo } = element;
72+
const val = __ctHelpers.derive({
73+
element,
74+
__ct_val_key
75+
}, ({ element: element, __ct_val_key: __ct_val_key }) => element[__ct_val_key]);
76+
return (<span>{__ctHelpers.derive({
77+
foo: foo,
78+
val: val
79+
}, ({ foo, val }) => foo + val)}</span>);
80+
}), {})}
81+
</div>),
82+
};
83+
});
84+
// @ts-ignore: Internals
85+
function h(...args: any[]) { return __ctHelpers.h.apply(null, args); }
86+
// @ts-ignore: Internals
87+
h.fragment = __ctHelpers.h.fragment;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/// <cts-enable />
2+
import { recipe, UI } from "commontools";
3+
4+
function dynamicKey(): "value" {
5+
return "value";
6+
}
7+
8+
interface Item {
9+
foo: number;
10+
value: number;
11+
}
12+
13+
interface State {
14+
items: Item[];
15+
}
16+
17+
export default recipe<State>("MapComputedAliasWithPlainBinding", (state) => {
18+
return {
19+
[UI]: (
20+
<div>
21+
{state.items.map(({ foo, [dynamicKey()]: val }) => (
22+
<span>{foo + val}</span>
23+
))}
24+
</div>
25+
),
26+
};
27+
});

0 commit comments

Comments
 (0)