Skip to content

Commit

Permalink
Add support for import.defer(...)
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolo-ribaudo committed Jan 16, 2025
1 parent bdf98f5 commit e8d76ea
Show file tree
Hide file tree
Showing 21 changed files with 359 additions and 9 deletions.
18 changes: 13 additions & 5 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
13 changes: 12 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
7 changes: 6 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -3181,6 +3181,11 @@ export interface ImportMetaProperty extends MetaProperty {
readonly name: Identifier & { readonly escapedText: __String & "meta"; };
}

export interface ImportDeferProperty extends MetaProperty {
readonly keywordToken: SyntaxKind.ImportKeyword;
readonly name: Identifier & { readonly escapedText: __String & "defer"; };
}

/// A JSX expression of the form <TagName attrs>...</TagName>
export interface JsxElement extends PrimaryExpression {
readonly kind: SyntaxKind.JsxElement;
Expand Down
8 changes: 7 additions & 1 deletion src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down
8 changes: 7 additions & 1 deletion tests/baselines/reference/api/typescript.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5127,7 +5127,7 @@ declare namespace ts {
readonly expression: SuperExpression;
}
interface ImportCall extends CallExpression {
readonly expression: ImportExpression;
readonly expression: ImportExpression | ImportDeferProperty;
}
interface ExpressionWithTypeArguments extends MemberExpression, NodeWithTypeArguments {
readonly kind: SyntaxKind.ExpressionWithTypeArguments;
Expand Down Expand Up @@ -5177,6 +5177,12 @@ declare namespace ts {
readonly keywordToken: SyntaxKind.NewKeyword | SyntaxKind.ImportKeyword;
readonly name: Identifier;
}
interface ImportDeferProperty extends MetaProperty {
readonly keywordToken: SyntaxKind.ImportKeyword;
readonly name: Identifier & {
readonly escapedText: __String & "defer";
};
}
interface JsxElement extends PrimaryExpression {
readonly kind: SyntaxKind.JsxElement;
readonly openingElement: JsxOpeningElement;
Expand Down
20 changes: 20 additions & 0 deletions tests/baselines/reference/dynamicImportDefer.errors.txt
Original file line number Diff line number Diff line change
@@ -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();
});

21 changes: 21 additions & 0 deletions tests/baselines/reference/dynamicImportDefer.js
Original file line number Diff line number Diff line change
@@ -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();
});
23 changes: 23 additions & 0 deletions tests/baselines/reference/dynamicImportDefer.symbols
Original file line number Diff line number Diff line change
@@ -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))

});

53 changes: 53 additions & 0 deletions tests/baselines/reference/dynamicImportDefer.types
Original file line number Diff line number Diff line change
@@ -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<void>
> : ^^^^^^^^^^^^^
>import.defer("a").then : <TResult1 = any, TResult2 = never>(onfulfilled?: (value: any) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>) => Promise<TResult1 | TResult2>
> : ^ ^^^^^^^^ ^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>import.defer("a") : Promise<any>
> : ^^^^^^^^^^^^
>import.defer : any
> : ^^^
>defer : any
> : ^^^
>"a" : "a"
> : ^^^
>then : <TResult1 = any, TResult2 = never>(onfulfilled?: (value: any) => TResult1 | PromiseLike<TResult1>, onrejected?: (reason: any) => TResult2 | PromiseLike<TResult2>) => Promise<TResult1 | TResult2>
> : ^ ^^^^^^^^ ^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^ ^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
>ns => { ns.foo();} : (ns: any) => void
> : ^ ^^^^^^^^^^^^^^
>ns : any
> : ^^^

ns.foo();
>ns.foo() : any
> : ^^^
>ns.foo : any
> : ^^^
>ns : any
> : ^^^
>foo : any
> : ^^^

});

Original file line number Diff line number Diff line change
@@ -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 '('

20 changes: 20 additions & 0 deletions tests/baselines/reference/dynamicImportDeferInvalidStandalone.js
Original file line number Diff line number Diff line change
@@ -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");
Original file line number Diff line number Diff line change
@@ -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");

Original file line number Diff line number Diff line change
@@ -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"
> : ^^^

Original file line number Diff line number Diff line change
@@ -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();
});

12 changes: 12 additions & 0 deletions tests/baselines/reference/dynamicImportDeferMissingModuleESNext.js
Original file line number Diff line number Diff line change
@@ -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();
});
Original file line number Diff line number Diff line change
@@ -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))

});

Loading

0 comments on commit e8d76ea

Please sign in to comment.