Skip to content

Commit 4b12d82

Browse files
authored
Fix auto import file extensions with package.json imports wildcards (#59564)
1 parent f3b118e commit 4b12d82

5 files changed

+161
-13
lines changed

src/compiler/moduleSpecifiers.ts

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
AmbientModuleDeclaration,
55
append,
66
arrayFrom,
7+
changeFullExtension,
78
CharacterCodes,
89
combinePaths,
910
compareBooleans,
@@ -59,6 +60,7 @@ import {
5960
getSupportedExtensions,
6061
getTemporaryModuleResolutionState,
6162
getTextOfIdentifierOrLiteral,
63+
hasImplementationTSFileExtension,
6264
hasJSFileExtension,
6365
hasTSFileExtension,
6466
hostGetCanonicalFileName,
@@ -599,7 +601,16 @@ function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOpt
599601
return pathsOnly ? undefined : relativePath;
600602
}
601603

602-
const fromPackageJsonImports = pathsOnly ? undefined : tryGetModuleNameFromPackageJsonImports(moduleFileName, sourceDirectory, compilerOptions, host, importMode);
604+
const fromPackageJsonImports = pathsOnly
605+
? undefined
606+
: tryGetModuleNameFromPackageJsonImports(
607+
moduleFileName,
608+
sourceDirectory,
609+
compilerOptions,
610+
host,
611+
importMode,
612+
prefersTsExtension(allowedEndings),
613+
);
603614

604615
const fromPaths = pathsOnly || fromPackageJsonImports === undefined ? paths && tryGetModuleNameFromPaths(relativeToBaseUrl, paths, allowedEndings, host, compilerOptions) : undefined;
605616
if (pathsOnly) {
@@ -997,7 +1008,18 @@ const enum MatchingMode {
9971008
Pattern,
9981009
}
9991010

1000-
function tryGetModuleNameFromExportsOrImports(options: CompilerOptions, host: ModuleSpecifierResolutionHost, targetFilePath: string, packageDirectory: string, packageName: string, exports: unknown, conditions: string[], mode: MatchingMode, isImports: boolean): { moduleFileToTry: string; } | undefined {
1011+
function tryGetModuleNameFromExportsOrImports(
1012+
options: CompilerOptions,
1013+
host: ModuleSpecifierResolutionHost,
1014+
targetFilePath: string,
1015+
packageDirectory: string,
1016+
packageName: string,
1017+
exports: unknown,
1018+
conditions: string[],
1019+
mode: MatchingMode,
1020+
isImports: boolean,
1021+
preferTsExtension: boolean,
1022+
): { moduleFileToTry: string; } | undefined {
10011023
if (typeof exports === "string") {
10021024
const ignoreCase = !hostUsesCaseSensitiveFileNames(host);
10031025
const getCommonSourceDirectory = () => host.getCommonSourceDirectory();
@@ -1006,6 +1028,7 @@ function tryGetModuleNameFromExportsOrImports(options: CompilerOptions, host: Mo
10061028

10071029
const pathOrPattern = getNormalizedAbsolutePath(combinePaths(packageDirectory, exports), /*currentDirectory*/ undefined);
10081030
const extensionSwappedTarget = hasTSFileExtension(targetFilePath) ? removeFileExtension(targetFilePath) + tryGetJSExtensionForFile(targetFilePath, options) : undefined;
1031+
const canTryTsExtension = preferTsExtension && hasImplementationTSFileExtension(targetFilePath);
10091032

10101033
switch (mode) {
10111034
case MatchingMode.Exact:
@@ -1019,11 +1042,15 @@ function tryGetModuleNameFromExportsOrImports(options: CompilerOptions, host: Mo
10191042
}
10201043
break;
10211044
case MatchingMode.Directory:
1045+
if (canTryTsExtension && containsPath(targetFilePath, pathOrPattern, ignoreCase)) {
1046+
const fragment = getRelativePathFromDirectory(pathOrPattern, targetFilePath, /*ignoreCase*/ false);
1047+
return { moduleFileToTry: getNormalizedAbsolutePath(combinePaths(combinePaths(packageName, exports), fragment), /*currentDirectory*/ undefined) };
1048+
}
10221049
if (extensionSwappedTarget && containsPath(pathOrPattern, extensionSwappedTarget, ignoreCase)) {
10231050
const fragment = getRelativePathFromDirectory(pathOrPattern, extensionSwappedTarget, /*ignoreCase*/ false);
10241051
return { moduleFileToTry: getNormalizedAbsolutePath(combinePaths(combinePaths(packageName, exports), fragment), /*currentDirectory*/ undefined) };
10251052
}
1026-
if (containsPath(pathOrPattern, targetFilePath, ignoreCase)) {
1053+
if (!canTryTsExtension && containsPath(pathOrPattern, targetFilePath, ignoreCase)) {
10271054
const fragment = getRelativePathFromDirectory(pathOrPattern, targetFilePath, /*ignoreCase*/ false);
10281055
return { moduleFileToTry: getNormalizedAbsolutePath(combinePaths(combinePaths(packageName, exports), fragment), /*currentDirectory*/ undefined) };
10291056
}
@@ -1032,19 +1059,23 @@ function tryGetModuleNameFromExportsOrImports(options: CompilerOptions, host: Mo
10321059
return { moduleFileToTry: combinePaths(packageName, fragment) };
10331060
}
10341061
if (declarationFile && containsPath(pathOrPattern, declarationFile, ignoreCase)) {
1035-
const fragment = getRelativePathFromDirectory(pathOrPattern, declarationFile, /*ignoreCase*/ false);
1062+
const fragment = changeFullExtension(getRelativePathFromDirectory(pathOrPattern, declarationFile, /*ignoreCase*/ false), getJSExtensionForFile(declarationFile, options));
10361063
return { moduleFileToTry: combinePaths(packageName, fragment) };
10371064
}
10381065
break;
10391066
case MatchingMode.Pattern:
10401067
const starPos = pathOrPattern.indexOf("*");
10411068
const leadingSlice = pathOrPattern.slice(0, starPos);
10421069
const trailingSlice = pathOrPattern.slice(starPos + 1);
1070+
if (canTryTsExtension && startsWith(targetFilePath, leadingSlice, ignoreCase) && endsWith(targetFilePath, trailingSlice, ignoreCase)) {
1071+
const starReplacement = targetFilePath.slice(leadingSlice.length, targetFilePath.length - trailingSlice.length);
1072+
return { moduleFileToTry: replaceFirstStar(packageName, starReplacement) };
1073+
}
10431074
if (extensionSwappedTarget && startsWith(extensionSwappedTarget, leadingSlice, ignoreCase) && endsWith(extensionSwappedTarget, trailingSlice, ignoreCase)) {
10441075
const starReplacement = extensionSwappedTarget.slice(leadingSlice.length, extensionSwappedTarget.length - trailingSlice.length);
10451076
return { moduleFileToTry: replaceFirstStar(packageName, starReplacement) };
10461077
}
1047-
if (startsWith(targetFilePath, leadingSlice, ignoreCase) && endsWith(targetFilePath, trailingSlice, ignoreCase)) {
1078+
if (!canTryTsExtension && startsWith(targetFilePath, leadingSlice, ignoreCase) && endsWith(targetFilePath, trailingSlice, ignoreCase)) {
10481079
const starReplacement = targetFilePath.slice(leadingSlice.length, targetFilePath.length - trailingSlice.length);
10491080
return { moduleFileToTry: replaceFirstStar(packageName, starReplacement) };
10501081
}
@@ -1054,20 +1085,22 @@ function tryGetModuleNameFromExportsOrImports(options: CompilerOptions, host: Mo
10541085
}
10551086
if (declarationFile && startsWith(declarationFile, leadingSlice, ignoreCase) && endsWith(declarationFile, trailingSlice, ignoreCase)) {
10561087
const starReplacement = declarationFile.slice(leadingSlice.length, declarationFile.length - trailingSlice.length);
1057-
return { moduleFileToTry: replaceFirstStar(packageName, starReplacement) };
1088+
const substituted = replaceFirstStar(packageName, starReplacement);
1089+
const jsExtension = tryGetJSExtensionForFile(declarationFile, options);
1090+
return jsExtension ? { moduleFileToTry: changeFullExtension(substituted, jsExtension) } : undefined;
10581091
}
10591092
break;
10601093
}
10611094
}
10621095
else if (Array.isArray(exports)) {
1063-
return forEach(exports, e => tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, e, conditions, mode, isImports));
1096+
return forEach(exports, e => tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, e, conditions, mode, isImports, preferTsExtension));
10641097
}
10651098
else if (typeof exports === "object" && exports !== null) { // eslint-disable-line no-restricted-syntax
10661099
// conditional mapping
10671100
for (const key of getOwnKeys(exports as MapLike<unknown>)) {
10681101
if (key === "default" || conditions.indexOf(key) >= 0 || isApplicableVersionedTypesKey(conditions, key)) {
10691102
const subTarget = (exports as MapLike<unknown>)[key];
1070-
const result = tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, subTarget, conditions, mode, isImports);
1103+
const result = tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, subTarget, conditions, mode, isImports, preferTsExtension);
10711104
if (result) {
10721105
return result;
10731106
}
@@ -1089,13 +1122,13 @@ function tryGetModuleNameFromExports(options: CompilerOptions, host: ModuleSpeci
10891122
const mode = endsWith(k, "/") ? MatchingMode.Directory
10901123
: k.includes("*") ? MatchingMode.Pattern
10911124
: MatchingMode.Exact;
1092-
return tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, subPackageName, (exports as MapLike<unknown>)[k], conditions, mode, /*isImports*/ false);
1125+
return tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, subPackageName, (exports as MapLike<unknown>)[k], conditions, mode, /*isImports*/ false, /*preferTsExtension*/ false);
10931126
});
10941127
}
1095-
return tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, exports, conditions, MatchingMode.Exact, /*isImports*/ false);
1128+
return tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, exports, conditions, MatchingMode.Exact, /*isImports*/ false, /*preferTsExtension*/ false);
10961129
}
10971130

