Skip to content

Commit b1fe76f

Browse files
committed
Add --allowNonJsExtensions, a flag for allowing arbitrary extensions on import paths
1 parent f43cd0a commit b1fe76f

File tree

117 files changed

+2008
-154
lines changed

Some content is hidden

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

117 files changed

+2008
-154
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: "allowNonJsExtensions",
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 '--allowNonJsExtensions' 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

+40-43
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
18731860
}
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);
1870+
}
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
}

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,
@@ -10124,7 +10128,7 @@ namespace IncrementalParser {
1012410128

1012510129
/** @internal */
1012610130
export function isDeclarationFileName(fileName: string): boolean {
10127-
return fileExtensionIsOneOf(fileName, supportedDeclarationExtensions);
10131+
return fileExtensionIsOneOf(fileName, supportedDeclarationExtensions) || (fileExtensionIs(fileName, Extension.Ts) && stringContains(getBaseFileName(fileName), ".d."));;
1012810132
}
1012910133

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

Diff for: src/compiler/program.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -3845,7 +3845,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
38453845
// Don't add the file if it has a bad extension (e.g. 'tsx' if we don't have '--allowJs')
38463846
// This may still end up being an untyped module -- the file won't be included but imports will be allowed.
38473847
const shouldAddFile = resolvedFileName
3848-
&& !getResolutionDiagnostic(optionsForFile, resolution)
3848+
&& !getResolutionDiagnostic(optionsForFile, resolution, file)
38493849
&& !optionsForFile.noResolve
38503850
&& index < file.imports.length
38513851
&& !elideImport
@@ -4947,20 +4947,28 @@ export function resolveProjectReferencePath(hostOrRef: ResolveProjectReferencePa
49474947
*
49484948
* @internal
49494949
*/
4950-
export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModuleFull): DiagnosticMessage | undefined {
4950+
export function getResolutionDiagnostic(options: CompilerOptions, { extension }: ResolvedModuleFull, { isDeclarationFile }: { isDeclarationFile: SourceFile["isDeclarationFile"] }): DiagnosticMessage | undefined {
49514951
switch (extension) {
49524952
case Extension.Ts:
49534953
case Extension.Dts:
4954+
case Extension.Mts:
4955+
case Extension.Dmts:
4956+
case Extension.Cts:
4957+
case Extension.Dcts:
49544958
// These are always allowed.
49554959
return undefined;
49564960
case Extension.Tsx:
49574961
return needJsx();
49584962
case Extension.Jsx:
49594963
return needJsx() || needAllowJs();
49604964
case Extension.Js:
4965+
case Extension.Mjs:
4966+
case Extension.Cjs:
49614967
return needAllowJs();
49624968
case Extension.Json:
49634969
return needResolveJsonModule();
4970+
default:
4971+
return needAllowNonJsExtensions();
49644972
}
49654973

49664974
function needJsx() {
@@ -4972,6 +4980,10 @@ export function getResolutionDiagnostic(options: CompilerOptions, { extension }:
49724980
function needResolveJsonModule() {
49734981
return getResolveJsonModule(options) ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_resolveJsonModule_is_not_used;
49744982
}
4983+
function needAllowNonJsExtensions() {
4984+
// But don't report the allowNonJsExtensions error from declaration files (no reason to report it, since the import doesn't have a runtime component)
4985+
return isDeclarationFile || options.allowNonJsExtensions ? undefined : Diagnostics.Module_0_was_resolved_to_1_but_allowNonJsExtensions_is_not_set;
4986+
}
49754987
}
49764988

49774989
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+
allowNonJsExtensions?: 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

Diff for: src/compiler/utilities.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ import {
103103
EmitResolver,
104104
EmitTextWriter,
105105
emptyArray,
106+
endsWith,
106107
ensurePathIsNonModuleName,
107108
ensureTrailingDirectorySeparator,
108109
EntityName,
@@ -5499,7 +5500,7 @@ export function getDeclarationEmitOutputFilePathWorker(fileName: string, options
54995500
export function getDeclarationEmitExtensionForPath(path: string) {
55005501
return fileExtensionIsOneOf(path, [Extension.Mjs, Extension.Mts]) ? Extension.Dmts :
55015502
fileExtensionIsOneOf(path, [Extension.Cjs, Extension.Cts]) ? Extension.Dcts :
5502-
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
5503+
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
55035504
Extension.Dts;
55045505
}
55055506

@@ -5511,7 +5512,7 @@ export function getDeclarationEmitExtensionForPath(path: string) {
55115512
export function getPossibleOriginalInputExtensionForExtension(path: string) {
55125513
return fileExtensionIsOneOf(path, [Extension.Dmts, Extension.Mjs, Extension.Mts]) ? [Extension.Mts, Extension.Mjs] :
55135514
fileExtensionIsOneOf(path, [Extension.Dcts, Extension.Cjs, Extension.Cts]) ? [Extension.Cts, Extension.Cjs]:
5514-
fileExtensionIsOneOf(path, [`.json.d.ts`]) ? [Extension.Json] :
5515+
fileExtensionIsOneOf(path, [`.d.json.ts`]) ? [Extension.Json] :
55155516
[Extension.Tsx, Extension.Ts, Extension.Jsx, Extension.Js];
55165517
}
55175518

@@ -8672,12 +8673,12 @@ export function positionIsSynthesized(pos: number): boolean {
86728673
*
86738674
* @internal
86748675
*/
8675-
export function extensionIsTS(ext: Extension): boolean {
8676-
return ext === Extension.Ts || ext === Extension.Tsx || ext === Extension.Dts || ext === Extension.Cts || ext === Extension.Mts || ext === Extension.Dmts || ext === Extension.Dcts;
8676+
export function extensionIsTS(ext: string): boolean {
8677+
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"));
86778678
}
86788679

86798680
/** @internal */
8680-
export function resolutionExtensionIsTSOrJson(ext: Extension) {
8681+
export function resolutionExtensionIsTSOrJson(ext: string) {
86818682
return extensionIsTS(ext) || ext === Extension.Json;
86828683
}
86838684

Diff for: src/harness/compilerImpl.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export class CompilationResult {
213213
}
214214
else {
215215
path = vpath.resolve(this.vfs.cwd(), path);
216-
const outDir = ext === ".d.ts" || ext === ".json.d.ts" || ext === ".d.mts" || ext === ".d.cts" ? this.options.declarationDir || this.options.outDir : this.options.outDir;
216+
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;
217217
if (outDir) {
218218
const common = this.commonSourceDirectory;
219219
if (common) {

0 commit comments

Comments
 (0)