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

Add support for import defer proposal #60757

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9852,7 +9852,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
factory.createImportDeclaration(
/*modifiers*/ undefined,
factory.createImportClause(
/*isTypeOnly*/ false,
/*phaseModifier*/ undefined,
/*name*/ undefined,
factory.createNamedImports([factory.createImportSpecifier(
/*isTypeOnly*/ false,
Expand Down Expand Up @@ -9945,7 +9945,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
addResult(
factory.createImportDeclaration(
/*modifiers*/ undefined,
factory.createImportClause(isTypeOnly, factory.createIdentifier(localName), /*namedBindings*/ undefined),
factory.createImportClause(
/* phaseModifier */ isTypeOnly ? SyntaxKind.TypeKeyword : undefined,
factory.createIdentifier(localName),
/*namedBindings*/ undefined,
),
specifier,
attributes,
),
Expand All @@ -9960,7 +9964,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
addResult(
factory.createImportDeclaration(
/*modifiers*/ undefined,
factory.createImportClause(isTypeOnly, /*name*/ undefined, factory.createNamespaceImport(factory.createIdentifier(localName))),
factory.createImportClause(
/* phaseModifier */ isTypeOnly ? SyntaxKind.TypeKeyword : undefined,
/*name*/ undefined,
factory.createNamespaceImport(factory.createIdentifier(localName)),
),
specifier,
(node as ImportClause).parent.attributes,
),
Expand All @@ -9987,7 +9995,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
factory.createImportDeclaration(
/*modifiers*/ undefined,
factory.createImportClause(
isTypeOnly,
/* phaseModifier */ isTypeOnly ? SyntaxKind.TypeKeyword : undefined,
/*name*/ undefined,
factory.createNamedImports([
factory.createImportSpecifier(
Expand Down Expand Up @@ -37642,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;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should import.defer be returning errorType here? Should we also report an error here for import.defer outside of a call?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I now moved the import.defer check to the checkMetaProperty, which also already calls checkGrammarMetaProperty to check if import.defer is outside of a call. I also changes isExpressionNode() to return false for the import.defer in import.defer(...).

So now this branch is only reached if we have a "standalone" import.defer, for which checkGrammarMetaProperty reports an error, and so returning errorType is correct.

}

Expand Down Expand Up @@ -41164,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 @@ -52604,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
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
? "meta' or 'defer"
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
: "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 @@ -52870,6 +52881,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (node.isTypeOnly && node.namedBindings?.kind === SyntaxKind.NamedImports) {
return checkGrammarNamedImportsOrExports(node.namedBindings);
}
if (node.phaseModifier === SyntaxKind.DeferKeyword && moduleKind !== ModuleKind.ESNext) {
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
return grammarErrorOnNode(node, Diagnostics.Deferred_imports_are_only_supported_when_the_module_flag_is_set_to_esnext);
}
return false;
}

Expand All @@ -52891,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) {
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
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
16 changes: 16 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -8433,5 +8433,21 @@
"String literal import and export names are not supported when the '--module' flag is set to 'es2015' or 'es2020'.": {
"category": "Error",
"code": 18057
},
"Default imports aren't allowed for deferred imports.": {
"category": "Error",
"code": 18058
},
"Named imports aren't allowed for deferred imports.": {
"category": "Error",
"code": 18059
},
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
"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
}
}
4 changes: 2 additions & 2 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3685,8 +3685,8 @@ export function createPrinter(printerOptions: PrinterOptions = {}, handlers: Pri
}

function emitImportClause(node: ImportClause) {
if (node.isTypeOnly) {
emitTokenWithComment(SyntaxKind.TypeKeyword, node.pos, writeKeyword, node);
if (node.phaseModifier !== undefined) {
emitTokenWithComment(node.phaseModifier, node.pos, writeKeyword, node);
writeSpace();
}
emit(node.name);
Expand Down
20 changes: 14 additions & 6 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ import {
ImportClause,
ImportDeclaration,
ImportEqualsDeclaration,
ImportPhaseModifier,
ImportSpecifier,
ImportTypeAssertionContainer,
ImportTypeNode,
Expand Down Expand Up @@ -4723,26 +4724,33 @@ export function createNodeFactory(flags: NodeFactoryFlags, baseFactory: BaseNode
}

// @api
function createImportClause(isTypeOnly: boolean, name: Identifier | undefined, namedBindings: NamedImportBindings | undefined): ImportClause {
function createImportClause(phaseModifier: ImportPhaseModifier | boolean | undefined, name: Identifier | undefined, namedBindings: NamedImportBindings | undefined): ImportClause {
const node = createBaseDeclaration<ImportClause>(SyntaxKind.ImportClause);
node.isTypeOnly = isTypeOnly;
if (typeof phaseModifier === "boolean") {
phaseModifier = phaseModifier ? SyntaxKind.TypeKeyword : undefined;
}
node.isTypeOnly = phaseModifier === SyntaxKind.TypeKeyword;
node.phaseModifier = phaseModifier;
node.name = name;
node.namedBindings = namedBindings;
node.transformFlags |= propagateChildFlags(node.name) |
propagateChildFlags(node.namedBindings);
if (isTypeOnly) {
if (phaseModifier === SyntaxKind.TypeKeyword) {
node.transformFlags |= TransformFlags.ContainsTypeScript;
}
node.transformFlags &= ~TransformFlags.ContainsPossibleTopLevelAwait; // always parsed in an Await context
return node;
}

// @api
function updateImportClause(node: ImportClause, isTypeOnly: boolean, name: Identifier | undefined, namedBindings: NamedImportBindings | undefined) {
return node.isTypeOnly !== isTypeOnly
function updateImportClause(node: ImportClause, phaseModifier: ImportPhaseModifier | boolean | undefined, name: Identifier | undefined, namedBindings: NamedImportBindings | undefined) {
if (typeof phaseModifier === "boolean") {
phaseModifier = phaseModifier ? SyntaxKind.TypeKeyword : undefined;
}
return node.phaseModifier !== phaseModifier
|| node.name !== name
|| node.namedBindings !== namedBindings
? update(createImportClause(isTypeOnly, name, namedBindings), node)
? update(createImportClause(phaseModifier, name, namedBindings), node)
: node;
}

Expand Down
2 changes: 1 addition & 1 deletion src/compiler/factory/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -730,7 +730,7 @@ export function createExternalHelpersImportDeclarationIfNeeded(nodeFactory: Node

const externalHelpersImportDeclaration = nodeFactory.createImportDeclaration(
/*modifiers*/ undefined,
nodeFactory.createImportClause(/*isTypeOnly*/ false, /*name*/ undefined, namedBindings),
nodeFactory.createImportClause(/*phaseModifier*/ undefined, /*name*/ undefined, namedBindings),
nodeFactory.createStringLiteral(externalHelpersModuleNameText),
/*attributes*/ undefined,
);
Expand Down
56 changes: 43 additions & 13 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ import {
ImportDeclaration,
ImportEqualsDeclaration,
ImportOrExportSpecifier,
ImportPhaseModifier,
ImportSpecifier,
ImportTypeAssertionContainer,
ImportTypeNode,
Expand Down Expand Up @@ -5938,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);
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
}
}
else {
sourceFlags |= NodeFlags.PossiblyContainsImportMeta;
}
}
else {
expression = parseMemberExpressionOrHigher();
Expand Down Expand Up @@ -7190,6 +7202,7 @@ namespace Parser {
// could be legal, it would add complexity for very little gain.
case SyntaxKind.InterfaceKeyword:
case SyntaxKind.TypeKeyword:
case SyntaxKind.DeferKeyword:
return nextTokenIsIdentifierOnSameLine();
case SyntaxKind.ModuleKeyword:
case SyntaxKind.NamespaceKeyword:
Expand Down Expand Up @@ -7221,7 +7234,7 @@ namespace Parser {

case SyntaxKind.ImportKeyword:
nextToken();
return token() === SyntaxKind.StringLiteral || token() === SyntaxKind.AsteriskToken ||
return token() === SyntaxKind.DeferKeyword || token() === SyntaxKind.StringLiteral || token() === SyntaxKind.AsteriskToken ||
token() === SyntaxKind.OpenBraceToken || tokenIsIdentifierOrKeyword(token());
case SyntaxKind.ExportKeyword:
let currentToken = nextToken();
Expand Down Expand Up @@ -7295,6 +7308,7 @@ namespace Parser {
case SyntaxKind.NamespaceKeyword:
case SyntaxKind.TypeKeyword:
case SyntaxKind.GlobalKeyword:
case SyntaxKind.DeferKeyword:
// When these don't start a declaration, they're an identifier in an expression statement
return true;

Expand Down Expand Up @@ -8365,21 +8379,29 @@ namespace Parser {
identifier = parseIdentifier();
}

let isTypeOnly = false;
let phaseModifier: ImportPhaseModifier | undefined;
if (
identifier?.escapedText === "type" &&
(token() !== SyntaxKind.FromKeyword || isIdentifier() && lookAhead(nextTokenIsFromKeywordOrEqualsToken)) &&
(isIdentifier() || tokenAfterImportDefinitelyProducesImportDeclaration())
) {
isTypeOnly = true;
phaseModifier = SyntaxKind.TypeKeyword;
identifier = isIdentifier() ? parseIdentifier() : undefined;
}
else if (identifier?.escapedText === "defer" && token() !== SyntaxKind.FromKeyword) {
phaseModifier = SyntaxKind.DeferKeyword;
identifier = undefined;
if (isIdentifier()) {
parseErrorAtCurrentToken(Diagnostics.Default_imports_aren_t_allowed_for_deferred_imports);
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
identifier = parseIdentifier();
}
}

if (identifier && !tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration()) {
return parseImportEqualsDeclaration(pos, hasJSDoc, modifiers, identifier, isTypeOnly);
if (identifier && !tokenAfterImportedIdentifierDefinitelyProducesImportDeclaration() && phaseModifier !== SyntaxKind.DeferKeyword) {
return parseImportEqualsDeclaration(pos, hasJSDoc, modifiers, identifier, phaseModifier === SyntaxKind.TypeKeyword);
}

const importClause = tryParseImportClause(identifier, afterImportPos, isTypeOnly);
const importClause = tryParseImportClause(identifier, afterImportPos, phaseModifier, /*skipJsDocLeadingAsterisks*/ undefined);
const moduleSpecifier = parseModuleSpecifier();
const attributes = tryParseImportAttributes();

Expand All @@ -8388,7 +8410,7 @@ namespace Parser {
return withJSDoc(finishNode(node, pos), hasJSDoc);
}

function tryParseImportClause(identifier: Identifier | undefined, pos: number, isTypeOnly: boolean, skipJsDocLeadingAsterisks = false) {
function tryParseImportClause(identifier: Identifier | undefined, pos: number, phaseModifier: undefined | ImportPhaseModifier, skipJsDocLeadingAsterisks = false) {
// ImportDeclaration:
// import ImportClause from ModuleSpecifier ;
// import ModuleSpecifier;
Expand All @@ -8398,7 +8420,7 @@ namespace Parser {
token() === SyntaxKind.AsteriskToken || // import *
token() === SyntaxKind.OpenBraceToken // import {
) {
importClause = parseImportClause(identifier, pos, isTypeOnly, skipJsDocLeadingAsterisks);
importClause = parseImportClause(identifier, pos, phaseModifier, skipJsDocLeadingAsterisks);
parseExpected(SyntaxKind.FromKeyword);
}
return importClause;
Expand Down Expand Up @@ -8464,7 +8486,7 @@ namespace Parser {
return finished;
}

function parseImportClause(identifier: Identifier | undefined, pos: number, isTypeOnly: boolean, skipJsDocLeadingAsterisks: boolean) {
function parseImportClause(identifier: Identifier | undefined, pos: number, phaseModifier: undefined | ImportPhaseModifier, skipJsDocLeadingAsterisks: boolean) {
// ImportClause:
// ImportedDefaultBinding
// NameSpaceImport
Expand All @@ -8480,11 +8502,19 @@ namespace Parser {
parseOptional(SyntaxKind.CommaToken)
) {
if (skipJsDocLeadingAsterisks) scanner.setSkipJsDocLeadingAsterisks(true);
namedBindings = token() === SyntaxKind.AsteriskToken ? parseNamespaceImport() : parseNamedImportsOrExports(SyntaxKind.NamedImports);
if (token() === SyntaxKind.AsteriskToken) {
namedBindings = parseNamespaceImport();
}
else {
if (phaseModifier === SyntaxKind.DeferKeyword) {
parseErrorAtCurrentToken(Diagnostics.Named_imports_aren_t_allowed_for_deferred_imports);
nicolo-ribaudo marked this conversation as resolved.
Show resolved Hide resolved
}
namedBindings = parseNamedImportsOrExports(SyntaxKind.NamedImports);
}
if (skipJsDocLeadingAsterisks) scanner.setSkipJsDocLeadingAsterisks(false);
}

return finishNode(factory.createImportClause(isTypeOnly, identifier, namedBindings), pos);
return finishNode(factory.createImportClause(phaseModifier, identifier, namedBindings), pos);
}

function parseModuleReference() {
Expand Down Expand Up @@ -9518,7 +9548,7 @@ namespace Parser {
identifier = parseIdentifier();
}

const importClause = tryParseImportClause(identifier, afterImportTagPos, /*isTypeOnly*/ true, /*skipJsDocLeadingAsterisks*/ true);
const importClause = tryParseImportClause(identifier, afterImportTagPos, SyntaxKind.TypeKeyword, /*skipJsDocLeadingAsterisks*/ true);
const moduleSpecifier = parseModuleSpecifier();
const attributes = tryParseImportAttributes();

Expand Down
1 change: 1 addition & 0 deletions src/compiler/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export const textToKeywordObj: MapLike<KeywordSyntaxKind> = {
true: SyntaxKind.TrueKeyword,
try: SyntaxKind.TryKeyword,
type: SyntaxKind.TypeKeyword,
defer: SyntaxKind.DeferKeyword,
typeof: SyntaxKind.TypeOfKeyword,
undefined: SyntaxKind.UndefinedKeyword,
unique: SyntaxKind.UniqueKeyword,
Expand Down
Loading
Loading