diff --git a/src/compiler/moduleNameResolver.ts b/src/compiler/moduleNameResolver.ts index 7028493771345..4538377dafd3d 100644 --- a/src/compiler/moduleNameResolver.ts +++ b/src/compiler/moduleNameResolver.ts @@ -9,6 +9,7 @@ import { CommandLineOption, comparePaths, Comparison, + CompilerHost, CompilerOptions, concatenate, contains, @@ -36,10 +37,12 @@ import { forEachAncestorDirectory, formatMessage, getAllowJSCompilerOption, + getAnyExtensionFromPath, getBaseFileName, GetCanonicalFileName, getCommonSourceDirectory, getCompilerOptionValue, + getDeclarationEmitExtensionForPath, getDirectoryPath, GetEffectiveTypeRootsHost, getEmitModuleKind, @@ -99,6 +102,7 @@ import { startsWith, stringContains, supportedDeclarationExtensions, + supportedJSExtensionsFlat, supportedTSImplementationExtensions, toPath, tryExtractTSExtension, @@ -194,6 +198,15 @@ function formatExtensions(extensions: Extensions) { return result.join(", "); } +function extensionsToExtensionsArray(extensions: Extensions) { + const result: Extension[] = []; + if (extensions & Extensions.TypeScript) result.push(...supportedTSImplementationExtensions); + if (extensions & Extensions.JavaScript) result.push(...supportedJSExtensionsFlat); + if (extensions & Extensions.Declaration) result.push(...supportedDeclarationExtensions); + if (extensions & Extensions.Json) result.push(Extension.Json); + return result; +} + interface PathAndPackageId { readonly fileName: string; readonly packageId: PackageId | undefined; @@ -2085,11 +2098,16 @@ function loadNodeModuleFromDirectory(extensions: Extensions, candidate: string, return withPackageId(packageInfo, loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageJsonContent, versionPaths)); } +/** @internal */ +export interface GetPackageJsonEntrypointsHost extends ModuleResolutionHost { + readDirectory: CompilerHost["readDirectory"]; +} + /** @internal */ export function getEntrypointsFromPackageJsonInfo( packageJsonInfo: PackageJsonInfo, options: CompilerOptions, - host: ModuleResolutionHost, + host: GetPackageJsonEntrypointsHost, cache: ModuleResolutionCache | undefined, resolveJs?: boolean, ): string[] | false { @@ -2120,7 +2138,7 @@ export function getEntrypointsFromPackageJsonInfo( arrayIsEqualTo ); for (const conditions of conditionSets) { - const loadPackageJsonExportsState = { ...loadPackageJsonMainState, failedLookupLocations: [], conditions }; + const loadPackageJsonExportsState = { ...loadPackageJsonMainState, failedLookupLocations: [], conditions, host }; const exportResolutions = loadEntrypointsFromExportMap( packageJsonInfo, packageJsonInfo.contents.packageJsonContent.exports, @@ -2140,7 +2158,7 @@ export function getEntrypointsFromPackageJsonInfo( function loadEntrypointsFromExportMap( scope: PackageJsonInfo, exports: object, - state: ModuleResolutionState, + state: ModuleResolutionState & { host: GetPackageJsonEntrypointsHost }, extensions: Extensions, ): PathAndExtension[] | undefined { let entrypoints: PathAndExtension[] | undefined; @@ -2161,17 +2179,37 @@ function loadEntrypointsFromExportMap( return entrypoints; function loadEntrypointsFromTargetExports(target: unknown): boolean | undefined { - if (typeof target === "string" && startsWith(target, "./") && target.indexOf("*") === -1) { - const partsAfterFirst = getPathComponents(target).slice(2); - if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) { - return false; + if (typeof target === "string" && startsWith(target, "./")) { + if (target.indexOf("*") >= 0 && state.host.readDirectory) { + if (target.indexOf("*") !== target.lastIndexOf("*")) { + return false; + } + + state.host.readDirectory( + scope.packageDirectory, + extensionsToExtensionsArray(extensions), + /*excludes*/ undefined, + [changeAnyExtension(target.replace("*", "**/*"), getDeclarationEmitExtensionForPath(target))] + ).forEach(entry => { + entrypoints = appendIfUnique(entrypoints, { + path: entry, + ext: getAnyExtensionFromPath(entry), + resolvedUsingTsExtension: undefined + }); + }); } - const resolvedTarget = combinePaths(scope.packageDirectory, target); - const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.()); - const result = loadFileNameFromPackageJsonField(extensions, finalPath, /*onlyRecordFailures*/ false, state); - if (result) { - entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path); - return true; + else { + const partsAfterFirst = getPathComponents(target).slice(2); + if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) { + return false; + } + const resolvedTarget = combinePaths(scope.packageDirectory, target); + const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.()); + const result = loadFileNameFromPackageJsonField(extensions, finalPath, /*onlyRecordFailures*/ false, state); + if (result) { + entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path); + return true; + } } } else if (Array.isArray(target)) { @@ -2685,12 +2723,6 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo return ensureTrailingDirectorySeparator(combinePaths(root, dir)); } - function useCaseSensitiveFileNames() { - return !state.host.useCaseSensitiveFileNames ? true : - typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames : - state.host.useCaseSensitiveFileNames(); - } - function tryLoadInputFileForPath(finalPath: string, entry: string, packagePath: string, isImports: boolean) { // Replace any references to outputs for files in the program with the input files to support package self-names used with outDir // PROBLEM: We don't know how to calculate the output paths yet, because the "common source directory" we use as the base of the file structure @@ -2700,13 +2732,13 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo if (!state.isConfigLookup && (state.compilerOptions.declarationDir || state.compilerOptions.outDir) && finalPath.indexOf("/node_modules/") === -1 - && (state.compilerOptions.configFile ? containsPath(scope.packageDirectory, toAbsolutePath(state.compilerOptions.configFile.fileName), !useCaseSensitiveFileNames()) : true) + && (state.compilerOptions.configFile ? containsPath(scope.packageDirectory, toAbsolutePath(state.compilerOptions.configFile.fileName), !useCaseSensitiveFileNames(state)) : true) ) { // So that all means we'll only try these guesses for files outside `node_modules` in a directory where the `package.json` and `tsconfig.json` are siblings. // Even with all that, we still don't know if the root of the output file structure will be (relative to the package file) // `.`, `./src` or any other deeper directory structure. (If project references are used, it's definitely `.` by fiat, so that should be pretty common.) - const getCanonicalFileName = hostGetCanonicalFileName({ useCaseSensitiveFileNames }); + const getCanonicalFileName = hostGetCanonicalFileName({ useCaseSensitiveFileNames: () => useCaseSensitiveFileNames(state) }); const commonSourceDirGuesses: string[] = []; // A `rootDir` compiler option strongly indicates the root location // A `composite` project is using project references and has it's common src dir set to `.`, so it shouldn't need to check any other locations @@ -2761,7 +2793,7 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo for (const commonSourceDirGuess of commonSourceDirGuesses) { const candidateDirectories = getOutputDirectoriesForBaseDirectory(commonSourceDirGuess); for (const candidateDir of candidateDirectories) { - if (containsPath(candidateDir, finalPath, !useCaseSensitiveFileNames())) { + if (containsPath(candidateDir, finalPath, !useCaseSensitiveFileNames(state))) { // The matched export is looking up something in either the out declaration or js dir, now map the written path back into the source dir and source extension const pathFragment = finalPath.slice(candidateDir.length + 1); // +1 to also remove directory seperator const possibleInputBase = combinePaths(commonSourceDirGuess, pathFragment); @@ -2771,7 +2803,7 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo const inputExts = getPossibleOriginalInputExtensionForExtension(possibleInputBase); for (const possibleExt of inputExts) { if (!extensionIsOk(extensions, possibleExt)) continue; - const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames()); + const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames(state)); if (state.host.fileExists(possibleInputWithInputExtension)) { return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state))); } @@ -3203,3 +3235,9 @@ function traceIfEnabled(state: ModuleResolutionState, diagnostic: DiagnosticMess trace(state.host, diagnostic, ...args); } } + +function useCaseSensitiveFileNames(state: ModuleResolutionState) { + return !state.host.useCaseSensitiveFileNames ? true : + typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames : + state.host.useCaseSensitiveFileNames(); +} diff --git a/src/compiler/moduleSpecifiers.ts b/src/compiler/moduleSpecifiers.ts index 8ebb6cf96554d..eeeeb75615994 100644 --- a/src/compiler/moduleSpecifiers.ts +++ b/src/compiler/moduleSpecifiers.ts @@ -862,7 +862,7 @@ function tryGetModuleNameFromExports(options: CompilerOptions, targetFilePath: s 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 = tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, subTarget, conditions); + const result = tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, subTarget, conditions, mode); if (result) { return result; } diff --git a/src/server/project.ts b/src/server/project.ts index 381ebb5e9df97..13bd6e1246ed7 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -60,6 +60,7 @@ import { getEntrypointsFromPackageJsonInfo, getNormalizedAbsolutePath, getOrUpdate, + GetPackageJsonEntrypointsHost, getStringComparer, HasInvalidatedLibResolutions, HasInvalidatedResolutions, @@ -2093,7 +2094,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo } /** @internal */ - getModuleResolutionHostForAutoImportProvider(): ModuleResolutionHost { + getHostForAutoImportProvider(): GetPackageJsonEntrypointsHost { if (this.program) { return { fileExists: this.program.fileExists, @@ -2104,6 +2105,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo getDirectories: this.projectService.host.getDirectories.bind(this.projectService.host), trace: this.projectService.host.trace?.bind(this.projectService.host), useCaseSensitiveFileNames: this.program.useCaseSensitiveFileNames(), + readDirectory: this.projectService.host.readDirectory.bind(this.projectService.host), }; } return this.projectService.host; @@ -2132,7 +2134,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo if (dependencySelection) { tracing?.push(tracing.Phase.Session, "getPackageJsonAutoImportProvider"); const start = timestamp(); - this.autoImportProviderHost = AutoImportProviderProject.create(dependencySelection, this, this.getModuleResolutionHostForAutoImportProvider(), this.documentRegistry); + this.autoImportProviderHost = AutoImportProviderProject.create(dependencySelection, this, this.getHostForAutoImportProvider(), this.documentRegistry); if (this.autoImportProviderHost) { updateProjectIfDirty(this.autoImportProviderHost); this.sendPerformanceEvent("CreatePackageJsonAutoImportProvider", timestamp() - start); @@ -2384,7 +2386,7 @@ export class AutoImportProviderProject extends Project { private static readonly maxDependencies = 10; /** @internal */ - static getRootFileNames(dependencySelection: PackageJsonAutoImportPreference, hostProject: Project, moduleResolutionHost: ModuleResolutionHost, compilerOptions: CompilerOptions): string[] { + static getRootFileNames(dependencySelection: PackageJsonAutoImportPreference, hostProject: Project, host: GetPackageJsonEntrypointsHost, compilerOptions: CompilerOptions): string[] { if (!dependencySelection) { return ts.emptyArray; } @@ -2422,7 +2424,7 @@ export class AutoImportProviderProject extends Project { name, hostProject.currentDirectory, compilerOptions, - moduleResolutionHost, + host, program.getModuleResolutionCache()); if (packageJson) { const entrypoints = getRootNamesFromPackageJson(packageJson, program, symlinkCache); @@ -2441,7 +2443,7 @@ export class AutoImportProviderProject extends Project { `@types/${name}`, directory, compilerOptions, - moduleResolutionHost, + host, program.getModuleResolutionCache()); if (typesPackageJson) { const entrypoints = getRootNamesFromPackageJson(typesPackageJson, program, symlinkCache); @@ -2480,11 +2482,11 @@ export class AutoImportProviderProject extends Project { const entrypoints = getEntrypointsFromPackageJsonInfo( packageJson, compilerOptions, - moduleResolutionHost, + host, program.getModuleResolutionCache(), resolveJs); if (entrypoints) { - const real = moduleResolutionHost.realpath?.(packageJson.packageDirectory); + const real = host.realpath?.(packageJson.packageDirectory); const isSymlink = real && real !== packageJson.packageDirectory; if (isSymlink) { symlinkCache.setSymlinkedDirectory(packageJson.packageDirectory, { @@ -2514,7 +2516,7 @@ export class AutoImportProviderProject extends Project { }; /** @internal */ - static create(dependencySelection: PackageJsonAutoImportPreference, hostProject: Project, moduleResolutionHost: ModuleResolutionHost, documentRegistry: DocumentRegistry): AutoImportProviderProject | undefined { + static create(dependencySelection: PackageJsonAutoImportPreference, hostProject: Project, host: GetPackageJsonEntrypointsHost, documentRegistry: DocumentRegistry): AutoImportProviderProject | undefined { if (dependencySelection === PackageJsonAutoImportPreference.Off) { return undefined; } @@ -2524,7 +2526,7 @@ export class AutoImportProviderProject extends Project { ...this.compilerOptionsOverrides, }; - const rootNames = this.getRootFileNames(dependencySelection, hostProject, moduleResolutionHost, compilerOptions); + const rootNames = this.getRootFileNames(dependencySelection, hostProject, host, compilerOptions); if (!rootNames.length) { return undefined; } @@ -2573,7 +2575,7 @@ export class AutoImportProviderProject extends Project { rootFileNames = AutoImportProviderProject.getRootFileNames( this.hostProject.includePackageJsonAutoImports(), this.hostProject, - this.hostProject.getModuleResolutionHostForAutoImportProvider(), + this.hostProject.getHostForAutoImportProvider(), this.getCompilationSettings()); } @@ -2620,7 +2622,7 @@ export class AutoImportProviderProject extends Project { throw new Error("package.json changes should be notified on an AutoImportProvider's host project"); } - override getModuleResolutionHostForAutoImportProvider(): never { + override getHostForAutoImportProvider(): never { throw new Error("AutoImportProviderProject cannot provide its own host; use `hostProject.getModuleResolutionHostForAutomImportProvider()` instead."); } diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index 4b702907d6c69..606d1a31964b3 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -3400,7 +3400,7 @@ declare namespace ts { markAsDirty(): void; getScriptFileNames(): string[]; getLanguageService(): never; - getModuleResolutionHostForAutoImportProvider(): never; + getHostForAutoImportProvider(): never; getProjectReferences(): readonly ts.ProjectReference[] | undefined; getTypeAcquisition(): TypeAcquisition; } diff --git a/tests/cases/fourslash/server/autoImportProvider_wildcardExports1.ts b/tests/cases/fourslash/server/autoImportProvider_wildcardExports1.ts new file mode 100644 index 0000000000000..1c1f30f6ca509 --- /dev/null +++ b/tests/cases/fourslash/server/autoImportProvider_wildcardExports1.ts @@ -0,0 +1,96 @@ +/// + +// @Filename: /node_modules/pkg/package.json +//// { +//// "name": "pkg", +//// "version": "1.0.0", +//// "exports": { +//// "./*": "./a/*.js", +//// "./b/*.js": "./b/*.js", +//// "./c/*": "./c/*", +//// "./d/*": { +//// "import": "./d/*.mjs" +//// } +//// } +//// } + +// @Filename: /node_modules/pkg/a/a1.d.ts +//// export const a1: number; + +// @Filename: /node_modules/pkg/b/b1.d.ts +//// export const b1: number; + +// @Filename: /node_modules/pkg/b/b2.d.mts +//// export const NOT_REACHABLE: number; + +// @Filename: /node_modules/pkg/c/c1.d.ts +//// export const c1: number; + +// @Filename: /node_modules/pkg/c/subfolder/c2.d.mts +//// export const c2: number; + +// @Filename: /node_modules/pkg/d/d1.d.mts +//// export const d1: number; + +// @Filename: /package.json +//// { +//// "type": "module", +//// "dependencies": { +//// "pkg": "1.0.0" +//// } +//// } + +// @Filename: /tsconfig.json +//// { +//// "compilerOptions": { +//// "module": "nodenext" +//// } +//// } + +// @Filename: /main.ts +//// /**/ + +verify.completions({ + marker: "", + includes: [ + { + name: "a1", + source: "pkg/a1", + sourceDisplay: "pkg/a1", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + { + name: "b1", + source: "pkg/b/b1.js", + sourceDisplay: "pkg/b/b1.js", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + { + name: "c1", + source: "pkg/c/c1.js", + sourceDisplay: "pkg/c/c1.js", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + { + name: "c2", + source: "pkg/c/subfolder/c2.mjs", + sourceDisplay: "pkg/c/subfolder/c2.mjs", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + }, + { + name: "d1", + source: "pkg/d/d1", + sourceDisplay: "pkg/d/d1", + hasAction: true, + sortText: completion.SortText.AutoImportSuggestions + } + ], + preferences: { + includeCompletionsForModuleExports: true, + allowIncompleteCompletions: true + } +});