Skip to content

Commit 9c5b09c

Browse files
authored
Updates to type reference directive resolution and module resolution when failed (#51715)
1 parent d8ba799 commit 9c5b09c

File tree

79 files changed

+879
-297
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

79 files changed

+879
-297
lines changed

src/compiler/diagnosticMessages.json

+4
Original file line numberDiff line numberDiff line change
@@ -5246,6 +5246,10 @@
52465246
"category": "Message",
52475247
"code": 6264
52485248
},
5249+
"Resolving type reference directive for program that specifies custom typeRoots, skipping lookup in 'node_modules' folder.": {
5250+
"category": "Message",
5251+
"code": 6265
5252+
},
52495253

52505254
"Directory '{0}' has no containing package.json scope. Imports will not resolve.": {
52515255
"category": "Message",

src/compiler/moduleNameResolver.ts

+56-29
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
hasProperty,
5656
hasTrailingDirectorySeparator,
5757
hostGetCanonicalFileName,
58+
inferredTypesContainingFile,
5859
isArray,
5960
isDeclarationFileName,
6061
isExternalModuleNameRelative,
@@ -450,27 +451,19 @@ export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffecti
450451
}
451452

452453
if (currentDirectory !== undefined) {
453-
return getDefaultTypeRoots(currentDirectory, host);
454+
return getDefaultTypeRoots(currentDirectory);
454455
}
455456
}
456457

