Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updates to type reference directive resolution and module resolution when failed #51715

Merged
merged 9 commits into from
Mar 1, 2023
4 changes: 4 additions & 0 deletions src/compiler/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -5254,6 +5254,10 @@
"category": "Message",
"code": 6264
},
"Resolving type reference directive for program that specifies custom typeRoots, skipping lookup in 'node_modules' folder.": {
"category": "Message",
"code": 6265
},

"Directory '{0}' has no containing package.json scope. Imports will not resolve.": {
"category": "Message",
Expand Down
85 changes: 56 additions & 29 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
hasProperty,
hasTrailingDirectorySeparator,
hostGetCanonicalFileName,
inferredTypesContainingFile,
isArray,
isDeclarationFileName,
isExternalModuleNameRelative,
Expand Down Expand Up @@ -451,27 +452,19 @@ export function getEffectiveTypeRoots(options: CompilerOptions, host: GetEffecti
}

if (currentDirectory !== undefined) {
return getDefaultTypeRoots(currentDirectory, host);
return getDefaultTypeRoots(currentDirectory);
}
}

/**
* Returns the path to every node_modules/@types directory from some ancestor directory.
* Returns undefined if there are none.
*/
function getDefaultTypeRoots(currentDirectory: string, host: { directoryExists?: (directoryName: string) => boolean }): string[] | undefined {
if (!host.directoryExists) {
return [combinePaths(currentDirectory, nodeModulesAtTypes)];
// And if it doesn't exist, tough.
}

function getDefaultTypeRoots(currentDirectory: string): string[] | undefined {
let typeRoots: string[] | undefined;
forEachAncestorDirectory(normalizePath(currentDirectory), directory => {
const atTypes = combinePaths(directory, nodeModulesAtTypes);
if (host.directoryExists!(atTypes)) {
(typeRoots || (typeRoots = [])).push(atTypes);
}
return undefined;
(typeRoots ??= []).push(atTypes);
});
return typeRoots;
}
Expand Down Expand Up @@ -628,10 +621,18 @@ export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string
}
return firstDefined(typeRoots, typeRoot => {
const candidate = combinePaths(typeRoot, typeReferenceDirectiveName);
const candidateDirectory = getDirectoryPath(candidate);
const directoryExists = directoryProbablyExists(candidateDirectory, host);
const directoryExists = directoryProbablyExists(typeRoot, host);
if (!directoryExists && traceEnabled) {
trace(host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, candidateDirectory);
trace(host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, typeRoot);
}
if (options.typeRoots) {
// Custom typeRoots resolve as file or directory just like we do modules
const resolvedFromFile = loadModuleFromFile(Extensions.Declaration, candidate, !directoryExists, moduleResolutionState);
if (resolvedFromFile) {
const packageDirectory = parseNodeModuleFromPath(resolvedFromFile.path);
const packageInfo = packageDirectory ? getPackageJsonInfo(packageDirectory, /*onlyRecordFailures*/ false, moduleResolutionState) : undefined;
return resolvedTypeScriptOnly(withPackageId(packageInfo, resolvedFromFile));
}
}
return resolvedTypeScriptOnly(
loadNodeModuleFromDirectory(Extensions.Declaration, candidate,
Expand All @@ -647,20 +648,24 @@ export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string

function secondaryLookup(): PathAndPackageId | undefined {
const initialLocationForSecondaryLookup = containingFile && getDirectoryPath(containingFile);

if (initialLocationForSecondaryLookup !== undefined) {
// check secondary locations
if (traceEnabled) {
trace(host, Diagnostics.Looking_up_in_node_modules_folder_initial_location_0, initialLocationForSecondaryLookup);
}
let result: Resolved | undefined;
if (!isExternalModuleNameRelative(typeReferenceDirectiveName)) {
const searchResult = loadModuleFromNearestNodeModulesDirectory(Extensions.Declaration, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined);
result = searchResult && searchResult.value;
if (!options.typeRoots || !endsWith(containingFile!, inferredTypesContainingFile)) {
// check secondary locations
if (traceEnabled) {
trace(host, Diagnostics.Looking_up_in_node_modules_folder_initial_location_0, initialLocationForSecondaryLookup);
}
if (!isExternalModuleNameRelative(typeReferenceDirectiveName)) {
const searchResult = loadModuleFromNearestNodeModulesDirectory(Extensions.Declaration, typeReferenceDirectiveName, initialLocationForSecondaryLookup, moduleResolutionState, /*cache*/ undefined, /*redirectedReference*/ undefined);
result = searchResult && searchResult.value;
}
else {
const { path: candidate } = normalizePathForCJSResolution(initialLocationForSecondaryLookup, typeReferenceDirectiveName);
result = nodeLoadModuleByRelativeName(Extensions.Declaration, candidate, /*onlyRecordFailures*/ false, moduleResolutionState, /*considerPackageJson*/ true);
}
}
else {
const { path: candidate } = normalizePathForCJSResolution(initialLocationForSecondaryLookup, typeReferenceDirectiveName);
result = nodeLoadModuleByRelativeName(Extensions.Declaration, candidate, /*onlyRecordFailures*/ false, moduleResolutionState, /*considerPackageJson*/ true);
else if (traceEnabled) {
trace(host, Diagnostics.Resolving_type_reference_directive_for_program_that_specifies_custom_typeRoots_skipping_lookup_in_node_modules_folder);
}
return resolvedTypeScriptOnly(result);
}
Expand Down Expand Up @@ -1775,6 +1780,9 @@ function nodeModuleNameResolverWorker(features: NodeResolutionFeatures, moduleNa
}
resolved = loadModuleFromNearestNodeModulesDirectory(extensions, moduleName, containingDirectory, state, cache, redirectedReference);
}
if (extensions & Extensions.Declaration) {
resolved ??= resolveFromTypeRoot(moduleName, state);
}
// For node_modules lookups, get the real path so that multiple accesses to an `npm link`-ed module do not create duplicate files.
return resolved && { value: resolved.value && { resolved: resolved.value, isExternalLibraryImport: true } };
}
Expand Down Expand Up @@ -3049,12 +3057,12 @@ export function classicNameResolver(moduleName: string, containingFile: string,
const searchName = normalizePath(combinePaths(directory, moduleName));
return toSearchResult(loadModuleFromFileNoPackageId(extensions, searchName, /*onlyRecordFailures*/ false, state));
});
if (resolved) {
return resolved;
}
if (resolved) return resolved;
if (extensions & (Extensions.TypeScript | Extensions.Declaration)) {
// If we didn't find the file normally, look it up in @types.
return loadModuleFromNearestNodeModulesDirectoryTypesScope(moduleName, containingDirectory, state);
let resolved = loadModuleFromNearestNodeModulesDirectoryTypesScope(moduleName, containingDirectory, state);
if (extensions & Extensions.Declaration) resolved ??= resolveFromTypeRoot(moduleName, state);
return resolved;
}
}
else {
Expand All @@ -3064,6 +3072,25 @@ export function classicNameResolver(moduleName: string, containingFile: string,
}
}

function resolveFromTypeRoot(moduleName: string, state: ModuleResolutionState) {
if (!state.compilerOptions.typeRoots) return;
for (const typeRoot of state.compilerOptions.typeRoots) {
const candidate = combinePaths(typeRoot, moduleName);
const directoryExists = directoryProbablyExists(typeRoot, state.host);
if (!directoryExists && state.traceEnabled) {
trace(state.host, Diagnostics.Directory_0_does_not_exist_skipping_all_lookups_in_it, typeRoot);
}
const resolvedFromFile = loadModuleFromFile(Extensions.Declaration, candidate, !directoryExists, state);
if (resolvedFromFile) {
const packageDirectory = parseNodeModuleFromPath(resolvedFromFile.path);
const packageInfo = packageDirectory ? getPackageJsonInfo(packageDirectory, /*onlyRecordFailures*/ false, state) : undefined;
return toSearchResult(withPackageId(packageInfo, resolvedFromFile));
}
const resolved = loadNodeModuleFromDirectory(Extensions.Declaration, candidate, !directoryExists, state);
if (resolved) return toSearchResult(resolved);
}
}

// Program errors validate that `noEmit` or `emitDeclarationOnly` is also set,
// so this function doesn't check them to avoid propagating errors.
export function shouldAllowImportingTsExtension(compilerOptions: CompilerOptions, fromFileName?: string) {
Expand Down
53 changes: 27 additions & 26 deletions src/compiler/resolutionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1167,26 +1167,28 @@ export function createResolutionCache(resolutionHost: ResolutionCacheHost, rootD

function createTypeRootsWatch(typeRootPath: Path, typeRoot: string): FileWatcher {
// Create new watch and recursive info
return resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => {
const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory);
if (cachedDirectoryStructureHost) {
// Since the file existence changed, update the sourceFiles cache
cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
}

// For now just recompile
// We could potentially store more data here about whether it was/would be really be used or not
// and with that determine to trigger compilation but for now this is enough
hasChangedAutomaticTypeDirectiveNames = true;
resolutionHost.onChangedAutomaticTypeDirectiveNames();
return canWatchTypeRootPath(typeRootPath) ?
resolutionHost.watchTypeRootsDirectory(typeRoot, fileOrDirectory => {
const fileOrDirectoryPath = resolutionHost.toPath(fileOrDirectory);
if (cachedDirectoryStructureHost) {
// Since the file existence changed, update the sourceFiles cache
cachedDirectoryStructureHost.addOrDeleteFileOrDirectory(fileOrDirectory, fileOrDirectoryPath);
}

// Since directory watchers invoked are flaky, the failed lookup location events might not be triggered
// So handle to failed lookup locations here as well to ensure we are invalidating resolutions
const dirPath = getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot, typeRootPath);
if (dirPath) {
scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath);
}
}, WatchDirectoryFlags.Recursive);
// For now just recompile
// We could potentially store more data here about whether it was/would be really be used or not
// and with that determine to trigger compilation but for now this is enough
hasChangedAutomaticTypeDirectiveNames = true;
resolutionHost.onChangedAutomaticTypeDirectiveNames();

// Since directory watchers invoked are flaky, the failed lookup location events might not be triggered
// So handle to failed lookup locations here as well to ensure we are invalidating resolutions
const dirPath = getDirectoryToWatchFailedLookupLocationFromTypeRoot(typeRoot, typeRootPath);
if (dirPath) {
scheduleInvalidateResolutionOfFailedLookupLocation(fileOrDirectoryPath, dirPath === fileOrDirectoryPath);
}
}, WatchDirectoryFlags.Recursive) :
noopFileWatcher;
}

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

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

/**
* Use this function to return if directory exists to get type roots to watch
* If we return directory exists then only the paths will be added to type roots
* Hence return true for all directories except root directories which are filtered from watching
*/
function directoryExistsForTypeRootWatch(nodeTypesDirectory: string) {
function canWatchTypeRootPath(nodeTypesDirectory: string) {
// If type roots is specified, watch that path
if (resolutionHost.getCompilationSettings().typeRoots) return true;

// Otherwise can watch directory only if we can watch the parent directory of node_modules/@types
const dir = getDirectoryPath(getDirectoryPath(nodeTypesDirectory));
const dirPath = resolutionHost.toPath(dir);
return dirPath === rootPath || canWatchDirectoryOrFile(dirPath);
Expand Down
1 change: 0 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9472,7 +9472,6 @@ export interface EmitTextWriter extends SymbolWriter {
}

export interface GetEffectiveTypeRootsHost {
directoryExists?(directoryName: string): boolean;
getCurrentDirectory?(): string;
}

Expand Down
2 changes: 1 addition & 1 deletion src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2827,7 +2827,7 @@ export class ConfiguredProject extends Project {
}

getEffectiveTypeRoots() {
return getEffectiveTypeRoots(this.getCompilationSettings(), this.directoryStructureHost) || [];
return getEffectiveTypeRoots(this.getCompilationSettings(), this) || [];
}

/** @internal */
Expand Down
52 changes: 30 additions & 22 deletions src/testRunner/unittests/tscWatch/programUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -970,28 +970,36 @@ declare const eval: any`
]
});

verifyTscWatch({
scenario,
subScenario: "types should load from config file path if config exists",
commandLineArgs: ["-w", "-p", configFilePath],
sys: () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const config = {
path: configFilePath,
content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } })
};
const node = {
path: "/a/b/node_modules/@types/node/index.d.ts",
content: "declare var process: any"
};
const cwd = {
path: "/a/c"
};
return createWatchedSystem([f1, config, node, cwd, libFile], { currentDirectory: cwd.path });
},
describe("types from config file", () => {
function verifyTypesLoad(includeTypeRoots: boolean) {
verifyTscWatch({
scenario,
subScenario: includeTypeRoots ?
"types should not load from config file path if config exists but does not specifies typeRoots" :
"types should load from config file path if config exists",
commandLineArgs: ["-w", "-p", configFilePath],
sys: () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const config = {
path: configFilePath,
content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: includeTypeRoots ? [] : undefined } })
};
const node = {
path: "/a/b/node_modules/@types/node/index.d.ts",
content: "declare var process: any"
};
const cwd = {
path: "/a/c"
};
return createWatchedSystem([f1, config, node, cwd, libFile], { currentDirectory: cwd.path });
},
});
}
verifyTypesLoad(/*includeTypeRoots*/ false);
verifyTypesLoad(/*includeTypeRoots*/ true);
});

verifyTscWatch({
Expand Down
47 changes: 26 additions & 21 deletions src/testRunner/unittests/tsserver/resolutionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,27 +345,32 @@ describe("unittests:: tsserver:: resolutionCache:: tsserverProjectSystem rename
projectService.checkNumberOfProjects({ configuredProjects: 1 });
});

it("types should load from config file path if config exists", () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const config = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: [] } })
};
const node = {
path: "/a/b/node_modules/@types/node/index.d.ts",
content: "declare var process: any"
};
const cwd = {
path: "/a/c"
};
const host = createServerHost([f1, config, node, cwd], { currentDirectory: cwd.path });
const projectService = createProjectService(host);
projectService.openClientFile(f1.path);
projectService.checkNumberOfProjects({ configuredProjects: 1 });
checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, node.path, config.path]);
describe("types from config file", () => {
function verifyTypesLoad(subScenario: string, includeTypeRoots: boolean) {
it(subScenario, () => {
const f1 = {
path: "/a/b/app.ts",
content: "let x = 1"
};
const config = {
path: "/a/b/tsconfig.json",
content: JSON.stringify({ compilerOptions: { types: ["node"], typeRoots: includeTypeRoots ? [] : undefined } })
};
const node = {
path: "/a/b/node_modules/@types/node/index.d.ts",
content: "declare var process: any"
};
const cwd = {
path: "/a/c"
};
const host = createServerHost([f1, config, node, cwd], { currentDirectory: cwd.path });
const projectService = createProjectService(host, { logger: createLoggerWithInMemoryLogs(host) });
projectService.openClientFile(f1.path);
baselineTsserverLogs("resolutionCache", subScenario, projectService);
});
}
verifyTypesLoad("types should load from config file path if config exists", /*includeTypeRoots*/ false);
verifyTypesLoad("types should not load from config file path if config exists but does not specifies typeRoots", /*includeTypeRoots*/ true);
});
});

Expand Down
Loading