Skip to content

Commit 45e3c3d

Browse files
committed
Add import assertions for type-only imports and import types to change resolver modes
1 parent 7997a62 commit 45e3c3d

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
@@ -3488,7 +3488,11 @@ namespace ts {
34883488
}
34893489
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
34903490
const isSyncImport = (currentSourceFile.impliedNodeFormat === ModuleKind.CommonJS && !findAncestor(location, isImportCall)) || !!findAncestor(location, isImportEqualsDeclaration);
3491-
if (isSyncImport && sourceFile.impliedNodeFormat === ModuleKind.ESNext) {
3491+
const overrideClauseHost = findAncestor(location, l => isImportTypeNode(l) || isExportDeclaration(l) || isImportDeclaration(l)) as ImportTypeNode | ImportDeclaration | ExportDeclaration | undefined;
3492+
const overrideClause = overrideClauseHost && isImportTypeNode(overrideClauseHost) ? overrideClauseHost.assertions?.assertClause : overrideClauseHost?.assertClause;
3493+
// An override clause will take effect for type-only imports and import types, and allows importing the types across formats, regardless of
3494+
// normal mode restrictions
3495+
if (isSyncImport && sourceFile.impliedNodeFormat === ModuleKind.ESNext && !getResolutionModeOverrideForClause(overrideClause)) {
34923496
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);
34933497
}
34943498
if (mode === ModuleKind.ESNext && compilerOptions.resolveJsonModule && resolvedModule.extension === Extension.Json) {
@@ -5969,7 +5973,7 @@ namespace ts {
59695973
return top;
59705974
}
59715975

5972-
function getSpecifierForModuleSymbol(symbol: Symbol, context: NodeBuilderContext) {
5976+
function getSpecifierForModuleSymbol(symbol: Symbol, context: NodeBuilderContext, overrideImportMode?: SourceFile["impliedNodeFormat"]) {
59735977
let file = getDeclarationOfKind<SourceFile>(symbol, SyntaxKind.SourceFile);
59745978
if (!file) {
59755979
const equivalentFileSymbol = firstDefined(symbol.declarations, d => getFileSymbolIfFileSymbolExportEqualsContainer(d, symbol));
@@ -6002,8 +6006,10 @@ namespace ts {
60026006
return getSourceFileOfNode(getNonAugmentationDeclaration(symbol)!).fileName; // A resolver may not be provided for baselines and errors - in those cases we use the fileName in full
60036007
}
60046008
const contextFile = getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration));
6009+
const resolutionMode = overrideImportMode || contextFile?.impliedNodeFormat;
6010+
const cacheKey = getSpecifierCacheKey(contextFile.path, resolutionMode);
60056011
const links = getSymbolLinks(symbol);
6006-
let specifier = links.specifierCache && links.specifierCache.get(contextFile.path);
6012+
let specifier = links.specifierCache && links.specifierCache.get(cacheKey);
60076013
if (!specifier) {
60086014
const isBundle = !!outFile(compilerOptions);
60096015
// For declaration bundles, we need to generate absolute paths relative to the common source dir for imports,
@@ -6018,12 +6024,25 @@ namespace ts {
60186024
specifierCompilerOptions,
60196025
contextFile,
60206026
moduleResolverHost,
6021-
{ importModuleSpecifierPreference: isBundle ? "non-relative" : "project-relative", importModuleSpecifierEnding: isBundle ? "minimal" : undefined },
6027+
{
6028+
importModuleSpecifierPreference: isBundle ? "non-relative" : "project-relative",
6029+
importModuleSpecifierEnding: isBundle ? "minimal"
6030+
: resolutionMode === ModuleKind.ESNext ? "js"
6031+
: undefined,
6032+
overrideImportMode:
6033+
overrideImportMode === ModuleKind.CommonJS ? "require"
6034+
: overrideImportMode === ModuleKind.ESNext ? "import"
6035+
: undefined
6036+
},
60226037
));
60236038
links.specifierCache ??= new Map();
6024-
links.specifierCache.set(contextFile.path, specifier);
6039+
links.specifierCache.set(cacheKey, specifier);
60256040
}
60266041
return specifier;
6042+
6043+
function getSpecifierCacheKey(path: string, mode: SourceFile["impliedNodeFormat"] | undefined) {
6044+
return mode === undefined ? path : `${mode}|${path}`;
6045+
}
60276046
}
60286047

