diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index f193aeb6784..4b7cbf2bae2 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -4,6 +4,7 @@ import { AmbientModuleDeclaration, append, arrayFrom, + changeFullExtension, CharacterCodes, combinePaths, compareBooleans, @@ -59,6 +60,7 @@ import { getSupportedExtensions, getTemporaryModuleResolutionState, getTextOfIdentifierOrLiteral, + hasImplementationTSFileExtension, hasJSFileExtension, hasTSFileExtension, hostGetCanonicalFileName, @@ -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) { @@ -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(); @@ -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: @@ -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) }; } @@ -1032,7 +1059,7 @@ 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; @@ -1040,11 +1067,15 @@ function tryGetModuleNameFromExportsOrImports(options: CompilerOptions, host: Mo 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) }; } @@ -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)) { if (key === "default" || conditions.indexOf(key) >= 0 || isApplicableVersionedTypesKey(conditions, key)) { const subTarget = (exports as MapLike)[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; } @@ -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)[k], conditions, mode, /*isImports*/ false); + return tryGetModuleNameFromExportsOrImports(options, host, targetFilePath, packageDirectory, subPackageName, (exports as MapLike)[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; } @@ -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)[k], conditions, mode, /*isImports*/ true); + return tryGetModuleNameFromExportsOrImports(options, host, moduleFileName, ancestorDirectoryWithPackageJson, k, (imports as MapLike)[k], conditions, mode, /*isImports*/ true, preferTsExtension); })?.moduleFileToTry; } @@ -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 }; @@ -1411,3 +1452,8 @@ function isPathRelativeToParent(path: string): boolean { function getDefaultResolutionModeForFile(file: Pick, host: Pick, 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); +} diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index b7600682b6c..2c647c697c4 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -265,6 +265,7 @@ import { isConstructorDeclaration, isConstTypeReference, isDeclaration, + isDeclarationFileName, isDecorator, isElementAccessExpression, isEnumDeclaration, @@ -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 diff --git a/tests/cases/fourslash/autoImportAllowImportingTsExtensionsPackageJsonImports1.ts b/tests/cases/fourslash/autoImportAllowImportingTsExtensionsPackageJsonImports1.ts new file mode 100644 index 00000000000..8db0bdb37d5 --- /dev/null +++ b/tests/cases/fourslash/autoImportAllowImportingTsExtensionsPackageJsonImports1.ts @@ -0,0 +1,41 @@ +/// + +// @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"]); diff --git a/tests/cases/fourslash/autoImportAllowImportingTsExtensionsPackageJsonImports2.ts b/tests/cases/fourslash/autoImportAllowImportingTsExtensionsPackageJsonImports2.ts new file mode 100644 index 00000000000..c5e99387477 --- /dev/null +++ b/tests/cases/fourslash/autoImportAllowImportingTsExtensionsPackageJsonImports2.ts @@ -0,0 +1,34 @@ +/// + +// @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"]); diff --git a/tests/cases/fourslash/autoImportPackageJsonImportsCaseSensitivity.ts b/tests/cases/fourslash/autoImportPackageJsonImportsCaseSensitivity.ts new file mode 100644 index 00000000000..79d62ec33ce --- /dev/null +++ b/tests/cases/fourslash/autoImportPackageJsonImportsCaseSensitivity.ts @@ -0,0 +1,20 @@ +/// + +// @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" }); \ No newline at end of file