Skip to content

Commit 1f8959f

Browse files
Josh-Cenarbuckton
andauthoredOct 18, 2022
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 <ron.buckton@microsoft.com> * [Experiment] Co-authored-by: Ron Buckton <ron.buckton@microsoft.com>
1 parent 11066b2 commit 1f8959f

14 files changed

+180
-42
lines changed
 

‎src/compiler/transformers/module/module.ts

+22-16
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

+3-2
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
});
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+
};
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+
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

+2-1
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

+7-5
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

+3-2
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

+2-1
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

+2-1
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
});

‎tests/baselines/reference/importCallExpressionReturnPromiseOfAny.js

+10-8
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,23 @@ class C {
4242
exports.C = C;
4343
//// [1.js]
4444
"use strict";
45+
var _a, _b, _c, _d, _e, _f, _g;
4546
Object.defineProperty(exports, "__esModule", { value: true });
46-
Promise.resolve().then(() => require(`${directory}\\${moduleFile}`));
47-
Promise.resolve().then(() => require(getSpecifier()));
48-
var p1 = Promise.resolve().then(() => require(ValidSomeCondition() ? "./0" : "externalModule"));
49-
var p1 = Promise.resolve().then(() => require(getSpecifier()));
50-
var p11 = Promise.resolve().then(() => require(getSpecifier()));
51-
const p2 = Promise.resolve().then(() => require(whatToLoad ? getSpecifier() : "defaulPath"));
47+
_a = `${directory}\\${moduleFile}`, Promise.resolve().then(() => require(_a));
48+
_b = getSpecifier(), Promise.resolve().then(() => require(_b));
49+
var p1 = (_c = ValidSomeCondition() ? "./0" : "externalModule", Promise.resolve().then(() => require(_c)));
50+
var p1 = (_d = getSpecifier(), Promise.resolve().then(() => require(_d)));
51+
var p11 = (_e = getSpecifier(), Promise.resolve().then(() => require(_e)));
52+
const p2 = (_f = whatToLoad ? getSpecifier() : "defaulPath", Promise.resolve().then(() => require(_f)));
5253
p1.then(zero => {
5354
return zero.foo(); // ok, zero is any
5455
});
5556
let j;
56-
var p3 = Promise.resolve().then(() => require(j = getSpecifier()));
57+
var p3 = (_g = j = getSpecifier(), Promise.resolve().then(() => require(_g)));
5758
function* loadModule(directories) {
59+
var _a;
5860
for (const directory of directories) {
5961
const path = `${directory}\\moduleFile`;
60-
Promise.resolve().then(() => require(yield path));
62+
_a = yield path, Promise.resolve().then(() => require(_a));
6163
}
6264
}

‎tests/baselines/reference/importCallExpressionSpecifierNotStringTypeError.js

+6-5
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,13 @@ var p3 = import(["path1", "path2"]);
1414
var p4 = import(()=>"PathToModule");
1515

1616
//// [importCallExpressionSpecifierNotStringTypeError.js]
17+
var _a, _b, _c, _d, _e;
1718
// Error specifier is not assignable to string
18-
Promise.resolve().then(() => require(getSpecifier()));
19-
var p1 = Promise.resolve().then(() => require(getSpecifier()));
20-
const p2 = Promise.resolve().then(() => require(whatToLoad ? getSpecifier() : "defaulPath"));
19+
_a = getSpecifier(), Promise.resolve().then(() => require(_a));
20+
var p1 = (_b = getSpecifier(), Promise.resolve().then(() => require(_b)));
21+
const p2 = (_c = whatToLoad ? getSpecifier() : "defaulPath", Promise.resolve().then(() => require(_c)));
2122
p1.then(zero => {
2223
return zero.foo(); // ok, zero is any
2324
});
24-
var p3 = Promise.resolve().then(() => require(["path1", "path2"]));
25-
var p4 = Promise.resolve().then(() => require(() => "PathToModule"));
25+
var p3 = (_d = ["path1", "path2"], Promise.resolve().then(() => require(_d)));
26+
var p4 = (_e = () => "PathToModule", Promise.resolve().then(() => require(_e)));

‎tests/baselines/reference/jsdocInTypeScript.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ var v = import(String());
5858

5959

6060
//// [jsdocInTypeScript.js]
61+
var _a;
6162
var T = /** @class */ (function () {
6263
function T() {
6364
}
@@ -92,4 +93,4 @@ var E = {};
9293
E[""];
9394
// make sure import types in JSDoc are not resolved
9495
/** @type {import("should-not-be-resolved").Type} */
95-
var v = Promise.resolve().then(function () { return require(String()); });
96+
var v = (_a = String(), Promise.resolve().then(function () { return require(_a); }));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// @lib: es2019
2+
// @target: es2019
3+
// @module: commonjs
4+
// https://github.com/microsoft/TypeScript/issues/48285
5+
let i = 0;
6+
7+
import(String(i++));
8+
import(String(i++));
9+
10+
const getPath = async () => {
11+
/* in reality this would do some async FS operation, or a web request */
12+
return "/root/my/cool/path";
13+
};
14+
15+
const someFunction = async () => {
16+
const result = await import(await getPath());
17+
};

0 commit comments

Comments
 (0)
Please sign in to comment.