Skip to content

Commit

Permalink
Add import assertions to type only imports and import types to force …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
weswigham authored Mar 2, 2022
1 parent ff1f7b1 commit ea0db9e
Show file tree
Hide file tree
Showing 60 changed files with 3,302 additions and 314 deletions.
105 changes: 92 additions & 13 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3491,7 +3491,11 @@ namespace ts {
}
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
const isSyncImport = (currentSourceFile.impliedNodeFormat === ModuleKind.CommonJS && !findAncestor(location, isImportCall)) || !!findAncestor(location, isImportEqualsDeclaration);
if (isSyncImport && sourceFile.impliedNodeFormat === ModuleKind.ESNext) {
const overrideClauseHost = findAncestor(location, l => isImportTypeNode(l) || isExportDeclaration(l) || isImportDeclaration(l)) as ImportTypeNode | ImportDeclaration | ExportDeclaration | undefined;
const overrideClause = overrideClauseHost && isImportTypeNode(overrideClauseHost) ? overrideClauseHost.assertions?.assertClause : overrideClauseHost?.assertClause;
// An override clause will take effect for type-only imports and import types, and allows importing the types across formats, regardless of
// normal mode restrictions
if (isSyncImport && sourceFile.impliedNodeFormat === ModuleKind.ESNext && !getResolutionModeOverrideForClause(overrideClause)) {
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);
}
if (mode === ModuleKind.ESNext && compilerOptions.resolveJsonModule && resolvedModule.extension === Extension.Json) {
Expand Down Expand Up @@ -5985,7 +5989,7 @@ namespace ts {
return top;
}

function getSpecifierForModuleSymbol(symbol: Symbol, context: NodeBuilderContext) {
function getSpecifierForModuleSymbol(symbol: Symbol, context: NodeBuilderContext, overrideImportMode?: SourceFile["impliedNodeFormat"]) {
let file = getDeclarationOfKind<SourceFile>(symbol, SyntaxKind.SourceFile);
if (!file) {
const equivalentFileSymbol = firstDefined(symbol.declarations, d => getFileSymbolIfFileSymbolExportEqualsContainer(d, symbol));
Expand Down Expand Up @@ -6018,8 +6022,10 @@ namespace ts {
return getSourceFileOfNode(getNonAugmentationDeclaration(symbol)!).fileName; // A resolver may not be provided for baselines and errors - in those cases we use the fileName in full
}
const contextFile = getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration));
const resolutionMode = overrideImportMode || contextFile?.impliedNodeFormat;
const cacheKey = getSpecifierCacheKey(contextFile.path, resolutionMode);
const links = getSymbolLinks(symbol);
let specifier = links.specifierCache && links.specifierCache.get(contextFile.path);
let specifier = links.specifierCache && links.specifierCache.get(cacheKey);
if (!specifier) {
const isBundle = !!outFile(compilerOptions);
// For declaration bundles, we need to generate absolute paths relative to the common source dir for imports,
Expand All @@ -6034,12 +6040,22 @@ namespace ts {
specifierCompilerOptions,
contextFile,
moduleResolverHost,
{ importModuleSpecifierPreference: isBundle ? "non-relative" : "project-relative", importModuleSpecifierEnding: isBundle ? "minimal" : undefined },
{
importModuleSpecifierPreference: isBundle ? "non-relative" : "project-relative",
importModuleSpecifierEnding: isBundle ? "minimal"
: resolutionMode === ModuleKind.ESNext ? "js"
: undefined,
},
{ overrideImportMode }
));
links.specifierCache ??= new Map();
links.specifierCache.set(contextFile.path, specifier);
links.specifierCache.set(cacheKey, specifier);
}
return specifier;

function getSpecifierCacheKey(path: string, mode: SourceFile["impliedNodeFormat"] | undefined) {
return mode === undefined ? path : `${mode}|${path}`;
}
}

