diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d336627667a0e..214b43aaba465 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -37650,7 +37650,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { error(node, Diagnostics.The_import_meta_meta_property_is_only_allowed_when_the_module_option_is_es2020_es2022_esnext_system_node16_node18_or_nodenext); } const file = getSourceFileOfNode(node); - Debug.assert(!!(file.flags & NodeFlags.PossiblyContainsImportMeta), "Containing file is missing import meta node flag."); + Debug.assert(node.name.escapedText === "defer" || !!(file.flags & NodeFlags.PossiblyContainsImportMeta), "Containing file is missing import meta node flag."); return node.name.escapedText === "meta" ? getGlobalImportMetaType() : errorType; } @@ -41172,8 +41172,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { case SyntaxKind.ElementAccessExpression: return checkIndexedAccess(node as ElementAccessExpression, checkMode); case SyntaxKind.CallExpression: - if ((node as CallExpression).expression.kind === SyntaxKind.ImportKeyword) { - return checkImportCallExpression(node as ImportCall); + if (isImportCall(node)) { + return checkImportCallExpression(node); } // falls through case SyntaxKind.NewExpression: @@ -52612,8 +52612,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { } break; case SyntaxKind.ImportKeyword: - if (escapedText !== "meta") { - return grammarErrorOnNode(node.name, Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, unescapeLeadingUnderscores(node.name.escapedText), tokenToString(node.keywordToken), "meta"); + if (escapedText !== "meta" && escapedText !== "defer") { + const suggestion = node.parent.kind === SyntaxKind.CallExpression && (node.parent as CallExpression).expression === node + ? "meta' or 'defer" + : "meta"; + return grammarErrorOnNode(node.name, Diagnostics._0_is_not_a_valid_meta_property_for_keyword_1_Did_you_mean_2, unescapeLeadingUnderscores(node.name.escapedText), tokenToString(node.keywordToken), suggestion); } break; } @@ -52902,6 +52905,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return grammarErrorOnNode(node, Diagnostics.ESM_syntax_is_not_allowed_in_a_CommonJS_module_when_verbatimModuleSyntax_is_enabled); } + if (node.expression.kind === SyntaxKind.MetaProperty) { + if (moduleKind !== ModuleKind.ESNext) { + return grammarErrorOnNode(node, Diagnostics.Deferred_imports_are_only_supported_when_the_module_flag_is_set_to_esnext); + } + } if (moduleKind === ModuleKind.ES2015) { return grammarErrorOnNode(node, Diagnostics.Dynamic_imports_are_only_supported_when_the_module_flag_is_set_to_es2020_es2022_esnext_commonjs_amd_system_umd_node16_node18_or_nodenext); } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index cfc166c130fdb..e8fcb5123e809 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -8445,5 +8445,9 @@ "Deferred imports are only supported when the '--module' flag is set to 'esnext'.": { "category": "Error", "code": 18060 + }, + "'import.defer' must be followed by '('": { + "category": "Error", + "code": 18061 } } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 44fa145fbc293..8c204ebb761b5 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -5939,7 +5939,18 @@ namespace Parser { nextToken(); // advance past the 'import' nextToken(); // advance past the dot expression = finishNode(factory.createMetaProperty(SyntaxKind.ImportKeyword, parseIdentifierName()), pos); - sourceFlags |= NodeFlags.PossiblyContainsImportMeta; + + if ((expression as MetaProperty).name.escapedText === "defer") { + if (token() === SyntaxKind.OpenParenToken || token() === SyntaxKind.LessThanToken) { + sourceFlags |= NodeFlags.PossiblyContainsDynamicImport; + } + else { + parseErrorAt(pos, getNodePos(), Diagnostics.import_defer_must_be_followed_by); + } + } + else { + sourceFlags |= NodeFlags.PossiblyContainsImportMeta; + } } else { expression = parseMemberExpressionOrHigher(); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index dce42356655b8..05617b1cc782b 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3101,7 +3101,7 @@ export interface SuperCall extends CallExpression { } export interface ImportCall extends CallExpression { - readonly expression: ImportExpression; + readonly expression: ImportExpression | ImportDeferProperty; } export interface ExpressionWithTypeArguments extends MemberExpression, NodeWithTypeArguments { @@ -3181,6 +3181,12 @@ export interface ImportMetaProperty extends MetaProperty { readonly name: Identifier & { readonly escapedText: __String & "meta"; }; } +/** @internal */ +export interface ImportDeferProperty extends MetaProperty { + readonly keywordToken: SyntaxKind.ImportKeyword; + readonly name: Identifier & { readonly escapedText: __String & "defer"; }; +} + /// A JSX expression of the form ... export interface JsxElement extends PrimaryExpression { readonly kind: SyntaxKind.JsxElement; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 287f04829bc1a..6c65fb2005d1c 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -2632,7 +2632,13 @@ export function isSuperCall(n: Node): n is SuperCall { /** @internal */ export function isImportCall(n: Node): n is ImportCall { - return n.kind === SyntaxKind.CallExpression && (n as CallExpression).expression.kind === SyntaxKind.ImportKeyword; + if (n.kind !== SyntaxKind.CallExpression) return false; + const e = (n as CallExpression).expression; + return e.kind === SyntaxKind.ImportKeyword || ( + isMetaProperty(e) + && e.keywordToken === SyntaxKind.ImportKeyword + && e.name.escapedText === "defer" + ); } /** @internal */ diff --git a/tests/baselines/reference/dynamicImportDefer.errors.txt b/tests/baselines/reference/dynamicImportDefer.errors.txt new file mode 100644 index 0000000000000..a67ed059cb6f6 --- /dev/null +++ b/tests/baselines/reference/dynamicImportDefer.errors.txt @@ -0,0 +1,20 @@ +error TS2468: Cannot find global value 'Promise'. +b.ts(1,1): error TS2712: A dynamic import call in ES5 requires the 'Promise' constructor. Make sure you have a declaration for the 'Promise' constructor or include 'ES2015' in your '--lib' option. +b.ts(1,14): error TS2792: Cannot find module 'a'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option? + + +!!! error TS2468: Cannot find global value 'Promise'. +==== a.ts (0 errors) ==== + export function foo() { + console.log("foo from a"); + } + +==== b.ts (2 errors) ==== + import.defer("a").then(ns => { + ~~~~~~~~~~~~~~~~~ +!!! error TS2712: A dynamic import call in ES5 requires the 'Promise' constructor. Make sure you have a declaration for the 'Promise' constructor or include 'ES2015' in your '--lib' option. + ~~~ +!!! error TS2792: Cannot find module 'a'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option? + ns.foo(); + }); + \ No newline at end of file diff --git a/tests/baselines/reference/dynamicImportDefer.js b/tests/baselines/reference/dynamicImportDefer.js new file mode 100644 index 0000000000000..0b7832a55d6c5 --- /dev/null +++ b/tests/baselines/reference/dynamicImportDefer.js @@ -0,0 +1,21 @@ +//// [tests/cases/conformance/importDefer/dynamicImportDefer.ts] //// + +//// [a.ts] +export function foo() { + console.log("foo from a"); +} + +//// [b.ts] +import.defer("a").then(ns => { + ns.foo(); +}); + + +//// [a.js] +export function foo() { + console.log("foo from a"); +} +//// [b.js] +import.defer("a").then(function (ns) { + ns.foo(); +}); diff --git a/tests/baselines/reference/dynamicImportDefer.symbols b/tests/baselines/reference/dynamicImportDefer.symbols new file mode 100644 index 0000000000000..b255c5fba2f6f --- /dev/null +++ b/tests/baselines/reference/dynamicImportDefer.symbols @@ -0,0 +1,23 @@ +//// [tests/cases/conformance/importDefer/dynamicImportDefer.ts] //// + +=== a.ts === +export function foo() { +>foo : Symbol(foo, Decl(a.ts, 0, 0)) + + console.log("foo from a"); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +} + +=== b.ts === +import.defer("a").then(ns => { +>import.defer("a").then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --)) +>then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --)) +>ns : Symbol(ns, Decl(b.ts, 0, 23)) + + ns.foo(); +>ns : Symbol(ns, Decl(b.ts, 0, 23)) + +}); + diff --git a/tests/baselines/reference/dynamicImportDefer.types b/tests/baselines/reference/dynamicImportDefer.types new file mode 100644 index 0000000000000..05ecaa37b5a7c --- /dev/null +++ b/tests/baselines/reference/dynamicImportDefer.types @@ -0,0 +1,53 @@ +//// [tests/cases/conformance/importDefer/dynamicImportDefer.ts] //// + +=== a.ts === +export function foo() { +>foo : () => void +> : ^^^^^^^^^^ + + console.log("foo from a"); +>console.log("foo from a") : void +> : ^^^^ +>console.log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>console : Console +> : ^^^^^^^ +>log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>"foo from a" : "foo from a" +> : ^^^^^^^^^^^^ +} + +=== b.ts === +import.defer("a").then(ns => { +>import.defer("a").then(ns => { ns.foo();}) : Promise +> : ^^^^^^^^^^^^^ +>import.defer("a").then : (onfulfilled?: (value: any) => TResult1 | PromiseLike, onrejected?: (reason: any) => TResult2 | PromiseLike) => Promise +> : ^ ^^^^^^^^ ^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>import.defer("a") : Promise +> : ^^^^^^^^^^^^ +>import.defer : any +> : ^^^ +>defer : any +> : ^^^ +>"a" : "a" +> : ^^^ +>then : (onfulfilled?: (value: any) => TResult1 | PromiseLike, onrejected?: (reason: any) => TResult2 | PromiseLike) => Promise +> : ^ ^^^^^^^^ ^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>ns => { ns.foo();} : (ns: any) => void +> : ^ ^^^^^^^^^^^^^^ +>ns : any +> : ^^^ + + ns.foo(); +>ns.foo() : any +> : ^^^ +>ns.foo : any +> : ^^^ +>ns : any +> : ^^^ +>foo : any +> : ^^^ + +}); + diff --git a/tests/baselines/reference/dynamicImportDeferInvalidStandalone.errors.txt b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.errors.txt new file mode 100644 index 0000000000000..69d00e1709ae2 --- /dev/null +++ b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.errors.txt @@ -0,0 +1,18 @@ +b.ts(1,1): error TS18061: 'import.defer' must be followed by '(' +b.ts(3,2): error TS18061: 'import.defer' must be followed by '(' + + +==== a.ts (0 errors) ==== + export function foo() { + console.log("foo from a"); + } + +==== b.ts (2 errors) ==== + import.defer; + ~~~~~~~~~~~~ +!!! error TS18061: 'import.defer' must be followed by '(' + + (import.defer)("a"); + ~~~~~~~~~~~~ +!!! error TS18061: 'import.defer' must be followed by '(' + \ No newline at end of file diff --git a/tests/baselines/reference/dynamicImportDeferInvalidStandalone.js b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.js new file mode 100644 index 0000000000000..60c43006b5c42 --- /dev/null +++ b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.js @@ -0,0 +1,20 @@ +//// [tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts] //// + +//// [a.ts] +export function foo() { + console.log("foo from a"); +} + +//// [b.ts] +import.defer; + +(import.defer)("a"); + + +//// [a.js] +export function foo() { + console.log("foo from a"); +} +//// [b.js] +import.defer; +(import.defer)("a"); diff --git a/tests/baselines/reference/dynamicImportDeferInvalidStandalone.symbols b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.symbols new file mode 100644 index 0000000000000..fa3e75096a4db --- /dev/null +++ b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.symbols @@ -0,0 +1,18 @@ +//// [tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts] //// + +=== a.ts === +export function foo() { +>foo : Symbol(foo, Decl(a.ts, 0, 0)) + + console.log("foo from a"); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +} + +=== b.ts === + +import.defer; + +(import.defer)("a"); + diff --git a/tests/baselines/reference/dynamicImportDeferInvalidStandalone.types b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.types new file mode 100644 index 0000000000000..c7c0f20db20de --- /dev/null +++ b/tests/baselines/reference/dynamicImportDeferInvalidStandalone.types @@ -0,0 +1,39 @@ +//// [tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts] //// + +=== a.ts === +export function foo() { +>foo : () => void +> : ^^^^^^^^^^ + + console.log("foo from a"); +>console.log("foo from a") : void +> : ^^^^ +>console.log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>console : Console +> : ^^^^^^^ +>log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>"foo from a" : "foo from a" +> : ^^^^^^^^^^^^ +} + +=== b.ts === +import.defer; +>import.defer : any +> : ^^^ +>defer : any +> : ^^^ + +(import.defer)("a"); +>(import.defer)("a") : any +> : ^^^ +>(import.defer) : any +> : ^^^ +>import.defer : any +> : ^^^ +>defer : any +> : ^^^ +>"a" : "a" +> : ^^^ + diff --git a/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.errors.txt b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.errors.txt new file mode 100644 index 0000000000000..dc89860117c4b --- /dev/null +++ b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.errors.txt @@ -0,0 +1,13 @@ +b.ts(1,1): error TS18060: Deferred imports are only supported when the '--module' flag is set to 'esnext'. +b.ts(1,14): error TS2792: Cannot find module 'a'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option? + + +==== b.ts (2 errors) ==== + import.defer("a").then(ns => { + ~~~~~~~~~~~~~~~~~ +!!! error TS18060: Deferred imports are only supported when the '--module' flag is set to 'esnext'. + ~~~ +!!! error TS2792: Cannot find module 'a'. Did you mean to set the 'moduleResolution' option to 'nodenext', or to add aliases to the 'paths' option? + ns.foo(); + }); + \ No newline at end of file diff --git a/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.js b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.js new file mode 100644 index 0000000000000..12fdcdfee2978 --- /dev/null +++ b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.js @@ -0,0 +1,12 @@ +//// [tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts] //// + +//// [b.ts] +import.defer("a").then(ns => { + ns.foo(); +}); + + +//// [b.js] +import.defer("a").then(ns => { + ns.foo(); +}); diff --git a/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.symbols b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.symbols new file mode 100644 index 0000000000000..af6712cc15aae --- /dev/null +++ b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.symbols @@ -0,0 +1,13 @@ +//// [tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts] //// + +=== b.ts === +import.defer("a").then(ns => { +>import.defer("a").then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --)) +>then : Symbol(Promise.then, Decl(lib.es5.d.ts, --, --)) +>ns : Symbol(ns, Decl(b.ts, 0, 23)) + + ns.foo(); +>ns : Symbol(ns, Decl(b.ts, 0, 23)) + +}); + diff --git a/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.types b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.types new file mode 100644 index 0000000000000..d5af4f5334730 --- /dev/null +++ b/tests/baselines/reference/dynamicImportDeferMissingModuleESNext.types @@ -0,0 +1,35 @@ +//// [tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts] //// + +=== b.ts === +import.defer("a").then(ns => { +>import.defer("a").then(ns => { ns.foo();}) : Promise +> : ^^^^^^^^^^^^^ +>import.defer("a").then : (onfulfilled?: (value: any) => TResult1 | PromiseLike, onrejected?: (reason: any) => TResult2 | PromiseLike) => Promise +> : ^ ^^^^^^^^ ^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>import.defer("a") : Promise +> : ^^^^^^^^^^^^ +>import.defer : any +> : ^^^ +>defer : any +> : ^^^ +>"a" : "a" +> : ^^^ +>then : (onfulfilled?: (value: any) => TResult1 | PromiseLike, onrejected?: (reason: any) => TResult2 | PromiseLike) => Promise +> : ^ ^^^^^^^^ ^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>ns => { ns.foo();} : (ns: any) => void +> : ^ ^^^^^^^^^^^^^^ +>ns : any +> : ^^^ + + ns.foo(); +>ns.foo() : any +> : ^^^ +>ns.foo : any +> : ^^^ +>ns : any +> : ^^^ +>foo : any +> : ^^^ + +}); + diff --git a/tests/cases/conformance/importDefer/dynamicImportDefer.ts b/tests/cases/conformance/importDefer/dynamicImportDefer.ts new file mode 100644 index 0000000000000..103ff29df7c9d --- /dev/null +++ b/tests/cases/conformance/importDefer/dynamicImportDefer.ts @@ -0,0 +1,10 @@ +// @module: esnext +// @filename: a.ts +export function foo() { + console.log("foo from a"); +} + +// @filename: b.ts +import.defer("a").then(ns => { + ns.foo(); +}); diff --git a/tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts b/tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts new file mode 100644 index 0000000000000..7e9db31ea2da2 --- /dev/null +++ b/tests/cases/conformance/importDefer/dynamicImportDeferInvalidStandalone.ts @@ -0,0 +1,10 @@ +// @module: esnext +// @filename: a.ts +export function foo() { + console.log("foo from a"); +} + +// @filename: b.ts +import.defer; + +(import.defer)("a"); diff --git a/tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts b/tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts new file mode 100644 index 0000000000000..7dd278d075558 --- /dev/null +++ b/tests/cases/conformance/importDefer/dynamicImportDeferMissingModuleESNext.ts @@ -0,0 +1,5 @@ +// @target: es2020 +// @filename: b.ts +import.defer("a").then(ns => { + ns.foo(); +});