Skip to content

Commit 1f8959f

Browse files
Josh-Cenarbuckton
andauthored
fix: avoid downleveled dynamic import closing over specifier expression (#49663)
* fix: evaluate dynamic import specifier expressions synchronously * refactor * Update src/compiler/transformers/module/module.ts Co-authored-by: Ron Buckton <[email protected]> * [Experiment] Co-authored-by: Ron Buckton <[email protected]>
1 parent 11066b2 commit 1f8959f

14 files changed

+180
-42
lines changed

src/compiler/transformers/module/module.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -721,7 +721,7 @@ namespace ts {
721721
return createImportCallExpressionUMD(argument ?? factory.createVoidZero(), containsLexicalThis);
722722
case ModuleKind.CommonJS:
723723
default:
724-
return createImportCallExpressionCommonJS(argument, containsLexicalThis);
724+
return createImportCallExpressionCommonJS(argument);
725725
}
726726
}
727727

@@ -745,7 +745,7 @@ namespace ts {
745745
return factory.createConditionalExpression(
746746
/*condition*/ factory.createIdentifier("__syncRequire"),
747747
/*questionToken*/ undefined,
748-
/*whenTrue*/ createImportCallExpressionCommonJS(arg, containsLexicalThis),
748+
/*whenTrue*/ createImportCallExpressionCommonJS(arg),
749749
/*colonToken*/ undefined,
750750
/*whenFalse*/ createImportCallExpressionAMD(argClone, containsLexicalThis)
751751
);
@@ -755,7 +755,7 @@ namespace ts {
755755
return factory.createComma(factory.createAssignment(temp, arg), factory.createConditionalExpression(
756756
/*condition*/ factory.createIdentifier("__syncRequire"),
757757
/*questionToken*/ undefined,
758-
/*whenTrue*/ createImportCallExpressionCommonJS(temp, containsLexicalThis),
758+
/*whenTrue*/ createImportCallExpressionCommonJS(temp, /* isInlineable */ true),
759759
/*colonToken*/ undefined,
760760
/*whenFalse*/ createImportCallExpressionAMD(temp, containsLexicalThis)
761761
));
@@ -820,14 +820,25 @@ namespace ts {
820820
return promise;
821821
}
822822

823-
function createImportCallExpressionCommonJS(arg: Expression | undefined, containsLexicalThis: boolean): Expression {
824-
// import("./blah")
823+
function createImportCallExpressionCommonJS(arg: Expression | undefined, isInlineable?: boolean): Expression {
824+
// import(x)
825825
// emit as
826-
// Promise.resolve().then(function () { return require(x); }) /*CommonJs Require*/
826+
// var _a;
827+
// (_a = x, Promise.resolve().then(() => require(_a)) /*CommonJs Require*/
827828
// We have to wrap require in then callback so that require is done in asynchronously
828829
// if we simply do require in resolve callback in Promise constructor. We will execute the loading immediately
829-
const promiseResolveCall = factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("Promise"), "resolve"), /*typeArguments*/ undefined, /*argumentsArray*/ []);
830-
let requireCall: Expression = factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, arg ? [arg] : []);
830+
// If the arg is not inlineable, we have to evaluate it in the current scope with a temp var
831+
const temp = arg && !isSimpleInlineableExpression(arg) && !isInlineable ? factory.createTempVariable(hoistVariableDeclaration) : undefined;
832+
const promiseResolveCall = factory.createCallExpression(
833+
factory.createPropertyAccessExpression(factory.createIdentifier("Promise"), "resolve"),
834+
/*typeArguments*/ undefined,
835+
/*argumentsArray*/ [],
836+
);
837+
let requireCall: Expression = factory.createCallExpression(
838+
factory.createIdentifier("require"),
839+
/*typeArguments*/ undefined,
840+
temp ? [temp] : arg ? [arg] : [],
841+
);
831842
if (getESModuleInterop(compilerOptions)) {
832843
requireCall = emitHelpers().createImportStarHelper(requireCall);
833844
}
@@ -851,16 +862,11 @@ namespace ts {
851862
/*parameters*/ [],
852863
/*type*/ undefined,
853864
factory.createBlock([factory.createReturnStatement(requireCall)]));
854-
855-
// if there is a lexical 'this' in the import call arguments, ensure we indicate
856-
// that this new function expression indicates it captures 'this' so that the
857-
// es2015 transformer will properly substitute 'this' with '_this'.
858-
if (containsLexicalThis) {
859-
setEmitFlags(func, EmitFlags.CapturesThis);
860-
}
861865
}
862866

