Skip to content

Add resolveLibrary method on hosts and store resolvedLibraries in program so that resolutions can be reused #53877

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

Merged
merged 6 commits into from
Apr 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1249,13 +1249,14 @@ function createModuleOrTypeReferenceResolutionCache<T>(
export function createModuleResolutionCache(
currentDirectory: string,
getCanonicalFileName: (s: string) => string,
options?: CompilerOptions
options?: CompilerOptions,
packageJsonInfoCache?: PackageJsonInfoCache,
): ModuleResolutionCache {
const result = createModuleOrTypeReferenceResolutionCache(
currentDirectory,
getCanonicalFileName,
options,
/*packageJsonInfoCache*/ undefined,
packageJsonInfoCache,
getOriginalOrResolvedModuleFileName,
) as ModuleResolutionCache;
result.getOrCreateCacheForModuleName = (nonRelativeName, mode, redirectedReference) => result.getOrCreateCacheForNonRelativeName(nonRelativeName, mode, redirectedReference);
Expand All @@ -1277,6 +1278,16 @@ export function createTypeReferenceDirectiveResolutionCache(
);
}

/** @internal */
export function getOptionsForLibraryResolution(options: CompilerOptions) {
return { moduleResolution: ModuleResolutionKind.Node10, traceResolution: options.traceResolution };
}

/** @internal */
export function resolveLibrary(libraryName: string, resolveFrom: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: ModuleResolutionCache): ResolvedModuleWithFailedLookupLocations {
return resolveModuleName(libraryName, resolveFrom, getOptionsForLibraryResolution(compilerOptions), host, cache);
}

export function resolveModuleNameFromCache(moduleName: string, containingFile: string, cache: ModuleResolutionCache, mode?: ResolutionMode): ResolvedModuleWithFailedLookupLocations | undefined {
const containingDirectory = getDirectoryPath(containingFile);
return cache.getFromDirectoryCache(moduleName, mode, containingDirectory, /*redirectedReference*/ undefined);
Expand Down
133 changes: 109 additions & 24 deletions src/compiler/program.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ import {
HasChangedAutomaticTypeDirectiveNames,
hasChangesInResolutions,
hasExtension,
HasInvalidatedLibResolutions,
HasInvalidatedResolutions,
hasJSDocNodes,
hasJSFileExtension,
Expand Down Expand Up @@ -211,6 +212,7 @@ import {
JsxEmit,
length,
libMap,
LibResolution,
libs,
mapDefined,
mapDefinedIterator,
Expand Down Expand Up @@ -276,6 +278,7 @@ import {
ResolvedModuleWithFailedLookupLocations,
ResolvedProjectReference,
ResolvedTypeReferenceDirectiveWithFailedLookupLocations,
resolveLibrary,
resolveModuleName,
resolveTypeReferenceDirective,
returnFalse,
Expand Down Expand Up @@ -1097,6 +1100,32 @@ function forEachProjectReference<T>(
/** @internal */
export const inferredTypesContainingFile = "__inferred type names__.ts";

/** @internal */
export function getInferredLibraryNameResolveFrom(options: CompilerOptions, currentDirectory: string, libFileName: string) {
const containingDirectory = options.configFilePath ? getDirectoryPath(options.configFilePath) : currentDirectory;
return combinePaths(containingDirectory, `__lib_node_modules_lookup_${libFileName}__.ts`);
}

function getLibraryNameFromLibFileName(libFileName: string) {
// Support resolving to lib.dom.d.ts -> @typescript/lib-dom, and
// lib.dom.iterable.d.ts -> @typescript/lib-dom/iterable
// lib.es2015.symbol.wellknown.d.ts -> @typescript/lib-es2015/symbol-wellknown
const components = libFileName.split(".");
let path = components[1];
let i = 2;
while (components[i] && components[i] !== "d") {
path += (i === 2 ? "/" : "-") + components[i];
i++;
}
return "@typescript/lib-" + path;
}

function getLibFileNameFromLibReference(libReference: FileReference) {
const libName = toFileNameLowerCase(libReference.fileName);
const libFileName = libMap.get(libName);
return { libName, libFileName };
}

interface DiagnosticCache<T extends Diagnostic> {
perFile?: Map<Path, readonly T[]>;
allDiagnostics?: readonly T[];
Expand Down Expand Up @@ -1176,6 +1205,7 @@ export function isProgramUptoDate(
getSourceVersion: (path: Path, fileName: string) => string | undefined,
fileExists: (fileName: string) => boolean,
hasInvalidatedResolutions: HasInvalidatedResolutions,
hasInvalidatedLibResolutions: HasInvalidatedLibResolutions,
hasChangedAutomaticTypeDirectiveNames: HasChangedAutomaticTypeDirectiveNames | undefined,
getParsedCommandLine: (fileName: string) => ParsedCommandLine | undefined,
projectReferences: readonly ProjectReference[] | undefined
Expand All @@ -1201,6 +1231,9 @@ export function isProgramUptoDate(
// If the compilation settings do no match, then the program is not up-to-date
if (!compareDataObjects(currentOptions, newOptions)) return false;

// If library resolution is invalidated, then the program is not up-to-date
if (program.resolvedLibReferences && forEachEntry(program.resolvedLibReferences, (_value, libFileName) => hasInvalidatedLibResolutions(libFileName))) return false;

// If everything matches but the text of config file is changed,
// error locations can change for program options, so update the program
if (currentOptions.configFile && newOptions.configFile) return currentOptions.configFile.text === newOptions.configFile.text;
Expand Down Expand Up @@ -1469,6 +1502,9 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
let automaticTypeDirectiveNames: string[] | undefined;
let automaticTypeDirectiveResolutions: ModeAwareCache<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>;

let resolvedLibReferences: Map<string, LibResolution> | undefined;
let resolvedLibProcessing: Map<string, LibResolution> | undefined;

// The below settings are to track if a .js file should be add to the program if loaded via searching under node_modules.
// This works as imported modules are discovered recursively in a depth first manner, specifically:
// - For each root file, findSourceFile is called.
Expand Down Expand Up @@ -1592,6 +1628,17 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
);
}

const hasInvalidatedLibResolutions = host.hasInvalidatedLibResolutions || returnFalse;
let actualResolveLibrary: (libraryName: string, resolveFrom: string, options: CompilerOptions, libFileName: string) => ResolvedModuleWithFailedLookupLocations;
if (host.resolveLibrary) {
actualResolveLibrary = host.resolveLibrary.bind(host);
}
else {
const libraryResolutionCache = createModuleResolutionCache(currentDirectory, getCanonicalFileName, options, moduleResolutionCache?.getPackageJsonInfoCache());
actualResolveLibrary = (libraryName, resolveFrom, options) =>
resolveLibrary(libraryName, resolveFrom, options, host, libraryResolutionCache);
}

// Map from a stringified PackageId to the source file with that id.
// Only one source file may have a given packageId. Others become redirects (see createRedirectSourceFile).
// `packageIdToSourceFile` is only used while building the program, while `sourceFileToPackageName` and `isSourceFileTargetOfRedirect` are kept around.
Expand Down Expand Up @@ -1772,6 +1819,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg

// unconditionally set oldProgram to undefined to prevent it from being captured in closure
oldProgram = undefined;
resolvedLibProcessing = undefined;

const program: Program = {
getRootFileNames: () => rootNames,
Expand Down Expand Up @@ -1813,6 +1861,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
sourceFileToPackageName,
redirectTargetsMap,
usesUriStyleNodeCoreModules,
resolvedLibReferences,
isEmittedFile,
getConfigFileParsingDiagnostics,
getProjectReferences,
Expand Down Expand Up @@ -2441,6 +2490,11 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
return StructureIsReused.SafeModules;
}

if (oldProgram.resolvedLibReferences &&
forEachEntry(oldProgram.resolvedLibReferences, (resolution, libFileName) => pathForLibFileWorker(libFileName).actual !== resolution.actual)) {
return StructureIsReused.SafeModules;
}

if (host.hasChangedAutomaticTypeDirectiveNames) {
if (host.hasChangedAutomaticTypeDirectiveNames()) return StructureIsReused.SafeModules;
}
Expand Down Expand Up @@ -2481,6 +2535,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
sourceFileToPackageName = oldProgram.sourceFileToPackageName;
redirectTargetsMap = oldProgram.redirectTargetsMap;
usesUriStyleNodeCoreModules = oldProgram.usesUriStyleNodeCoreModules;
resolvedLibReferences = oldProgram.resolvedLibReferences;

return StructureIsReused.Completely;
}
Expand Down Expand Up @@ -2596,7 +2651,7 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
return equalityComparer(file.fileName, getDefaultLibraryFileName());
}
else {
return some(options.lib, libFileName => equalityComparer(file.fileName, pathForLibFile(libFileName)));
return some(options.lib, libFileName => equalityComparer(file.fileName, resolvedLibReferences!.get(libFileName)!.actual));
}
}

Expand Down Expand Up @@ -3310,11 +3365,9 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
}

function getLibFileFromReference(ref: FileReference) {
const libName = toFileNameLowerCase(ref.fileName);
const libFileName = libMap.get(libName);
if (libFileName) {
return getSourceFile(pathForLibFile(libFileName));
}
const { libFileName } = getLibFileNameFromLibReference(ref);
const actualFileName = libFileName && resolvedLibReferences?.get(libFileName)?.actual;
return actualFileName !== undefined ? getSourceFile(actualFileName) : undefined;
}

/** This should have similar behavior to 'processSourceFile' without diagnostics or mutation. */
Expand Down Expand Up @@ -3810,29 +3863,61 @@ export function createProgram(rootNamesOrOptions: readonly string[] | CreateProg
}

function pathForLibFile(libFileName: string): string {
// Support resolving to lib.dom.d.ts -> @typescript/lib-dom, and
// lib.dom.iterable.d.ts -> @typescript/lib-dom/iterable
// lib.es2015.symbol.wellknown.d.ts -> @typescript/lib-es2015/symbol-wellknown
const components = libFileName.split(".");
let path = components[1];
let i = 2;
while (components[i] && components[i] !== "d") {
path += (i === 2 ? "/" : "-") + components[i];
i++;
}
const containingDirectory = options.configFilePath ? getDirectoryPath(options.configFilePath) : currentDirectory;
const resolveFrom = combinePaths(containingDirectory, `__lib_node_modules_lookup_${libFileName}__.ts`);
const localOverrideModuleResult = resolveModuleName("@typescript/lib-" + path, resolveFrom, { moduleResolution: ModuleResolutionKind.Node10, traceResolution: options.traceResolution }, host, moduleResolutionCache);
if (localOverrideModuleResult?.resolvedModule) {
return localOverrideModuleResult.resolvedModule.resolvedFileName;
const existing = resolvedLibReferences?.get(libFileName);
if (existing) return existing.actual;
const result = pathForLibFileWorker(libFileName);
(resolvedLibReferences ??= new Map()).set(libFileName, result);
return result.actual;
}

function pathForLibFileWorker(libFileName: string): LibResolution {
const existing = resolvedLibProcessing?.get(libFileName);
if (existing) return existing;

if (structureIsReused !== StructureIsReused.Not && oldProgram && !hasInvalidatedLibResolutions(libFileName)) {
const oldResolution = oldProgram.resolvedLibReferences?.get(libFileName);
if (oldResolution) {
if (oldResolution.resolution && isTraceEnabled(options, host)) {
const libraryName = getLibraryNameFromLibFileName(libFileName);
const resolveFrom = getInferredLibraryNameResolveFrom(options, currentDirectory, libFileName);
trace(host,
oldResolution.resolution.resolvedModule ?
oldResolution.resolution.resolvedModule.packageId ?
Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2_with_Package_ID_3 :
Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_successfully_resolved_to_2 :
Diagnostics.Reusing_resolution_of_module_0_from_1_of_old_program_it_was_not_resolved,
libraryName,
getNormalizedAbsolutePath(resolveFrom, currentDirectory),
oldResolution.resolution.resolvedModule?.resolvedFileName,
oldResolution.resolution.resolvedModule?.packageId && packageIdToString(oldResolution.resolution.resolvedModule.packageId)
);
}
(resolvedLibProcessing ??= new Map()).set(libFileName, oldResolution);
return oldResolution;
}
}
return combinePaths(defaultLibraryPath, libFileName);

const libraryName = getLibraryNameFromLibFileName(libFileName);
const resolveFrom = getInferredLibraryNameResolveFrom(options, currentDirectory, libFileName);
tracing?.push(tracing.Phase.Program, "resolveLibrary", { resolveFrom });
performance.mark("beforeResolveLibrary");
const resolution = actualResolveLibrary(libraryName, resolveFrom, options, libFileName);
performance.mark("afterResolveLibrary");
performance.measure("ResolveLibrary", "beforeResolveLibrary", "afterResolveLibrary");
tracing?.pop();
const result: LibResolution = {
resolution,
actual: resolution.resolvedModule ?
resolution.resolvedModule.resolvedFileName :
combinePaths(defaultLibraryPath, libFileName)
};
(resolvedLibProcessing ??= new Map()).set(libFileName, result);
return result;
}

function processLibReferenceDirectives(file: SourceFile) {
forEach(file.libReferenceDirectives, (libReference, index) => {
const libName = toFileNameLowerCase(libReference.fileName);
const libFileName = libMap.get(libName);
const { libName, libFileName } = getLibFileNameFromLibReference(libReference);
if (libFileName) {
// we ignore any 'no-default-lib' reference set on this file.
processRootFile(pathForLibFile(libFileName), /*isDefaultLib*/ true, /*ignoreNoDefaultLib*/ true, { kind: FileIncludeKind.LibReferenceDirective, file: file.path, index, });
Expand Down
Loading