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 --allowArbitraryExtensions, a flag for allowing arbitrary extensions on import paths #51435

Merged
Merged
Show file tree
Hide file tree
Changes from 5 commits
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
2 changes: 1 addition & 1 deletion src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4738,7 +4738,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const mode = contextSpecifier && isStringLiteralLike(contextSpecifier) ? getModeForUsageLocation(currentSourceFile, contextSpecifier) : currentSourceFile.impliedNodeFormat;
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
const resolvedModule = getResolvedModule(currentSourceFile, moduleReference, mode);
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule);
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule, currentSourceFile);
const sourceFile = resolvedModule
&& (!resolutionDiagnostic || resolutionDiagnostic === Diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set)
&& host.getSourceFile(resolvedModule.resolvedFileName);
Expand Down
8 changes: 8 additions & 0 deletions src/compiler/commandLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1206,6 +1206,14 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
description: Diagnostics.Enable_importing_json_files,
defaultValueDescription: false,
},
{
name: "allowArbitraryExtensions",
type: "boolean",
affectsModuleResolution: true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was gathering module-resolution-affecting compiler options for docs, and noticed this. AFAICT it doesn’t affect module resolution, just whether an error is issued. This should probably be swapped for affectsSemanticDiagnostics.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something that should be in 5.0 beta? I just got to this comment in my email and noticed a PR wasn't sent for this (but I don't know how these two settings matter myself)

category: Diagnostics.Modules,
description: Diagnostics.Enable_importing_files_with_any_extension_provided_a_declaration_file_is_present,
defaultValueDescription: false,
},

{
name: "out",
Expand Down
12 changes: 12 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5190,6 +5190,18 @@
"category": "Message",
"code": 6261
},
"File name '{0}' has a '{1}' extension - looking up '{2}' instead.": {
"category": "Message",
"code": 6262
},
"Module '{0}' was resolved to '{1}', but '--allowArbitraryExtensions' is not set.": {
"category": "Error",
"code": 6263
},
"Enable importing files with any extension, provided a declaration file is present.": {
"category": "Message",
"code": 6264
},

