Skip to content

Commit 22a9ae9

Browse files
authored
Offer auto-imports from wildcard exports with AutoImportProvider (#54831)
1 parent e374eba commit 22a9ae9

File tree

5 files changed

+172
-36
lines changed

5 files changed

+172
-36
lines changed

src/compiler/moduleNameResolver.ts

Lines changed: 61 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
CommandLineOption,
1010
comparePaths,
1111
Comparison,
12+
CompilerHost,
1213
CompilerOptions,
1314
concatenate,
1415
contains,
@@ -36,10 +37,12 @@ import {
3637
forEachAncestorDirectory,
3738
formatMessage,
3839
getAllowJSCompilerOption,
40+
getAnyExtensionFromPath,
3941
getBaseFileName,
4042
GetCanonicalFileName,
4143
getCommonSourceDirectory,
4244
getCompilerOptionValue,
45+
getDeclarationEmitExtensionForPath,
4346
getDirectoryPath,
4447
GetEffectiveTypeRootsHost,
4548
getEmitModuleKind,
@@ -99,6 +102,7 @@ import {
99102
startsWith,
100103
stringContains,
101104
supportedDeclarationExtensions,
105+
supportedJSExtensionsFlat,
102106
supportedTSImplementationExtensions,
103107
toPath,
104108
tryExtractTSExtension,
@@ -194,6 +198,15 @@ function formatExtensions(extensions: Extensions) {
194198
return result.join(", ");
195199
}
196200

201+
function extensionsToExtensionsArray(extensions: Extensions) {
202+
const result: Extension[] = [];
203+
if (extensions & Extensions.TypeScript) result.push(...supportedTSImplementationExtensions);
204+
if (extensions & Extensions.JavaScript) result.push(...supportedJSExtensionsFlat);
205+
if (extensions & Extensions.Declaration) result.push(...supportedDeclarationExtensions);
206+
if (extensions & Extensions.Json) result.push(Extension.Json);
207+
return result;
208+
}
209+
197210
interface PathAndPackageId {
198211
readonly fileName: string;
199212
readonly packageId: PackageId | undefined;
@@ -2085,11 +2098,16 @@ function loadNodeModuleFromDirectory(extensions: Extensions, candidate: string,
20852098
return withPackageId(packageInfo, loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageJsonContent, versionPaths));
20862099
}
20872100

2101+
/** @internal */
2102+
export interface GetPackageJsonEntrypointsHost extends ModuleResolutionHost {
2103+
readDirectory: CompilerHost["readDirectory"];
2104+
}
2105+
20882106
/** @internal */
20892107
export function getEntrypointsFromPackageJsonInfo(
20902108
packageJsonInfo: PackageJsonInfo,
20912109
options: CompilerOptions,
2092-
host: ModuleResolutionHost,
2110+
host: GetPackageJsonEntrypointsHost,
20932111
cache: ModuleResolutionCache | undefined,
20942112
resolveJs?: boolean,
20952113
): string[] | false {
@@ -2120,7 +2138,7 @@ export function getEntrypointsFromPackageJsonInfo(
21202138
arrayIsEqualTo
21212139
);
21222140
for (const conditions of conditionSets) {
2123-
const loadPackageJsonExportsState = { ...loadPackageJsonMainState, failedLookupLocations: [], conditions };
2141+
const loadPackageJsonExportsState = { ...loadPackageJsonMainState, failedLookupLocations: [], conditions, host };
21242142
const exportResolutions = loadEntrypointsFromExportMap(
21252143
packageJsonInfo,
21262144
packageJsonInfo.contents.packageJsonContent.exports,
@@ -2140,7 +2158,7 @@ export function getEntrypointsFromPackageJsonInfo(
21402158
function loadEntrypointsFromExportMap(
21412159
scope: PackageJsonInfo,
21422160
exports: object,
2143-
state: ModuleResolutionState,
2161+
state: ModuleResolutionState & { host: GetPackageJsonEntrypointsHost },
21442162
extensions: Extensions,
21452163
): PathAndExtension[] | undefined {
21462164
let entrypoints: PathAndExtension[] | undefined;
@@ -2161,17 +2179,37 @@ function loadEntrypointsFromExportMap(
21612179
return entrypoints;
21622180

21632181
function loadEntrypointsFromTargetExports(target: unknown): boolean | undefined {
2164-
if (typeof target === "string" && startsWith(target, "./") && target.indexOf("*") === -1) {
2165-
const partsAfterFirst = getPathComponents(target).slice(2);
2166-
if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) {
2167-
return false;
2182+
if (typeof target === "string" && startsWith(target, "./")) {
2183+
if (target.indexOf("*") >= 0 && state.host.readDirectory) {
2184+
if (target.indexOf("*") !== target.lastIndexOf("*")) {
2185+
return false;
2186+
}
2187+
2188+
state.host.readDirectory(
2189+
scope.packageDirectory,
2190+
extensionsToExtensionsArray(extensions),
2191+
/*excludes*/ undefined,
2192+
[changeAnyExtension(target.replace("*", "**/*"), getDeclarationEmitExtensionForPath(target))]
2193+
).forEach(entry => {
2194+
entrypoints = appendIfUnique(entrypoints, {
2195+
path: entry,
2196+
ext: getAnyExtensionFromPath(entry),
2197+
resolvedUsingTsExtension: undefined
2198+
});
2199+
});
21682200
}
2169-
const resolvedTarget = combinePaths(scope.packageDirectory, target);
2170-
const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.());
2171-
const result = loadFileNameFromPackageJsonField(extensions, finalPath, /*onlyRecordFailures*/ false, state);
2172-
if (result) {
2173-
entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path);
2174-
return true;
2201+
else {
2202+
const partsAfterFirst = getPathComponents(target).slice(2);
2203+
if (partsAfterFirst.indexOf("..") >= 0 || partsAfterFirst.indexOf(".") >= 0 || partsAfterFirst.indexOf("node_modules") >= 0) {
2204+
return false;
2205+
}
2206+
const resolvedTarget = combinePaths(scope.packageDirectory, target);
2207+
const finalPath = getNormalizedAbsolutePath(resolvedTarget, state.host.getCurrentDirectory?.());
2208+
const result = loadFileNameFromPackageJsonField(extensions, finalPath, /*onlyRecordFailures*/ false, state);
2209+
if (result) {
2210+
entrypoints = appendIfUnique(entrypoints, result, (a, b) => a.path === b.path);
2211+
return true;
2212+
}
21752213
}
21762214
}
21772215
else if (Array.isArray(target)) {
@@ -2685,12 +2723,6 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo
26852723
return ensureTrailingDirectorySeparator(combinePaths(root, dir));
26862724
}
26872725

2688-
function useCaseSensitiveFileNames() {
2689-
return !state.host.useCaseSensitiveFileNames ? true :
2690-
typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames :
2691-
state.host.useCaseSensitiveFileNames();
2692-
}
2693-
26942726
function tryLoadInputFileForPath(finalPath: string, entry: string, packagePath: string, isImports: boolean) {
26952727
// Replace any references to outputs for files in the program with the input files to support package self-names used with outDir
26962728
// 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
27002732
if (!state.isConfigLookup
27012733
&& (state.compilerOptions.declarationDir || state.compilerOptions.outDir)
27022734
&& finalPath.indexOf("/node_modules/") === -1
2703-
&& (state.compilerOptions.configFile ? containsPath(scope.packageDirectory, toAbsolutePath(state.compilerOptions.configFile.fileName), !useCaseSensitiveFileNames()) : true)
2735+
&& (state.compilerOptions.configFile ? containsPath(scope.packageDirectory, toAbsolutePath(state.compilerOptions.configFile.fileName), !useCaseSensitiveFileNames(state)) : true)
27042736
) {
27052737
// 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.
27062738
// Even with all that, we still don't know if the root of the output file structure will be (relative to the package file)
27072739
// `.`, `./src` or any other deeper directory structure. (If project references are used, it's definitely `.` by fiat, so that should be pretty common.)
27082740

2709-
const getCanonicalFileName = hostGetCanonicalFileName({ useCaseSensitiveFileNames });
2741+
const getCanonicalFileName = hostGetCanonicalFileName({ useCaseSensitiveFileNames: () => useCaseSensitiveFileNames(state) });
27102742
const commonSourceDirGuesses: string[] = [];
27112743
// A `rootDir` compiler option strongly indicates the root location
27122744
// 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
27612793
for (const commonSourceDirGuess of commonSourceDirGuesses) {
27622794
const candidateDirectories = getOutputDirectoriesForBaseDirectory(commonSourceDirGuess);
27632795
for (const candidateDir of candidateDirectories) {
2764-
if (containsPath(candidateDir, finalPath, !useCaseSensitiveFileNames())) {
2796+
if (containsPath(candidateDir, finalPath, !useCaseSensitiveFileNames(state))) {
27652797
// 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
27662798
const pathFragment = finalPath.slice(candidateDir.length + 1); // +1 to also remove directory seperator
27672799
const possibleInputBase = combinePaths(commonSourceDirGuess, pathFragment);
@@ -2771,7 +2803,7 @@ function getLoadModuleFromTargetImportOrExport(extensions: Extensions, state: Mo
27712803
const inputExts = getPossibleOriginalInputExtensionForExtension(possibleInputBase);
27722804
for (const possibleExt of inputExts) {
27732805
if (!extensionIsOk(extensions, possibleExt)) continue;
2774-
const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames());
2806+
const possibleInputWithInputExtension = changeAnyExtension(possibleInputBase, possibleExt, ext, !useCaseSensitiveFileNames(state));
27752807
if (state.host.fileExists(possibleInputWithInputExtension)) {
27762808
return toSearchResult(withPackageId(scope, loadFileNameFromPackageJsonField(extensions, possibleInputWithInputExtension, /*onlyRecordFailures*/ false, state)));
27772809
}
@@ -3203,3 +3235,9 @@ function traceIfEnabled(state: ModuleResolutionState, diagnostic: DiagnosticMess
32033235
trace(state.host, diagnostic, ...args);
32043236
}
32053237
}
3238+
3239+
function useCaseSensitiveFileNames(state: ModuleResolutionState) {
3240+
return !state.host.useCaseSensitiveFileNames ? true :
3241+
typeof state.host.useCaseSensitiveFileNames === "boolean" ? state.host.useCaseSensitiveFileNames :
3242+
state.host.useCaseSensitiveFileNames();
3243+
}

src/compiler/moduleSpecifiers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -862,7 +862,7 @@ function tryGetModuleNameFromExports(options: CompilerOptions, targetFilePath: s
862862
for (const key of getOwnKeys(exports as MapLike<unknown>)) {
863863
if (key === "default" || conditions.indexOf(key) >= 0 || isApplicableVersionedTypesKey(conditions, key)) {
864864
const subTarget = (exports as MapLike<unknown>)[key];
865-
const result = tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, subTarget, conditions);
865+
const result = tryGetModuleNameFromExports(options, targetFilePath, packageDirectory, packageName, subTarget, conditions, mode);
866866
if (result) {
867867
return result;
868868
}

src/server/project.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ import {
6060
getEntrypointsFromPackageJsonInfo,
6161
getNormalizedAbsolutePath,
6262
getOrUpdate,
63+
GetPackageJsonEntrypointsHost,
6364
getStringComparer,
6465
HasInvalidatedLibResolutions,
6566
HasInvalidatedResolutions,
@@ -2093,7 +2094,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
20932094
}
20942095

20952096
/** @internal */
2096-
getModuleResolutionHostForAutoImportProvider(): ModuleResolutionHost {
2097+
getHostForAutoImportProvider(): GetPackageJsonEntrypointsHost {
20972098
if (this.program) {
20982099
return {
20992100
fileExists: this.program.fileExists,
@@ -2104,6 +2105,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
21042105
getDirectories: this.projectService.host.getDirectories.bind(this.projectService.host),
21052106
trace: this.projectService.host.trace?.bind(this.projectService.host),
21062107
useCaseSensitiveFileNames: this.program.useCaseSensitiveFileNames(),
2108+
readDirectory: this.projectService.host.readDirectory.bind(this.projectService.host),
21072109
};
21082110
}
21092111
return this.projectService.host;
@@ -2132,7 +2134,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
21322134
if (dependencySelection) {
21332135
tracing?.push(tracing.Phase.Session, "getPackageJsonAutoImportProvider");
21342136
const start = timestamp();
2135-
this.autoImportProviderHost = AutoImportProviderProject.create(dependencySelection, this, this.getModuleResolutionHostForAutoImportProvider(), this.documentRegistry);
2137+
this.autoImportProviderHost = AutoImportProviderProject.create(dependencySelection, this, this.getHostForAutoImportProvider(), this.documentRegistry);
21362138
if (this.autoImportProviderHost) {
21372139
updateProjectIfDirty(this.autoImportProviderHost);
21382140
this.sendPerformanceEvent("CreatePackageJsonAutoImportProvider", timestamp() - start);
@@ -2384,7 +2386,7 @@ export class AutoImportProviderProject extends Project {
23842386
private static readonly maxDependencies = 10;
23852387

23862388
/** @internal */
2387-
static getRootFileNames(dependencySelection: PackageJsonAutoImportPreference, hostProject: Project, moduleResolutionHost: ModuleResolutionHost, compilerOptions: CompilerOptions): string[] {
2389+
static getRootFileNames(dependencySelection: PackageJsonAutoImportPreference, hostProject: Project, host: GetPackageJsonEntrypointsHost, compilerOptions: CompilerOptions): string[] {
23882390
if (!dependencySelection) {
23892391
return ts.emptyArray;
23902392
}
@@ -2422,7 +2424,7 @@ export class AutoImportProviderProject extends Project {
24222424
name,
24232425
hostProject.currentDirectory,
24242426
compilerOptions,
2425-
moduleResolutionHost,
2427+
host,
24262428
program.getModuleResolutionCache());
24272429
if (packageJson) {
24282430
const entrypoints = getRootNamesFromPackageJson(packageJson, program, symlinkCache);
@@ -2441,7 +2443,7 @@ export class AutoImportProviderProject extends Project {
24412443
`@types/${name}`,
24422444
directory,
24432445
compilerOptions,
2444-
moduleResolutionHost,
2446+
host,
24452447
program.getModuleResolutionCache());
24462448
if (typesPackageJson) {
24472449
const entrypoints = getRootNamesFromPackageJson(typesPackageJson, program, symlinkCache);
@@ -2480,11 +2482,11 @@ export class AutoImportProviderProject extends Project {
24802482
const entrypoints = getEntrypointsFromPackageJsonInfo(
24812483
packageJson,
24822484
compilerOptions,
2483-
moduleResolutionHost,
2485+
host,
24842486
program.getModuleResolutionCache(),
24852487
resolveJs);
24862488
if (entrypoints) {
2487-
const real = moduleResolutionHost.realpath?.(packageJson.packageDirectory);
2489+
const real = host.realpath?.(packageJson.packageDirectory);
24882490
const isSymlink = real && real !== packageJson.packageDirectory;
24892491
if (isSymlink) {
24902492
symlinkCache.setSymlinkedDirectory(packageJson.packageDirectory, {
@@ -2514,7 +2516,7 @@ export class AutoImportProviderProject extends Project {
25142516
};
25152517

25162518
/** @internal */
2517-
static create(dependencySelection: PackageJsonAutoImportPreference, hostProject: Project, moduleResolutionHost: ModuleResolutionHost, documentRegistry: DocumentRegistry): AutoImportProviderProject | undefined {
2519+
static create(dependencySelection: PackageJsonAutoImportPreference, hostProject: Project, host: GetPackageJsonEntrypointsHost, documentRegistry: DocumentRegistry): AutoImportProviderProject | undefined {
25182520
if (dependencySelection === PackageJsonAutoImportPreference.Off) {
25192521
return undefined;
25202522
}
@@ -2524,7 +2526,7 @@ export class AutoImportProviderProject extends Project {
25242526
...this.compilerOptionsOverrides,
25252527
};
25262528

2527-
const rootNames = this.getRootFileNames(dependencySelection, hostProject, moduleResolutionHost, compilerOptions);
2529+
const rootNames = this.getRootFileNames(dependencySelection, hostProject, host, compilerOptions);
25282530
if (!rootNames.length) {
25292531
return undefined;
25302532
}
@@ -2573,7 +2575,7 @@ export class AutoImportProviderProject extends Project {
25732575
rootFileNames = AutoImportProviderProject.getRootFileNames(
25742576
this.hostProject.includePackageJsonAutoImports(),
25752577
this.hostProject,
2576-
this.hostProject.getModuleResolutionHostForAutoImportProvider(),
2578+
this.hostProject.getHostForAutoImportProvider(),
25772579
this.getCompilationSettings());
25782580
}
25792581

@@ -2620,7 +2622,7 @@ export class AutoImportProviderProject extends Project {
26202622
throw new Error("package.json changes should be notified on an AutoImportProvider's host project");
26212623
}
26222624

2623-
override getModuleResolutionHostForAutoImportProvider(): never {
2625+
override getHostForAutoImportProvider(): never {
26242626
throw new Error("AutoImportProviderProject cannot provide its own host; use `hostProject.getModuleResolutionHostForAutomImportProvider()` instead.");
26252627
}
26262628

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3400,7 +3400,7 @@ declare namespace ts {
34003400
markAsDirty(): void;
34013401
getScriptFileNames(): string[];
34023402
getLanguageService(): never;
3403-
getModuleResolutionHostForAutoImportProvider(): never;
3403+
getHostForAutoImportProvider(): never;
34043404
getProjectReferences(): readonly ts.ProjectReference[] | undefined;
34053405
getTypeAcquisition(): TypeAcquisition;
34063406
}

0 commit comments

Comments
 (0)