1098-
function tryGetModuleNameFromPackageJsonImports(moduleFileName: string, sourceDirectory: string, options: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode) {
1131+
function tryGetModuleNameFromPackageJsonImports(moduleFileName: string, sourceDirectory: string, options: CompilerOptions, host: ModuleSpecifierResolutionHost, importMode: ResolutionMode, preferTsExtension: boolean) {
10991132
if (!host.readFile || !getResolvePackageJsonImports(options)) {
11001133
return undefined;
11011134
}
@@ -1120,7 +1153,7 @@ function tryGetModuleNameFromPackageJsonImports(moduleFileName: string, sourceDi
11201153
const mode = endsWith(k, "/") ? MatchingMode.Directory
11211154
: k.includes("*") ? MatchingMode.Pattern
11221155
: MatchingMode.Exact;
1123-
return tryGetModuleNameFromExportsOrImports(options, host, moduleFileName, ancestorDirectoryWithPackageJson, k, (imports as MapLike<unknown>)[k], conditions, mode, /*isImports*/ true);
1156+
return tryGetModuleNameFromExportsOrImports(options, host, moduleFileName, ancestorDirectoryWithPackageJson, k, (imports as MapLike<unknown>)[k], conditions, mode, /*isImports*/ true, preferTsExtension);
11241157
})?.moduleFileToTry;
11251158
}
11261159

