Skip to content

Commit 89e928e

Browse files
authored
Add --allowArbitraryExtensions, a flag for allowing arbitrary extensions on import paths (#51435)
1 parent 9b718d0 commit 89e928e

File tree

118 files changed

+3056
-1401
lines changed

Some content is hidden

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

118 files changed

+3056
-1401
lines changed

Diff for: src/compiler/checker.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -4738,7 +4738,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
47384738
const mode = contextSpecifier && isStringLiteralLike(contextSpecifier) ? getModeForUsageLocation(currentSourceFile, contextSpecifier) : currentSourceFile.impliedNodeFormat;
47394739
const moduleResolutionKind = getEmitModuleResolutionKind(compilerOptions);
47404740
const resolvedModule = getResolvedModule(currentSourceFile, moduleReference, mode);
4741-
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule);
4741+
const resolutionDiagnostic = resolvedModule && getResolutionDiagnostic(compilerOptions, resolvedModule, currentSourceFile);
47424742
const sourceFile = resolvedModule
47434743
&& (!resolutionDiagnostic || resolutionDiagnostic === Diagnostics.Module_0_was_resolved_to_1_but_jsx_is_not_set)
47444744
&& host.getSourceFile(resolvedModule.resolvedFileName);

Diff for: src/compiler/commandLineParser.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,14 @@ const commandOptionsWithoutBuild: CommandLineOption[] = [
12061206
description: Diagnostics.Enable_importing_json_files,
12071207
defaultValueDescription: false,
12081208
},
1209+
{
1210+
name: "allowArbitraryExtensions",
1211+
type: "boolean",
1212+
affectsModuleResolution: true,
1213+
category: Diagnostics.Modules,
1214+
description: Diagnostics.Enable_importing_files_with_any_extension_provided_a_declaration_file_is_present,
1215+
defaultValueDescription: false,
1216+
},
12091217

12101218
{
12111219
name: "out",

Diff for: src/compiler/diagnosticMessages.json

+12
Original file line numberDiff line numberDiff line change
@@ -5190,6 +5190,18 @@
51905190
"category": "Message",
51915191
"code": 6261
51925192
},
5193+
"File name '{0}' has a '{1}' extension - looking up '{2}' instead.": {
5194+
"category": "Message",
5195+
"code": 6262
5196+
},
5197+
"Module '{0}' was resolved to '{1}', but '--allowArbitraryExtensions' is not set.": {
5198+
"category": "Error",
5199+
"code": 6263
5200+
},
5201+
"Enable importing files with any extension, provided a declaration file is present.": {
5202+
"category": "Message",
5203+
"code": 6264
5204+
},
51935205