60296048
function symbolToEntityNameNode(symbol: Symbol): EntityName {
@@ -6039,13 +6058,53 @@ namespace ts {
60396058
// module is root, must use `ImportTypeNode`
60406059
const nonRootParts = chain.length > 1 ? createAccessFromSymbolChain(chain, chain.length - 1, 1) : undefined;
60416060
const typeParameterNodes = overrideTypeArguments || lookupTypeParameterNodes(chain, 0, context);
6042-
const specifier = getSpecifierForModuleSymbol(chain[0], context);
6061+
const contextFile = getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration));
6062+
const targetFile = getSourceFileOfModule(chain[0]);
6063+
let specifier: string | undefined;
6064+
let assertion: ImportTypeAssertionContainer | undefined;
6065+
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
6066+
// An `import` type directed at an esm format file is only going to resolve in esm mode - set the esm mode assertion
6067+
if (targetFile?.impliedNodeFormat === ModuleKind.ESNext && targetFile.impliedNodeFormat !== contextFile?.impliedNodeFormat) {
6068+
specifier = getSpecifierForModuleSymbol(chain[0], context, ModuleKind.ESNext);
6069+
assertion = factory.createImportTypeAssertionContainer(factory.createAssertClause(factory.createNodeArray([
6070+
factory.createAssertEntry(
6071+
factory.createStringLiteral("resolution-mode"),
6072+
factory.createStringLiteral("import")
6073+
)
6074+
])));
6075+
}
6076+
}
6077+
if (!specifier) {
6078+
specifier = getSpecifierForModuleSymbol(chain[0], context);
6079+
}
60436080
if (!(context.flags & NodeBuilderFlags.AllowNodeModulesRelativePaths) && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Classic && specifier.indexOf("/node_modules/") >= 0) {
6044-
// If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error
6045-
// since declaration files with these kinds of references are liable to fail when published :(
6046-
context.encounteredError = true;
6047-
if (context.tracker.reportLikelyUnsafeImportRequiredError) {
6048-
context.tracker.reportLikelyUnsafeImportRequiredError(specifier);
6081+
const oldSpecifier = specifier;
6082+
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
6083+
// We might be able to write a portable import type using a mode override; try specifier generation again, but with a different mode set
6084+
const swappedMode = contextFile?.impliedNodeFormat === ModuleKind.ESNext ? ModuleKind.CommonJS : ModuleKind.ESNext;
6085+
specifier = getSpecifierForModuleSymbol(chain[0], context, swappedMode);
6086+
6087+
if (specifier.indexOf("/node_modules/") >= 0) {
6088+
// Still unreachable :(
6089+
specifier = oldSpecifier;
6090+
}
6091+
else {
6092+
assertion = factory.createImportTypeAssertionContainer(factory.createAssertClause(factory.createNodeArray([
6093+
factory.createAssertEntry(
6094+
factory.createStringLiteral("resolution-mode"),
6095+
factory.createStringLiteral(swappedMode === ModuleKind.ESNext ? "import" : "require")
6096+
)
6097+
])));
6098+
}
6099+
}
6100+
6101+
if (!assertion) {
6102+
// If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error
6103+
// since declaration files with these kinds of references are liable to fail when published :(
6104+
context.encounteredError = true;
6105+
if (context.tracker.reportLikelyUnsafeImportRequiredError) {
6106+
context.tracker.reportLikelyUnsafeImportRequiredError(oldSpecifier);
6107+
}
60496108
}
60506109
}
60516110
const lit = factory.createLiteralTypeNode(factory.createStringLiteral(specifier));
@@ -6056,12 +6115,12 @@ namespace ts {
60566115
const lastId = isIdentifier(nonRootParts) ? nonRootParts : nonRootParts.right;
60576116
lastId.typeArguments = undefined;
60586117
}
6059-
return factory.createImportTypeNode(lit, nonRootParts as EntityName, typeParameterNodes as readonly TypeNode[], isTypeOf);
6118+
return factory.createImportTypeNode(lit, assertion, nonRootParts as EntityName, typeParameterNodes as readonly TypeNode[], isTypeOf);
60606119
}
60616120
else {
60626121
const splitNode = getTopmostIndexedAccessType(nonRootParts);
60636122
const qualifier = (splitNode.objectType as TypeReferenceNode).typeName;
6064-
return factory.createIndexedAccessTypeNode(factory.createImportTypeNode(lit, qualifier, typeParameterNodes as readonly TypeNode[], isTypeOf), splitNode.indexType);
6123+
return factory.createIndexedAccessTypeNode(factory.createImportTypeNode(lit, assertion, qualifier, typeParameterNodes as readonly TypeNode[], isTypeOf), splitNode.indexType);
60656124
}
60666125
}
60676126