@@ -1221,7 +1254,15 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
12211254
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
12221255
const conditions = getConditions(options, importMode);
12231256
const fromExports = packageJsonContent?.exports
1224-
? tryGetModuleNameFromExports(options, host, path, packageRootPath, packageName, packageJsonContent.exports, conditions)
1257+
? tryGetModuleNameFromExports(
1258+
options,
1259+
host,
1260+
path,
1261+
packageRootPath,
1262+
packageName,
1263+
packageJsonContent.exports,
1264+
conditions,
1265+
)
12251266
: undefined;
12261267
if (fromExports) {
12271268
return { ...fromExports, verbatimFromExports: true };
@@ -1411,3 +1452,8 @@ function isPathRelativeToParent(path: string): boolean {
14111452
function getDefaultResolutionModeForFile(file: Pick<SourceFile, "fileName" | "impliedNodeFormat" | "packageJsonScope">, host: Pick<ModuleSpecifierResolutionHost, "getDefaultResolutionModeForFile">, compilerOptions: CompilerOptions) {
14121453
return isFullSourceFile(file) ? host.getDefaultResolutionModeForFile(file) : getDefaultResolutionModeForFileWorker(file, compilerOptions);
14131454
}
1455+
1456+
function prefersTsExtension(allowedEndings: readonly ModuleSpecifierEnding[]) {
1457+
const tsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.TsExtension);
1458+
return tsPriority > -1 && tsPriority < allowedEndings.indexOf(ModuleSpecifierEnding.JsExtension);
1459+
}

src/compiler/utilities.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@ import {
265265
isConstructorDeclaration,
266266
isConstTypeReference,
267267
isDeclaration,
268+
isDeclarationFileName,
268269
isDecorator,
269270
isElementAccessExpression,
270271
isEnumDeclaration,
@@ -9785,6 +9786,12 @@ export function hasTSFileExtension(fileName: string): boolean {
97859786
return some(supportedTSExtensionsFlat, extension => fileExtensionIs(fileName, extension));
97869787
}
97879788

9789+
/** @internal */
9790+
export function hasImplementationTSFileExtension(fileName: string): boolean {
9791+
return some(supportedTSImplementationExtensions, extension => fileExtensionIs(fileName, extension))
9792+
&& !isDeclarationFileName(fileName);
9793+
}
9794+
97889795
/**
97899796
* @internal
97909797
* Corresponds to UserPreferences#importPathEnding
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: nodenext
4+
// @allowImportingTsExtensions: true
5+
6+
// @Filename: /node_modules/pkg/package.json
7+
//// {
8+
//// "name": "pkg",
9+
//// "type": "module",
10+
//// "exports": {
11+
//// "./*": {
12+
//// "types": "./types/*",
13+
//// "default": "./dist/*"
14+
//// }
15+
//// }
16+
//// }
17+
18+
// @Filename: /node_modules/pkg/types/external.d.ts
19+
//// export declare function external(name: string): any;
20+
21+
// @Filename: /package.json
22+
//// {
23+
//// "name": "self",
24+
//// "type": "module",
25+
//// "imports": {
26+
//// "#*": "./src/*"
27+
//// },
28+
//// "dependencies": {
29+
//// "pkg": "*"
30+
//// }
31+
//// }
32+
33+
// @Filename: /src/add.ts
34+
//// export function add(a: number, b: number) {}
35+
36+
// @Filename: /src/index.ts
37+
//// add/*imports*/;
38+
//// external/*exports*/;
39+
40+
verify.importFixModuleSpecifiers("imports", ["#add.ts"]);
41+
verify.importFixModuleSpecifiers("exports", ["pkg/external.js"]);
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @Filename: /tsconfig.json
4+
//// {
5+
//// "compilerOptions": {
6+
//// "module": "nodenext",
7+
//// "allowImportingTsExtensions": true,
8+
//// "rootDir": "src",
9+
//// "outDir": "dist",
10+
//// "declarationDir": "types",
11+
//// "declaration": true
12+
//// }
13+
//// }
14+
15+
// @Filename: /package.json
16+
//// {
17+
//// "name": "self",
18+
//// "type": "module",
19+
//// "imports": {
20+
//// "#*": {
21+
//// "types": "./types/*",
22+
//// "default": "./dist/*"
23+
//// }
24+
//// }
25+
//// }
26+
27+
// @Filename: /src/add.ts
28+
//// export function add(a: number, b: number) {}
29+
30+
// @Filename: /src/index.ts
31+
//// add/*imports*/;
32+
//// external/*exports*/;
33+
34+
verify.importFixModuleSpecifiers("imports", ["#add.js"]);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/// <reference path="fourslash.ts" />
2+
3+
// @module: nodenext
4+
// @allowImportingTsExtensions: true
5+
6+
// @Filename: /package.json
7+
//// {
8+
//// "type": "module",
9+
//// "imports": {
10+
//// "#src/*": "./SRC/*"
11+
//// }
12+
//// }
13+
14+
// @Filename: /src/add.ts
15+
//// export function add(a: number, b: number) {}
16+
17+
// @Filename: /src/index.ts
18+
//// add/*imports*/;
19+
20+
verify.importFixModuleSpecifiers("imports", ["#src/add.ts"], { importModuleSpecifierPreference: "non-relative" });

0 commit comments

Comments
 (0)