Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: avoid downleveled dynamic import closing over specifier expression #49663

Merged
merged 4 commits into from Oct 18, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
47 changes: 30 additions & 17 deletions src/compiler/transformers/module/module.ts
Expand Up @@ -721,7 +721,7 @@ namespace ts {
return createImportCallExpressionUMD(argument ?? factory.createVoidZero(), containsLexicalThis);
case ModuleKind.CommonJS:
default:
return createImportCallExpressionCommonJS(argument, containsLexicalThis);
return createImportCallExpressionCommonJS(argument);
}
}

Expand All @@ -745,7 +745,7 @@ namespace ts {
return factory.createConditionalExpression(
/*condition*/ factory.createIdentifier("__syncRequire"),
/*questionToken*/ undefined,
/*whenTrue*/ createImportCallExpressionCommonJS(arg, containsLexicalThis),
/*whenTrue*/ createImportCallExpressionCommonJS(arg),
/*colonToken*/ undefined,
/*whenFalse*/ createImportCallExpressionAMD(argClone, containsLexicalThis)
);
Expand All @@ -755,7 +755,7 @@ namespace ts {
return factory.createComma(factory.createAssignment(temp, arg), factory.createConditionalExpression(
/*condition*/ factory.createIdentifier("__syncRequire"),
/*questionToken*/ undefined,
/*whenTrue*/ createImportCallExpressionCommonJS(temp, containsLexicalThis),
/*whenTrue*/ createImportCallExpressionCommonJS(temp),
/*colonToken*/ undefined,
/*whenFalse*/ createImportCallExpressionAMD(temp, containsLexicalThis)
));
Expand Down Expand Up @@ -820,14 +820,24 @@ namespace ts {
return promise;
}

function createImportCallExpressionCommonJS(arg: Expression | undefined, containsLexicalThis: boolean): Expression {
// import("./blah")
function createImportCallExpressionCommonJS(arg: Expression | undefined): Expression {
// import(x)
// emit as
// Promise.resolve().then(function () { return require(x); }) /*CommonJs Require*/
// Promise.resolve(x).then(c => require(c)) /*CommonJs Require*/
Josh-Cena marked this conversation as resolved.
Show resolved Hide resolved
// We have to wrap require in then callback so that require is done in asynchronously
// if we simply do require in resolve callback in Promise constructor. We will execute the loading immediately
const promiseResolveCall = factory.createCallExpression(factory.createPropertyAccessExpression(factory.createIdentifier("Promise"), "resolve"), /*typeArguments*/ undefined, /*argumentsArray*/ []);
let requireCall: Expression = factory.createCallExpression(factory.createIdentifier("require"), /*typeArguments*/ undefined, arg ? [arg] : []);
// For import("StringLiteral"), we prefer to emit require("StringLiteral") so that bundlers can analyze this.
const isStringLiteralSpecifier = arg?.kind === SyntaxKind.StringLiteral;
Josh-Cena marked this conversation as resolved.
Show resolved Hide resolved
const promiseResolveCall = factory.createCallExpression(
factory.createPropertyAccessExpression(factory.createIdentifier("Promise"), "resolve"),
/*typeArguments*/ undefined,
/*argumentsArray*/ !arg || isStringLiteralSpecifier ? [] : [arg],
);
let requireCall: Expression = factory.createCallExpression(
factory.createIdentifier("require"),
/*typeArguments*/ undefined,
isStringLiteralSpecifier ? [arg] : arg ? [factory.createIdentifier("c")] : [],
);
if (getESModuleInterop(compilerOptions)) {
requireCall = emitHelpers().createImportStarHelper(requireCall);
}
Expand All @@ -837,7 +847,11 @@ namespace ts {
func = factory.createArrowFunction(
/*modifiers*/ undefined,
/*typeParameters*/ undefined,
/*parameters*/ [],
/*parameters*/ isStringLiteralSpecifier || !arg ? [] : [factory.createParameterDeclaration(
/*modifiers*/ undefined,
/*dotDotDotToken*/ undefined,
"c",
)],
Josh-Cena marked this conversation as resolved.
Show resolved Hide resolved
/*type*/ undefined,
/*equalsGreaterThanToken*/ undefined,
requireCall);
Expand All @@ -848,16 +862,15 @@ namespace ts {
/*asteriskToken*/ undefined,
/*name*/ undefined,
/*typeParameters*/ undefined,
/*parameters*/ [],
/*parameters*/ isStringLiteralSpecifier ? [] : [factory.createParameterDeclaration(
/*modifiers*/ undefined,
/*dotDotDotToken*/ undefined,
"c",
)],
Josh-Cena marked this conversation as resolved.
Show resolved Hide resolved
/*type*/ undefined,
factory.createBlock([factory.createReturnStatement(requireCall)]));

// if there is a lexical 'this' in the import call arguments, ensure we indicate
// that this new function expression indicates it captures 'this' so that the
// es2015 transformer will properly substitute 'this' with '_this'.
if (containsLexicalThis) {
setEmitFlags(func, EmitFlags.CapturesThis);
}
// Note: because non-literal expressions are already evaluated
// synchronously, we don't need to capture `this`.
Josh-Cena marked this conversation as resolved.
Show resolved Hide resolved
}

return factory.createCallExpression(factory.createPropertyAccessExpression(promiseResolveCall, "then"), /*typeArguments*/ undefined, [func]);
Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/asyncImportNestedYield.js
Expand Up @@ -49,9 +49,9 @@ function foo() {
switch (_a.label) {
case 0: return [4 /*yield*/, __await("foo")];
case 1: return [4 /*yield*/, _a.sent()];
case 2: return [4 /*yield*/, __await.apply(void 0, [Promise.resolve().then(function () { return require(_a.sent()); })])];
case 2: return [4 /*yield*/, __await.apply(void 0, [Promise.resolve(_a.sent()).then(function (c) { return require(c); })])];
case 3:
Promise.resolve().then(function () { return require((_a.sent())["default"]); });
Promise.resolve((_a.sent())["default"]).then(function (c) { return require(c); });
return [2 /*return*/];
}
});
Expand Down
29 changes: 29 additions & 0 deletions tests/baselines/reference/dynamicImportEvaluateSpecifier.js
@@ -0,0 +1,29 @@
//// [dynamicImportEvaluateSpecifier.ts]
// https://github.com/microsoft/TypeScript/issues/48285
let i = 0;

import(String(i++));
import(String(i++));

const getPath = async () => {
/* in reality this would do some async FS operation, or a web request */
return "/root/my/cool/path";
};

const someFunction = async () => {
const result = await import(await getPath());
};


//// [dynamicImportEvaluateSpecifier.js]
// https://github.com/microsoft/TypeScript/issues/48285
let i = 0;
Promise.resolve(String(i++)).then(c => require(c));
Promise.resolve(String(i++)).then(c => require(c));
const getPath = async () => {
/* in reality this would do some async FS operation, or a web request */
return "/root/my/cool/path";
};
const someFunction = async () => {
const result = await Promise.resolve(await getPath()).then(c => require(c));
};
29 changes: 29 additions & 0 deletions tests/baselines/reference/dynamicImportEvaluateSpecifier.symbols
@@ -0,0 +1,29 @@
=== tests/cases/compiler/dynamicImportEvaluateSpecifier.ts ===
// https://github.com/microsoft/TypeScript/issues/48285
let i = 0;
>i : Symbol(i, Decl(dynamicImportEvaluateSpecifier.ts, 1, 3))

import(String(i++));
>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)
>i : Symbol(i, Decl(dynamicImportEvaluateSpecifier.ts, 1, 3))

import(String(i++));
>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)
>i : Symbol(i, Decl(dynamicImportEvaluateSpecifier.ts, 1, 3))

const getPath = async () => {
>getPath : Symbol(getPath, Decl(dynamicImportEvaluateSpecifier.ts, 6, 5))

/* in reality this would do some async FS operation, or a web request */
return "/root/my/cool/path";
};

const someFunction = async () => {
>someFunction : Symbol(someFunction, Decl(dynamicImportEvaluateSpecifier.ts, 11, 5))

const result = await import(await getPath());
>result : Symbol(result, Decl(dynamicImportEvaluateSpecifier.ts, 12, 6))
>getPath : Symbol(getPath, Decl(dynamicImportEvaluateSpecifier.ts, 6, 5))

};

44 changes: 44 additions & 0 deletions tests/baselines/reference/dynamicImportEvaluateSpecifier.types
@@ -0,0 +1,44 @@
=== tests/cases/compiler/dynamicImportEvaluateSpecifier.ts ===
// https://github.com/microsoft/TypeScript/issues/48285
let i = 0;
>i : number
>0 : 0

import(String(i++));
>import(String(i++)) : Promise<any>
>String(i++) : string
>String : StringConstructor
>i++ : number
>i : number

import(String(i++));
>import(String(i++)) : Promise<any>
>String(i++) : string
>String : StringConstructor
>i++ : number
>i : number

const getPath = async () => {
>getPath : () => Promise<string>
>async () => { /* in reality this would do some async FS operation, or a web request */ return "/root/my/cool/path";} : () => Promise<string>

/* in reality this would do some async FS operation, or a web request */
return "/root/my/cool/path";
>"/root/my/cool/path" : "/root/my/cool/path"

};

const someFunction = async () => {
>someFunction : () => Promise<void>
>async () => { const result = await import(await getPath());} : () => Promise<void>

const result = await import(await getPath());
>result : any
>await import(await getPath()) : any
>import(await getPath()) : Promise<any>
>await getPath() : string
>getPath() : Promise<string>
>getPath : () => Promise<string>

};

2 changes: 1 addition & 1 deletion tests/baselines/reference/dynamicImportTrailingComma.js
Expand Up @@ -4,4 +4,4 @@ import(path,);

//// [dynamicImportTrailingComma.js]
var path = './foo';
Promise.resolve().then(function () { return require(path); });
Promise.resolve(path).then(function (c) { return require(c); });
Expand Up @@ -30,7 +30,7 @@ c.dynamic();
}
dynamic() {
var _a;
return _a = this._path, __syncRequire ? Promise.resolve().then(() => require(_a)) : new Promise((resolve_1, reject_1) => { require([_a], resolve_1, reject_1); });
return _a = this._path, __syncRequire ? Promise.resolve(_a).then(c => require(c)) : new Promise((resolve_1, reject_1) => { require([_a], resolve_1, reject_1); });
}
}
const c = new C();
Expand Down
Expand Up @@ -30,7 +30,7 @@ c.dynamic();
}
C.prototype.dynamic = function () {
var _a;
return _a = this._path, __syncRequire ? Promise.resolve().then(function () { return require(_a); }) : new Promise(function (resolve_1, reject_1) { require([_a], resolve_1, reject_1); });
return _a = this._path, __syncRequire ? Promise.resolve(_a).then(function (c) { return require(c); }) : new Promise(function (resolve_1, reject_1) { require([_a], resolve_1, reject_1); });
};
return C;
}());
Expand Down
Expand Up @@ -15,12 +15,12 @@ function returnDynamicLoad(path: string) {
}

