Skip to content
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

Add way to exclude files and directories to watch #39243

Merged
merged 23 commits into from
Nov 4, 2020
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
869abb1
Parse excludeDirectories and excludeFiles
sheetalkamat Jun 18, 2020
b8727a9
Use watch factory in typings installer
sheetalkamat Jun 22, 2020
ac325de
Some refactoring for watchFactory
sheetalkamat Jun 23, 2020
9cd3e93
Create Noop watcher if file or directory being watched is excluded
sheetalkamat Jun 24, 2020
1b752cf
Baselines without using exclude watch options
sheetalkamat Jun 24, 2020
317a35d
Baselines including exclude option
sheetalkamat Jun 24, 2020
dddd906
Handle exclude options in the system watches
sheetalkamat Jun 24, 2020
7668535
Add test without exclude option for recursive directory watching
sheetalkamat Jun 24, 2020
ae2ba8e
Test baselines with exclude option
sheetalkamat Jun 24, 2020
cd676bf
Always set sysLog
sheetalkamat Jun 24, 2020
5fa195c
Test for exclude option in server
sheetalkamat Jun 24, 2020
11cd926
Add exclude options in the config file and fix the test
sheetalkamat Jun 24, 2020
3777897
Fix host configuration for server
sheetalkamat Jun 24, 2020
0a8d84f
Handle host configuration for watch options
sheetalkamat Jun 24, 2020
81d0abe
Fix sysLog time log so baselines can be clean
sheetalkamat Jun 25, 2020
eaa55a7
Merge branch 'master' into watchIgnore
sheetalkamat Sep 2, 2020
496939d
Handle reloadProjects to reload the project from scratch
sheetalkamat Sep 4, 2020
3bfa3d4
Merge branch 'master' into watchIgnore
sheetalkamat Sep 4, 2020
cd18da8
Ensure that file updates are reflected
sheetalkamat Sep 4, 2020
3563fa4
Merge branch 'master' into watchIgnore
sheetalkamat Sep 14, 2020
8a69e5c
Feedback
sheetalkamat Sep 14, 2020
6ca000a
Merge branch 'master' into watchIgnore
sheetalkamat Nov 3, 2020
90bcea5
Feedback
sheetalkamat Nov 4, 2020
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
140 changes: 107 additions & 33 deletions src/compiler/commandLineParser.ts

Large diffs are not rendered by default.

