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 import assertions to type only imports and import types to force the resolution mode of the specifier #47807

Merged
merged 4 commits into from
Mar 2, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
108 changes: 95 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 @@ -5979,7 +5983,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 @@ -6012,8 +6016,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 @@ -6028,12 +6034,25 @@ 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:
overrideImportMode === ModuleKind.CommonJS ? "require"
: overrideImportMode === ModuleKind.ESNext ? "import"
: undefined
},
));
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 @@ -6049,13 +6068,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 @@ -6066,12 +6125,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 @@ -35202,6 +35261,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 @@ -40031,6 +40100,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 @@ -40042,6 +40120,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 @@ -1420,6 +1420,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 @@ -2386,6 +2386,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