//// [importCallExpressionDeclarationEmit1.js]
Promise.resolve().then(() => require(getSpecifier()));
var p0 = Promise.resolve().then(() => require(`${directory}\\${moduleFile}`));
var p1 = Promise.resolve().then(() => require(getSpecifier()));
const p2 = Promise.resolve().then(() => require(whatToLoad ? getSpecifier() : "defaulPath"));
Promise.resolve(getSpecifier()).then(c => require(c));
var p0 = Promise.resolve(`${directory}\\${moduleFile}`).then(c => require(c));
var p1 = Promise.resolve(getSpecifier()).then(c => require(c));
const p2 = Promise.resolve(whatToLoad ? getSpecifier() : "defaulPath").then(c => require(c));
function returnDynamicLoad(path) {
return Promise.resolve().then(() => require(path));
return Promise.resolve(path).then(c => require(c));
}


Expand Down
4 changes: 2 additions & 2 deletions tests/baselines/reference/importCallExpressionGrammarError.js
Expand Up @@ -11,7 +11,7 @@ const p4 = import("pathToModule", "secondModule");

//// [importCallExpressionGrammarError.js]
var a = ["./0"];
Promise.resolve().then(() => require(...["PathModule"]));
var p1 = Promise.resolve().then(() => require(...a));
Promise.resolve(...["PathModule"]).then(c => require(c));
var p1 = Promise.resolve(...a).then(c => require(c));
const p2 = Promise.resolve().then(() => require());
const p4 = Promise.resolve().then(() => require("pathToModule"));
2 changes: 1 addition & 1 deletion tests/baselines/reference/importCallExpressionInUMD5.js
Expand Up @@ -53,7 +53,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
return __awaiter(this, void 0, void 0, function* () {
var _a;
const packageName = '.';
const packageJson = yield (_a = packageName + '/package.json', __syncRequire ? Promise.resolve().then(() => require(_a)) : new Promise((resolve_1, reject_1) => { require([_a], resolve_1, reject_1); }));
const packageJson = yield (_a = packageName + '/package.json', __syncRequire ? Promise.resolve(_a).then(c => require(c)) : new Promise((resolve_1, reject_1) => { require([_a], resolve_1, reject_1); }));
});
}
});
2 changes: 1 addition & 1 deletion tests/baselines/reference/importCallExpressionNestedCJS.js
Expand Up @@ -24,6 +24,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
};
function foo() {
return __awaiter(this, void 0, void 0, function* () {
return yield Promise.resolve().then(() => require((yield Promise.resolve().then(() => require("./foo"))).default));
return yield Promise.resolve((yield Promise.resolve().then(() => require("./foo"))).default).then(c => require(c));
});
}
Expand Up @@ -54,7 +54,7 @@ function foo() {
return __generator(this, function (_a) {
switch (_a.label) {
case 0: return [4 /*yield*/, Promise.resolve().then(function () { return require("./foo"); })];
case 1: return [4 /*yield*/, Promise.resolve().then(function () { return require((_a.sent()).default); })];
case 1: return [4 /*yield*/, Promise.resolve((_a.sent()).default).then(function (c) { return require(c); })];
case 2: return [2 /*return*/, _a.sent()];
}
});
Expand Down
2 changes: 1 addition & 1 deletion tests/baselines/reference/importCallExpressionNestedUMD.js
Expand Up @@ -46,7 +46,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
function foo() {
return __awaiter(this, void 0, void 0, function* () {
var _a;
return yield (_a = (yield __syncRequire ? Promise.resolve().then(() => require("./foo")) : new Promise((resolve_1, reject_1) => { require(["./foo"], resolve_1, reject_1); })).default, __syncRequire ? Promise.resolve().then(() => require(_a)) : new Promise((resolve_2, reject_2) => { require([_a], resolve_2, reject_2); }));
return yield (_a = (yield __syncRequire ? Promise.resolve().then(() => require("./foo")) : new Promise((resolve_1, reject_1) => { require(["./foo"], resolve_1, reject_1); })).default, __syncRequire ? Promise.resolve(_a).then(c => require(c)) : new Promise((resolve_2, reject_2) => { require([_a], resolve_2, reject_2); }));
});
}
});
Expand Up @@ -76,7 +76,7 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
var _b;
switch (_a.label) {
case 0: return [4 /*yield*/, __syncRequire ? Promise.resolve().then(function () { return require("./foo"); }) : new Promise(function (resolve_1, reject_1) { require(["./foo"], resolve_1, reject_1); })];
case 1: return [4 /*yield*/, (_b = (_a.sent()).default, __syncRequire ? Promise.resolve().then(function () { return require(_b); }) : new Promise(function (resolve_2, reject_2) { require([_b], resolve_2, reject_2); }))];
case 1: return [4 /*yield*/, (_b = (_a.sent()).default, __syncRequire ? Promise.resolve(_b).then(function (c) { return require(c); }) : new Promise(function (resolve_2, reject_2) { require([_b], resolve_2, reject_2); }))];
case 2: return [2 /*return*/, _a.sent()];
}
});
Expand Down
Expand Up @@ -43,20 +43,20 @@ exports.C = C;
//// [1.js]
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
Promise.resolve().then(() => require(`${directory}\\${moduleFile}`));
Promise.resolve().then(() => require(getSpecifier()));
var p1 = Promise.resolve().then(() => require(ValidSomeCondition() ? "./0" : "externalModule"));
var p1 = Promise.resolve().then(() => require(getSpecifier()));
var p11 = Promise.resolve().then(() => require(getSpecifier()));
const p2 = Promise.resolve().then(() => require(whatToLoad ? getSpecifier() : "defaulPath"));
Promise.resolve(`${directory}\\${moduleFile}`).then(c => require(c));
Promise.resolve(getSpecifier()).then(c => require(c));
var p1 = Promise.resolve(ValidSomeCondition() ? "./0" : "externalModule").then(c => require(c));
var p1 = Promise.resolve(getSpecifier()).then(c => require(c));
var p11 = Promise.resolve(getSpecifier()).then(c => require(c));
const p2 = Promise.resolve(whatToLoad ? getSpecifier() : "defaulPath").then(c => require(c));
p1.then(zero => {
return zero.foo(); // ok, zero is any
});
let j;
var p3 = Promise.resolve().then(() => require(j = getSpecifier()));
var p3 = Promise.resolve(j = getSpecifier()).then(c => require(c));
function* loadModule(directories) {
for (const directory of directories) {
const path = `${directory}\\moduleFile`;
Promise.resolve().then(() => require(yield path));
Promise.resolve(yield path).then(c => require(c));
}
}
Expand Up @@ -15,11 +15,11 @@ var p4 = import(()=>"PathToModule");

