Skip to content

Commit ea0db9e

Browse files
authoredMar 2, 2022
Add import assertions to type only imports and import types to force the resolution mode of the specifier (#47807)
* Add import assertions for type-only imports and import types to change resolver modes * By popular request, only allow mode assertions on top-level type only imports * Add specifier options parameter to specifier generation
1 parent ff1f7b1 commit ea0db9e

File tree

60 files changed

+3302
-314
lines changed

Some content is hidden

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

60 files changed

+3302
-314
lines changed
 

‎src/compiler/checker.ts

+92-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) {
@@ -5985,7 +5989,7 @@ namespace ts {
59855989
return top;
59865990
}
59875991

5988-
function getSpecifierForModuleSymbol(symbol: Symbol, context: NodeBuilderContext) {
5992+
function getSpecifierForModuleSymbol(symbol: Symbol, context: NodeBuilderContext, overrideImportMode?: SourceFile["impliedNodeFormat"]) {
59895993
let file = getDeclarationOfKind<SourceFile>(symbol, SyntaxKind.SourceFile);
59905994
if (!file) {
59915995
const equivalentFileSymbol = firstDefined(symbol.declarations, d => getFileSymbolIfFileSymbolExportEqualsContainer(d, symbol));
@@ -6018,8 +6022,10 @@ namespace ts {
60186022
return getSourceFileOfNode(getNonAugmentationDeclaration(symbol)!).fileName; // A resolver may not be provided for baselines and errors - in those cases we use the fileName in full
60196023
}
60206024
const contextFile = getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration));
6025+
const resolutionMode = overrideImportMode || contextFile?.impliedNodeFormat;
6026+
const cacheKey = getSpecifierCacheKey(contextFile.path, resolutionMode);
60216027
const links = getSymbolLinks(symbol);
6022-
let specifier = links.specifierCache && links.specifierCache.get(contextFile.path);
6028+
let specifier = links.specifierCache && links.specifierCache.get(cacheKey);
60236029
if (!specifier) {
60246030
const isBundle = !!outFile(compilerOptions);
60256031
// For declaration bundles, we need to generate absolute paths relative to the common source dir for imports,
@@ -6034,12 +6040,22 @@ namespace ts {
60346040
specifierCompilerOptions,
60356041
contextFile,
60366042
moduleResolverHost,
6037-
{ importModuleSpecifierPreference: isBundle ? "non-relative" : "project-relative", importModuleSpecifierEnding: isBundle ? "minimal" : undefined },
6043+
{
6044+
importModuleSpecifierPreference: isBundle ? "non-relative" : "project-relative",
6045+
importModuleSpecifierEnding: isBundle ? "minimal"
6046+
: resolutionMode === ModuleKind.ESNext ? "js"
6047+
: undefined,
6048+
},
6049+
{ overrideImportMode }
60386050
));
60396051
links.specifierCache ??= new Map();
6040-
links.specifierCache.set(contextFile.path, specifier);
6052+
links.specifierCache.set(cacheKey, specifier);
60416053
}
60426054
return specifier;
6055+
6056+
function getSpecifierCacheKey(path: string, mode: SourceFile["impliedNodeFormat"] | undefined) {
6057+
return mode === undefined ? path : `${mode}|${path}`;
6058+
}
60436059
}
60446060

