Skip to content

Commit 0d5e6c9

Browse files
committed
Use cache for module resolution even in watch mode
1 parent 031a637 commit 0d5e6c9

5 files changed

Lines changed: 244 additions & 177 deletions

File tree

src/compiler/resolutionCache.ts

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
/// <reference path="types.ts"/>
2+
/// <reference path="core.ts"/>
3+
4+
namespace ts {
5+
export interface ResolutionCache {
6+
setModuleResolutionHost(host: ModuleResolutionHost): void;
7+
startRecordingFilesWithChangedResolutions(): void;
8+
finishRecordingFilesWithChangedResolutions(): Path[];
9+
resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[];
10+
resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[];
11+
invalidateResolutionOfDeletedFile(filePath: Path): void;
12+
clear(): void;
13+
}
14+
15+
type NameResolutionWithFailedLookupLocations = { failedLookupLocations: string[], isInvalidated?: boolean };
16+
type ResolverWithGlobalCache = (primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost) => ResolvedModuleWithFailedLookupLocations | undefined;
17+
18+
/*@internal*/
19+
export function resolveWithGlobalCache(primaryResult: ResolvedModuleWithFailedLookupLocations, moduleName: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost, globalCache: string | undefined, projectName: string): ResolvedModuleWithFailedLookupLocations | undefined {
20+
if (!isExternalModuleNameRelative(moduleName) && !(primaryResult.resolvedModule && extensionIsTypeScript(primaryResult.resolvedModule.extension)) && globalCache !== undefined) {
21+
// otherwise try to load typings from @types
22+
23+
// create different collection of failed lookup locations for second pass
24+
// if it will fail and we've already found something during the first pass - we don't want to pollute its results
25+
const { resolvedModule, failedLookupLocations } = loadModuleFromGlobalCache(moduleName, projectName, compilerOptions, host, globalCache);
26+
if (resolvedModule) {
27+
return { resolvedModule, failedLookupLocations: primaryResult.failedLookupLocations.concat(failedLookupLocations) };
28+
}
29+
}
30+
}
31+
32+
/*@internal*/
33+
export function createResolutionCache(
34+
toPath: (fileName: string) => Path,
35+
getCompilerOptions: () => CompilerOptions,
36+
resolveWithGlobalCache?: ResolverWithGlobalCache): ResolutionCache {
37+
38+
let host: ModuleResolutionHost;
39+
let filesWithChangedSetOfUnresolvedImports: Path[];
40+
const resolvedModuleNames = createMap<Map<ResolvedModuleWithFailedLookupLocations>>();
41+
const resolvedTypeReferenceDirectives = createMap<Map<ResolvedTypeReferenceDirectiveWithFailedLookupLocations>>();
42+
43+
return {
44+
setModuleResolutionHost,
45+
startRecordingFilesWithChangedResolutions,
46+
finishRecordingFilesWithChangedResolutions,
47+
resolveModuleNames,
48+
resolveTypeReferenceDirectives,
49+
invalidateResolutionOfDeletedFile,
50+
clear
51+
};
52+
53+
function setModuleResolutionHost(updatedHost: ModuleResolutionHost) {
54+
host = updatedHost;
55+
}
56+
57+
function clear() {
58+
resolvedModuleNames.clear();
59+
resolvedTypeReferenceDirectives.clear();
60+
}
61+
62+
function startRecordingFilesWithChangedResolutions() {
63+
filesWithChangedSetOfUnresolvedImports = [];
64+
}
65+
66+
function finishRecordingFilesWithChangedResolutions() {
67+
const collected = filesWithChangedSetOfUnresolvedImports;
68+
filesWithChangedSetOfUnresolvedImports = undefined;
69+
return collected;
70+
}
71+
72+
function resolveModuleName(moduleName: string, containingFile: string, compilerOptions: CompilerOptions, host: ModuleResolutionHost): ResolvedModuleWithFailedLookupLocations {
73+
const primaryResult = ts.resolveModuleName(moduleName, containingFile, compilerOptions, host);
74+
// return result immediately only if it is .ts, .tsx or .d.ts
75+
// otherwise try to load typings from @types
76+
return (resolveWithGlobalCache && resolveWithGlobalCache(primaryResult, moduleName, compilerOptions, host)) || primaryResult;
77+
}
78+
79+
function resolveNamesWithLocalCache<T extends NameResolutionWithFailedLookupLocations, R>(
80+
names: string[],
81+
containingFile: string,
82+
cache: Map<Map<T>>,
83+
loader: (name: string, containingFile: string, options: CompilerOptions, host: ModuleResolutionHost) => T,
84+
getResult: (s: T) => R,
85+
getResultFileName: (result: R) => string | undefined,
86+
logChanges: boolean): R[] {
87+
88+
const path = toPath(containingFile);
89+
const currentResolutionsInFile = cache.get(path);
90+
91+
const newResolutions: Map<T> = createMap<T>();
92+
const resolvedModules: R[] = [];
93+
const compilerOptions = getCompilerOptions();
94+
95+
for (const name of names) {
96+
// check if this is a duplicate entry in the list
97+
let resolution = newResolutions.get(name);
98+
if (!resolution) {
99+
const existingResolution = currentResolutionsInFile && currentResolutionsInFile.get(name);
100+
if (moduleResolutionIsValid(existingResolution)) {
101+
// ok, it is safe to use existing name resolution results
102+
resolution = existingResolution;
103+
}
104+
else {
105+
resolution = loader(name, containingFile, compilerOptions, host);
106+
newResolutions.set(name, resolution);
107+
}
108+
if (logChanges && filesWithChangedSetOfUnresolvedImports && !resolutionIsEqualTo(existingResolution, resolution)) {
109+
filesWithChangedSetOfUnresolvedImports.push(path);
110+
// reset log changes to avoid recording the same file multiple times
111+
logChanges = false;
112+
}
113+
}
114+
115+
Debug.assert(resolution !== undefined);
116+
117+
resolvedModules.push(getResult(resolution));
118+
}
119+
120+
// replace old results with a new one
121+
cache.set(path, newResolutions);
122+
return resolvedModules;
123+
124+
function resolutionIsEqualTo(oldResolution: T, newResolution: T): boolean {
125+
if (oldResolution === newResolution) {
126+
return true;
127+
}
128+
if (!oldResolution || !newResolution || oldResolution.isInvalidated) {
129+
return false;
130+
}
131+
const oldResult = getResult(oldResolution);
132+
const newResult = getResult(newResolution);
133+
if (oldResult === newResult) {
134+
return true;
135+
}
136+
if (!oldResult || !newResult) {
137+
return false;
138+
}
139+
return getResultFileName(oldResult) === getResultFileName(newResult);
140+
}
141+
142+
function moduleResolutionIsValid(resolution: T): boolean {
143+
if (!resolution || resolution.isInvalidated) {
144+
return false;
145+
}
146+
147+
const result = getResult(resolution);
148+
if (result) {
149+
return true;
150+
}
151+
152+
// consider situation if we have no candidate locations as valid resolution.
153+
// after all there is no point to invalidate it if we have no idea where to look for the module.
154+
return resolution.failedLookupLocations.length === 0;
155+
}
156+
}
157+
158+
159+
function resolveTypeReferenceDirectives(typeDirectiveNames: string[], containingFile: string): ResolvedTypeReferenceDirective[] {
160+
return resolveNamesWithLocalCache(typeDirectiveNames, containingFile, resolvedTypeReferenceDirectives, resolveTypeReferenceDirective,
161+
m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName, /*logChanges*/ false);
162+
}
163+
164+
function resolveModuleNames(moduleNames: string[], containingFile: string, logChanges: boolean): ResolvedModuleFull[] {
165+
return resolveNamesWithLocalCache(moduleNames, containingFile, resolvedModuleNames, resolveModuleName,
166+
m => m.resolvedModule, r => r.resolvedFileName, logChanges);
167+
}
168+
169+
function invalidateResolutionCacheOfDeletedFile<T extends NameResolutionWithFailedLookupLocations, R>(
170+
deletedFilePath: Path,
171+
cache: Map<Map<T>>,
172+
getResult: (s: T) => R,
173+
getResultFileName: (result: R) => string | undefined) {
174+
cache.forEach((value, path) => {
175+
if (path === deletedFilePath) {
176+
cache.delete(path);
177+
}
178+
else if (value) {
179+
value.forEach((resolution) => {
180+
if (resolution && !resolution.isInvalidated) {
181+
const result = getResult(resolution);
182+
if (result) {
183+
if (getResultFileName(result) === deletedFilePath) {
184+
resolution.isInvalidated = true;
185+
}
186+
}
187+
}
188+
});
189+
}
190+
});
191+
}
192+
193+
function invalidateResolutionOfDeletedFile(filePath: Path) {
194+
invalidateResolutionCacheOfDeletedFile(filePath, resolvedModuleNames, m => m.resolvedModule, r => r.resolvedFileName);
195+
invalidateResolutionCacheOfDeletedFile(filePath, resolvedTypeReferenceDirectives, m => m.resolvedTypeReferenceDirective, r => r.resolvedFileName);
196+
}
197+
}
198+
}