@@ -35124,6 +35183,16 @@ namespace ts {
3512435183

3512535184
function checkImportType(node: ImportTypeNode) {
3512635185
checkSourceElement(node.argument);
35186+
35187+
if (node.assertions) {
35188+
const override = getResolutionModeOverrideForClause(node.assertions.assertClause, grammarErrorOnNode);
35189+
if (override) {
35190+
if (getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Node12 && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeNext) {
35191+
grammarErrorOnNode(node.assertions.assertClause, Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node12_or_nodenext);
35192+
}
35193+
}
35194+
}
35195+
3512735196
getTypeFromTypeNode(node);
3512835197
}
3512935198

@@ -39952,6 +40021,15 @@ namespace ts {
3995240021

3995340022
function checkAssertClause(declaration: ImportDeclaration | ExportDeclaration) {
3995440023
if (declaration.assertClause) {
40024+
const validForTypeAssertions = isExclusivelyTypeOnlyImportOrExport(declaration);
40025+
const override = getResolutionModeOverrideForClause(declaration.assertClause, validForTypeAssertions ? grammarErrorOnNode : undefined);
40026+
if (validForTypeAssertions && override) {
40027+
if (getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Node12 && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeNext) {
40028+
return grammarErrorOnNode(declaration.assertClause, Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node12_or_nodenext);
40029+
}
40030+
return; // Other grammar checks do not apply to type-only imports with resolution mode assertions
40031+
}
40032+
3995540033
const mode = (moduleKind === ModuleKind.NodeNext) && declaration.moduleSpecifier && getUsageModeForExpression(declaration.moduleSpecifier);
3995640034
if (mode !== ModuleKind.ESNext && moduleKind !== ModuleKind.ESNext) {
3995740035
return grammarErrorOnNode(declaration.assertClause,
@@ -39963,6 +40041,10 @@ namespace ts {
3996340041
if (isImportDeclaration(declaration) ? declaration.importClause?.isTypeOnly : declaration.isTypeOnly) {
3996440042
return grammarErrorOnNode(declaration.assertClause, Diagnostics.Import_assertions_cannot_be_used_with_type_only_imports_or_exports);
3996540043
}
40044+
40045+
if (override) {
40046+
return grammarErrorOnNode(declaration.assertClause, Diagnostics.resolution_mode_can_only_be_set_for_type_only_imports);
40047+
}
3996640048
}
3996740049
}
3996840050

src/compiler/diagnosticMessages.json

+12
Original file line numberDiff line numberDiff line change
@@ -1408,6 +1408,18 @@
14081408
"category": "Error",
14091409
"code": 1453
14101410
},
1411+
"`resolution-mode` can only be set for type-only imports.": {
1412+
"category": "Error",
1413+
"code": 1454
1414+
},
1415+
"`resolution-mode` is the only valid key for type import assertions.": {
1416+
"category": "Error",
1417+
"code": 1455
1418+
},
1419+
"Type import assertions should have exactly one key - `resolution-mode` - with value `import` or `require`.": {
1420+
"category": "Error",
1421+
"code": 1456
1422+
},
14111423

14121424
"The 'import.meta' meta-property is not allowed in files which will build into CommonJS output.": {
14131425
"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)