863-
return factory.createCallExpression(factory.createPropertyAccessExpression(promiseResolveCall, "then"), /*typeArguments*/ undefined, [func]);
867+
const downleveledImport = factory.createCallExpression(factory.createPropertyAccessExpression(promiseResolveCall, "then"), /*typeArguments*/ undefined, [func]);
868+
869+
return temp === undefined ? downleveledImport : factory.createCommaListExpression([factory.createAssignment(temp, arg!), downleveledImport]);
864870
}
865871

866872
function getHelperExpressionForExport(node: ExportDeclaration, innerExpr: Expression) {

tests/baselines/reference/asyncImportNestedYield.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,13 @@ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _ar
4646
function foo() {
4747
return __asyncGenerator(this, arguments, function foo_1() {
4848
return __generator(this, function (_a) {
49+
var _b, _c;
4950
switch (_a.label) {
5051
case 0: return [4 /*yield*/, __await("foo")];
5152
case 1: return [4 /*yield*/, _a.sent()];
52-
case 2: return [4 /*yield*/, __await.apply(void 0, [Promise.resolve().then(function () { return require(_a.sent()); })])];
53+
case 2: return [4 /*yield*/, __await.apply(void 0, [(_b = _a.sent(), Promise.resolve().then(function () { return require(_b); }))])];
5354
case 3:
54-
Promise.resolve().then(function () { return require((_a.sent())["default"]); });
55+
_c = (_a.sent())["default"], Promise.resolve().then(function () { return require(_c); });
5556
return [2 /*return*/];
5657
}
5758
});
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//// [dynamicImportEvaluateSpecifier.ts]
2+
// https://github.com/microsoft/TypeScript/issues/48285
3+
let i = 0;
4+
5+
import(String(i++));
6+
import(String(i++));
7+
8+
const getPath = async () => {
9+
/* in reality this would do some async FS operation, or a web request */
10+
return "/root/my/cool/path";
11+
};
12+
13+
const someFunction = async () => {
14+
const result = await import(await getPath());
15+
};
16+
17+
18+
//// [dynamicImportEvaluateSpecifier.js]
19+
var _a, _b;
20+
// https://github.com/microsoft/TypeScript/issues/48285
21+
let i = 0;
22+
_a = String(i++), Promise.resolve().then(() => require(_a));
23+
_b = String(i++), Promise.resolve().then(() => require(_b));
24+
const getPath = async () => {
25+
/* in reality this would do some async FS operation, or a web request */
26+
return "/root/my/cool/path";
27+
};
28+
const someFunction = async () => {
29+
var _a;
30+
const result = await (_a = await getPath(), Promise.resolve().then(() => require(_a)));
31+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
=== tests/cases/compiler/dynamicImportEvaluateSpecifier.ts ===
2+
// https://github.com/microsoft/TypeScript/issues/48285
3+
let i = 0;
4+
>i : Symbol(i, Decl(dynamicImportEvaluateSpecifier.ts, 1, 3))
5+
6+
import(String(i++));
7+
>String : Symbol(String, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --) ... and 3 more)
8+
>i : Symbol(i, Decl(dynamicImportEvaluateSpecifier.ts, 1, 3))
9+
10+
import(String(i++));
11+
>String : Symbol(String, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --), Decl(lib.es2015.core.d.ts, --, --), Decl(lib.es2015.iterable.d.ts, --, --) ... and 3 more)
12+
>i : Symbol(i, Decl(dynamicImportEvaluateSpecifier.ts, 1, 3))
13+
14+
const getPath = async () => {
15+
>getPath : Symbol(getPath, Decl(dynamicImportEvaluateSpecifier.ts, 6, 5))
16+
17+
/* in reality this would do some async FS operation, or a web request */
18+
return "/root/my/cool/path";
19+
};
20+
21+
const someFunction = async () => {
22+
>someFunction : Symbol(someFunction, Decl(dynamicImportEvaluateSpecifier.ts, 11, 5))
23+
24+
const result = await import(await getPath());
25+
>result : Symbol(result, Decl(dynamicImportEvaluateSpecifier.ts, 12, 6))
26+
>getPath : Symbol(getPath, Decl(dynamicImportEvaluateSpecifier.ts, 6, 5))
27+
28+
};
29+
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
=== tests/cases/compiler/dynamicImportEvaluateSpecifier.ts ===
2+
// https://github.com/microsoft/TypeScript/issues/48285
3+
let i = 0;
4+
>i : number
5+
>0 : 0
6+
7+
import(String(i++));
8+
>import(String(i++)) : Promise<any>
9+
>String(i++) : string
10+
>String : StringConstructor
11+
>i++ : number
12+
>i : number
13+
14+
import(String(i++));
15+
>import(String(i++)) : Promise<any>
16+
>String(i++) : string
17+
>String : StringConstructor
18+
>i++ : number
19+
>i : number
20+
21+
const getPath = async () => {
22+
>getPath : () => Promise<string>
23+
>async () => { /* in reality this would do some async FS operation, or a web request */ return "/root/my/cool/path";} : () => Promise<string>
24+
25+
/* in reality this would do some async FS operation, or a web request */
26+
return "/root/my/cool/path";
27+
>"/root/my/cool/path" : "/root/my/cool/path"
28+
29+
};
30+
31+
const someFunction = async () => {
32+
>someFunction : () => Promise<void>
33+
>async () => { const result = await import(await getPath());} : () => Promise<void>
34+
35+
const result = await import(await getPath());
36+
>result : any
37+
>await import(await getPath()) : any
38+
>import(await getPath()) : Promise<any>
39+
>await getPath() : string
40+
>getPath() : Promise<string>
41+
>getPath : () => Promise<string>
42+
43+
};
44+

tests/baselines/reference/dynamicImportTrailingComma.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ const path = './foo';
33
import(path,);
44

55
//// [dynamicImportTrailingComma.js]
6+
var _a;
67
var path = './foo';
7-
Promise.resolve().then(function () { return require(path); });
8+
_a = path, Promise.resolve().then(function () { return require(_a); });

tests/baselines/reference/importCallExpressionDeclarationEmit1.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ function returnDynamicLoad(path: string) {
1515
}
1616

1717
//// [importCallExpressionDeclarationEmit1.js]
18-
Promise.resolve().then(() => require(getSpecifier()));
19-
var p0 = Promise.resolve().then(() => require(`${directory}\\${moduleFile}`));
20-
var p1 = Promise.resolve().then(() => require(getSpecifier()));
21-
const p2 = Promise.resolve().then(() => require(whatToLoad ? getSpecifier() : "defaulPath"));
18+
var _a, _b, _c, _d;
19+
_a = getSpecifier(), Promise.resolve().then(() => require(_a));
20+
var p0 = (_b = `${directory}\\${moduleFile}`, Promise.resolve().then(() => require(_b)));
21+
var p1 = (_c = getSpecifier(), Promise.resolve().then(() => require(_c)));
22+
const p2 = (_d = whatToLoad ? getSpecifier() : "defaulPath", Promise.resolve().then(() => require(_d)));
2223
function returnDynamicLoad(path) {
23-
return Promise.resolve().then(() => require(path));
24+
var _a;
25+
return _a = path, Promise.resolve().then(() => require(_a));
2426
}
2527

2628

tests/baselines/reference/importCallExpressionGrammarError.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ const p2 = import();
1010
const p4 = import("pathToModule", "secondModule");
1111

1212
//// [importCallExpressionGrammarError.js]
13+
var _a, _b;
1314
var a = ["./0"];
14-
Promise.resolve().then(() => require(...["PathModule"]));
15-
var p1 = Promise.resolve().then(() => require(...a));
15+
_a = (...["PathModule"]), Promise.resolve().then(() => require(_a));
16+
var p1 = (_b = (...a), Promise.resolve().then(() => require(_b)));
1617
const p2 = Promise.resolve().then(() => require());
1718
const p4 = Promise.resolve().then(() => require("pathToModule"));

tests/baselines/reference/importCallExpressionNestedCJS.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
2424
};
2525
function foo() {
2626
return __awaiter(this, void 0, void 0, function* () {
27-
return yield Promise.resolve().then(() => require((yield Promise.resolve().then(() => require("./foo"))).default));
27+
var _a;
28+
return yield (_a = (yield Promise.resolve().then(() => require("./foo"))).default, Promise.resolve().then(() => require(_a)));
2829
});
2930
}

tests/baselines/reference/importCallExpressionNestedCJS2.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,10 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
5252
function foo() {
5353
return __awaiter(this, void 0, void 0, function () {
5454
return __generator(this, function (_a) {
55+
var _b;
5556
switch (_a.label) {
5657
case 0: return [4 /*yield*/, Promise.resolve().then(function () { return require("./foo"); })];
57-
case 1: return [4 /*yield*/, Promise.resolve().then(function () { return require((_a.sent()).default); })];
58+
case 1: return [4 /*yield*/, (_b = (_a.sent()).default, Promise.resolve().then(function () { return require(_b); }))];
5859
case 2: return [2 /*return*/, _a.sent()];
5960
}
6061
});

0 commit comments

Comments
 (0)