src/compiler/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
"emitter.ts",
3838
"program.ts",
3939
"builder.ts",
40+
"resolutionCache.ts",
4041
"watchedProgram.ts",
4142
"commandLineParser.ts",
4243
"tsc.ts",

src/compiler/watchedProgram.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// <reference path="program.ts" />
22
/// <reference path="builder.ts" />
3+
/// <reference path="resolutionCache.ts"/>
34

45
namespace ts {
56
export type DiagnosticReporter = (diagnostic: Diagnostic) => void;
@@ -254,6 +255,7 @@ namespace ts {
254255
let timerToUpdateProgram: any; // timer callback to recompile the program
255256

256257
const sourceFilesCache = createMap<HostFileInfo | string>(); // Cache that stores the source file and version info
258+
257259
watchingHost = watchingHost || createWatchingSystemHost(compilerOptions.pretty);
258260
const { system, parseConfigFile, reportDiagnostic, reportWatchDiagnostic, beforeCompile, afterCompile } = watchingHost;
259261

@@ -268,6 +270,9 @@ namespace ts {
268270
const currentDirectory = host.getCurrentDirectory();
269271
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
270272

273+
// Cache for the module resolution
274+
const resolutionCache = createResolutionCache(fileName => toPath(fileName), () => compilerOptions);
275+
271276
// There is no extra check needed since we can just rely on the program to decide emit
272277
const builder = createBuilder(getCanonicalFileName, getFileEmitOutput, computeHash, _sourceFile => true);
273278

@@ -287,6 +292,10 @@ namespace ts {
287292

288293
// Create the compiler host
289294
const compilerHost = createWatchedCompilerHost(compilerOptions);
295+
resolutionCache.setModuleResolutionHost(compilerHost);
296+
if (changesAffectModuleResolution(program && program.getCompilerOptions(), compilerOptions)) {
297+
resolutionCache.clear();
298+
}
290299
beforeCompile(compilerOptions);
291300

292301
// Compile the program
@@ -321,22 +330,18 @@ namespace ts {
321330
getEnvironmentVariable: name => host.getEnvironmentVariable ? host.getEnvironmentVariable(name) : "",
322331
getDirectories: (path: string) => host.getDirectories(path),
323332
realpath,
324-
onReleaseOldSourceFile,
333+
resolveTypeReferenceDirectives: (typeDirectiveNames, containingFile) => resolutionCache.resolveTypeReferenceDirectives(typeDirectiveNames, containingFile),
334+
resolveModuleNames: (moduleNames, containingFile) => resolutionCache.resolveModuleNames(moduleNames, containingFile, /*logChanges*/ false),
335+
onReleaseOldSourceFile
325336
};
337+
}
326338

327-
// TODO: cache module resolution
328-
// if (host.resolveModuleNames) {
329-
// compilerHost.resolveModuleNames = (moduleNames, containingFile) => host.resolveModuleNames(moduleNames, containingFile);
330-
// }
331-
// if (host.resolveTypeReferenceDirectives) {
332-
// compilerHost.resolveTypeReferenceDirectives = (typeReferenceDirectiveNames, containingFile) => {
333-
// return host.resolveTypeReferenceDirectives(typeReferenceDirectiveNames, containingFile);
334-
// };
335-
// }
339+
function toPath(fileName: string) {
340+
return ts.toPath(fileName, currentDirectory, getCanonicalFileName);
336341
}
337342

338343
function fileExists(fileName: string) {
339-
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
344+
const path = toPath(fileName);
340345
const hostSourceFileInfo = sourceFilesCache.get(path);
341346
if (hostSourceFileInfo !== undefined) {
342347
return !isString(hostSourceFileInfo);
@@ -350,7 +355,7 @@ namespace ts {
350355
}
351356

352357
function getVersionedSourceFile(fileName: string, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
353-
return getVersionedSourceFileByPath(fileName, toPath(fileName, currentDirectory, getCanonicalFileName), languageVersion, onError, shouldCreateNewSourceFile);
358+
return getVersionedSourceFileByPath(fileName, toPath(fileName), languageVersion, onError, shouldCreateNewSourceFile);
354359
}
355360

356361
function getVersionedSourceFileByPath(fileName: string, path: Path, languageVersion: ScriptTarget, onError?: (message: string) => void, shouldCreateNewSourceFile?: boolean): SourceFile {
@@ -418,6 +423,7 @@ namespace ts {
418423
if (hostSourceFile !== undefined) {
419424
if (!isString(hostSourceFile)) {
420425
hostSourceFile.fileWatcher.close();
426+
resolutionCache.invalidateResolutionOfDeletedFile(path);
421427
}
422428
sourceFilesCache.delete(path);
423429
}
@@ -501,6 +507,7 @@ namespace ts {
501507
if (hostSourceFile) {
502508
// Update the cache
503509
if (eventKind === FileWatcherEventKind.Deleted) {
510+
resolutionCache.invalidateResolutionOfDeletedFile(path);
504511
if (!isString(hostSourceFile)) {
505512
hostSourceFile.fileWatcher.close();
506513
sourceFilesCache.set(path, (hostSourceFile.version++).toString());
@@ -574,7 +581,7 @@ namespace ts {
574581
function onFileAddOrRemoveInWatchedDirectory(fileName: string) {
575582
Debug.assert(!!configFileName);
576583

577-
const path = toPath(fileName, currentDirectory, getCanonicalFileName);
584+
const path = toPath(fileName);
578585

579586
// Since the file existance changed, update the sourceFiles cache
580587
updateCachedSystem(fileName, path);

0 commit comments

Comments
 (0)