Skip to content

Commit 355ae11

Browse files
committed
Add import assertions for type-only imports and import types to change resolver modes
1 parent 67172e4 commit 355ae11

File tree

53 files changed

+3111
-281
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+3111
-281
lines changed

src/compiler/checker.ts

+95-13
Original file line numberDiff line numberDiff line change
@@ -3491,7 +3491,11 @@ namespace ts {
34913491
}
34923492
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
34933493
const isSyncImport = (currentSourceFile.impliedNodeFormat === ModuleKind.CommonJS && !findAncestor(location, isImportCall)) || !!findAncestor(location, isImportEqualsDeclaration);
3494-
if (isSyncImport && sourceFile.impliedNodeFormat === ModuleKind.ESNext) {
3494+
const overrideClauseHost = findAncestor(location, l => isImportTypeNode(l) || isExportDeclaration(l) || isImportDeclaration(l)) as ImportTypeNode | ImportDeclaration | ExportDeclaration | undefined;
3495+
const overrideClause = overrideClauseHost && isImportTypeNode(overrideClauseHost) ? overrideClauseHost.assertions?.assertClause : overrideClauseHost?.assertClause;
3496+
// An override clause will take effect for type-only imports and import types, and allows importing the types across formats, regardless of
3497+
// normal mode restrictions
3498+
if (isSyncImport && sourceFile.impliedNodeFormat === ModuleKind.ESNext && !getResolutionModeOverrideForClause(overrideClause)) {
34953499
error(errorNode, Diagnostics.Module_0_cannot_be_imported_using_this_construct_The_specifier_only_resolves_to_an_ES_module_which_cannot_be_imported_synchronously_Use_dynamic_import_instead, moduleReference);
34963500
}
34973501
if (mode === ModuleKind.ESNext && compilerOptions.resolveJsonModule && resolvedModule.extension === Extension.Json) {
@@ -5979,7 +5983,7 @@ namespace ts {
59795983
return top;
59805984
}
59815985

5982-
function getSpecifierForModuleSymbol(symbol: Symbol, context: NodeBuilderContext) {
5986+
function getSpecifierForModuleSymbol(symbol: Symbol, context: NodeBuilderContext, overrideImportMode?: SourceFile["impliedNodeFormat"]) {
59835987
let file = getDeclarationOfKind<SourceFile>(symbol, SyntaxKind.SourceFile);
59845988
if (!file) {
59855989
const equivalentFileSymbol = firstDefined(symbol.declarations, d => getFileSymbolIfFileSymbolExportEqualsContainer(d, symbol));
@@ -6012,8 +6016,10 @@ namespace ts {
60126016
return getSourceFileOfNode(getNonAugmentationDeclaration(symbol)!).fileName; // A resolver may not be provided for baselines and errors - in those cases we use the fileName in full
60136017
}
60146018
const contextFile = getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration));
6019+
const resolutionMode = overrideImportMode || contextFile?.impliedNodeFormat;
6020+
const cacheKey = getSpecifierCacheKey(contextFile.path, resolutionMode);
60156021
const links = getSymbolLinks(symbol);
6016-
let specifier = links.specifierCache && links.specifierCache.get(contextFile.path);
6022+
let specifier = links.specifierCache && links.specifierCache.get(cacheKey);
60176023
if (!specifier) {
60186024
const isBundle = !!outFile(compilerOptions);
60196025
// For declaration bundles, we need to generate absolute paths relative to the common source dir for imports,
@@ -6028,12 +6034,25 @@ namespace ts {
60286034
specifierCompilerOptions,
60296035
contextFile,
60306036
moduleResolverHost,
6031-
{ importModuleSpecifierPreference: isBundle ? "non-relative" : "project-relative", importModuleSpecifierEnding: isBundle ? "minimal" : undefined },
6037+
{
6038+
importModuleSpecifierPreference: isBundle ? "non-relative" : "project-relative",
6039+
importModuleSpecifierEnding: isBundle ? "minimal"
6040+
: resolutionMode === ModuleKind.ESNext ? "js"
6041+
: undefined,
6042+
overrideImportMode:
6043+
overrideImportMode === ModuleKind.CommonJS ? "require"
6044+
: overrideImportMode === ModuleKind.ESNext ? "import"
6045+
: undefined
6046+
},
60326047
));
60336048
links.specifierCache ??= new Map();
6034-
links.specifierCache.set(contextFile.path, specifier);
6049+
links.specifierCache.set(cacheKey, specifier);
60356050
}
60366051
return specifier;
6052+
6053+
function getSpecifierCacheKey(path: string, mode: SourceFile["impliedNodeFormat"] | undefined) {
6054+
return mode === undefined ? path : `${mode}|${path}`;
6055+
}
60376056
}
60386057

60396058
function symbolToEntityNameNode(symbol: Symbol): EntityName {
@@ -6049,13 +6068,53 @@ namespace ts {
60496068
// module is root, must use `ImportTypeNode`
60506069
const nonRootParts = chain.length > 1 ? createAccessFromSymbolChain(chain, chain.length - 1, 1) : undefined;
60516070
const typeParameterNodes = overrideTypeArguments || lookupTypeParameterNodes(chain, 0, context);
6052-
const specifier = getSpecifierForModuleSymbol(chain[0], context);
6071+
const contextFile = getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration));
6072+
const targetFile = getSourceFileOfModule(chain[0]);
6073+
let specifier: string | undefined;
6074+
let assertion: ImportTypeAssertionContainer | undefined;
6075+
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
6076+
// An `import` type directed at an esm format file is only going to resolve in esm mode - set the esm mode assertion
6077+
if (targetFile?.impliedNodeFormat === ModuleKind.ESNext && targetFile.impliedNodeFormat !== contextFile?.impliedNodeFormat) {
6078+
specifier = getSpecifierForModuleSymbol(chain[0], context, ModuleKind.ESNext);
6079+
assertion = factory.createImportTypeAssertionContainer(factory.createAssertClause(factory.createNodeArray([
6080+
factory.createAssertEntry(
6081+
factory.createStringLiteral("resolution-mode"),
6082+
factory.createStringLiteral("import")
6083+
)
6084+
])));
6085+
}
6086+
}
6087+
if (!specifier) {
6088+
specifier = getSpecifierForModuleSymbol(chain[0], context);
6089+
}
60536090
if (!(context.flags & NodeBuilderFlags.AllowNodeModulesRelativePaths) && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Classic && specifier.indexOf("/node_modules/") >= 0) {
6054-
// If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error
6055-
// since declaration files with these kinds of references are liable to fail when published :(
6056-
context.encounteredError = true;
6057-
if (context.tracker.reportLikelyUnsafeImportRequiredError) {
6058-
context.tracker.reportLikelyUnsafeImportRequiredError(specifier);
6091+
const oldSpecifier = specifier;
6092+
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
6093+
// We might be able to write a portable import type using a mode override; try specifier generation again, but with a different mode set
6094+
const swappedMode = contextFile?.impliedNodeFormat === ModuleKind.ESNext ? ModuleKind.CommonJS : ModuleKind.ESNext;
6095+
specifier = getSpecifierForModuleSymbol(chain[0], context, swappedMode);
6096+
6097+
if (specifier.indexOf("/node_modules/") >= 0) {
6098+
// Still unreachable :(
6099+
specifier = oldSpecifier;
6100+
}
6101+
else {
6102+
assertion = factory.createImportTypeAssertionContainer(factory.createAssertClause(factory.createNodeArray([
6103+
factory.createAssertEntry(
6104+
factory.createStringLiteral("resolution-mode"),
6105+
factory.createStringLiteral(swappedMode === ModuleKind.ESNext ? "import" : "require")
6106+
)
6107+
])));
6108+
}
6109+
}
6110+
6111+
if (!assertion) {
6112+
// If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error
6113+
// since declaration files with these kinds of references are liable to fail when published :(
6114+
context.encounteredError = true;
6115+
if (context.tracker.reportLikelyUnsafeImportRequiredError) {
6116+
context.tracker.reportLikelyUnsafeImportRequiredError(oldSpecifier);
6117+
}
60596118
}
60606119
}
60616120
const lit = factory.createLiteralTypeNode(factory.createStringLiteral(specifier));
@@ -6066,12 +6125,12 @@ namespace ts {
60666125
const lastId = isIdentifier(nonRootParts) ? nonRootParts : nonRootParts.right;
60676126
lastId.typeArguments = undefined;
60686127
}
6069-
return factory.createImportTypeNode(lit, nonRootParts as EntityName, typeParameterNodes as readonly TypeNode[], isTypeOf);
6128+
return factory.createImportTypeNode(lit, assertion, nonRootParts as EntityName, typeParameterNodes as readonly TypeNode[], isTypeOf);
60706129
}
60716130
else {
60726131
const splitNode = getTopmostIndexedAccessType(nonRootParts);
60736132
const qualifier = (splitNode.objectType as TypeReferenceNode).typeName;
6074-
return factory.createIndexedAccessTypeNode(factory.createImportTypeNode(lit, qualifier, typeParameterNodes as readonly TypeNode[], isTypeOf), splitNode.indexType);
6133+
return factory.createIndexedAccessTypeNode(factory.createImportTypeNode(lit, assertion, qualifier, typeParameterNodes as readonly TypeNode[], isTypeOf), splitNode.indexType);
60756134
}
60766135
}
60776136

