Skip to content

Commit

Permalink
Fix auto import file extensions with package.json imports wildcards (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewbranch authored Aug 9, 2024
1 parent f3b118e commit 4b12d82
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 13 deletions.
72 changes: 59 additions & 13 deletions src/compiler/moduleSpecifiers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
AmbientModuleDeclaration,
append,
arrayFrom,
changeFullExtension,
CharacterCodes,
combinePaths,
compareBooleans,
Expand Down Expand Up @@ -59,6 +60,7 @@ import {
getSupportedExtensions,
getTemporaryModuleResolutionState,
getTextOfIdentifierOrLiteral,
hasImplementationTSFileExtension,
hasJSFileExtension,
hasTSFileExtension,
hostGetCanonicalFileName,
Expand Down Expand Up @@ -599,7 +601,16 @@ function getLocalModuleSpecifier(moduleFileName: string, info: Info, compilerOpt
return pathsOnly ? undefined : relativePath;
}

const fromPackageJsonImports = pathsOnly ? undefined : tryGetModuleNameFromPackageJsonImports(moduleFileName, sourceDirectory, compilerOptions, host, importMode);
const fromPackageJsonImports = pathsOnly
? undefined
: tryGetModuleNameFromPackageJsonImports(
moduleFileName,
sourceDirectory,
compilerOptions,
host,
importMode,
prefersTsExtension(allowedEndings),
);

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

function tryGetModuleNameFromExportsOrImports(options: CompilerOptions, host: ModuleSpecifierResolutionHost, targetFilePath: string, packageDirectory: string, packageName: string, exports: unknown, conditions: string[], mode: MatchingMode, isImports: boolean): { moduleFileToTry: string; } | undefined {
function tryGetModuleNameFromExportsOrImports(
options: CompilerOptions,
host: ModuleSpecifierResolutionHost,
targetFilePath: string,
packageDirectory: string,
packageName: string,
exports: unknown,
conditions: string[],
mode: MatchingMode,
isImports: boolean,
preferTsExtension: boolean,
): { moduleFileToTry: string; } | undefined {
if (typeof exports === "string") {
const ignoreCase = !hostUsesCaseSensitiveFileNames(host);
const getCommonSourceDirectory = () => host.getCommonSourceDirectory();
Expand All @@ -1006,6 +1028,7 @@ function tryGetModuleNameFromExportsOrImports(options: CompilerOptions, host: Mo

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

switch (mode) {
case MatchingMode.Exact:
Expand All @@ -1019,11 +1042,15 @@ function tryGetModuleNameFromExportsOrImports(options: CompilerOptions, host: Mo
}
break;
case MatchingMode.Directory:
if (canTryTsExtension && containsPath(targetFilePath, pathOrPattern, ignoreCase)) {
const fragment = getRelativePathFromDirectory(pathOrPattern, targetFilePath, /*ignoreCase*/ false);
return { moduleFileToTry: getNormalizedAbsolutePath(combinePaths(combinePaths(packageName, exports), fragment), /*currentDirectory*/ undefined) };
}
if (extensionSwappedTarget && containsPath(pathOrPattern, extensionSwappedTarget, ignoreCase)) {
const fragment = getRelativePathFromDirectory(pathOrPattern, extensionSwappedTarget, /*ignoreCase*/ false);
return { moduleFileToTry: getNormalizedAbsolutePath(combinePaths(combinePaths(packageName, exports), fragment), /*currentDirectory*/ undefined) };
}
if (containsPath(pathOrPattern, targetFilePath, ignoreCase)) {
if (!canTryTsExtension && containsPath(pathOrPattern, targetFilePath, ignoreCase)) {
const fragment = getRelativePathFromDirectory(pathOrPattern, targetFilePath, /*ignoreCase*/ false);
return { moduleFileToTry: getNormalizedAbsolutePath(combinePaths(combinePaths(packageName, exports), fragment), /*currentDirectory*/ undefined) };
}
Expand All @@ -1032,19 +1059,23 @@ function tryGetModuleNameFromExportsOrImports(options: CompilerOptions, host: Mo
return { moduleFileToTry: combinePaths(packageName, fragment) };
}
if (declarationFile && containsPath(pathOrPattern, declarationFile, ignoreCase)) {
const fragment = getRelativePathFromDirectory(pathOrPattern, declarationFile, /*ignoreCase*/ false);
const fragment = changeFullExtension(getRelativePathFromDirectory(pathOrPattern, declarationFile, /*ignoreCase*/ false), getJSExtensionForFile(declarationFile, options));
return { moduleFileToTry: combinePaths(packageName, fragment) };
}
break;
case MatchingMode.Pattern:
const starPos = pathOrPattern.indexOf("*");
const leadingSlice = pathOrPattern.slice(0, starPos);
const trailingSlice = pathOrPattern.slice(starPos + 1);
if (canTryTsExtension && startsWith(targetFilePath, leadingSlice, ignoreCase) && endsWith(targetFilePath, trailingSlice, ignoreCase)) {
const starReplacement = targetFilePath.slice(leadingSlice.length, targetFilePath.length - trailingSlice.length);
return { moduleFileToTry: replaceFirstStar(packageName, starReplacement) };
}
if (extensionSwappedTarget && startsWith(extensionSwappedTarget, leadingSlice, ignoreCase) && endsWith(extensionSwappedTarget, trailingSlice, ignoreCase)) {
const starReplacement = extensionSwappedTarget.slice(leadingSlice.length, extensionSwappedTarget.length - trailingSlice.length);
return { moduleFileToTry: replaceFirstStar(packageName, starReplacement) };
}
if (startsWith(targetFilePath, leadingSlice, ignoreCase) && endsWith(targetFilePath, trailingSlice, ignoreCase)) {
if (!canTryTsExtension && startsWith(targetFilePath, leadingSlice, ignoreCase) && endsWith(targetFilePath, trailingSlice, ignoreCase)) {
const starReplacement = targetFilePath.slice(leadingSlice.length, targetFilePath.length - trailingSlice.length);
return { moduleFileToTry: replaceFirstStar(packageName, starReplacement) };
}
Expand All @@ -1054,20 +1085,22 @@ function tryGetModuleNameFromExportsOrImports(options: CompilerOptions, host: Mo
}
if (declarationFile && startsWith(declarationFile, leadingSlice, ignoreCase) && endsWith(declarationFile, trailingSlice, ignoreCase)) {
const starReplacement = declarationFile.slice(leadingSlice.length, declarationFile.length - trailingSlice.length);
return { moduleFileToTry: replaceFirstStar(packageName, starReplacement) };
const substituted = replaceFirstStar(packageName, starReplacement);
const jsExtension = tryGetJSExtensionForFile(declarationFile, options);
return jsExtension ? { moduleFileToTry: changeFullExtension(substituted, jsExtension) } : undefined;
}
break;
}
}
else if (Array.isArray(exports)) {
return forEach(exports, e => tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, e, conditions, mode, isImports));
return forEach(exports, e => tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, e, conditions, mode, isImports, preferTsExtension));
}
else if (typeof exports === "object" && exports !== null) { // eslint-disable-line no-restricted-syntax
// conditional mapping
for (const key of getOwnKeys(exports as MapLike<unknown>)) {
if (key === "default" || conditions.indexOf(key) >= 0 || isApplicableVersionedTypesKey(conditions, key)) {
const subTarget = (exports as MapLike<unknown>)[key];
const result = tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, subTarget, conditions, mode, isImports);
const result = tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, subTarget, conditions, mode, isImports, preferTsExtension);
if (result) {
return result;
}
Expand All @@ -1089,13 +1122,13 @@ function tryGetModuleNameFromExports(options: CompilerOptions, host: ModuleSpeci
const mode = endsWith(k, "/") ? MatchingMode.Directory
: k.includes("*") ? MatchingMode.Pattern
: MatchingMode.Exact;
return tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, subPackageName, (exports as MapLike<unknown>)[k], conditions, mode, /*isImports*/ false);
return tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, subPackageName, (exports as MapLike<unknown>)[k], conditions, mode, /*isImports*/ false, /*preferTsExtension*/ false);
});
}
return tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, exports, conditions, MatchingMode.Exact, /*isImports*/ false);
return tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, packageName, exports, conditions, MatchingMode.Exact, /*isImports*/ false, /*preferTsExtension*/ false);
}

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