//// [importCallExpressionSpecifierNotStringTypeError.js]
// Error specifier is not assignable to string
Promise.resolve().then(() => require(getSpecifier()));
var p1 = Promise.resolve().then(() => require(getSpecifier()));
const p2 = Promise.resolve().then(() => require(whatToLoad ? getSpecifier() : "defaulPath"));
Promise.resolve(getSpecifier()).then(c => require(c));
var p1 = Promise.resolve(getSpecifier()).then(c => require(c));
const p2 = Promise.resolve(whatToLoad ? getSpecifier() : "defaulPath").then(c => require(c));
p1.then(zero => {
return zero.foo(); // ok, zero is any
});
var p3 = Promise.resolve().then(() => require(["path1", "path2"]));
var p4 = Promise.resolve().then(() => require(() => "PathToModule"));
var p3 = Promise.resolve(["path1", "path2"]).then(c => require(c));
var p4 = Promise.resolve(() => "PathToModule").then(c => require(c));
2 changes: 1 addition & 1 deletion tests/baselines/reference/jsdocInTypeScript.js
Expand Up @@ -92,4 +92,4 @@ var E = {};
E[""];
// make sure import types in JSDoc are not resolved
/** @type {import("should-not-be-resolved").Type} */
var v = Promise.resolve().then(function () { return require(String()); });
var v = Promise.resolve(String()).then(function (c) { return require(c); });
17 changes: 17 additions & 0 deletions tests/cases/compiler/dynamicImportEvaluateSpecifier.ts
@@ -0,0 +1,17 @@
// @lib: es2019
// @target: es2019
// @module: commonjs
// https://github.com/microsoft/TypeScript/issues/48285
let i = 0;

import(String(i++));
import(String(i++));

const getPath = async () => {
/* in reality this would do some async FS operation, or a web request */
return "/root/my/cool/path";
};

const someFunction = async () => {
const result = await import(await getPath());
};