@@ -35202,6 +35261,16 @@ namespace ts {
3520235261

3520335262
function checkImportType(node: ImportTypeNode) {
3520435263
checkSourceElement(node.argument);
35264+
35265+
if (node.assertions) {
35266+
const override = getResolutionModeOverrideForClause(node.assertions.assertClause, grammarErrorOnNode);
35267+
if (override) {
35268+
if (getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Node12 && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeNext) {
35269+
grammarErrorOnNode(node.assertions.assertClause, Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node12_or_nodenext);
35270+
}
35271+
}
35272+
}
35273+
3520535274
getTypeFromTypeNode(node);
3520635275
}
3520735276

@@ -40031,6 +40100,15 @@ namespace ts {
4003140100

4003240101
function checkAssertClause(declaration: ImportDeclaration | ExportDeclaration) {
4003340102
if (declaration.assertClause) {
40103+
const validForTypeAssertions = isExclusivelyTypeOnlyImportOrExport(declaration);
40104+
const override = getResolutionModeOverrideForClause(declaration.assertClause, validForTypeAssertions ? grammarErrorOnNode : undefined);
40105+
if (validForTypeAssertions && override) {
40106+
if (getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Node12 && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeNext) {
40107+
return grammarErrorOnNode(declaration.assertClause, Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node12_or_nodenext);
40108+
}
40109+
return; // Other grammar checks do not apply to type-only imports with resolution mode assertions
40110+
}
40111+
4003440112
const mode = (moduleKind === ModuleKind.NodeNext) && declaration.moduleSpecifier && getUsageModeForExpression(declaration.moduleSpecifier);
4003540113
if (mode !== ModuleKind.ESNext && moduleKind !== ModuleKind.ESNext) {
4003640114
return grammarErrorOnNode(declaration.assertClause,
@@ -40042,6 +40120,10 @@ namespace ts {
4004240120
if (isImportDeclaration(declaration) ? declaration.importClause?.isTypeOnly : declaration.isTypeOnly) {
4004340121
return grammarErrorOnNode(declaration.assertClause, Diagnostics.Import_assertions_cannot_be_used_with_type_only_imports_or_exports);
4004440122
}
40123+
40124+
if (override) {
40125+
return grammarErrorOnNode(declaration.assertClause, Diagnostics.resolution_mode_can_only_be_set_for_type_only_imports);
40126+
}
4004540127
}
4004640128
}
4004740129

src/compiler/diagnosticMessages.json

+12
Original file line numberDiff line numberDiff line change
@@ -1420,6 +1420,18 @@
14201420
"category": "Error",
14211421
"code": 1453
14221422
},
1423+
"`resolution-mode` can only be set for type-only imports.": {
1424+
"category": "Error",
1425+
"code": 1454
1426+
},
1427+
"`resolution-mode` is the only valid key for type import assertions.": {
1428+
"category": "Error",
1429+
"code": 1455
1430+
},
1431+
"Type import assertions should have exactly one key - `resolution-mode` - with value `import` or `require`.": {
1432+
"category": "Error",
1433+
"code": 1456
1434+
},
14231435

14241436
"The 'import.meta' meta-property is not allowed in files which will build into CommonJS output.": {
14251437
"category": "Error",

src/compiler/emitter.ts

+13
Original file line numberDiff line numberDiff line change
@@ -2386,6 +2386,19 @@ namespace ts {
23862386
writeKeyword("import");
23872387
writePunctuation("(");
23882388
emit(node.argument);
2389+
if (node.assertions) {
2390+
writePunctuation(",");
2391+
writeSpace();
2392+
writePunctuation("{");
2393+
writeSpace();
2394+
writeKeyword("assert");
2395+
writePunctuation(":");
2396+
writeSpace();
2397+
const elements = node.assertions.assertClause.elements;
2398+
emitList(node.assertions.assertClause, elements, ListFormat.ImportClauseEntries);
2399+
writeSpace();
2400+
writePunctuation("}");
2401+
}
23892402
writePunctuation(")");
23902403
if (node.qualifier) {
23912404
writePunctuation(".");

0 commit comments

Comments
 (0)