457458
/**
458459
* Returns the path to every node_modules/@types directory from some ancestor directory.
459460
* Returns undefined if there are none.
460461
*/
461-
function getDefaultTypeRoots(currentDirectory: string, host: { directoryExists?: (directoryName: string) => boolean }): string[] | undefined {
462-
if (!host.directoryExists) {
463-
return [combinePaths(currentDirectory, nodeModulesAtTypes)];
464-
// And if it doesn't exist, tough.
465-
}
466-
462+
function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
467463
let typeRoots: string[] | undefined;
468464
forEachAncestorDirectory(normalizePath(currentDirectory), directory => {
469465
const atTypes = combinePaths(directory, nodeModulesAtTypes);
470-
if (host.directoryExists!(atTypes)) {
471-
(typeRoots || (typeRoots = [])).push(atTypes);
472-
}
473-
return undefined;
466+
(typeRoots ??= []).push(atTypes);
474467
});
475468
return typeRoots;
476469
}
@@ -627,10 +620,18 @@ export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string
627620
}
628621
return firstDefined(typeRoots, typeRoot => {
629622
const candidate = combinePaths(typeRoot, typeReferenceDirectiveName);
630-
const candidateDirectory = getDirectoryPath(candidate);
631-
const directoryExists = directoryProbablyExists(candidateDirectory, host);
623+
const directoryExists = directoryProbablyExists(typeRoot, host);
632624
if (!directoryExists && traceEnabled) {
633-
trace(host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidateDirectory);
625+
trace(host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, typeRoot);
626+
}
627+
if (options.typeRoots) {
628+
// Custom typeRoots resolve as file or directory just like we do modules
629+
const resolvedFromFile = loadModuleFromFile(Extensions.Declaration, candidate, !directoryExists, moduleResolutionState);
630+
if (resolvedFromFile) {
631+
const packageDirectory = parseNodeModuleFromPath(resolvedFromFile.path);
632+
const packageInfo = packageDirectory ? getPackageJsonInfo(packageDirectory, /*onlyRecordFailures*/ false, moduleResolutionState) : undefined;
633+
return resolvedTypeScriptOnly(withPackageId(packageInfo, resolvedFromFile));
634+
}
634635
}
635636
return resolvedTypeScriptOnly(
636637
loadNodeModuleFromDirectory(Extensions.Declaration, candidate,
@@ -646,20 +647,24 @@ export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string
646647

647648
function secondaryLookup(): PathAndPackageId | undefined {
648649
const initialLocationForSecondaryLookup = containingFile && getDirectoryPath(containingFile);
649-
650650
if (initialLocationForSecondaryLookup !== undefined) {
651-
// check secondary locations
652-
if (traceEnabled) {
653-
trace(host, Diagnostics.Looking_up_in_node_modules_folder_initial_location_0, initialLocationForSecondaryLookup);
654-
}
655651
let result: Resolved | undefined;
656-
if (!isExternalModuleNameRelative(typeReferenceDirectiveName)) {
657-
const searchResult = loadModuleFromNearestNodeModulesDirectory(Extensions.Declaration, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined);
658-
result = searchResult && searchResult.value;
652+
if (!options.typeRoots || !endsWith(containingFile!, inferredTypesContainingFile)) {
653+
// check secondary locations
654+
if (traceEnabled) {
655+
trace(host, Diagnostics.Looking_up_in_node_modules_folder_initial_location_0, initialLocationForSecondaryLookup);
656+
}
657+
if (!isExternalModuleNameRelative(typeReferenceDirectiveName)) {
658+
const searchResult = loadModuleFromNearestNodeModulesDirectory(Extensions.Declaration, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined);
659+
result = searchResult && searchResult.value;
660+
}
661+
else {
662+
const { path: candidate } = normalizePathForCJSResolution(initialLocationForSecondaryLookup, typeReferenceDirectiveName);
663+
result = nodeLoadModuleByRelativeName(Extensions.Declaration, candidate, /*onlyRecordFailures*/ false, moduleResolutionState, /*considerPackageJson*/ true);
664+
}
659665
}
660-
else {
661-
const { path: candidate } = normalizePathForCJSResolution(initialLocationForSecondaryLookup, typeReferenceDirectiveName);
662-
result = nodeLoadModuleByRelativeName(Extensions.Declaration, candidate, /*onlyRecordFailures*/ false, moduleResolutionState, /*considerPackageJson*/ true);
666+
else if (traceEnabled) {
667+
trace(host, Diagnostics.Resolving_type_reference_directive_for_program_that_specifies_custom_typeRoots_skipping_lookup_in_node_modules_folder);
663668
}
664669
return resolvedTypeScriptOnly(result);
665670
}
@@ -1777,6 +1782,9 @@ function nodeModuleNameResolverWorker(features: NodeResolutionFeatures, moduleNa
17771782
}
17781783
resolved = loadModuleFromNearestNodeModulesDirectory(extensions, moduleName, containingDirectory, state, cache, redirectedReference);
17791784
}
1785+
if (extensions & Extensions.Declaration) {
1786+
resolved ??= resolveFromTypeRoot(moduleName, state);
1787+
}
17801788
// For node_modules lookups, get the real path so that multiple accesses to an `npm link`-ed module do not create duplicate files.
17811789
return resolved && { value: resolved.value && { resolved: resolved.value, isExternalLibraryImport: true } };
17821790
}
@@ -3058,12 +3066,12 @@ export function classicNameResolver(moduleName: string, containingFile: string,
30583066
const searchName = normalizePath(combinePaths(directory, moduleName));
30593067
return toSearchResult(loadModuleFromFileNoPackageId(extensions, searchName, /*onlyRecordFailures*/ false, state));
30603068
});
3061-
if (resolved) {
3062-
return resolved;
3063-
}
3069+
if (resolved) return resolved;
30643070
if (extensions & (Extensions.TypeScript | Extensions.Declaration)) {
30653071
// If we didn't find the file normally, look it up in @types.
3066-
return loadModuleFromNearestNodeModulesDirectoryTypesScope(moduleName, containingDirectory, state);
3072+
let resolved = loadModuleFromNearestNodeModulesDirectoryTypesScope(moduleName, containingDirectory, state);
3073+
if (extensions & Extensions.Declaration) resolved ??= resolveFromTypeRoot(moduleName, state);
3074+
return resolved;
30673075
}
30683076
}
30693077
else {
@@ -3073,6 +3081,25 @@ export function classicNameResolver(moduleName: string, containingFile: string,
30733081
}
30743082
}
30753083

3084+
function resolveFromTypeRoot(moduleName: string, state: ModuleResolutionState) {
3085+
if (!state.compilerOptions.typeRoots) return;
3086+
for (const typeRoot of state.compilerOptions.typeRoots) {
3087+
const candidate = combinePaths(typeRoot, moduleName);
3088+
const directoryExists = directoryProbablyExists(typeRoot, state.host);
3089+
if (!directoryExists && state.traceEnabled) {
3090+
trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, typeRoot);
3091+
}
3092+
const resolvedFromFile = loadModuleFromFile(Extensions.Declaration, candidate, !directoryExists, state);
3093+
if (resolvedFromFile) {
3094+
const packageDirectory = parseNodeModuleFromPath(resolvedFromFile.path);
3095+
const packageInfo = packageDirectory ? getPackageJsonInfo(packageDirectory, /*onlyRecordFailures*/ false, state) : undefined;
3096+
return toSearchResult(withPackageId(packageInfo, resolvedFromFile));
3097+
}
3098+
const resolved = loadNodeModuleFromDirectory(Extensions.Declaration, candidate, !directoryExists, state);
3099+
if (resolved) return toSearchResult(resolved);
3100+
}
3101+
}
3102+
30763103
// Program errors validate that `noEmit` or `emitDeclarationOnly` is also set,
30773104
// so this function doesn't check them to avoid propagating errors.
30783105
/** @internal */

src/compiler/resolutionCache.ts

+27-26
Original file line numberDiff line numberDiff line change
@@ -1167,26 +1167,28 @@ export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootD
11671167

11681168
function createTypeRootsWatch(typeRootPath: Path, typeRoot: string): FileWatcher {
11691169
// Create new watch and recursive info
1170-
return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => {
1171-
const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory);
1172-
if (cachedDirectoryStructureHost) {
1173-
// Since the file existence changed, update the sourceFiles cache
1174-
cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
1175-
}
1176-
1177-
// For now just recompile
1178-
// We could potentially store more data here about whether it was/would be really be used or not
1179-
// and with that determine to trigger compilation but for now this is enough
1180-
hasChangedAutomaticTypeDirectiveNames = true;
1181-
resolutionHost.onChangedAutomaticTypeDirectiveNames();
1170+
return canWatchTypeRootPath(typeRootPath) ?
1171+
resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => {
1172+
const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory);
1173+
if (cachedDirectoryStructureHost) {
1174+
// Since the file existence changed, update the sourceFiles cache
1175+
cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
1176+
}
11821177

1183-
// Since directory watchers invoked are flaky, the failed lookup location events might not be triggered
1184-
// So handle to failed lookup locations here as well to ensure we are invalidating resolutions
1185-
const dirPath = getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot, typeRootPath);
1186-
if (dirPath) {
1187-
scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath);
1188-
}
1189-
}, WatchDirectoryFlags.Recursive);
1178+
// For now just recompile
1179+
// We could potentially store more data here about whether it was/would be really be used or not
1180+
// and with that determine to trigger compilation but for now this is enough
1181+
hasChangedAutomaticTypeDirectiveNames = true;
1182+
resolutionHost.onChangedAutomaticTypeDirectiveNames();
1183+
1184+
// Since directory watchers invoked are flaky, the failed lookup location events might not be triggered
1185+
// So handle to failed lookup locations here as well to ensure we are invalidating resolutions
1186+
const dirPath = getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot, typeRootPath);
1187+
if (dirPath) {
1188+
scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath);
1189+
}
1190+
}, WatchDirectoryFlags.Recursive) :
1191+
noopFileWatcher;
11901192
}
11911193

