Skip to content

Commit

Permalink
Cache package.json lookup results from module resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
sheetalkamat committed Apr 19, 2021
1 parent 9e4cd0b commit 960403a
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 38 deletions.
8 changes: 8 additions & 0 deletions src/compiler/diagnosticMessages.json
Expand Up @@ -5041,6 +5041,14 @@
"code": 6387,
"reportsDeprecated": true
},
"Using cached result of 'package.json' at '{0}' that indicates it was found.": {
"category": "Message",
"code": 6388
},
"Using cached result of 'package.json' at '{0}' that indicates it was not found.": {
"category": "Message",
"code": 6389
},

"The expected type comes from property '{0}' which is declared here on type '{1}'": {
"category": "Message",
Expand Down
69 changes: 57 additions & 12 deletions src/compiler/moduleNameResolver.ts
Expand Up @@ -101,9 +101,11 @@ namespace ts {
traceEnabled: boolean;
failedLookupLocations: Push<string>;
resultFromCache?: ResolvedModuleWithFailedLookupLocations;
packageJsonInfoCache: PackageJsonInfoCache | undefined;
}

/** Just the fields that we use for module resolution. */
/*@internal*/
interface PackageJsonPathFields {
typings?: string;
types?: string;
Expand Down Expand Up @@ -179,6 +181,7 @@ namespace ts {
return typesVersions;
}

/*@internal*/
interface VersionPaths {
version: string;
paths: MapLike<string[]>;
Expand Down Expand Up @@ -281,13 +284,13 @@ namespace ts {
* This is possible in case if resolution is performed for directives specified via 'types' parameter. In this case initial path for secondary lookups
* is assumed to be the same as root directory of the project.
*/
export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference): ResolvedTypeReferenceDirectiveWithFailedLookupLocations {
export function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference, packageJsonInfoCache?: PackageJsonInfoCache): ResolvedTypeReferenceDirectiveWithFailedLookupLocations {
const traceEnabled = isTraceEnabled(options, host);
if (redirectedReference) {
options = redirectedReference.commandLine.options;
}
const failedLookupLocations: string[] = [];
const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations };
const moduleResolutionState: ModuleResolutionState = { compilerOptions: options, host, traceEnabled, failedLookupLocations, packageJsonInfoCache };

const typeRoots = getEffectiveTypeRoots(options, host);
if (traceEnabled) {
Expand Down Expand Up @@ -441,7 +444,7 @@ namespace ts {
* Cached module resolutions per containing directory.
* This assumes that any module id will have the same resolution for sibling files located in the same folder.
*/
export interface ModuleResolutionCache extends NonRelativeModuleNameResolutionCache {
export interface ModuleResolutionCache extends NonRelativeModuleNameResolutionCache, PackageJsonInfoCache {
getOrCreateCacheForDirectory(directoryName: string, redirectedReference?: ResolvedProjectReference): Map<ResolvedModuleWithFailedLookupLocations>;
clear(): void;
/**
Expand All @@ -455,10 +458,16 @@ namespace ts {
* Stored map from non-relative module name to a table: directory -> result of module lookup in this directory
* We support only non-relative module names because resolution of relative module names is usually more deterministic and thus less expensive.
*/
export interface NonRelativeModuleNameResolutionCache {
export interface NonRelativeModuleNameResolutionCache extends PackageJsonInfoCache {
getOrCreateCacheForModuleName(nonRelativeModuleName: string, redirectedReference?: ResolvedProjectReference): PerModuleNameCache;
}

export interface PackageJsonInfoCache {
/*@internal*/ getPackageJsonInfo(packageJsonPath: string): PackageJsonInfo | false | undefined;
/*@internal*/ setPackageJsonInfo(packageJsonPath: string, info: PackageJsonInfo | false): void;
clear(): void;
}

export interface PerModuleNameCache {
get(directory: string): ResolvedModuleWithFailedLookupLocations | undefined;
set(directory: string, result: ResolvedModuleWithFailedLookupLocations): void;
Expand Down Expand Up @@ -525,14 +534,29 @@ namespace ts {
}
}

export function createPackageJsonInfoCache(currentDirectory: string, getCanonicalFileName: (s: string) => string): PackageJsonInfoCache {
let cache: ESMap<Path, PackageJsonInfo | false> | undefined;
return { getPackageJsonInfo, setPackageJsonInfo, clear };
function getPackageJsonInfo(packageJsonPath: string) {
return cache?.get(toPath(packageJsonPath, currentDirectory, getCanonicalFileName));
}
function setPackageJsonInfo(packageJsonPath: string, info: PackageJsonInfo | false) {
(cache ||= new Map()).set(toPath(packageJsonPath, currentDirectory, getCanonicalFileName), info);
}
function clear() {
cache = undefined;
}
}

/*@internal*/
export function createModuleResolutionCacheWithMaps(
directoryToModuleNameMap: CacheWithRedirects<ESMap<string, ResolvedModuleWithFailedLookupLocations>>,
moduleNameToDirectoryMap: CacheWithRedirects<PerModuleNameCache>,
currentDirectory: string,
getCanonicalFileName: GetCanonicalFileName): ModuleResolutionCache {

const packageJsonInfoCache = createPackageJsonInfoCache(currentDirectory, getCanonicalFileName);
return {
...packageJsonInfoCache,
getOrCreateCacheForDirectory,
getOrCreateCacheForModuleName,
clear,
Expand All @@ -542,6 +566,7 @@ namespace ts {
function clear() {
directoryToModuleNameMap.clear();
moduleNameToDirectoryMap.clear();
packageJsonInfoCache.clear();
}

function update(options: CompilerOptions) {
Expand Down Expand Up @@ -969,7 +994,7 @@ namespace ts {
const traceEnabled = isTraceEnabled(compilerOptions, host);

const failedLookupLocations: string[] = [];
const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations };
const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache };

const result = forEach(extensions, ext => tryResolve(ext));
return createResolvedModuleWithFailedLookupLocations(result?.value?.resolved, result?.value?.isExternalLibraryImport, failedLookupLocations, state.resultFromCache);
Expand Down Expand Up @@ -1175,6 +1200,7 @@ namespace ts {
return withPackageId(packageInfo, loadNodeModuleFromDirectoryWorker(extensions, candidate, onlyRecordFailures, state, packageJsonContent, versionPaths));
}

/*@internal*/
interface PackageJsonInfo {
packageDirectory: string;
packageJsonContent: PackageJsonPathFields;
Expand All @@ -1183,21 +1209,40 @@ namespace ts {

function getPackageJsonInfo(packageDirectory: string, onlyRecordFailures: boolean, state: ModuleResolutionState): PackageJsonInfo | undefined {
const { host, traceEnabled } = state;
const directoryExists = !onlyRecordFailures && directoryProbablyExists(packageDirectory, host);
const packageJsonPath = combinePaths(packageDirectory, "package.json");
if (onlyRecordFailures) {
state.failedLookupLocations.push(packageJsonPath);
return undefined;
}

const existing = state.packageJsonInfoCache?.getPackageJsonInfo(packageJsonPath);
if (existing !== undefined) {
if (existing) {
if (traceEnabled) trace(host, Diagnostics.Using_cached_result_of_package_json_at_0_that_indicates_it_was_found, packageJsonPath);
return existing;
}
else {
if (traceEnabled) trace(host, Diagnostics.Using_cached_result_of_package_json_at_0_that_indicates_it_was_not_found, packageJsonPath);
state.failedLookupLocations.push(packageJsonPath);
return undefined;
}
}
const directoryExists = directoryProbablyExists(packageDirectory, host);
if (directoryExists && host.fileExists(packageJsonPath)) {
const packageJsonContent = readJson(packageJsonPath, host) as PackageJson;
if (traceEnabled) {
trace(host, Diagnostics.Found_package_json_at_0, packageJsonPath);
}
const versionPaths = readPackageJsonTypesVersionPaths(packageJsonContent, state);
return { packageDirectory, packageJsonContent, versionPaths };
const result = { packageDirectory, packageJsonContent, versionPaths };
state.packageJsonInfoCache?.setPackageJsonInfo(packageJsonPath, result);
return result;
}
else {
if (directoryExists && traceEnabled) {
trace(host, Diagnostics.File_0_does_not_exist, packageJsonPath);
}

state.packageJsonInfoCache?.setPackageJsonInfo(packageJsonPath, false);
// record package json as one of failed lookup locations - in the future if this file will appear it will invalidate resolution results
state.failedLookupLocations.push(packageJsonPath);
}
Expand Down Expand Up @@ -1486,7 +1531,7 @@ namespace ts {
export function classicNameResolver(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, cache?: NonRelativeModuleNameResolutionCache, redirectedReference?: ResolvedProjectReference): ResolvedModuleWithFailedLookupLocations {
const traceEnabled = isTraceEnabled(compilerOptions, host);
const failedLookupLocations: string[] = [];
const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations };
const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations, packageJsonInfoCache: cache };
const containingDirectory = getDirectoryPath(containingFile);

const resolved = tryResolve(Extensions.TypeScript) || tryResolve(Extensions.JavaScript);
Expand Down Expand Up @@ -1530,13 +1575,13 @@ namespace ts {
* This is the minumum code needed to expose that functionality; the rest is in the host.
*/
/* @internal */
export function loadModuleFromGlobalCache(moduleName: string, projectName: string | undefined, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string): ResolvedModuleWithFailedLookupLocations {
export function loadModuleFromGlobalCache(moduleName: string, projectName: string | undefined, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string, packageJsonInfoCache: PackageJsonInfoCache): ResolvedModuleWithFailedLookupLocations {
const traceEnabled = isTraceEnabled(compilerOptions, host);
if (traceEnabled) {
trace(host, Diagnostics.Auto_discovery_for_typings_is_enabled_in_project_0_Running_extra_resolution_pass_for_module_1_using_cache_location_2, projectName, moduleName, globalCache);
}
const failedLookupLocations: string[] = [];
const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations };
const state: ModuleResolutionState = { compilerOptions, host, traceEnabled, failedLookupLocations, packageJsonInfoCache };
const resolved = loadModuleFromImmediateNodeModulesDirectory(Extensions.DtsOnly, moduleName, globalCache, state, /*typesScopeOnly*/ false);
return createResolvedModuleWithFailedLookupLocations(resolved, /*isExternalLibraryImport*/ true, failedLookupLocations, state.resultFromCache);
}
Expand Down
15 changes: 13 additions & 2 deletions src/compiler/program.ts
Expand Up @@ -843,6 +843,7 @@ namespace ts {
let _compilerOptionsObjectLiteralSyntax: ObjectLiteralExpression | false | undefined;

let moduleResolutionCache: ModuleResolutionCache | undefined;
let packageJsonInfoCache: PackageJsonInfoCache | undefined;
let actualResolveModuleNamesWorker: (moduleNames: string[], containingFile: string, reusedNames?: string[], redirectedReference?: ResolvedProjectReference) => ResolvedModuleFull[];
const hasInvalidatedResolution = host.hasInvalidatedResolution || returnFalse;
if (host.resolveModuleNames) {
Expand All @@ -857,7 +858,7 @@ namespace ts {
});
}
else {
moduleResolutionCache = createModuleResolutionCache(currentDirectory, x => host.getCanonicalFileName(x), options);
moduleResolutionCache = createModuleResolutionCache(currentDirectory, getCanonicalFileName, options);
const loader = (moduleName: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveModuleName(moduleName, containingFile, options, host, moduleResolutionCache, redirectedReference).resolvedModule!; // TODO: GH#18217
actualResolveModuleNamesWorker = (moduleNames, containingFile, _reusedNames, redirectedReference) => loadWithLocalCache<ResolvedModuleFull>(Debug.checkEachDefined(moduleNames), containingFile, redirectedReference, loader);
}
Expand All @@ -867,7 +868,15 @@ namespace ts {
actualResolveTypeReferenceDirectiveNamesWorker = (typeDirectiveNames, containingFile, redirectedReference) => host.resolveTypeReferenceDirectives!(Debug.checkEachDefined(typeDirectiveNames), containingFile, redirectedReference, options);
}
else {
const loader = (typesRef: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveTypeReferenceDirective(typesRef, containingFile, options, host, redirectedReference).resolvedTypeReferenceDirective!; // TODO: GH#18217
packageJsonInfoCache = moduleResolutionCache || createPackageJsonInfoCache(currentDirectory, getCanonicalFileName);
const loader = (typesRef: string, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveTypeReferenceDirective(
typesRef,
containingFile,
options,
host,
redirectedReference,
packageJsonInfoCache,
).resolvedTypeReferenceDirective!; // TODO: GH#18217
actualResolveTypeReferenceDirectiveNamesWorker = (typeReferenceDirectiveNames, containingFile, redirectedReference) => loadWithLocalCache<ResolvedTypeReferenceDirective>(Debug.checkEachDefined(typeReferenceDirectiveNames), containingFile, redirectedReference, loader);
}

Expand Down Expand Up @@ -1036,6 +1045,8 @@ namespace ts {
);
}

packageJsonInfoCache = undefined;

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

Expand Down
8 changes: 7 additions & 1 deletion src/compiler/resolutionCache.ts
Expand Up @@ -319,7 +319,9 @@ namespace ts {
resolutionHost.projectName,
compilerOptions,
host,
globalCache);
globalCache,
moduleResolutionCache,
);
if (resolvedModule) {
// Modify existing resolution so its saved in the directory cache as well
(primaryResult.resolvedModule as any) = resolvedModule;
Expand All @@ -332,6 +334,10 @@ namespace ts {
return primaryResult;
}

function resolveTypeReferenceDirective(typeReferenceDirectiveName: string, containingFile: string | undefined, options: CompilerOptions, host: ModuleResolutionHost, redirectedReference?: ResolvedProjectReference): CachedResolvedTypeReferenceDirectiveWithFailedLookupLocations {
return ts.resolveTypeReferenceDirective(typeReferenceDirectiveName, containingFile, options, host, redirectedReference, moduleResolutionCache);
}

interface ResolveNamesWithLocalCacheInput<T extends ResolutionWithFailedLookupLocations, R extends ResolutionWithResolvedFileName> {
names: readonly string[];
containingFile: string;
Expand Down
2 changes: 1 addition & 1 deletion src/testRunner/unittests/reuseProgramStructure.ts
Expand Up @@ -474,7 +474,7 @@ namespace ts {
"File 'node_modules/@types/a.d.ts' does not exist.",
"File 'node_modules/@types/a/index.d.ts' does not exist.",
"Loading module 'a' from 'node_modules' folder, target file type 'JavaScript'.",
"File 'node_modules/a/package.json' does not exist.",
"Using cached result of 'package.json' at 'node_modules/a/package.json' that indicates it was not found.",
"File 'node_modules/a.js' does not exist.",
"File 'node_modules/a.jsx' does not exist.",
"File 'node_modules/a/index.js' does not exist.",
Expand Down

0 comments on commit 960403a

Please sign in to comment.