60456061
function symbolToEntityNameNode(symbol: Symbol): EntityName {
@@ -6055,13 +6071,53 @@ namespace ts {
60556071
// module is root, must use `ImportTypeNode`
60566072
const nonRootParts = chain.length > 1 ? createAccessFromSymbolChain(chain, chain.length - 1, 1) : undefined;
60576073
const typeParameterNodes = overrideTypeArguments || lookupTypeParameterNodes(chain, 0, context);
6058-
const specifier = getSpecifierForModuleSymbol(chain[0], context);
6074+
const contextFile = getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration));
6075+
const targetFile = getSourceFileOfModule(chain[0]);
6076+
let specifier: string | undefined;
6077+
let assertion: ImportTypeAssertionContainer | undefined;
6078+
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
6079+
// An `import` type directed at an esm format file is only going to resolve in esm mode - set the esm mode assertion
6080+
if (targetFile?.impliedNodeFormat === ModuleKind.ESNext && targetFile.impliedNodeFormat !== contextFile?.impliedNodeFormat) {
6081+
specifier = getSpecifierForModuleSymbol(chain[0], context, ModuleKind.ESNext);
6082+
assertion = factory.createImportTypeAssertionContainer(factory.createAssertClause(factory.createNodeArray([
6083+
factory.createAssertEntry(
6084+
factory.createStringLiteral("resolution-mode"),
6085+
factory.createStringLiteral("import")
6086+
)
6087+
])));
6088+
}
6089+
}
6090+
if (!specifier) {
6091+
specifier = getSpecifierForModuleSymbol(chain[0], context);
6092+
}
60596093
if (!(context.flags & NodeBuilderFlags.AllowNodeModulesRelativePaths) && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Classic && specifier.indexOf("/node_modules/") >= 0) {
6060-
// If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error
6061-
// since declaration files with these kinds of references are liable to fail when published :(
6062-
context.encounteredError = true;
6063-
if (context.tracker.reportLikelyUnsafeImportRequiredError) {
6064-
context.tracker.reportLikelyUnsafeImportRequiredError(specifier);
6094+
const oldSpecifier = specifier;
6095+
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
6096+
// We might be able to write a portable import type using a mode override; try specifier generation again, but with a different mode set
6097+
const swappedMode = contextFile?.impliedNodeFormat === ModuleKind.ESNext ? ModuleKind.CommonJS : ModuleKind.ESNext;
6098+
specifier = getSpecifierForModuleSymbol(chain[0], context, swappedMode);
6099+
6100+
if (specifier.indexOf("/node_modules/") >= 0) {
6101+
// Still unreachable :(
6102+
specifier = oldSpecifier;
6103+
}
6104+
else {
6105+
assertion = factory.createImportTypeAssertionContainer(factory.createAssertClause(factory.createNodeArray([
6106+
factory.createAssertEntry(
6107+
factory.createStringLiteral("resolution-mode"),
6108+
factory.createStringLiteral(swappedMode === ModuleKind.ESNext ? "import" : "require")
6109+
)
6110+
])));
6111+
}
6112+
}
6113+
6114+
if (!assertion) {
6115+
// If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error
6116+
// since declaration files with these kinds of references are liable to fail when published :(
6117+
context.encounteredError = true;
6118+
if (context.tracker.reportLikelyUnsafeImportRequiredError) {
6119+
context.tracker.reportLikelyUnsafeImportRequiredError(oldSpecifier);
6120+
}
60656121
}
60666122
}
60676123
const lit = factory.createLiteralTypeNode(factory.createStringLiteral(specifier));
@@ -6072,12 +6128,12 @@ namespace ts {
60726128
const lastId = isIdentifier(nonRootParts) ? nonRootParts : nonRootParts.right;
60736129
lastId.typeArguments = undefined;
60746130
}
6075-
return factory.createImportTypeNode(lit, nonRootParts as EntityName, typeParameterNodes as readonly TypeNode[], isTypeOf);
6131+
return factory.createImportTypeNode(lit, assertion, nonRootParts as EntityName, typeParameterNodes as readonly TypeNode[], isTypeOf);
60766132
}
60776133
else {
60786134
const splitNode = getTopmostIndexedAccessType(nonRootParts);
60796135
const qualifier = (splitNode.objectType as TypeReferenceNode).typeName;
6080-
return factory.createIndexedAccessTypeNode(factory.createImportTypeNode(lit, qualifier, typeParameterNodes as readonly TypeNode[], isTypeOf), splitNode.indexType);
6136+
return factory.createIndexedAccessTypeNode(factory.createImportTypeNode(lit, assertion, qualifier, typeParameterNodes as readonly TypeNode[], isTypeOf), splitNode.indexType);
60816137
}
60826138
}
60836139

@@ -35293,6 +35349,16 @@ namespace ts {
3529335349

3529435350
function checkImportType(node: ImportTypeNode) {
3529535351
checkSourceElement(node.argument);
35352+
35353+
if (node.assertions) {
35354+
const override = getResolutionModeOverrideForClause(node.assertions.assertClause, grammarErrorOnNode);
35355+
if (override) {
35356+
if (getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Node12 && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeNext) {
35357+
grammarErrorOnNode(node.assertions.assertClause, Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node12_or_nodenext);
35358+
}
35359+
}
35360+
}
35361+
3529635362
getTypeFromTypeNode(node);
3529735363
}
3529835364

@@ -40133,6 +40199,15 @@ namespace ts {
4013340199

4013440200
function checkAssertClause(declaration: ImportDeclaration | ExportDeclaration) {
4013540201
if (declaration.assertClause) {
40202+
const validForTypeAssertions = isExclusivelyTypeOnlyImportOrExport(declaration);
40203+
const override = getResolutionModeOverrideForClause(declaration.assertClause, validForTypeAssertions ? grammarErrorOnNode : undefined);
40204+
if (validForTypeAssertions && override) {
40205+
if (getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Node12 && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeNext) {
40206+
return grammarErrorOnNode(declaration.assertClause, Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node12_or_nodenext);
40207+
}
40208+
return; // Other grammar checks do not apply to type-only imports with resolution mode assertions
40209+
}
40210+
4013640211
const mode = (moduleKind === ModuleKind.NodeNext) && declaration.moduleSpecifier && getUsageModeForExpression(declaration.moduleSpecifier);
4013740212
if (mode !== ModuleKind.ESNext && moduleKind !== ModuleKind.ESNext) {
4013840213
return grammarErrorOnNode(declaration.assertClause,
@@ -40144,6 +40219,10 @@ namespace ts {
4014440219
if (isImportDeclaration(declaration) ? declaration.importClause?.isTypeOnly : declaration.isTypeOnly) {
4014540220
return grammarErrorOnNode(declaration.assertClause, Diagnostics.Import_assertions_cannot_be_used_with_type_only_imports_or_exports);
4014640221
}
40222+
40223+
if (override) {
40224+
return grammarErrorOnNode(declaration.assertClause, Diagnostics.resolution_mode_can_only_be_set_for_type_only_imports);
40225+
}
4014740226
}
4014840227
}
4014940228

‎src/compiler/diagnosticMessages.json

+12
Original file line numberDiff line numberDiff line change
@@ -1416,6 +1416,18 @@
14161416
"category": "Error",
14171417
"code": 1453
14181418
},
1419+
"`resolution-mode` can only be set for type-only imports.": {
1420+
"category": "Error",
1421+
"code": 1454
1422+
},
1423+
"`resolution-mode` is the only valid key for type import assertions.": {
1424+
"category": "Error",
1425+
"code": 1455
1426+
},
1427+
"Type import assertions should have exactly one key - `resolution-mode` - with value `import` or `require`.": {
1428+
"category": "Error",
1429+
"code": 1456
1430+
},
14191431

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

0 commit comments

Comments
 (0)
Please sign in to comment.