51945206
"Directory '{0}' has no containing package.json scope. Imports will not resolve.": {
51955207
"category": "Message",

Diff for: src/compiler/moduleNameResolver.ts

+41-48
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import {
5252
getRelativePathFromDirectory,
5353
getResolveJsonModule,
5454
getRootLength,
55-
hasJSFileExtension,
5655
hasProperty,
5756
hasTrailingDirectorySeparator,
5857
hostGetCanonicalFileName,
@@ -99,7 +98,6 @@ import {
9998
startsWith,
10099
stringContains,
101100
supportedDeclarationExtensions,
102-
supportedTSExtensionsFlat,
103101
supportedTSImplementationExtensions,
104102
toPath,
105103
tryExtractTSExtension,
@@ -151,7 +149,7 @@ function removeIgnoredPackageId(r: Resolved | undefined): PathAndExtension | und
151149
/** Result of trying to resolve a module. */
152150
interface Resolved {
153151
path: string;
154-
extension: Extension;
152+
extension: string;
155153
packageId: PackageId | undefined;
156154
/**
157155
* When the resolved is not created from cache, the value is
@@ -170,7 +168,7 @@ interface Resolved {
170168
interface PathAndExtension {
171169
path: string;
172170
// (Use a different name than `extension` to make sure Resolved isn't assignable to PathAndExtension.)
173-
ext: Extension;
171+
ext: string;
174172
resolvedUsingTsExtension: boolean | undefined;
175173
}
176174

@@ -1856,21 +1854,21 @@ function loadModuleFromFile(extensions: Extensions, candidate: string, onlyRecor
18561854
}
18571855

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

18761874
/**
@@ -1909,47 +1907,46 @@ function tryAddingExtensions(candidate: string, extensions: Extensions, original
19091907
case Extension.Mjs:
19101908
case Extension.Mts:
19111909
case Extension.Dmts:
1912-
return extensions & Extensions.TypeScript && tryExtension(Extension.Mts)
1913-
|| extensions & Extensions.Declaration && tryExtension(Extension.Dmts)
1910+
return extensions & Extensions.TypeScript && tryExtension(Extension.Mts, originalExtension === Extension.Mts || originalExtension === Extension.Dmts)
1911+
|| extensions & Extensions.Declaration && tryExtension(Extension.Dmts, originalExtension === Extension.Mts || originalExtension === Extension.Dmts)
19141912
|| extensions & Extensions.JavaScript && tryExtension(Extension.Mjs)
19151913
|| undefined;
19161914
case Extension.Cjs:
19171915
case Extension.Cts:
19181916
case Extension.Dcts:
1919-
return extensions & Extensions.TypeScript && tryExtension(Extension.Cts)
1920-
|| extensions & Extensions.Declaration && tryExtension(Extension.Dcts)
1917+
return extensions & Extensions.TypeScript && tryExtension(Extension.Cts, originalExtension === Extension.Cts || originalExtension === Extension.Dcts)
1918+
|| extensions & Extensions.Declaration && tryExtension(Extension.Dcts, originalExtension === Extension.Cts || originalExtension === Extension.Dcts)
19211919
|| extensions & Extensions.JavaScript && tryExtension(Extension.Cjs)
19221920
|| undefined;
19231921
case Extension.Json:
1924-
const originalCandidate = candidate;
1925-
if (extensions & Extensions.Declaration) {
1926-
candidate += Extension.Json;
1927-
const result = tryExtension(Extension.Dts);
1928-
if (result) return result;
1929-
}
1930-
if (extensions & Extensions.Json) {
1931-
candidate = originalCandidate;
1932-
const result = tryExtension(Extension.Json);
1933-
if (result) return result;
1934-
}
1935-
return undefined;
1936-
case Extension.Ts:
1922+
return extensions & Extensions.Declaration && tryExtension(".d.json.ts")
1923+
|| extensions & Extensions.Json && tryExtension(Extension.Json)
1924+
|| undefined;
19371925
case Extension.Tsx:
1926+
case Extension.Jsx:
1927+
// 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
1928+
// (historically, we disallow having both a a.ts and a.tsx file in the same compilation, since their outputs clash)
1929+
// TODO: We should probably error if `"./a.tsx"` resolved to `"./a.ts"`, right?
1930+
return extensions & Extensions.TypeScript && (tryExtension(Extension.Tsx, originalExtension === Extension.Tsx) || tryExtension(Extension.Ts, originalExtension === Extension.Tsx))
1931+
|| extensions & Extensions.Declaration && tryExtension(Extension.Dts, originalExtension === Extension.Tsx)
1932+
|| extensions & Extensions.JavaScript && (tryExtension(Extension.Jsx) || tryExtension(Extension.Js))
1933+
|| undefined;
1934+
case Extension.Ts:
19381935
case Extension.Dts:
1939-
if (moduleResolutionSupportsResolvingTsExtensions(state.compilerOptions) && extensionIsOk(extensions, originalExtension)) {
1940-
return tryExtension(originalExtension, /*resolvedUsingTsExtension*/ true);
1941-
}
1942-
// falls through
1943-
default:
1944-
return extensions & Extensions.TypeScript && (tryExtension(Extension.Ts) || tryExtension(Extension.Tsx))
1945-
|| extensions & Extensions.Declaration && tryExtension(Extension.Dts)
1936+
case Extension.Js:
1937+
case "":
1938+
return extensions & Extensions.TypeScript && (tryExtension(Extension.Ts, originalExtension === Extension.Ts || originalExtension === Extension.Dts) || tryExtension(Extension.Tsx, originalExtension === Extension.Ts || originalExtension === Extension.Dts))
1939+
|| extensions & Extensions.Declaration && tryExtension(Extension.Dts, originalExtension === Extension.Ts || originalExtension === Extension.Dts)
19461940
|| extensions & Extensions.JavaScript && (tryExtension(Extension.Js) || tryExtension(Extension.Jsx))
19471941
|| state.isConfigLookup && tryExtension(Extension.Json)
19481942
|| undefined;
1943+
default:
1944+
return extensions & Extensions.Declaration && !isDeclarationFileName(candidate + originalExtension) && tryExtension(`.d${originalExtension}.ts`)
1945+
|| undefined;
19491946

19501947
}
19511948

1952-
function tryExtension(ext: Extension, resolvedUsingTsExtension?: boolean): PathAndExtension | undefined {
1949+
function tryExtension(ext: string, resolvedUsingTsExtension?: boolean): PathAndExtension | undefined {
19531950
const path = tryFile(candidate + ext, onlyRecordFailures, state);
19541951
return path === undefined ? undefined : { path, ext, resolvedUsingTsExtension };
19551952
}
@@ -2984,14 +2981,10 @@ export function classicNameResolver(moduleName: string, containingFile: string,
29842981
}
29852982
}
29862983

2987-
export function moduleResolutionSupportsResolvingTsExtensions(compilerOptions: CompilerOptions) {
2988-
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Bundler;
2989-
}
2990-
29912984
// Program errors validate that `noEmit` or `emitDeclarationOnly` is also set,
29922985
// so this function doesn't check them to avoid propagating errors.
29932986
export function shouldAllowImportingTsExtension(compilerOptions: CompilerOptions, fromFileName?: string) {
2994-
return moduleResolutionSupportsResolvingTsExtensions(compilerOptions) && (
2987+
return getEmitModuleResolutionKind(compilerOptions) === ModuleResolutionKind.Bundler && (
29952988
!!compilerOptions.allowImportingTsExtensions ||
29962989
fromFileName && isDeclarationFileName(fromFileName));
29972990
}

Diff for: src/compiler/moduleSpecifiers.ts

+15
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
flatten,
3232
forEach,
3333
forEachAncestorDirectory,
34+
getBaseFileName,
3435
GetCanonicalFileName,
3536
getDirectoryPath,
3637
getEmitModuleResolutionKind,
@@ -85,6 +86,7 @@ import {
8586
pathIsBareSpecifier,
8687
pathIsRelative,
8788
PropertyAccessExpression,
89+
removeExtension,
8890
removeFileExtension,
8991
removeSuffix,
9092
ResolutionMode,
@@ -1036,6 +1038,10 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie
10361038
if (fileExtensionIsOneOf(fileName, [Extension.Dmts, Extension.Mts, Extension.Dcts, Extension.Cts])) {
10371039
return noExtension + getJSExtensionForFile(fileName, options);
10381040
}
1041+
else if (!fileExtensionIsOneOf(fileName, [Extension.Dts]) && fileExtensionIsOneOf(fileName, [Extension.Ts]) && stringContains(fileName, ".d.")) {
1042+
// `foo.d.json.ts` and the like - remap back to `foo.json`
1043+
return tryGetRealFileNameForNonJsDeclarationFileName(fileName)!;
1044+
}
10391045

10401046
switch (allowedEndings[0]) {
10411047
case ModuleSpecifierEnding.Minimal:
@@ -1066,6 +1072,15 @@ function processEnding(fileName: string, allowedEndings: readonly ModuleSpecifie
10661072
}
10671073
}
10681074

1075+
/** @internal */
1076+
export function tryGetRealFileNameForNonJsDeclarationFileName(fileName: string) {
1077+
const baseName = getBaseFileName(fileName);
1078+
if (!endsWith(fileName, Extension.Ts) || !stringContains(baseName, ".d.") || fileExtensionIsOneOf(baseName, [Extension.Dts])) return undefined;
1079+
const noExtension = removeExtension(fileName, Extension.Ts);
1080+
const ext = noExtension.substring(noExtension.lastIndexOf("."));
1081+
return noExtension.substring(0, noExtension.indexOf(".d.")) + ext;
1082+
}
1083+
10691084
function getJSExtensionForFile(fileName: string, options: CompilerOptions): Extension {
10701085
return tryGetJSExtensionForFile(fileName, options) ?? Debug.fail(`Extension ${extensionFromPath(fileName)} is unsupported:: FileName:: ${fileName}`);
10711086
}

Diff for: src/compiler/parser.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ import {
8484
Expression,
8585
ExpressionStatement,
8686
ExpressionWithTypeArguments,
87+
Extension,
8788
ExternalModuleReference,
89+
fileExtensionIs,
8890
fileExtensionIsOneOf,
8991
findIndex,
9092
forEach,
@@ -98,6 +100,7 @@ import {
98100
FunctionOrConstructorTypeNode,
99101
FunctionTypeNode,
100102
GetAccessorDeclaration,
103+
getBaseFileName,
101104
getBinaryOperatorPrecedence,
102105
getFullWidth,
103106
getJSDocCommentRanges,
@@ -328,6 +331,7 @@ import {
328331
SpreadElement,
329332
startsWith,
330333
Statement,
334+
stringContains,
331335
StringLiteral,
332336
supportedDeclarationExtensions,
333337
SwitchStatement,
@@ -10114,7 +10118,7 @@ namespace IncrementalParser {
1011410118

1011510119
/** @internal */
1011610120
export function isDeclarationFileName(fileName: string): boolean {
10117-
return fileExtensionIsOneOf(fileName, supportedDeclarationExtensions);
10121+
return fileExtensionIsOneOf(fileName, supportedDeclarationExtensions) || (fileExtensionIs(fileName, Extension.Ts) && stringContains(getBaseFileName(fileName), ".d."));
1011810122
}
1011910123

1012010124
function parseResolutionMode(mode: string | undefined, pos: number, end: number, reportDiagnostic: PragmaDiagnosticReporter): ResolutionMode {

Diff for: src/compiler/program.ts

+15-4
Original file line numberDiff line numberDiff line change
@@ -218,7 +218,6 @@ import {
218218
moduleResolutionIsEqualTo,
219219
ModuleResolutionKind,
220220
moduleResolutionSupportsPackageJsonExportsAndImports,
221-
moduleResolutionSupportsResolvingTsExtensions,
222221
Mutable,
223222
Node,
224223
NodeArray,
@@ -3845,7 +3844,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
38453844
// Don't add the file if it has a bad extension (e.g. 'tsx' if we don't have '--allowJs')
38463845
// This may still end up being an untyped module -- the file won't be included but imports will be allowed.
38473846
const shouldAddFile = resolvedFileName
3848-
&& !getResolutionDiagnostic(optionsForFile, resolution)
3847+
&& !getResolutionDiagnostic(optionsForFile, resolution, file)
38493848
&& !optionsForFile.noResolve
38503849
&& index < file.imports.length
38513850
&& !elideImport
@@ -4213,7 +4212,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
42134212
createOptionValueDiagnostic("importsNotUsedAsValues", Diagnostics.Option_preserveValueImports_can_only_be_used_when_module_is_set_to_es2015_or_later);
42144213
}
42154214

4216-
if (options.allowImportingTsExtensions && !(moduleResolutionSupportsResolvingTsExtensions(options) && (options.noEmit || options.emitDeclarationOnly))) {
4215+
if (options.allowImportingTsExtensions && !(options.noEmit || options.emitDeclarationOnly)) {
42174216
createOptionValueDiagnostic("allowImportingTsExtensions", Diagnostics.Option_allowImportingTsExtensions_can_only_be_used_when_moduleResolution_is_set_to_bundler_and_either_noEmit_or_emitDeclarationOnly_is_set);
42184217
}
42194218

@@ -4947,20 +4946,28 @@ export function resolveProjectReferencePath(hostOrRef: ResolveProjectReferencePa
49474946
*
49484947
* @internal
49494948
*/
4950-
export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModuleFull): DiagnosticMessage | undefined {
4949+
export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModuleFull, { isDeclarationFile }: { isDeclarationFile: SourceFile["isDeclarationFile"] }): DiagnosticMessage | undefined {
49514950
switch (extension) {
49524951
case Extension.Ts:
49534952
case Extension.Dts:
4953+
case Extension.Mts:
4954+
case Extension.Dmts:
4955+
case Extension.Cts:
4956+
case Extension.Dcts:
49544957
// These are always allowed.
49554958
return undefined;
49564959
case Extension.Tsx:
49574960
return needJsx();
49584961
case Extension.Jsx:
49594962
return needJsx() || needAllowJs();
49604963
case Extension.Js:
4964+
case Extension.Mjs:
4965+
case Extension.Cjs:
49614966
return needAllowJs();
49624967
case Extension.Json:
49634968
return needResolveJsonModule();
4969+
default:
4970+
return needAllowArbitraryExtensions();
49644971
}
49654972

49664973
function needJsx() {
@@ -4972,6 +4979,10 @@ export function getResolutionDiagnostic(options: CompilerOptions, { extension }:
49724979
function needResolveJsonModule() {
49734980
return getResolveJsonModule(options) ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used;
49744981
}
4982+
function needAllowArbitraryExtensions() {
4983+
// But don't report the allowArbitraryExtensions error from declaration files (no reason to report it, since the import doesn't have a runtime component)
4984+
return isDeclarationFile || options.allowArbitraryExtensions ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_allowArbitraryExtensions_is_not_set;
4985+
}
49754986
}
49764987

49774988
function getModuleNames({ imports, moduleAugmentations }: SourceFile): StringLiteralLike[] {

Diff for: src/compiler/types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -6974,6 +6974,7 @@ export interface CompilerOptions {
69746974
allowImportingTsExtensions?: boolean;
69756975
allowJs?: boolean;
69766976
/** @internal */ allowNonTsExtensions?: boolean;
6977+
allowArbitraryExtensions?: boolean;
69776978
allowSyntheticDefaultImports?: boolean;
69786979
allowUmdGlobalAccess?: boolean;
69796980
allowUnreachableCode?: boolean;
@@ -7557,7 +7558,7 @@ export interface ResolvedModuleFull extends ResolvedModule {
75577558
* Extension of resolvedFileName. This must match what's at the end of resolvedFileName.
75587559
* This is optional for backwards-compatibility, but will be added if not provided.
75597560
*/
7560-
extension: Extension;
7561+
extension: string;
75617562
packageId?: PackageId;
75627563
}
75637564

0 commit comments

Comments
 (0)