Skip to content

Commit ef5935b

Browse files
committed
Initial refactoring so that watch from tsc follows the tsserver projects
1 parent 94a589b commit ef5935b

12 files changed

Lines changed: 960 additions & 508 deletions

File tree

src/compiler/commandLineParser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1979,7 +1979,7 @@ namespace ts {
19791979
* @param host The host used to resolve files and directories.
19801980
* @param errors An array for diagnostic reporting.
19811981
*/
1982-
export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray<JsFileExtensionInfo>): ExpandResult {
1982+
export function getFileNamesFromConfigSpecs(spec: ConfigFileSpecs, basePath: string, options: CompilerOptions, host: ParseConfigHost, extraFileExtensions: ReadonlyArray<JsFileExtensionInfo> = []): ExpandResult {
19831983
basePath = normalizePath(basePath);
19841984

19851985
const keyMapper = host.useCaseSensitiveFileNames ? caseSensitiveKeyMapper : caseInsensitiveKeyMapper;

src/compiler/core.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2559,4 +2559,168 @@ namespace ts {
25592559
export function isCheckJsEnabledForFile(sourceFile: SourceFile, compilerOptions: CompilerOptions) {
25602560
return sourceFile.checkJsDirective ? sourceFile.checkJsDirective.enabled : compilerOptions.checkJs;
25612561
}
2562+
2563+
export interface HostForCaching {
2564+
useCaseSensitiveFileNames: boolean;
2565+
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void;
2566+
fileExists(path: string): boolean;
2567+
directoryExists(path: string): boolean;
2568+
createDirectory(path: string): void;
2569+
getCurrentDirectory(): string;
2570+
getDirectories(path: string): string[];
2571+
readDirectory(path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[];
2572+
}
2573+
2574+
export interface CachedHost {
2575+
writeFile(path: string, data: string, writeByteOrderMark?: boolean): void;
2576+
fileExists(path: string): boolean;
2577+
directoryExists(path: string): boolean;
2578+
createDirectory(path: string): void;
2579+
getCurrentDirectory(): string;
2580+
getDirectories(path: string): string[];
2581+
readDirectory(path: string, extensions?: ReadonlyArray<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, depth?: number): string[];
2582+
addOrDeleteFileOrFolder(fileOrFolder: string): void;
2583+
clearCache(): void;
2584+
}
2585+
2586+
export function createCachedHost(host: HostForCaching): CachedHost {
2587+
const cachedReadDirectoryResult = createMap<FileSystemEntries>();
2588+
const getCurrentDirectory = memoize(() => host.getCurrentDirectory());
2589+
const getCanonicalFileName = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
2590+
return {
2591+
writeFile,
2592+
fileExists,
2593+
directoryExists,
2594+
createDirectory,
2595+
getCurrentDirectory,
2596+
getDirectories,
2597+
readDirectory,
2598+
addOrDeleteFileOrFolder,
2599+
clearCache
2600+
};
2601+
2602+
function toPath(fileName: string) {
2603+
return ts.toPath(fileName, getCurrentDirectory(), getCanonicalFileName);
2604+
}
2605+
2606+
function getFileSystemEntries(rootDir: string) {
2607+
const path = toPath(rootDir);
2608+
const cachedResult = cachedReadDirectoryResult.get(path);
2609+
if (cachedResult) {
2610+
return cachedResult;
2611+
}
2612+
2613+
const resultFromHost: FileSystemEntries = {
2614+
files: host.readDirectory(rootDir, /*extensions*/ undefined, /*exclude*/ undefined, /*include*/["*.*"]) || [],
2615+
directories: host.getDirectories(rootDir) || []
2616+
};
2617+
2618+
cachedReadDirectoryResult.set(path, resultFromHost);
2619+
return resultFromHost;
2620+
}
2621+
2622+
function canWorkWithCacheForDir(rootDir: string) {
2623+
// Some of the hosts might not be able to handle read directory or getDirectories
2624+
const path = toPath(rootDir);
2625+
if (cachedReadDirectoryResult.get(path)) {
2626+
return true;
2627+
}
2628+
try {
2629+
return getFileSystemEntries(rootDir);
2630+
}
2631+
catch (_e) {
2632+
return false;
2633+
}
2634+
}
2635+
2636+
function fileNameEqual(name1: string, name2: string) {
2637+
return getCanonicalFileName(name1) === getCanonicalFileName(name2);
2638+
}
2639+
2640+
function hasEntry(entries: ReadonlyArray<string>, name: string) {
2641+
return some(entries, file => fileNameEqual(file, name));
2642+
}
2643+
2644+
function updateFileSystemEntry(entries: ReadonlyArray<string>, baseName: string, isValid: boolean) {
2645+
if (hasEntry(entries, baseName)) {
2646+
if (!isValid) {
2647+
return filter(entries, entry => !fileNameEqual(entry, baseName));
2648+
}
2649+
}
2650+
else if (isValid) {
2651+
return entries.concat(baseName);
2652+
}
2653+
return entries;
2654+
}
2655+
2656+
function writeFile(fileName: string, data: string, writeByteOrderMark?: boolean): void {
2657+
const path = toPath(fileName);
2658+
const result = cachedReadDirectoryResult.get(getDirectoryPath(path));
2659+
const baseFileName = getBaseFileName(normalizePath(fileName));
2660+
if (result) {
2661+
result.files = updateFileSystemEntry(result.files, baseFileName, /*isValid*/ true);
2662+
}
2663+
return host.writeFile(fileName, data, writeByteOrderMark);
2664+
}
2665+
2666+
function fileExists(fileName: string): boolean {
2667+
const path = toPath(fileName);
2668+
const result = cachedReadDirectoryResult.get(getDirectoryPath(path));
2669+
const baseName = getBaseFileName(normalizePath(fileName));
2670+
return (result && hasEntry(result.files, baseName)) || host.fileExists(fileName);
2671+
}
2672+
2673+
function directoryExists(dirPath: string): boolean {
2674+
const path = toPath(dirPath);
2675+
return cachedReadDirectoryResult.has(path) || host.directoryExists(dirPath);
2676+
}
2677+
2678+
function createDirectory(dirPath: string) {
2679+
const path = toPath(dirPath);
2680+
const result = cachedReadDirectoryResult.get(getDirectoryPath(path));
2681+
const baseFileName = getBaseFileName(path);
2682+
if (result) {
2683+
result.directories = updateFileSystemEntry(result.directories, baseFileName, /*isValid*/ true);
2684+
}
2685+
host.createDirectory(dirPath);
2686+
}
2687+
2688+
function getDirectories(rootDir: string): string[] {
2689+
if (canWorkWithCacheForDir(rootDir)) {
2690+
return getFileSystemEntries(rootDir).directories.slice();
2691+
}
2692+
return host.getDirectories(rootDir);
2693+
}
2694+
function readDirectory(rootDir: string, extensions?: ReadonlyArray<string>, excludes?: ReadonlyArray<string>, includes?: ReadonlyArray<string>, depth?: number): string[] {
2695+
if (canWorkWithCacheForDir(rootDir)) {
2696+
return matchFiles(rootDir, extensions, excludes, includes, host.useCaseSensitiveFileNames, getCurrentDirectory(), depth, path => getFileSystemEntries(path));
2697+
}
2698+
return host.readDirectory(rootDir, extensions, excludes, includes, depth);
2699+
}
2700+
2701+
function addOrDeleteFileOrFolder(fileOrFolder: string) {
2702+
const path = toPath(fileOrFolder);
2703+
const existingResult = cachedReadDirectoryResult.get(path);
2704+
if (existingResult) {
2705+
if (!host.directoryExists(fileOrFolder)) {
2706+
cachedReadDirectoryResult.delete(path);
2707+
}
2708+
}
2709+
else {
2710+
// Was this earlier file
2711+
const parentResult = cachedReadDirectoryResult.get(getDirectoryPath(path));
2712+
if (parentResult) {
2713+
const baseName = getBaseFileName(fileOrFolder);
2714+
if (parentResult) {
2715+
parentResult.files = updateFileSystemEntry(parentResult.files, baseName, host.fileExists(path));
2716+
parentResult.directories = updateFileSystemEntry(parentResult.directories, baseName, host.directoryExists(path));
2717+
}
2718+
}
2719+
}
2720+
}
2721+
2722+
function clearCache() {
2723+
cachedReadDirectoryResult.clear();
2724+
}
2725+
}
25622726
}

src/compiler/program.ts

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,114 @@ namespace ts {
386386
allDiagnostics?: Diagnostic[];
387387
}
388388

389+
export function isProgramUptoDate(program: Program, rootFileNames: string[], newOptions: CompilerOptions, getSourceVersion: (path: Path) => string): boolean {
390+
// If we haven't create a program yet, then it is not up-to-date
391+
if (!program) {
392+
return false;
393+
}
394+
395+
// If number of files in the program do not match, it is not up-to-date
396+
if (program.getRootFileNames().length !== rootFileNames.length) {
397+
return false;
398+
}
399+
400+
const fileNames = concatenate(rootFileNames, map(program.getSourceFiles(), sourceFile => sourceFile.fileName));
401+
// If any file is not up-to-date, then the whole program is not up-to-date
402+
for (const fileName of fileNames) {
403+
if (!sourceFileUpToDate(program.getSourceFile(fileName))) {
404+
return false;
405+
}
406+
}
407+
408+
const currentOptions = program.getCompilerOptions();
409+
// If the compilation settings do no match, then the program is not up-to-date
410+
if (!compareDataObjects(currentOptions, newOptions)) {
411+
return false;
412+
}
413+
414+
// If everything matches but the text of config file is changed,
415+
// error locations can change for program options, so update the program
416+
if (currentOptions.configFile && newOptions.configFile) {
417+
return currentOptions.configFile.text === newOptions.configFile.text;
418+
}
419+
420+
return true;
421+
422+
function sourceFileUpToDate(sourceFile: SourceFile): boolean {
423+
if (!sourceFile) {
424+
return false;
425+
}
426+
return sourceFile.version === getSourceVersion(sourceFile.path);
427+
}
428+
}
429+
430+
function shouldProgramCreateNewSourceFiles(program: Program, newOptions: CompilerOptions) {
431+
// If any of these options change, we cant reuse old source file even if version match
432+
const oldOptions = program && program.getCompilerOptions();
433+
return oldOptions &&
434+
(oldOptions.target !== newOptions.target ||
435+
oldOptions.module !== newOptions.module ||
436+
oldOptions.moduleResolution !== newOptions.moduleResolution ||
437+
oldOptions.noResolve !== newOptions.noResolve ||
438+
oldOptions.jsx !== newOptions.jsx ||
439+
oldOptions.allowJs !== newOptions.allowJs ||
440+
oldOptions.disableSizeLimit !== newOptions.disableSizeLimit ||
441+
oldOptions.baseUrl !== newOptions.baseUrl ||
442+
!equalOwnProperties(oldOptions.paths, newOptions.paths));
443+
}
444+
445+
/**
446+
* Updates the existing missing file watches with the new set of missing files after new program is created
447+
* @param program
448+
* @param existingMap
449+
* @param createMissingFileWatch
450+
* @param closeExistingFileWatcher
451+
*/
452+
export function updateMissingFilePathsWatch(program: Program, existingMap: Map<FileWatcher>,
453+
createMissingFileWatch: (missingFilePath: Path) => FileWatcher,
454+
closeExistingFileWatcher: (missingFilePath: Path, fileWatcher: FileWatcher) => void) {
455+
456+
const missingFilePaths = program.getMissingFilePaths();
457+
const newMissingFilePathMap = arrayToSet(missingFilePaths);
458+
// Update the missing file paths watcher
459+
return mutateExistingMapWithNewSet(
460+
existingMap, newMissingFilePathMap,
461+
// Watch the missing files
462+
createMissingFileWatch,
463+
// Files that are no longer missing (e.g. because they are no longer required)
464+
// should no longer be watched.
465+
closeExistingFileWatcher
466+
);
467+
}
468+
469+
export type WildCardDirectoryWatchers = { watcher: FileWatcher, recursive: boolean };
470+
471+
export function updateWatchingWildcardDirectories(existingWatchedForWildcards: Map<WildCardDirectoryWatchers>, wildcardDirectories: Map<WatchDirectoryFlags>,
472+
watchDirectory: (directory: string, recursive: boolean) => FileWatcher,
473+
closeDirectoryWatcher: (directory: string, watcher: FileWatcher, recursive: boolean, recursiveChanged: boolean) => void) {
474+
return mutateExistingMap(
475+
existingWatchedForWildcards, wildcardDirectories,
476+
// Create new watch and recursive info
477+
(directory, flag) => {
478+
const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0;
479+
return {
480+
watcher: watchDirectory(directory, recursive),
481+
recursive
482+
};
483+
},
484+
// Close existing watch thats not needed any more
485+
(directory, { watcher, recursive }) => closeDirectoryWatcher(directory, watcher, recursive, /*recursiveChanged*/ false),
486+
// Watcher is same if the recursive flags match
487+
({ recursive: existingRecursive }, flag) => {
488+
// If the recursive dont match, it needs update
489+
const recursive = (flag & WatchDirectoryFlags.Recursive) !== 0;
490+
return existingRecursive !== recursive;
491+
},
492+
// Close existing watch that doesnt match in recursive flag
493+
(directory, { watcher, recursive }) => closeDirectoryWatcher(directory, watcher, recursive, /*recursiveChanged*/ true)
494+
);
495+
}
496+
389497
/**
390498
* Create a new 'Program' instance. A Program is an immutable collection of 'SourceFile's and a 'CompilerOptions'
391499
* that represent a compilation unit.
@@ -478,6 +586,7 @@ namespace ts {
478586
// used to track cases when two file names differ only in casing
479587
const filesByNameIgnoreCase = host.useCaseSensitiveFileNames() ? createMap<SourceFile>() : undefined;
480588

589+
const shouldCreateNewSourceFile = shouldProgramCreateNewSourceFiles(oldProgram, options);
481590
const structuralIsReused = tryReuseStructureFromOldProgram();
482591
if (structuralIsReused !== StructureIsReused.Completely) {
483592
forEach(rootNames, name => processRootFile(name, /*isDefaultLib*/ false));
@@ -519,6 +628,17 @@ namespace ts {
519628
// unconditionally set moduleResolutionCache to undefined to avoid unnecessary leaks
520629
moduleResolutionCache = undefined;
521630

631+
// Release any files we have acquired in the old program but are
632+
// not part of the new program.
633+
if (oldProgram && host.onReleaseOldSourceFile) {
634+
const oldSourceFiles = oldProgram.getSourceFiles();
635+
for (const oldSourceFile of oldSourceFiles) {
636+
if (!getSourceFile(oldSourceFile.path) || shouldCreateNewSourceFile) {
637+
host.onReleaseOldSourceFile(oldSourceFile, oldProgram.getCompilerOptions());
638+
}
639+
}
640+
}
641+
522642
// unconditionally set oldProgram to undefined to prevent it from being captured in closure
523643
oldProgram = undefined;
524644

@@ -783,8 +903,8 @@ namespace ts {
783903

784904
for (const oldSourceFile of oldProgram.getSourceFiles()) {
785905
const newSourceFile = host.getSourceFileByPath
786-
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target)
787-
: host.getSourceFile(oldSourceFile.fileName, options.target);
906+
? host.getSourceFileByPath(oldSourceFile.fileName, oldSourceFile.path, options.target, /*onError*/ undefined, shouldCreateNewSourceFile)
907+
: host.getSourceFile(oldSourceFile.fileName, options.target, /*onError*/ undefined, shouldCreateNewSourceFile);
788908

789909
if (!newSourceFile) {
790910
return oldProgram.structureIsReused = StructureIsReused.Not;
@@ -1593,7 +1713,7 @@ namespace ts {
15931713
else {
15941714
fileProcessingDiagnostics.add(createCompilerDiagnostic(Diagnostics.Cannot_read_file_0_Colon_1, fileName, hostErrorMessage));
15951715
}
1596-
});
1716+
}, shouldCreateNewSourceFile);
15971717

15981718
filesByName.set(path, file);
15991719
if (file) {

0 commit comments

Comments
 (0)