Expand Down Expand Up @@ -1221,7 +1254,15 @@ function tryGetModuleNameAsNodeModule({ path, isRedirect }: ModulePath, { getCan
const packageName = getPackageNameFromTypesPackageName(nodeModulesDirectoryName);
const conditions = getConditions(options, importMode);
const fromExports = packageJsonContent?.exports
? tryGetModuleNameFromExports(options, host, path, packageRootPath, packageName, packageJsonContent.exports, conditions)
? tryGetModuleNameFromExports(
options,
host,
path,
packageRootPath,
packageName,
packageJsonContent.exports,
conditions,
)
: undefined;
if (fromExports) {
return { ...fromExports, verbatimFromExports: true };
Expand Down Expand Up @@ -1411,3 +1452,8 @@ function isPathRelativeToParent(path: string): boolean {
function getDefaultResolutionModeForFile(file: Pick<SourceFile, "fileName" | "impliedNodeFormat" | "packageJsonScope">, host: Pick<ModuleSpecifierResolutionHost, "getDefaultResolutionModeForFile">, compilerOptions: CompilerOptions) {
return isFullSourceFile(file) ? host.getDefaultResolutionModeForFile(file) : getDefaultResolutionModeForFileWorker(file, compilerOptions);
}

function prefersTsExtension(allowedEndings: readonly ModuleSpecifierEnding[]) {
const tsPriority = allowedEndings.indexOf(ModuleSpecifierEnding.TsExtension);
return tsPriority > -1 && tsPriority < allowedEndings.indexOf(ModuleSpecifierEnding.JsExtension);
}
7 changes: 7 additions & 0 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ import {
isConstructorDeclaration,
isConstTypeReference,
isDeclaration,
isDeclarationFileName,
isDecorator,
isElementAccessExpression,
isEnumDeclaration,
Expand Down Expand Up @@ -9785,6 +9786,12 @@ export function hasTSFileExtension(fileName: string): boolean {
return some(supportedTSExtensionsFlat, extension => fileExtensionIs(fileName, extension));
}

/** @internal */
export function hasImplementationTSFileExtension(fileName: string): boolean {
return some(supportedTSImplementationExtensions, extension => fileExtensionIs(fileName, extension))
&& !isDeclarationFileName(fileName);
}

/**
* @internal
* Corresponds to UserPreferences#importPathEnding
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/// <reference path="fourslash.ts" />

// @module: nodenext
// @allowImportingTsExtensions: true

// @Filename: /node_modules/pkg/package.json
//// {
//// "name": "pkg",
//// "type": "module",
//// "exports": {
//// "./*": {
//// "types": "./types/*",
//// "default": "./dist/*"
//// }
//// }
//// }

// @Filename: /node_modules/pkg/types/external.d.ts
//// export declare function external(name: string): any;

// @Filename: /package.json
//// {
//// "name": "self",
//// "type": "module",
//// "imports": {
//// "#*": "./src/*"
//// },
//// "dependencies": {
//// "pkg": "*"
//// }
//// }

// @Filename: /src/add.ts
//// export function add(a: number, b: number) {}

// @Filename: /src/index.ts
//// add/*imports*/;
//// external/*exports*/;

verify.importFixModuleSpecifiers("imports", ["#add.ts"]);
verify.importFixModuleSpecifiers("exports", ["pkg/external.js"]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/// <reference path="fourslash.ts" />

// @Filename: /tsconfig.json
//// {
//// "compilerOptions": {
//// "module": "nodenext",
//// "allowImportingTsExtensions": true,
//// "rootDir": "src",
//// "outDir": "dist",
//// "declarationDir": "types",
//// "declaration": true
//// }
//// }

// @Filename: /package.json
//// {
//// "name": "self",
//// "type": "module",
//// "imports": {
//// "#*": {
//// "types": "./types/*",
//// "default": "./dist/*"
//// }
//// }
//// }

// @Filename: /src/add.ts
//// export function add(a: number, b: number) {}

// @Filename: /src/index.ts
//// add/*imports*/;
//// external/*exports*/;

verify.importFixModuleSpecifiers("imports", ["#add.js"]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/// <reference path="fourslash.ts" />

// @module: nodenext
// @allowImportingTsExtensions: true

// @Filename: /package.json
//// {
//// "type": "module",
//// "imports": {
//// "#src/*": "./SRC/*"
//// }
//// }

// @Filename: /src/add.ts
//// export function add(a: number, b: number) {}

// @Filename: /src/index.ts
//// add/*imports*/;

verify.importFixModuleSpecifiers("imports", ["#src/add.ts"], { importModuleSpecifierPreference: "non-relative" });

0 comments on commit 4b12d82

Please sign in to comment.