Skip to content
25 changes: 17 additions & 8 deletions src/compiler/moduleNameResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -734,8 +734,9 @@ namespace ts {

/* @internal */
export function createModeAwareCache<T>(): ModeAwareCache<T> {
const underlying = new Map<string, T>();
const memoizedReverseKeys = new Map<string, [specifier: string, mode: ModuleKind.CommonJS | ModuleKind.ESNext | undefined]>();
const underlying = new Map<ModeAwareCacheKey, T>();
type ModeAwareCacheKey = string & { __modeAwareCacheKey: any; };
const memoizedReverseKeys = new Map<ModeAwareCacheKey, [specifier: string, mode: ModuleKind.CommonJS | ModuleKind.ESNext | undefined]>();

const cache: ModeAwareCache<T> = {
get(specifier, mode) {
Expand Down Expand Up @@ -765,22 +766,30 @@ namespace ts {
return cache;

function getUnderlyingCacheKey(specifier: string, mode: ModuleKind.CommonJS | ModuleKind.ESNext | undefined) {
const result = mode === undefined ? specifier : `${mode}|${specifier}`;
const result = (mode === undefined ? specifier : `${mode}|${specifier}`) as ModeAwareCacheKey;
memoizedReverseKeys.set(result, [specifier, mode]);
return result;
}
}

/* @internal */
export function zipToModeAwareCache<V>(file: SourceFile, keys: readonly string[] | readonly FileReference[], values: readonly V[]): ModeAwareCache<V> {
export function getResolutionName(entry: FileReference | StringLiteralLike) {
// We lower-case all type references because npm automatically lowercases all packages. See GH#9824.
return isStringLiteralLike(entry) ? entry.text : entry.fileName.toLowerCase();
}

/* @internal */
export function getResolutionMode(entry: FileReference | StringLiteralLike, file: SourceFile) {
return isStringLiteralLike(entry) ? getModeForUsageLocation(file, entry) : entry.resolutionMode || file.impliedNodeFormat;
}

/* @internal */
export function zipToModeAwareCache<V>(file: SourceFile, keys: readonly StringLiteralLike[] | readonly FileReference[], values: readonly V[]): ModeAwareCache<V> {
Debug.assert(keys.length === values.length);
const map = createModeAwareCache<V>();
for (let i = 0; i < keys.length; ++i) {
const entry = keys[i];
// We lower-case all type references because npm automatically lowercases all packages. See GH#9824.
const name = !isString(entry) ? entry.fileName.toLowerCase() : entry;
const mode = !isString(entry) ? entry.resolutionMode || file.impliedNodeFormat : getModeForResolutionAtIndex(file, i);
map.set(name, mode, values[i]);
map.set(getResolutionName(entry), getResolutionMode(entry, file), values[i]);
}
return map;
}
Expand Down
115 changes: 65 additions & 50 deletions src/compiler/program.ts

Large diffs are not rendered by default.

55 changes: 41 additions & 14 deletions src/compiler/resolutionCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,14 @@ namespace ts {
startRecordingFilesWithChangedResolutions(): void;
finishRecordingFilesWithChangedResolutions(): Path[] | undefined;

resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ResolvedProjectReference, containingSourceFile?: SourceFile): (ResolvedModuleFull | undefined)[];
resolveModuleNames(
moduleNames: string[],
containingFile: string,
reusedNames: string[] | undefined,
redirectedReference: ResolvedProjectReference | undefined,
containingSourceFile: SourceFile | undefined,
resolutionInfo: ModuleResolutionInfo | undefined
): (ResolvedModuleFull | undefined)[];
getResolvedModuleWithFailedLookupLocationsFromCache(moduleName: string, containingFile: string, resolutionMode?: ModuleKind.CommonJS | ModuleKind.ESNext): CachedResolvedModuleWithFailedLookupLocations | undefined;
resolveTypeReferenceDirectives(typeDirectiveNames: string[] | readonly FileReference[], containingFile: string, redirectedReference?: ResolvedProjectReference, containingFileMode?: SourceFile["impliedNodeFormat"]): (ResolvedTypeReferenceDirective | undefined)[];

Expand Down Expand Up @@ -409,6 +416,7 @@ namespace ts {
getResolutionWithResolvedFileName: GetResolutionWithResolvedFileName<T, R>;
shouldRetryResolution: (t: T) => boolean;
reusedNames?: readonly string[];
resolutionInfo?: ModuleResolutionInfo;
logChanges?: boolean;
containingSourceFile?: SourceFile;
containingSourceFileMode?: SourceFile["impliedNodeFormat"];
Expand All @@ -417,7 +425,7 @@ namespace ts {
names, containingFile, redirectedReference,
cache, perDirectoryCacheWithRedirects,
loader, getResolutionWithResolvedFileName,
shouldRetryResolution, reusedNames, logChanges, containingSourceFile, containingSourceFileMode
shouldRetryResolution, reusedNames, resolutionInfo, logChanges, containingSourceFile, containingSourceFileMode
}: ResolveNamesWithLocalCacheInput<T, R>): (R | undefined)[] {
const path = resolutionHost.toPath(containingFile);
const resolutionsInFile = cache.get(path) || cache.set(path, createModeAwareCache()).get(path)!;
Expand All @@ -441,15 +449,20 @@ namespace ts {

const seenNamesInFile = createModeAwareCache<true>();
let i = 0;
for (const entry of names) {
const name = isString(entry) ? entry : entry.fileName.toLowerCase();
for (const entry of containingSourceFile && resolutionInfo ? resolutionInfo.names : names) {
const name = !isString(entry) ? getResolutionName(entry) : entry;
// Imports supply a `containingSourceFile` but no `containingSourceFileMode` - it would be redundant
// they require calculating the mode for a given import from it's position in the resolution table, since a given
// import's syntax may override the file's default mode.
// Type references instead supply a `containingSourceFileMode` and a non-string entry which contains
// a default file mode override if applicable.
const mode = !isString(entry) ? getModeForFileReference(entry, containingSourceFileMode) :
containingSourceFile ? getModeForResolutionAtIndex(containingSourceFile, i) : undefined;
const mode = !isString(entry) ?
isStringLiteralLike(entry) ?
getModeForUsageLocation(containingSourceFile!, entry) :
getModeForFileReference(entry, containingSourceFileMode) :
containingSourceFile ?
getModeForResolutionAtIndex(containingSourceFile, i) :
undefined;
i++;
let resolution = resolutionsInFile.get(name, mode);
// Resolution is valid if it is present and not invalidated
Expand Down Expand Up @@ -533,13 +546,19 @@ namespace ts {
resolvedModules.push(getResolutionWithResolvedFileName(resolution));
}

// Stop watching and remove the unused name
resolutionsInFile.forEach((resolution, name, mode) => {
if (!seenNamesInFile.has(name, mode) && !contains(reusedNames, name)) {
stopWatchFailedLookupLocationOfResolution(resolution, path, getResolutionWithResolvedFileName);
resolutionsInFile.delete(name, mode);
}
});
if (containingSourceFile && resolutionInfo) {
resolutionInfo.reusedNames?.forEach(literal => seenNamesInFile.set(literal.text, getModeForUsageLocation(containingSourceFile, literal), true));
reusedNames = undefined;
}
if (resolutionsInFile.size() !== seenNamesInFile.size()) {
// Stop watching and remove the unused name
resolutionsInFile.forEach((resolution, name, mode) => {
if (!seenNamesInFile.has(name, mode) && !contains(reusedNames, name)) {
stopWatchFailedLookupLocationOfResolution(resolution, path, getResolutionWithResolvedFileName);
resolutionsInFile.delete(name, mode);
}
});
}

return resolvedModules;

Expand Down Expand Up @@ -576,7 +595,14 @@ namespace ts {
});
}

function resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference?: ResolvedProjectReference, containingSourceFile?: SourceFile): (ResolvedModuleFull | undefined)[] {
function resolveModuleNames(
moduleNames: string[],
containingFile: string,
reusedNames: string[] | undefined,
redirectedReference?: ResolvedProjectReference,
containingSourceFile?: SourceFile,
resolutionInfo?: ModuleResolutionInfo
): (ResolvedModuleFull | undefined)[] {
return resolveNamesWithLocalCache<CachedResolvedModuleWithFailedLookupLocations, ResolvedModuleFull>({
names: moduleNames,
containingFile,
Expand All @@ -587,6 +613,7 @@ namespace ts {
getResolutionWithResolvedFileName: getResolvedModule,
shouldRetryResolution: resolution => !resolution.resolvedModule || !resolutionExtensionIsTSOrJson(resolution.resolvedModule.extension),
reusedNames,
resolutionInfo,
logChanges: logChangesWhenResolvingModule,
containingSourceFile,
});
Expand Down
7 changes: 4 additions & 3 deletions src/compiler/tsbuildPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,10 @@ namespace ts {
const moduleResolutionCache = !compilerHost.resolveModuleNames ? createModuleResolutionCache(currentDirectory, getCanonicalFileName) : undefined;
const typeReferenceDirectiveResolutionCache = !compilerHost.resolveTypeReferenceDirectives ? createTypeReferenceDirectiveResolutionCache(currentDirectory, getCanonicalFileName, /*options*/ undefined, moduleResolutionCache?.getPackageJsonInfoCache()) : undefined;
if (!compilerHost.resolveModuleNames) {
const loader = (moduleName: string, resolverMode: ModuleKind.CommonJS | ModuleKind.ESNext | undefined, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) => resolveModuleName(moduleName, containingFile, state.projectCompilerOptions, compilerHost, moduleResolutionCache, redirectedReference, resolverMode).resolvedModule!;
compilerHost.resolveModuleNames = (moduleNames, containingFile, _reusedNames, redirectedReference, _options, containingSourceFile) =>
loadWithModeAwareCache<ResolvedModuleFull>(Debug.checkEachDefined(moduleNames), Debug.checkDefined(containingSourceFile), containingFile, redirectedReference, loader);
const loader = (moduleName: string, resolverMode: ModuleKind.CommonJS | ModuleKind.ESNext | undefined, containingFile: string, redirectedReference: ResolvedProjectReference | undefined) =>
resolveModuleName(moduleName, containingFile, state.projectCompilerOptions, compilerHost, moduleResolutionCache, redirectedReference, resolverMode).resolvedModule;
compilerHost.resolveModuleNames = (moduleNames, containingFile, _reusedNames, redirectedReference, _options, containingSourceFile, resolutionInfo) =>
loadWithModeAwareCache(Debug.checkEachDefined(moduleNames), Debug.checkDefined(containingSourceFile), containingFile, redirectedReference, resolutionInfo, loader);
compilerHost.getModuleResolutionCache = () => moduleResolutionCache;
}
if (!compilerHost.resolveTypeReferenceDirectives) {
Expand Down
7 changes: 6 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7193,6 +7193,11 @@ namespace ts {
/* @internal */
export type HasChangedAutomaticTypeDirectiveNames = () => boolean;

export interface ModuleResolutionInfo {
names: readonly StringLiteralLike[];
reusedNames: readonly StringLiteralLike[] | undefined;
}

export interface CompilerHost extends ModuleResolutionHost {
getSourceFile(fileName: string, languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined;
getSourceFileByPath?(fileName: string, path: Path, languageVersionOrOptions: ScriptTarget | CreateSourceFileOptions, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile | undefined;
Expand All @@ -7213,7 +7218,7 @@ namespace ts {
* If resolveModuleNames is implemented then implementation for members from ModuleResolutionHost can be just
* 'throw new Error("NotImplemented")'
*/
resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions, containingSourceFile?: SourceFile): (ResolvedModule | undefined)[];
resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions, containingSourceFile?: SourceFile, resolutionInfo?: ModuleResolutionInfo): (ResolvedModule | undefined)[];
/**
* Returns the module resolution cache used by a provided `resolveModuleNames` implementation so that any non-name module resolution operations (eg, package.json lookup) can reuse it
*/
Expand Down
9 changes: 4 additions & 5 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,19 +211,18 @@ namespace ts {
}

export function hasChangesInResolutions<T>(
names: readonly string[] | readonly FileReference[],
names: readonly StringLiteralLike[] | readonly FileReference[],
newSourceFile: SourceFile,
newResolutions: readonly T[],
oldResolutions: ModeAwareCache<T> | undefined,
oldSourceFile: SourceFile | undefined,
comparer: (oldResolution: T, newResolution: T) => boolean): boolean {
Debug.assert(names.length === newResolutions.length);

for (let i = 0; i < names.length; i++) {
const newResolution = newResolutions[i];
const entry = names[i];
// We lower-case all type references because npm automatically lowercases all packages. See GH#9824.
const name = !isString(entry) ? entry.fileName.toLowerCase() : entry;
const mode = !isString(entry) ? getModeForFileReference(entry, oldSourceFile?.impliedNodeFormat) : oldSourceFile && getModeForResolutionAtIndex(oldSourceFile, i);
const name = getResolutionName(entry);
const mode = getResolutionMode(entry, newSourceFile);
const oldResolution = oldResolutions && oldResolutions.get(name, mode);
const changed =
oldResolution
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/utilitiesPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2072,8 +2072,8 @@ namespace ts {
return indentation === MAX_SMI_X86 ? undefined : indentation;
}

export function isStringLiteralLike(node: Node): node is StringLiteralLike {
return node.kind === SyntaxKind.StringLiteral || node.kind === SyntaxKind.NoSubstitutionTemplateLiteral;
export function isStringLiteralLike(node: Node | FileReference): node is StringLiteralLike {
return (node as Node).kind === SyntaxKind.StringLiteral || (node as Node).kind === SyntaxKind.NoSubstitutionTemplateLiteral;
}

export function isJSDocLinkLike(node: Node): node is JSDocLink | JSDocLinkCode | JSDocLinkPlain {
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/watchPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ namespace ts {
getEnvironmentVariable?(name: string): string | undefined;

/** If provided, used to resolve the module names, otherwise typescript's default module resolution */
resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions, containingSourceFile?: SourceFile): (ResolvedModule | undefined)[];
resolveModuleNames?(moduleNames: string[], containingFile: string, reusedNames: string[] | undefined, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions, containingSourceFile?: SourceFile, resolutionInfo?: ModuleResolutionInfo): (ResolvedModule | undefined)[];
/** If provided, used to resolve type reference directives, otherwise typescript's default resolution */
resolveTypeReferenceDirectives?(typeReferenceDirectiveNames: string[] | readonly FileReference[], containingFile: string, redirectedReference: ResolvedProjectReference | undefined, options: CompilerOptions, containingFileMode?: SourceFile["impliedNodeFormat"] | undefined): (ResolvedTypeReferenceDirective | undefined)[];
/** If provided along with custom resolveModuleNames or resolveTypeReferenceDirectives, used to determine if unchanged file path needs to re-resolve modules/type reference directives */
Expand Down Expand Up @@ -365,7 +365,7 @@ namespace ts {
// Resolve module using host module resolution strategy if provided otherwise use resolution cache to resolve module names
compilerHost.resolveModuleNames = host.resolveModuleNames ?
((...args) => host.resolveModuleNames!(...args)) :
((moduleNames, containingFile, reusedNames, redirectedReference, _options, sourceFile) => resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames, redirectedReference, sourceFile));
((moduleNames, containingFile, reusedNames, redirectedReference, _options, sourceFile, resolutionInfo) => resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames, redirectedReference, sourceFile, resolutionInfo));
compilerHost.resolveTypeReferenceDirectives = host.resolveTypeReferenceDirectives ?
((...args) => host.resolveTypeReferenceDirectives!(...args)) :
((typeDirectiveNames, containingFile, redirectedReference, _options, containingFileMode) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile, redirectedReference, containingFileMode));
Expand Down
4 changes: 2 additions & 2 deletions src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -506,8 +506,8 @@ namespace ts.server {
return !this.isWatchedMissingFile(path) && this.directoryStructureHost.fileExists(file);
}

resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames?: string[], redirectedReference?: ResolvedProjectReference, _options?: CompilerOptions, containingSourceFile?: SourceFile): (ResolvedModuleFull | undefined)[] {
return this.resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames, redirectedReference, containingSourceFile);
resolveModuleNames(moduleNames: string[], containingFile: string, reusedNames?: string[], redirectedReference?: ResolvedProjectReference, _options?: CompilerOptions, containingSourceFile?: SourceFile, resolutionInfo?: ModuleResolutionInfo): (ResolvedModuleFull | undefined)[] {
return this.resolutionCache.resolveModuleNames(moduleNames, containingFile, reusedNames, redirectedReference, containingSourceFile, resolutionInfo);
}

getModuleResolutionCache(): ModuleResolutionCache | undefined {
Expand Down
Loading