function symbolToEntityNameNode(symbol: Symbol): EntityName {
Expand All @@ -6055,13 +6071,53 @@ namespace ts {
// module is root, must use `ImportTypeNode`
const nonRootParts = chain.length > 1 ? createAccessFromSymbolChain(chain, chain.length - 1, 1) : undefined;
const typeParameterNodes = overrideTypeArguments || lookupTypeParameterNodes(chain, 0, context);
const specifier = getSpecifierForModuleSymbol(chain[0], context);
const contextFile = getSourceFileOfNode(getOriginalNode(context.enclosingDeclaration));
const targetFile = getSourceFileOfModule(chain[0]);
let specifier: string | undefined;
let assertion: ImportTypeAssertionContainer | undefined;
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
// An `import` type directed at an esm format file is only going to resolve in esm mode - set the esm mode assertion
if (targetFile?.impliedNodeFormat === ModuleKind.ESNext && targetFile.impliedNodeFormat !== contextFile?.impliedNodeFormat) {
specifier = getSpecifierForModuleSymbol(chain[0], context, ModuleKind.ESNext);
assertion = factory.createImportTypeAssertionContainer(factory.createAssertClause(factory.createNodeArray([
factory.createAssertEntry(
factory.createStringLiteral("resolution-mode"),
factory.createStringLiteral("import")
)
])));
}
}
if (!specifier) {
specifier = getSpecifierForModuleSymbol(chain[0], context);
}
if (!(context.flags & NodeBuilderFlags.AllowNodeModulesRelativePaths) && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Classic && specifier.indexOf("/node_modules/") >= 0) {
// If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error
// since declaration files with these kinds of references are liable to fail when published :(
context.encounteredError = true;
if (context.tracker.reportLikelyUnsafeImportRequiredError) {
context.tracker.reportLikelyUnsafeImportRequiredError(specifier);
const oldSpecifier = specifier;
if (getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Node12 || getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.NodeNext) {
// We might be able to write a portable import type using a mode override; try specifier generation again, but with a different mode set
const swappedMode = contextFile?.impliedNodeFormat === ModuleKind.ESNext ? ModuleKind.CommonJS : ModuleKind.ESNext;
specifier = getSpecifierForModuleSymbol(chain[0], context, swappedMode);

if (specifier.indexOf("/node_modules/") >= 0) {
// Still unreachable :(
specifier = oldSpecifier;
}
else {
assertion = factory.createImportTypeAssertionContainer(factory.createAssertClause(factory.createNodeArray([
factory.createAssertEntry(
factory.createStringLiteral("resolution-mode"),
factory.createStringLiteral(swappedMode === ModuleKind.ESNext ? "import" : "require")
)
])));
}
}

if (!assertion) {
// If ultimately we can only name the symbol with a reference that dives into a `node_modules` folder, we should error
// since declaration files with these kinds of references are liable to fail when published :(
context.encounteredError = true;
if (context.tracker.reportLikelyUnsafeImportRequiredError) {
context.tracker.reportLikelyUnsafeImportRequiredError(oldSpecifier);
}
}
}
const lit = factory.createLiteralTypeNode(factory.createStringLiteral(specifier));
Expand All @@ -6072,12 +6128,12 @@ namespace ts {
const lastId = isIdentifier(nonRootParts) ? nonRootParts : nonRootParts.right;
lastId.typeArguments = undefined;
}
return factory.createImportTypeNode(lit, nonRootParts as EntityName, typeParameterNodes as readonly TypeNode[], isTypeOf);
return factory.createImportTypeNode(lit, assertion, nonRootParts as EntityName, typeParameterNodes as readonly TypeNode[], isTypeOf);
}
else {
const splitNode = getTopmostIndexedAccessType(nonRootParts);
const qualifier = (splitNode.objectType as TypeReferenceNode).typeName;
return factory.createIndexedAccessTypeNode(factory.createImportTypeNode(lit, qualifier, typeParameterNodes as readonly TypeNode[], isTypeOf), splitNode.indexType);
return factory.createIndexedAccessTypeNode(factory.createImportTypeNode(lit, assertion, qualifier, typeParameterNodes as readonly TypeNode[], isTypeOf), splitNode.indexType);
}
}

Expand Down Expand Up @@ -35293,6 +35349,16 @@ namespace ts {

function checkImportType(node: ImportTypeNode) {
checkSourceElement(node.argument);

if (node.assertions) {
const override = getResolutionModeOverrideForClause(node.assertions.assertClause, grammarErrorOnNode);
if (override) {
if (getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Node12 && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeNext) {
grammarErrorOnNode(node.assertions.assertClause, Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node12_or_nodenext);
}
}
}

getTypeFromTypeNode(node);
}

Expand Down Expand Up @@ -40133,6 +40199,15 @@ namespace ts {

function checkAssertClause(declaration: ImportDeclaration | ExportDeclaration) {
if (declaration.assertClause) {
const validForTypeAssertions = isExclusivelyTypeOnlyImportOrExport(declaration);
const override = getResolutionModeOverrideForClause(declaration.assertClause, validForTypeAssertions ? grammarErrorOnNode : undefined);
if (validForTypeAssertions && override) {
if (getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.Node12 && getEmitModuleResolutionKind(compilerOptions) !== ModuleResolutionKind.NodeNext) {
return grammarErrorOnNode(declaration.assertClause, Diagnostics.Resolution_modes_are_only_supported_when_moduleResolution_is_node12_or_nodenext);
}
return; // Other grammar checks do not apply to type-only imports with resolution mode assertions
}

const mode = (moduleKind === ModuleKind.NodeNext) && declaration.moduleSpecifier && getUsageModeForExpression(declaration.moduleSpecifier);
if (mode !== ModuleKind.ESNext && moduleKind !== ModuleKind.ESNext) {
return grammarErrorOnNode(declaration.assertClause,
Expand All @@ -40144,6 +40219,10 @@ namespace ts {
if (isImportDeclaration(declaration) ? declaration.importClause?.isTypeOnly : declaration.isTypeOnly) {
return grammarErrorOnNode(declaration.assertClause, Diagnostics.Import_assertions_cannot_be_used_with_type_only_imports_or_exports);
}

if (override) {
return grammarErrorOnNode(declaration.assertClause, Diagnostics.resolution_mode_can_only_be_set_for_type_only_imports);
}
}
}

Expand Down
12 changes: 12 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,18 @@
"category": "Error",
"code": 1453
},
"`resolution-mode` can only be set for type-only imports.": {
"category": "Error",
"code": 1454
},
"`resolution-mode` is the only valid key for type import assertions.": {
"category": "Error",
"code": 1455
},
"Type import assertions should have exactly one key - `resolution-mode` - with value `import` or `require`.": {
"category": "Error",
"code": 1456
},

"The 'import.meta' meta-property is not allowed in files which will build into CommonJS output.": {
"category": "Error",
Expand Down
13 changes: 13 additions & 0 deletions src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2389,6 +2389,19 @@ namespace ts {
writeKeyword("import");
writePunctuation("(");
emit(node.argument);
if (node.assertions) {
writePunctuation(",");
writeSpace();
writePunctuation("{");
writeSpace();
writeKeyword("assert");
writePunctuation(":");
writeSpace();
const elements = node.assertions.assertClause.elements;
emitList(node.assertions.assertClause, elements, ListFormat.ImportClauseEntries);
writeSpace();
writePunctuation("}");
}
writePunctuation(")");
if (node.qualifier) {
writePunctuation(".");
Expand Down
Loading

0 comments on commit ea0db9e

Please sign in to comment.