Skip to content

Commit 654d05c

Browse files
Copilotjakebailey
andauthored
Preserve native destructuring for CommonJS exports to keep iterator semantics (#3705)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
1 parent 0d82a2c commit 654d05c

21 files changed

Lines changed: 273 additions & 28 deletions

internal/transformers/moduletransforms/commonjsmodule.go

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,13 +1109,14 @@ func (tx *CommonJSModuleTransformer) transformInitializedVariable(node *ast.Vari
11091109
}
11101110
name := node.Name()
11111111
if ast.IsBindingPattern(name) {
1112-
return transformers.FlattenDestructuringAssignment(
1113-
&tx.Transformer,
1114-
tx.Visitor().VisitNode(node.AsNode()),
1115-
false, /*needsValue*/
1116-
transformers.FlattenLevelAll,
1117-
tx.createAllExportExpressions,
1118-
)
1112+
// Convert the binding pattern into an equivalent assignment expression and visit it
1113+
// as a destructuring assignment. This preserves native destructuring (and therefore
1114+
// iterator semantics for array patterns) whenever each leaf identifier can be
1115+
// substituted to an export reference. Only when the destructuring would assign to
1116+
// re-aliased or multi-exported names (where native destructuring cannot update all
1117+
// targets) does `visitDestructuringAssignment` fall back to flattening.
1118+
assignment := transformers.ConvertVariableDeclarationToAssignmentExpression(tx.EmitContext(), node)
1119+
return tx.visitDestructuringAssignment(assignment.AsBinaryExpression(), true /*valueIsDiscarded*/)
11191120
}
11201121
propertyAccess := tx.Factory().NewPropertyAccessExpression(
11211122
tx.Factory().NewIdentifier("exports"),
@@ -1452,11 +1453,22 @@ func (tx *CommonJSModuleTransformer) destructuringNeedsFlattening(node *ast.Node
14521453
}
14531454
} else if ast.IsIdentifier(node) {
14541455
exportedNames := tx.getExports(node)
1455-
threshold := 0
14561456
if transformers.IsExportName(tx.EmitContext(), node) {
1457-
threshold = 1
1457+
// The identifier is already wrapped to be an export reference; tolerate up to one
1458+
// matching export.
1459+
return len(exportedNames) > 1
1460+
}
1461+
if len(exportedNames) == 0 {
1462+
return false
14581463
}
1459-
return len(exportedNames) > threshold
1464+
// A single direct export whose export name matches the identifier text can be handled
1465+
// natively: substitution will rewrite the identifier to `exports.X`, so no flattening
1466+
// is needed. Re-aliased exports (where the export name differs from the local name) or
1467+
// multi-exported names cannot be expressed natively in a destructuring assignment.
1468+
if len(exportedNames) == 1 && tx.isDirectExport(node) && exportedNames[0].Text() == node.Text() {
1469+
return false
1470+
}
1471+
return true
14601472
}
14611473
return false
14621474
}