"Directory '{0}' has no containing package.json scope. Imports will not resolve.": {
"category": "Message",
Expand Down
83 changes: 40 additions & 43 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ import {
getRelativePathFromDirectory,
getResolveJsonModule,
getRootLength,
hasJSFileExtension,
hasProperty,
hasTrailingDirectorySeparator,
hostGetCanonicalFileName,
Expand Down Expand Up @@ -99,7 +98,6 @@ import {
startsWith,
stringContains,
supportedDeclarationExtensions,
supportedTSExtensionsFlat,
supportedTSImplementationExtensions,
toPath,
tryExtractTSExtension,
Expand Down Expand Up @@ -151,7 +149,7 @@ function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | und
/** Result of trying to resolve a module. */
interface Resolved {
path: string;
extension: Extension;
extension: string;
packageId: PackageId | undefined;
/**
* When the resolved is not created from cache, the value is
Expand All @@ -170,7 +168,7 @@ interface Resolved {
interface PathAndExtension {
path: string;
// (Use a different name than `extension` to make sure Resolved isn't assignable to PathAndExtension.)
ext: Extension;
ext: string;
resolvedUsingTsExtension: boolean | undefined;
}

Expand Down Expand Up @@ -1856,21 +1854,21 @@ function loadModuleFromFile(extensions: Extensions, candidate: string, onlyRecor
}

function loadModuleFromFileNoImplicitExtensions(extensions: Extensions, candidate: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PathAndExtension | undefined {
// If that didn't work, try stripping a ".js" or ".jsx" extension and replacing it with a TypeScript one;
// e.g. "./foo.js" can be matched by "./foo.ts" or "./foo.d.ts"
if (hasJSFileExtension(candidate) ||
extensions & Extensions.Json && fileExtensionIs(candidate, Extension.Json) ||
extensions & (Extensions.TypeScript | Extensions.Declaration)
&& moduleResolutionSupportsResolvingTsExtensions(state.compilerOptions)
&& fileExtensionIsOneOf(candidate, supportedTSExtensionsFlat)
) {
const extensionless = removeFileExtension(candidate);
const extension = candidate.substring(extensionless.length);
if (state.traceEnabled) {
trace(state.host, Diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension);
}
return tryAddingExtensions(extensionless, extensions, extension, onlyRecordFailures, state);
const filename = getBaseFileName(candidate);
if (filename.indexOf(".") === -1) {
return undefined; // extensionless import, no lookups performed, since we don't support extensionless files
}
let extensionless = removeFileExtension(candidate);
if (extensionless === candidate) {
// Once TS native extensions are handled, handle arbitrary extensions for declaration file mapping
extensionless = candidate.substring(0, candidate.lastIndexOf("."));
}

const extension = candidate.substring(extensionless.length);
if (state.traceEnabled) {
trace(state.host, Diagnostics.File_name_0_has_a_1_extension_stripping_it, candidate, extension);
}
return tryAddingExtensions(extensionless, extensions, extension, onlyRecordFailures, state);
}

/**
Expand Down Expand Up @@ -1909,47 +1907,46 @@ function tryAddingExtensions(candidate: string, extensions: Extensions, original
case Extension.Mjs:
case Extension.Mts:
case Extension.Dmts:
return extensions & Extensions.TypeScript && tryExtension(Extension.Mts)
|| extensions & Extensions.Declaration && tryExtension(Extension.Dmts)
return extensions & Extensions.TypeScript && tryExtension(Extension.Mts, originalExtension === Extension.Mts || originalExtension === Extension.Dmts)
|| extensions & Extensions.Declaration && tryExtension(Extension.Dmts, originalExtension === Extension.Mts || originalExtension === Extension.Dmts)
|| extensions & Extensions.JavaScript && tryExtension(Extension.Mjs)
|| undefined;
case Extension.Cjs:
case Extension.Cts:
case Extension.Dcts:
return extensions & Extensions.TypeScript && tryExtension(Extension.Cts)
|| extensions & Extensions.Declaration && tryExtension(Extension.Dcts)
return extensions & Extensions.TypeScript && tryExtension(Extension.Cts, originalExtension === Extension.Cts || originalExtension === Extension.Dcts)
|| extensions & Extensions.Declaration && tryExtension(Extension.Dcts, originalExtension === Extension.Cts || originalExtension === Extension.Dcts)
|| extensions & Extensions.JavaScript && tryExtension(Extension.Cjs)
|| undefined;
case Extension.Json:
const originalCandidate = candidate;
if (extensions & Extensions.Declaration) {
candidate += Extension.Json;
const result = tryExtension(Extension.Dts);
if (result) return result;
}
if (extensions & Extensions.Json) {
candidate = originalCandidate;
const result = tryExtension(Extension.Json);
if (result) return result;
}
return undefined;
case Extension.Ts:
return extensions & Extensions.Declaration && tryExtension(".d.json.ts")
|| extensions & Extensions.Json && tryExtension(Extension.Json)
|| undefined;
case Extension.Tsx:
case Extension.Jsx:
// basically idendical to the ts/js case below, but prefers matching tsx and jsx files exactly before falling back to the ts or js file path
// (historically, we disallow having both a a.ts and a.tsx file in the same compilation, since their outputs clash)
// TODO: We should probably error if `"./a.tsx"` resolved to `"./a.ts"`, right?
return extensions & Extensions.TypeScript && (tryExtension(Extension.Tsx, originalExtension === Extension.Tsx) || tryExtension(Extension.Ts, originalExtension === Extension.Tsx))
|| extensions & Extensions.Declaration && tryExtension(Extension.Dts, originalExtension === Extension.Tsx)
|| extensions & Extensions.JavaScript && (tryExtension(Extension.Jsx) || tryExtension(Extension.Js))
|| undefined;
case Extension.Ts:
case Extension.Dts:
if (moduleResolutionSupportsResolvingTsExtensions(state.compilerOptions) && extensionIsOk(extensions, originalExtension)) {
weswigham marked this conversation as resolved.
Show resolved Hide resolved
return tryExtension(originalExtension, /*resolvedUsingTsExtension*/ true);
}
// falls through
default:
return extensions & Extensions.TypeScript && (tryExtension(Extension.Ts) || tryExtension(Extension.Tsx))
|| extensions & Extensions.Declaration && tryExtension(Extension.Dts)
case Extension.Js:
case "":
return extensions & Extensions.TypeScript && (tryExtension(Extension.Ts, originalExtension === Extension.Ts || originalExtension === Extension.Dts) || tryExtension(Extension.Tsx, originalExtension === Extension.Ts || originalExtension === Extension.Dts))
|| extensions & Extensions.Declaration && tryExtension(Extension.Dts, originalExtension === Extension.Ts || originalExtension === Extension.Dts)
|| extensions & Extensions.JavaScript && (tryExtension(Extension.Js) || tryExtension(Extension.Jsx))
|| state.isConfigLookup && tryExtension(Extension.Json)
|| undefined;
default:
return extensions & Extensions.Declaration && !isDeclarationFileName(candidate + originalExtension) && tryExtension(`.d${originalExtension}.ts`)
|| undefined;

}

function tryExtension(ext: Extension, resolvedUsingTsExtension?: boolean): PathAndExtension | undefined {
function tryExtension(ext: string, resolvedUsingTsExtension?: boolean): PathAndExtension | undefined {
const path = tryFile(candidate + ext, onlyRecordFailures, state);
return path === undefined ? undefined : { path, ext, resolvedUsingTsExtension };
}
Expand Down
15 changes: 15 additions & 0 deletions src/compiler/moduleSpecifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
flatten,
forEach,
forEachAncestorDirectory,
getBaseFileName,
GetCanonicalFileName,
getDirectoryPath,
getEmitModuleResolutionKind,
Expand Down Expand Up @@ -85,6 +86,7 @@ import {
pathIsBareSpecifier,
pathIsRelative,
PropertyAccessExpression,
removeExtension,
removeFileExtension,
removeSuffix,
ResolutionMode,
Expand Down Expand Up @@ -1036,6 +1038,10 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie
if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) {
return noExtension + getJSExtensionForFile(fileName, options);
}
else if (!fileExtensionIsOneOf(fileName, [Extension.Dts]) && fileExtensionIsOneOf(fileName, [Extension.Ts]) && stringContains(fileName, ".d.")) {
// `foo.d.json.ts` and the like - remap back to `foo.json`
return tryGetRealFileNameForNonJsDeclarationFileName(fileName)!;
}

switch (allowedEndings[0]) {
case ModuleSpecifierEnding.Minimal:
Expand Down Expand Up @@ -1066,6 +1072,15 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie
}
}

/** @internal */
export function tryGetRealFileNameForNonJsDeclarationFileName(fileName: string) {
const baseName = getBaseFileName(fileName);
if (!endsWith(fileName, Extension.Ts) || !stringContains(baseName, ".d.") || fileExtensionIsOneOf(baseName, [Extension.Dts])) return undefined;
const noExtension = removeExtension(fileName, Extension.Ts);
const ext = noExtension.substring(noExtension.lastIndexOf("."));
return noExtension.substring(0, noExtension.indexOf(".d.")) + ext;
}

function getJSExtensionForFile(fileName: string, options: CompilerOptions): Extension {
return tryGetJSExtensionForFile(fileName, options) ?? Debug.fail(`Extension ${extensionFromPath(fileName)} is unsupported:: FileName:: ${fileName}`);
}
Expand Down
6 changes: 5 additions & 1 deletion src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,9 @@ import {
Expression,
ExpressionStatement,
ExpressionWithTypeArguments,
Extension,
ExternalModuleReference,
fileExtensionIs,
fileExtensionIsOneOf,
findIndex,
forEach,
Expand All @@ -98,6 +100,7 @@ import {
FunctionOrConstructorTypeNode,
FunctionTypeNode,
GetAccessorDeclaration,
getBaseFileName,
getBinaryOperatorPrecedence,
getFullWidth,
getJSDocCommentRanges,
Expand Down Expand Up @@ -328,6 +331,7 @@ import {
SpreadElement,
startsWith,
Statement,
stringContains,
StringLiteral,
supportedDeclarationExtensions,
SwitchStatement,
Expand Down Expand Up @@ -10114,7 +10118,7 @@ namespace IncrementalParser {

/** @internal */
export function isDeclarationFileName(fileName: string): boolean {
return fileExtensionIsOneOf(fileName, supportedDeclarationExtensions);
return fileExtensionIsOneOf(fileName, supportedDeclarationExtensions) || (fileExtensionIs(fileName, Extension.Ts) && stringContains(getBaseFileName(fileName), ".d."));
}

function parseResolutionMode(mode: string | undefined, pos: number, end: number, reportDiagnostic: PragmaDiagnosticReporter): ResolutionMode {
Expand Down
16 changes: 14 additions & 2 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3845,7 +3845,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
// Don't add the file if it has a bad extension (e.g. 'tsx' if we don't have '--allowJs')
// This may still end up being an untyped module -- the file won't be included but imports will be allowed.
const shouldAddFile = resolvedFileName
&& !getResolutionDiagnostic(optionsForFile, resolution)
&& !getResolutionDiagnostic(optionsForFile, resolution, file)
&& !optionsForFile.noResolve
&& index < file.imports.length
&& !elideImport
Expand Down Expand Up @@ -4947,20 +4947,28 @@ export function resolveProjectReferencePath(hostOrRef: ResolveProjectReferencePa
*
* @internal
*/
export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModuleFull): DiagnosticMessage | undefined {
export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModuleFull, { isDeclarationFile }: { isDeclarationFile: SourceFile["isDeclarationFile"] }): DiagnosticMessage | undefined {
switch (extension) {
case Extension.Ts:
case Extension.Dts:
case Extension.Mts:
case Extension.Dmts:
case Extension.Cts:
case Extension.Dcts:
// These are always allowed.
return undefined;
case Extension.Tsx:
return needJsx();
case Extension.Jsx:
return needJsx() || needAllowJs();
case Extension.Js:
case Extension.Mjs:
case Extension.Cjs:
return needAllowJs();
case Extension.Json:
return needResolveJsonModule();
default:
return needAllowNonJsExtensions();
weswigham marked this conversation as resolved.
Show resolved Hide resolved
}

function needJsx() {
Expand All @@ -4972,6 +4980,10 @@ export function getResolutionDiagnostic(options: CompilerOptions, { extension }:
function needResolveJsonModule() {
return getResolveJsonModule(options) ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used;
}
function needAllowNonJsExtensions() {
// But don't report the allowArbitraryExtensions error from declaration files (no reason to report it, since the import doesn't have a runtime component)
return isDeclarationFile || options.allowArbitraryExtensions ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_allowArbitraryExtensions_is_not_set;
}
}

function getModuleNames({ imports, moduleAugmentations }: SourceFile): StringLiteralLike[] {
Expand Down
3 changes: 2 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6974,6 +6974,7 @@ export interface CompilerOptions {
allowImportingTsExtensions?: boolean;
allowJs?: boolean;
/** @internal */ allowNonTsExtensions?: boolean;
allowArbitraryExtensions?: boolean;
allowSyntheticDefaultImports?: boolean;
allowUmdGlobalAccess?: boolean;
allowUnreachableCode?: boolean;
Expand Down Expand Up @@ -7557,7 +7558,7 @@ export interface ResolvedModuleFull extends ResolvedModule {
* Extension of resolvedFileName. This must match what's at the end of resolvedFileName.
* This is optional for backwards-compatibility, but will be added if not provided.
*/
extension: Extension;
extension: string;
packageId?: PackageId;
}

Expand Down
11 changes: 6 additions & 5 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ import {
EmitResolver,
EmitTextWriter,
emptyArray,
endsWith,
ensurePathIsNonModuleName,
ensureTrailingDirectorySeparator,
EntityName,
Expand Down Expand Up @@ -5497,7 +5498,7 @@ export function getDeclarationEmitOutputFilePathWorker(fileName: string, options
export function getDeclarationEmitExtensionForPath(path: string) {
return fileExtensionIsOneOf(path, [Extension.Mjs, Extension.Mts]) ? Extension.Dmts :
fileExtensionIsOneOf(path, [Extension.Cjs, Extension.Cts]) ? Extension.Dcts :
fileExtensionIsOneOf(path, [Extension.Json]) ? `.json.d.ts` : // Drive-by redefinition of json declaration file output name so if it's ever enabled, it behaves well
fileExtensionIsOneOf(path, [Extension.Json]) ? `.d.json.ts` : // Drive-by redefinition of json declaration file output name so if it's ever enabled, it behaves well
Extension.Dts;
}

Expand All @@ -5509,7 +5510,7 @@ export function getDeclarationEmitExtensionForPath(path: string) {
export function getPossibleOriginalInputExtensionForExtension(path: string) {
return fileExtensionIsOneOf(path, [Extension.Dmts, Extension.Mjs, Extension.Mts]) ? [Extension.Mts, Extension.Mjs] :
fileExtensionIsOneOf(path, [Extension.Dcts, Extension.Cjs, Extension.Cts]) ? [Extension.Cts, Extension.Cjs]:
fileExtensionIsOneOf(path, [`.json.d.ts`]) ? [Extension.Json] :
fileExtensionIsOneOf(path, [`.d.json.ts`]) ? [Extension.Json] :
[Extension.Tsx, Extension.Ts, Extension.Jsx, Extension.Js];
}

Expand Down Expand Up @@ -8670,12 +8671,12 @@ export function positionIsSynthesized(pos: number): boolean {
*
* @internal
*/
export function extensionIsTS(ext: Extension): boolean {
return ext === Extension.Ts || ext === Extension.Tsx || ext === Extension.Dts || ext === Extension.Cts || ext === Extension.Mts || ext === Extension.Dmts || ext === Extension.Dcts;
export function extensionIsTS(ext: string): boolean {
return ext === Extension.Ts || ext === Extension.Tsx || ext === Extension.Dts || ext === Extension.Cts || ext === Extension.Mts || ext === Extension.Dmts || ext === Extension.Dcts || (startsWith(ext, ".d.") && endsWith(ext, ".ts"));
}

/** @internal */
export function resolutionExtensionIsTSOrJson(ext: Extension) {
export function resolutionExtensionIsTSOrJson(ext: string) {
return extensionIsTS(ext) || ext === Extension.Json;
}

Expand Down
2 changes: 1 addition & 1 deletion src/harness/compilerImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,7 +213,7 @@ export class CompilationResult {
}
else {
path = vpath.resolve(this.vfs.cwd(), path);
const outDir = ext === ".d.ts" || ext === ".json.d.ts" || ext === ".d.mts" || ext === ".d.cts" ? this.options.declarationDir || this.options.outDir : this.options.outDir;
const outDir = ext === ".d.ts" || ext === ".d.mts" || ext === ".d.cts" || (ext.endsWith(".ts") || ts.stringContains(ext, ".d.")) ? this.options.declarationDir || this.options.outDir : this.options.outDir;
if (outDir) {
const common = this.commonSourceDirectory;
if (common) {
Expand Down
Loading