87 changes: 61 additions & 26 deletions src/compiler/sys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ namespace ts {
export interface RecursiveDirectoryWatcherHost {
watchDirectory: HostWatchDirectory;
useCaseSensitiveFileNames: boolean;
getCurrentDirectory: System["getCurrentDirectory"];
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
getAccessibleSortedChildDirectories(path: string): readonly string[];
directoryExists(dir: string): boolean;
realpath(s: string): string;
Expand All @@ -462,7 +463,16 @@ namespace ts {
* (eg on OS that dont support recursive watch using fs.watch use fs.watchFile)
*/
/*@internal*/
export function createDirectoryWatcherSupportingRecursive(host: RecursiveDirectoryWatcherHost): HostWatchDirectory {
export function createDirectoryWatcherSupportingRecursive({
watchDirectory,
useCaseSensitiveFileNames,
getCurrentDirectory,
getAccessibleSortedChildDirectories,
directoryExists,
realpath,
setTimeout,
clearTimeout
}: RecursiveDirectoryWatcherHost): HostWatchDirectory {
interface ChildDirectoryWatcher extends FileWatcher {
dirName: string;
}
Expand All @@ -478,12 +488,12 @@ namespace ts {
const cacheToUpdateChildWatches = new Map<Path, { dirName: string; options: WatchOptions | undefined; fileNames: string[]; }>();
let timerToUpdateChildWatches: any;

const filePathComparer = getStringComparer(!host.useCaseSensitiveFileNames);
const toCanonicalFilePath = createGetCanonicalFileName(host.useCaseSensitiveFileNames);
const filePathComparer = getStringComparer(!useCaseSensitiveFileNames);
const toCanonicalFilePath = createGetCanonicalFileName(useCaseSensitiveFileNames);

return (dirName, callback, recursive, options) => recursive ?
createDirectoryWatcher(dirName, options, callback) :
host.watchDirectory(dirName, callback, recursive, options);
watchDirectory(dirName, callback, recursive, options);

/**
* Create the directory watcher for the dirPath.
Expand All @@ -496,8 +506,8 @@ namespace ts {
}
else {
directoryWatcher = {
watcher: host.watchDirectory(dirName, fileName => {
if (isIgnoredPath(fileName)) return;
watcher: watchDirectory(dirName, fileName => {
if (isIgnoredPath(fileName, options)) return;

if (options?.synchronousWatchDirectory) {
// Call the actual callback
Expand Down Expand Up @@ -578,7 +588,7 @@ namespace ts {
function nonSyncUpdateChildWatches(dirName: string, dirPath: Path, fileName: string, options: WatchOptions | undefined) {
// Iterate through existing children and update the watches if needed
const parentWatcher = cache.get(dirPath);
if (parentWatcher && host.directoryExists(dirName)) {
if (parentWatcher && directoryExists(dirName)) {
// Schedule the update and postpone invoke for callbacks
scheduleUpdateChildWatches(dirName, dirPath, fileName, options);
return;
Expand All @@ -598,10 +608,10 @@ namespace ts {
cacheToUpdateChildWatches.set(dirPath, { dirName, options, fileNames: [fileName] });
}
if (timerToUpdateChildWatches) {
host.clearTimeout(timerToUpdateChildWatches);
clearTimeout(timerToUpdateChildWatches);
timerToUpdateChildWatches = undefined;
}
timerToUpdateChildWatches = host.setTimeout(onTimerToUpdateChildWatches, 1000);
timerToUpdateChildWatches = setTimeout(onTimerToUpdateChildWatches, 1000);
}

function onTimerToUpdateChildWatches() {
Expand All @@ -620,7 +630,7 @@ namespace ts {
invokeCallbacks(dirPath, invokeMap, hasChanges ? undefined : fileNames);
}

sysLog(`sysLog:: invokingWatchers:: ${timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`);
sysLog(`sysLog:: invokingWatchers:: Elapsed:: ${timestamp() - start}ms:: ${cacheToUpdateChildWatches.size}`);
callbackCache.forEach((callbacks, rootDirName) => {
const existing = invokeMap.get(rootDirName);
if (existing) {
Expand All @@ -636,7 +646,7 @@ namespace ts {
});

const elapsed = timestamp() - start;
sysLog(`sysLog:: Elapsed ${elapsed}ms:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size} ${timerToUpdateChildWatches}`);
sysLog(`sysLog:: Elapsed:: ${elapsed}ms:: onTimerToUpdateChildWatches:: ${cacheToUpdateChildWatches.size} ${timerToUpdateChildWatches}`);
}

function removeChildWatches(parentWatcher: HostDirectoryWatcher | undefined) {
Expand All @@ -655,11 +665,11 @@ namespace ts {
if (!parentWatcher) return false;
let newChildWatches: ChildDirectoryWatcher[] | undefined;
const hasChanges = enumerateInsertsAndDeletes<string, ChildDirectoryWatcher>(
host.directoryExists(parentDir) ? mapDefined(host.getAccessibleSortedChildDirectories(parentDir), child => {
directoryExists(parentDir) ? mapDefined(getAccessibleSortedChildDirectories(parentDir), child => {
const childFullName = getNormalizedAbsolutePath(child, parentDir);
// Filter our the symbolic link directories since those arent included in recursive watch
// which is same behaviour when recursive: true is passed to fs.watch
return !isIgnoredPath(childFullName) && filePathComparer(childFullName, normalizePath(host.realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined;
return !isIgnoredPath(childFullName, options) && filePathComparer(childFullName, normalizePath(realpath(childFullName))) === Comparison.EqualTo ? childFullName : undefined;
}) : emptyArray,
parentWatcher.childWatches,
(child, childWatcher) => filePathComparer(child, childWatcher.dirName),
Expand All @@ -686,13 +696,14 @@ namespace ts {
}
}

function isIgnoredPath(path: string) {
return some(ignoredPaths, searchPath => isInPath(path, searchPath));
function isIgnoredPath(path: string, options: WatchOptions | undefined) {
return some(ignoredPaths, searchPath => isInPath(path, searchPath)) ||
isIgnoredByWatchOptions(path, options, useCaseSensitiveFileNames, getCurrentDirectory);
}

function isInPath(path: string, searchPath: string) {
if (stringContains(path, searchPath)) return true;
if (host.useCaseSensitiveFileNames) return false;
if (useCaseSensitiveFileNames) return false;
return stringContains(toCanonicalFilePath(path), searchPath);
}
}
Expand Down Expand Up @@ -729,14 +740,35 @@ namespace ts {
};
}

function createFsWatchCallbackForDirectoryWatcherCallback(directoryName: string, callback: DirectoryWatcherCallback): FsWatchCallback {
function isIgnoredByWatchOptions(
pathToCheck: string,
options: WatchOptions | undefined,
useCaseSensitiveFileNames: boolean,
getCurrentDirectory: System["getCurrentDirectory"],
) {
return (options?.excludeDirectories || options?.excludeFiles) && (
matchesExclude(pathToCheck, options?.excludeFiles, useCaseSensitiveFileNames, getCurrentDirectory()) ||
matchesExclude(pathToCheck, options?.excludeDirectories, useCaseSensitiveFileNames, getCurrentDirectory())
);
}

function createFsWatchCallbackForDirectoryWatcherCallback(
directoryName: string,
callback: DirectoryWatcherCallback,
options: WatchOptions | undefined,
useCaseSensitiveFileNames: boolean,
getCurrentDirectory: System["getCurrentDirectory"],
): FsWatchCallback {
return (eventName, relativeFileName) => {
// In watchDirectory we only care about adding and removing files (when event name is
// "rename"); changes made within files are handled by corresponding fileWatchers (when
// event name is "change")
if (eventName === "rename") {
// When deleting a file, the passed baseFileName is null
callback(!relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName)));
const fileName = !relativeFileName ? directoryName : normalizePath(combinePaths(directoryName, relativeFileName));
if (!relativeFileName || !isIgnoredByWatchOptions(fileName, options, useCaseSensitiveFileNames, getCurrentDirectory)) {
callback(fileName);
}
}
};
}
Expand All @@ -753,6 +785,7 @@ namespace ts {
fsWatch: FsWatch;
fileExists: System["fileExists"];
useCaseSensitiveFileNames: boolean;
getCurrentDirectory: System["getCurrentDirectory"];
fsSupportsRecursiveFsWatch: boolean;
directoryExists: System["directoryExists"];
getAccessibleSortedChildDirectories(path: string): readonly string[];
Expand All @@ -772,6 +805,7 @@ namespace ts {
fsWatch,
fileExists,
useCaseSensitiveFileNames,
getCurrentDirectory,
fsSupportsRecursiveFsWatch,
directoryExists,
getAccessibleSortedChildDirectories,
Expand Down Expand Up @@ -868,7 +902,7 @@ namespace ts {
return fsWatch(
directoryName,
FileSystemEntryKind.Directory,
createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback),
createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback, options, useCaseSensitiveFileNames, getCurrentDirectory),
recursive,
PollingInterval.Medium,
getFallbackOptions(options)
Expand All @@ -878,6 +912,7 @@ namespace ts {
if (!hostRecursiveDirectoryWatcher) {
hostRecursiveDirectoryWatcher = createDirectoryWatcherSupportingRecursive({
useCaseSensitiveFileNames,
getCurrentDirectory,
directoryExists,
getAccessibleSortedChildDirectories,
watchDirectory: nonRecursiveWatchDirectory,
Expand All @@ -891,8 +926,8 @@ namespace ts {

function nonRecursiveWatchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean, options: WatchOptions | undefined): FileWatcher {
Debug.assert(!recursive);
options = updateOptionsForWatchDirectory(options);
const watchDirectoryKind = Debug.checkDefined(options.watchDirectory);
const watchDirectoryOptions = updateOptionsForWatchDirectory(options);
const watchDirectoryKind = Debug.checkDefined(watchDirectoryOptions.watchDirectory);
switch (watchDirectoryKind) {
case WatchDirectoryKind.FixedPollingInterval:
return pollingWatchFile(
Expand All @@ -912,10 +947,10 @@ namespace ts {
return fsWatch(
directoryName,
FileSystemEntryKind.Directory,
createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback),
createFsWatchCallbackForDirectoryWatcherCallback(directoryName, callback, options, useCaseSensitiveFileNames, getCurrentDirectory),
recursive,
PollingInterval.Medium,
getFallbackOptions(options)
getFallbackOptions(watchDirectoryOptions)
);
default:
Debug.assertNever(watchDirectoryKind);
Expand Down Expand Up @@ -1161,13 +1196,15 @@ namespace ts {
const platform: string = _os.platform();
const useCaseSensitiveFileNames = isFileSystemCaseSensitive();
const fsSupportsRecursiveFsWatch = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin");
const getCurrentDirectory = memoize(() => process.cwd());
sheetalkamat marked this conversation as resolved.
Show resolved Hide resolved
const { watchFile, watchDirectory } = createSystemWatchFunctions({
pollingWatchFile: createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames),
getModifiedTime,
setTimeout,
clearTimeout,
fsWatch,
useCaseSensitiveFileNames,
getCurrentDirectory,
fileExists,
// Node 4.0 `fs.watch` function supports the "recursive" option on both OSX and Windows
// (ref: https://github.com/nodejs/node/pull/2649 and https://github.com/Microsoft/TypeScript/issues/4643)
Expand Down Expand Up @@ -1214,9 +1251,7 @@ namespace ts {
getExecutingFilePath() {
return __filename;
},
getCurrentDirectory() {
return process.cwd();
},
getCurrentDirectory,
getDirectories,
getEnvironmentVariable(name: string) {
return process.env[name] || "";
Expand Down
18 changes: 4 additions & 14 deletions src/compiler/tsbuildPublic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ namespace ts {
originalGetSourceFile: CompilerHost["getSourceFile"];
}

interface SolutionBuilderState<T extends BuilderProgram = BuilderProgram> {
interface SolutionBuilderState<T extends BuilderProgram = BuilderProgram> extends WatchFactory<WatchType, ResolvedConfigFileName> {
readonly host: SolutionBuilderHost<T>;
readonly hostWithWatch: SolutionBuilderWithWatchHost<T>;
readonly currentDirectory: string;
Expand Down Expand Up @@ -256,9 +256,6 @@ namespace ts {

timerToBuildInvalidatedProject: any;
reportFileChangeDetected: boolean;
watchFile: WatchFile<WatchType, ResolvedConfigFileName>;
watchFilePath: WatchFilePath<WatchType, ResolvedConfigFileName>;
watchDirectory: WatchDirectory<WatchType, ResolvedConfigFileName>;
writeLog: (s: string) => void;
}

Expand All @@ -282,7 +279,7 @@ namespace ts {
loadWithLocalCache<ResolvedModuleFull>(Debug.checkEachDefined(moduleNames), containingFile, redirectedReference, loader);
}

const { watchFile, watchFilePath, watchDirectory, writeLog } = createWatchFactory<ResolvedConfigFileName>(hostWithWatch, options);
const { watchFile, watchDirectory, writeLog } = createWatchFactory<ResolvedConfigFileName>(hostWithWatch, options);

const state: SolutionBuilderState<T> = {
host,
Expand Down Expand Up @@ -331,7 +328,6 @@ namespace ts {
timerToBuildInvalidatedProject: undefined,
reportFileChangeDetected: false,
watchFile,
watchFilePath,
watchDirectory,
writeLog,
};
Expand Down Expand Up @@ -1783,7 +1779,6 @@ namespace ts {
function watchConfigFile(state: SolutionBuilderState, resolved: ResolvedConfigFileName, resolvedPath: ResolvedConfigFilePath, parsed: ParsedCommandLine | undefined) {
if (!state.watch || state.allWatchedConfigFiles.has(resolvedPath)) return;
state.allWatchedConfigFiles.set(resolvedPath, state.watchFile(
state.hostWithWatch,
resolved,
() => {
invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.Full);
Expand All @@ -1801,7 +1796,6 @@ namespace ts {
getOrCreateValueMapFromConfigFileMap(state.allWatchedWildcardDirectories, resolvedPath),
new Map(getEntries(parsed.configFileSpecs!.wildcardDirectories)),
(dir, flags) => state.watchDirectory(
state.hostWithWatch,
dir,
fileOrDirectory => {
if (isIgnoredFileFromWildCardWatching({
Expand Down Expand Up @@ -1833,13 +1827,11 @@ namespace ts {
getOrCreateValueMapFromConfigFileMap(state.allWatchedInputFiles, resolvedPath),
arrayToMap(parsed.fileNames, fileName => toPath(state, fileName)),
{
createNewValue: (path, input) => state.watchFilePath(
state.hostWithWatch,
createNewValue: (_path, input) => state.watchFile(
input,
() => invalidateProjectAndScheduleBuilds(state, resolvedPath, ConfigFileProgramReloadLevel.None),
PollingInterval.Low,
parsed?.watchOptions,
path as Path,
WatchType.SourceFile,
resolved
),
Expand Down Expand Up @@ -1914,9 +1906,7 @@ namespace ts {
}

function reportWatchStatus(state: SolutionBuilderState, message: DiagnosticMessage, ...args: (string | number | undefined)[]) {
if (state.hostWithWatch.onWatchStatusChange) {
state.hostWithWatch.onWatchStatusChange(createCompilerDiagnostic(message, ...args), state.host.getNewLine(), state.baseCompilerOptions);
}
state.hostWithWatch.onWatchStatusChange?.(createCompilerDiagnostic(message, ...args), state.host.getNewLine(), state.baseCompilerOptions);
}

function reportErrors({ host }: SolutionBuilderState, errors: readonly Diagnostic[]) {
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5846,6 +5846,8 @@ namespace ts {
watchDirectory?: WatchDirectoryKind;
fallbackPolling?: PollingWatchKind;
synchronousWatchDirectory?: boolean;
excludeDirectories?: string[];
excludeFiles?: string[];

[option: string]: CompilerOptionsValue | undefined;
}
Expand Down Expand Up @@ -6016,6 +6018,7 @@ namespace ts {
affectsSemanticDiagnostics?: true; // true if option affects semantic diagnostics
affectsEmit?: true; // true if the options affects emit
transpileOptionValue?: boolean | undefined; // If set this means that the option should be set to this value when transpiling
extraValidation?: (value: CompilerOptionsValue) => [DiagnosticMessage, ...string[]] | undefined; // Additional validation to be performed for the value to be valid
}

/* @internal */
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -270,10 +270,10 @@ namespace ts {
writeLog: (s: string) => void;
}

export function createWatchFactory<Y = undefined>(host: { trace?(s: string): void; }, options: { extendedDiagnostics?: boolean; diagnostics?: boolean; }) {
export function createWatchFactory<Y = undefined>(host: WatchFactoryHost & { trace?(s: string): void; }, options: { extendedDiagnostics?: boolean; diagnostics?: boolean; }) {
const watchLogLevel = host.trace ? options.extendedDiagnostics ? WatchLogLevel.Verbose : options.diagnostics ? WatchLogLevel.TriggerOnly : WatchLogLevel.None : WatchLogLevel.None;
const writeLog: (s: string) => void = watchLogLevel !== WatchLogLevel.None ? (s => host.trace!(s)) : noop;
const result = getWatchFactory<WatchType, Y>(watchLogLevel, writeLog) as WatchFactory<WatchType, Y>;
const result = getWatchFactory<WatchType, Y>(host, watchLogLevel, writeLog) as WatchFactory<WatchType, Y>;
result.writeLog = writeLog;
return result;
}
Expand Down
Loading