testdata/baselines/reference/compiler/exportDestructuring.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ export const [a, b] = arr;
1010
Object.defineProperty(exports, "__esModule", { value: true });
1111
exports.b = exports.a = void 0;
1212
const arr = [1, 2];
13-
exports.a = arr[0], exports.b = arr[1];
13+
[exports.a, exports.b] = arr;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
//// [tests/cases/compiler/exportDestructuringIterator.ts] ////
2+
3+
//// [exportDestructuringIterator.ts]
4+
declare function foo(): any;
5+
export const [A, V] = foo();
6+
export const { x, y } = foo();
7+
export const [a = 1, b = 2] = foo();
8+
export const [c, ...d] = foo();
9+
export const [, e, , f] = foo();
10+
export const [[g, h], { i, j: k }] = foo();
11+
export const { m: [n, o], p: { q } } = foo();
12+
13+
14+
//// [exportDestructuringIterator.js]
15+
"use strict";
16+
Object.defineProperty(exports, "__esModule", { value: true });
17+
exports.q = exports.o = exports.n = exports.k = exports.i = exports.h = exports.g = exports.f = exports.e = exports.d = exports.c = exports.b = exports.a = exports.y = exports.x = exports.V = exports.A = void 0;
18+
[exports.A, exports.V] = foo();
19+
({ x: exports.x, y: exports.y } = foo());
20+
[exports.a = 1, exports.b = 2] = foo();
21+
[exports.c, ...exports.d] = foo();
22+
[, exports.e, , exports.f] = foo();
23+
[[exports.g, exports.h], { i: exports.i, j: exports.k }] = foo();
24+
({ m: [exports.n, exports.o], p: { q: exports.q } } = foo());
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
//// [tests/cases/compiler/exportDestructuringIterator.ts] ////
2+
3+
=== exportDestructuringIterator.ts ===
4+
declare function foo(): any;
5+
>foo : Symbol(foo, Decl(exportDestructuringIterator.ts, 0, 0))
6+
7+
export const [A, V] = foo();
8+
>A : Symbol(A, Decl(exportDestructuringIterator.ts, 1, 14))
9+
>V : Symbol(V, Decl(exportDestructuringIterator.ts, 1, 16))
10+
>foo : Symbol(foo, Decl(exportDestructuringIterator.ts, 0, 0))
11+
12+
export const { x, y } = foo();
13+
>x : Symbol(x, Decl(exportDestructuringIterator.ts, 2, 14))
14+
>y : Symbol(y, Decl(exportDestructuringIterator.ts, 2, 17))
15+
>foo : Symbol(foo, Decl(exportDestructuringIterator.ts, 0, 0))
16+
17+
export const [a = 1, b = 2] = foo();
18+
>a : Symbol(a, Decl(exportDestructuringIterator.ts, 3, 14))
19+
>b : Symbol(b, Decl(exportDestructuringIterator.ts, 3, 20))
20+
>foo : Symbol(foo, Decl(exportDestructuringIterator.ts, 0, 0))
21+
22+
export const [c, ...d] = foo();
23+
>c : Symbol(c, Decl(exportDestructuringIterator.ts, 4, 14))
24+
>d : Symbol(d, Decl(exportDestructuringIterator.ts, 4, 16))
25+
>foo : Symbol(foo, Decl(exportDestructuringIterator.ts, 0, 0))
26+
27+
export const [, e, , f] = foo();
28+
>e : Symbol(e, Decl(exportDestructuringIterator.ts, 5, 15))
29+
>f : Symbol(f, Decl(exportDestructuringIterator.ts, 5, 20))
30+
>foo : Symbol(foo, Decl(exportDestructuringIterator.ts, 0, 0))
31+
32+
export const [[g, h], { i, j: k }] = foo();
33+
>g : Symbol(g, Decl(exportDestructuringIterator.ts, 6, 15))
34+
>h : Symbol(h, Decl(exportDestructuringIterator.ts, 6, 17))
35+
>i : Symbol(i, Decl(exportDestructuringIterator.ts, 6, 23))
36+
>k : Symbol(k, Decl(exportDestructuringIterator.ts, 6, 26))
37+
>foo : Symbol(foo, Decl(exportDestructuringIterator.ts, 0, 0))
38+
39+
export const { m: [n, o], p: { q } } = foo();
40+
>n : Symbol(n, Decl(exportDestructuringIterator.ts, 7, 19))
41+
>o : Symbol(o, Decl(exportDestructuringIterator.ts, 7, 21))
42+
>q : Symbol(q, Decl(exportDestructuringIterator.ts, 7, 30))
43+
>foo : Symbol(foo, Decl(exportDestructuringIterator.ts, 0, 0))
44+
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//// [tests/cases/compiler/exportDestructuringIterator.ts] ////
2+
3+
=== exportDestructuringIterator.ts ===
4+
declare function foo(): any;
5+
>foo : () => any
6+
7+
export const [A, V] = foo();
8+
>A : any
9+
>V : any
10+
>foo() : any
11+
>foo : () => any
12+
13+
export const { x, y } = foo();
14+
>x : any
15+
>y : any
16+
>foo() : any
17+
>foo : () => any
18+
19+
export const [a = 1, b = 2] = foo();
20+
>a : any
21+
>1 : 1
22+
>b : any
23+
>2 : 2
24+
>foo() : any
25+
>foo : () => any
26+
27+
export const [c, ...d] = foo();
28+
>c : any
29+
>d : any
30+
>foo() : any
31+
>foo : () => any
32+
33+
export const [, e, , f] = foo();
34+
>e : any
35+
>f : any
36+
>foo() : any
37+
>foo : () => any
38+
39+
export const [[g, h], { i, j: k }] = foo();
40+
>g : any
41+
>h : any
42+
>i : any
43+
>j : any
44+
>k : any
45+
>foo() : any
46+
>foo : () => any
47+
48+
export const { m: [n, o], p: { q } } = foo();
49+
>m : any
50+
>n : any
51+
>o : any
52+
>p : any
53+
>q : any
54+
>foo() : any
55+
>foo : () => any
56+