11921194
/**
@@ -1204,7 +1206,7 @@ export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootD
12041206

12051207
// we need to assume the directories exist to ensure that we can get all the type root directories that get included
12061208
// But filter directories that are at root level to say directory doesnt exist, so that we arent watching them
1207-
const typeRoots = getEffectiveTypeRoots(options, { directoryExists: directoryExistsForTypeRootWatch, getCurrentDirectory });
1209+
const typeRoots = getEffectiveTypeRoots(options, { getCurrentDirectory });
12081210
if (typeRoots) {
12091211
mutateMap(
12101212
typeRootsWatches,
@@ -1220,12 +1222,11 @@ export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootD
12201222
}
12211223
}
12221224

1223-
/**
1224-
* Use this function to return if directory exists to get type roots to watch
1225-
* If we return directory exists then only the paths will be added to type roots
1226-
* Hence return true for all directories except root directories which are filtered from watching
1227-
*/
1228-
function directoryExistsForTypeRootWatch(nodeTypesDirectory: string) {
1225+
function canWatchTypeRootPath(nodeTypesDirectory: string) {
1226+
// If type roots is specified, watch that path
1227+
if (resolutionHost.getCompilationSettings().typeRoots) return true;
1228+
1229+
// Otherwise can watch directory only if we can watch the parent directory of node_modules/@types
12291230
const dir = getDirectoryPath(getDirectoryPath(nodeTypesDirectory));
12301231
const dirPath = resolutionHost.toPath(dir);
12311232
return dirPath === rootPath || canWatchDirectoryOrFile(dirPath);

src/compiler/types.ts

-1
Original file line numberDiff line numberDiff line change
@@ -9473,7 +9473,6 @@ export interface EmitTextWriter extends SymbolWriter {
94739473
}
94749474

94759475
export interface GetEffectiveTypeRootsHost {
9476-
directoryExists?(directoryName: string): boolean;
94779476
getCurrentDirectory?(): string;
94789477
}
94799478

src/server/project.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2827,7 +2827,7 @@ export class ConfiguredProject extends Project {
28272827
}
28282828

28292829
getEffectiveTypeRoots() {
2830-
return getEffectiveTypeRoots(this.getCompilationSettings(), this.directoryStructureHost) || [];
2830+
return getEffectiveTypeRoots(this.getCompilationSettings(), this) || [];
28312831
}
28322832

28332833
/** @internal */

src/testRunner/unittests/tscWatch/programUpdates.ts

+30-22
Original file line numberDiff line numberDiff line change
@@ -970,28 +970,36 @@ declare const eval: any`
970970
]
971971
});
972972

973-
verifyTscWatch({
974-
scenario,
975-
subScenario: "types should load from config file path if config exists",
976-
commandLineArgs: ["-w", "-p", configFilePath],
977-
sys: () => {
978-
const f1 = {
979-
path: "/a/b/app.ts",
980-
content: "let x = 1"
981-
};
982-
const config = {
983-
path: configFilePath,
984-
content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } })
985-
};
986-
const node = {
987-
path: "/a/b/node_modules/@types/node/index.d.ts",
988-
content: "declare var process: any"
989-
};
990-
const cwd = {
991-
path: "/a/c"
992-
};
993-
return createWatchedSystem([f1, config, node, cwd, libFile], { currentDirectory: cwd.path });
994-
},
973+
describe("types from config file", () => {
974+
function verifyTypesLoad(includeTypeRoots: boolean) {
975+
verifyTscWatch({
976+
scenario,
977+
subScenario: includeTypeRoots ?
978+
"types should not load from config file path if config exists but does not specifies typeRoots" :
979+
"types should load from config file path if config exists",
980+
commandLineArgs: ["-w", "-p", configFilePath],
981+
sys: () => {
982+
const f1 = {
983+
path: "/a/b/app.ts",
984+
content: "let x = 1"
985+
};
986+
const config = {
987+
path: configFilePath,
988+
content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: includeTypeRoots ? [] : undefined } })
989+
};
990+
const node = {
991+
path: "/a/b/node_modules/@types/node/index.d.ts",
992+
content: "declare var process: any"
993+
};
994+
const cwd = {
995+
path: "/a/c"
996+
};
997+
return createWatchedSystem([f1, config, node, cwd, libFile], { currentDirectory: cwd.path });
998+
},
999+
});
1000+
}
1001+
verifyTypesLoad(/*includeTypeRoots*/ false);
1002+
verifyTypesLoad(/*includeTypeRoots*/ true);
9951003
});
9961004

9971005
verifyTscWatch({

src/testRunner/unittests/tsserver/resolutionCache.ts

+26-21
Original file line numberDiff line numberDiff line change
@@ -345,27 +345,32 @@ describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem rename
345345
projectService.checkNumberOfProjects({ configuredProjects: 1 });
346346
});
347347

348-
it("types should load from config file path if config exists", () => {
349-
const f1 = {
350-
path: "/a/b/app.ts",
351-
content: "let x = 1"
352-
};
353-
const config = {
354-
path: "/a/b/tsconfig.json",
355-
content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } })
356-
};
357-
const node = {
358-
path: "/a/b/node_modules/@types/node/index.d.ts",
359-
content: "declare var process: any"
360-
};
361-
const cwd = {
362-
path: "/a/c"
363-
};
364-
const host = createServerHost([f1, config, node, cwd], { currentDirectory: cwd.path });
365-
const projectService = createProjectService(host);
366-
projectService.openClientFile(f1.path);
367-
projectService.checkNumberOfProjects({ configuredProjects: 1 });
368-
checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, node.path, config.path]);
348+
describe("types from config file", () => {
349+
function verifyTypesLoad(subScenario: string, includeTypeRoots: boolean) {
350+
it(subScenario, () => {
351+
const f1 = {
352+
path: "/a/b/app.ts",
353+
content: "let x = 1"
354+
};
355+
const config = {
356+
path: "/a/b/tsconfig.json",
357+
content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: includeTypeRoots ? [] : undefined } })
358+
};
359+
const node = {
360+
path: "/a/b/node_modules/@types/node/index.d.ts",
361+
content: "declare var process: any"
362+
};
363+
const cwd = {
364+
path: "/a/c"
365+
};
366+
const host = createServerHost([f1, config, node, cwd], { currentDirectory: cwd.path });
367+
const projectService = createProjectService(host, { logger: createLoggerWithInMemoryLogs(host) });
368+
projectService.openClientFile(f1.path);
369+
baselineTsserverLogs("resolutionCache", subScenario, projectService);
370+
});
371+
}
372+
verifyTypesLoad("types should load from config file path if config exists", /*includeTypeRoots*/ false);
373+
verifyTypesLoad("types should not load from config file path if config exists but does not specifies typeRoots", /*includeTypeRoots*/ true);
369374
});
370375
});
371376

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

-1
Original file line numberDiff line numberDiff line change
@@ -8151,7 +8151,6 @@ declare namespace ts {
81518151
noEmitHelpers?: boolean;
81528152
}
81538153
interface GetEffectiveTypeRootsHost {
8154-
directoryExists?(directoryName: string): boolean;
81558154
getCurrentDirectory?(): string;
81568155
}
81578156
interface TextSpan {

0 commit comments

Comments
 (0)