testdata/baselines/reference/submodule/compiler/bindingPatternOmittedExpressionNesting.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,8 @@ export let [,,[,[],,[],]] = undefined as any;
55

66
//// [bindingPatternOmittedExpressionNesting.js]
77
"use strict";
8-
var _a, _b, _c, _d;
98
Object.defineProperty(exports, "__esModule", { value: true });
10-
_a = undefined, _b = _a[2], _c = _b[1], _d = _b[3];
9+
[, , [, [], , []]] = undefined;
1110

1211

1312
//// [bindingPatternOmittedExpressionNesting.d.ts]

testdata/baselines/reference/submodule/compiler/declarationEmitRetainsJsdocyComments.js

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,11 @@ class Foo {
7878
}
7979
}
8080
exports.Foo = Foo;
81-
/**
82-
* comment5
83-
*/
84-
exports.someMethod = null.someMethod;
81+
({
82+
/**
83+
* comment5
84+
*/
85+
someMethod: exports.someMethod } = null);
8586

8687

8788
//// [declarationEmitRetainsJsdocyComments.d.ts]

testdata/baselines/reference/submodule/compiler/destructuringInVariableDeclarations1.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export let { toString } = 1;
1111
"use strict";
1212
Object.defineProperty(exports, "__esModule", { value: true });
1313
exports.toString = void 0;
14-
exports.toString = 1..toString;
14+
({ toString: exports.toString } = 1);
1515
{
1616
let { toFixed } = 1;
1717
}

testdata/baselines/reference/submodule/compiler/downlevelLetConst13(target=es2015).js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ exports.M = exports.bar4 = exports.bar3 = exports.bar2 = exports.bar1 = exports.
2727
// exported let\const bindings should not be renamed
2828
exports.foo = 10;
2929
exports.bar = "123";
30-
exports.bar1 = [1][0];
31-
exports.bar2 = [2][0];
32-
exports.bar3 = { a: 1 }.a;
33-
exports.bar4 = { a: 1 }.a;
30+
[exports.bar1] = [1];
31+
[exports.bar2] = [2];
32+
({ a: exports.bar3 } = { a: 1 });
33+
({ a: exports.bar4 } = { a: 1 });
3434
var M;
3535
(function (M) {
3636
M.baz = 100;

testdata/baselines/reference/submodule/compiler/exportEmptyArrayBindingPattern(module=commonjs,target=esnext).js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,5 @@ export const [] = [];
55

66
//// [exportEmptyArrayBindingPattern.js]
77
"use strict";
8-
var _a;
98
Object.defineProperty(exports, "__esModule", { value: true });
10-
_a = [];
9+
[] = [];

0 commit comments

Comments
 (0)