mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Make useFsEvents as default strategy for the watching (#50366)
* Remove unnecessary parameter * Dont store name unnecessarily in the watchers * Polled watches and not files * Use fs events as default watching * Some refactoring * Make single per directory native watchers now that we are using it as default * Rename * Comment
This commit is contained in:
+86
-74
@@ -35,8 +35,7 @@ namespace ts {
|
||||
|
||||
export type FileWatcherCallback = (fileName: string, eventKind: FileWatcherEventKind, modifiedTime?: Date) => void;
|
||||
export type DirectoryWatcherCallback = (fileName: string) => void;
|
||||
/*@internal*/
|
||||
export interface WatchedFile {
|
||||
interface WatchedFile {
|
||||
readonly fileName: string;
|
||||
readonly callback: FileWatcherCallback;
|
||||
mtime: Date;
|
||||
@@ -81,8 +80,7 @@ namespace ts {
|
||||
/* @internal */
|
||||
export let unchangedPollThresholds = createPollingIntervalBasedLevels(defaultChunkLevels);
|
||||
|
||||
/* @internal */
|
||||
export function setCustomPollingValues(system: System) {
|
||||
function setCustomPollingValues(system: System) {
|
||||
if (!system.getEnvironmentVariable) {
|
||||
return;
|
||||
}
|
||||
@@ -189,31 +187,28 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function createDynamicPriorityPollingWatchFile(host: {
|
||||
interface WatchedFileWithUnchangedPolls extends WatchedFileWithIsClosed {
|
||||
unchangedPolls: number;
|
||||
}
|
||||
function createDynamicPriorityPollingWatchFile(host: {
|
||||
getModifiedTime: NonNullable<System["getModifiedTime"]>;
|
||||
setTimeout: NonNullable<System["setTimeout"]>;
|
||||
}): HostWatchFile {
|
||||
interface WatchedFile extends ts.WatchedFile {
|
||||
isClosed?: boolean;
|
||||
unchangedPolls: number;
|
||||
}
|
||||
|
||||
interface PollingIntervalQueue extends Array<WatchedFile> {
|
||||
interface PollingIntervalQueue extends Array<WatchedFileWithUnchangedPolls> {
|
||||
pollingInterval: PollingInterval;
|
||||
pollIndex: number;
|
||||
pollScheduled: boolean;
|
||||
}
|
||||
|
||||
const watchedFiles: WatchedFile[] = [];
|
||||
const changedFilesInLastPoll: WatchedFile[] = [];
|
||||
const watchedFiles: WatchedFileWithUnchangedPolls[] = [];
|
||||
const changedFilesInLastPoll: WatchedFileWithUnchangedPolls[] = [];
|
||||
const lowPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Low);
|
||||
const mediumPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.Medium);
|
||||
const highPollingIntervalQueue = createPollingIntervalQueue(PollingInterval.High);
|
||||
return watchFile;
|
||||
|
||||
function watchFile(fileName: string, callback: FileWatcherCallback, defaultPollingInterval: PollingInterval): FileWatcher {
|
||||
const file: WatchedFile = {
|
||||
const file: WatchedFileWithUnchangedPolls = {
|
||||
fileName,
|
||||
callback,
|
||||
unchangedPolls: 0,
|
||||
@@ -233,7 +228,7 @@ namespace ts {
|
||||
}
|
||||
|
||||
function createPollingIntervalQueue(pollingInterval: PollingInterval): PollingIntervalQueue {
|
||||
const queue = [] as WatchedFile[] as PollingIntervalQueue;
|
||||
const queue = [] as WatchedFileWithUnchangedPolls[] as PollingIntervalQueue;
|
||||
queue.pollingInterval = pollingInterval;
|
||||
queue.pollIndex = 0;
|
||||
queue.pollScheduled = false;
|
||||
@@ -265,7 +260,7 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
function pollQueue(queue: (WatchedFile | undefined)[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) {
|
||||
function pollQueue(queue: (WatchedFileWithUnchangedPolls | undefined)[], pollingInterval: PollingInterval, pollIndex: number, chunkSize: number) {
|
||||
return pollWatchedFileQueue(
|
||||
host,
|
||||
queue,
|
||||
@@ -274,7 +269,7 @@ namespace ts {
|
||||
onWatchFileStat
|
||||
);
|
||||
|
||||
function onWatchFileStat(watchedFile: WatchedFile, pollIndex: number, fileChanged: boolean) {
|
||||
function onWatchFileStat(watchedFile: WatchedFileWithUnchangedPolls, pollIndex: number, fileChanged: boolean) {
|
||||
if (fileChanged) {
|
||||
watchedFile.unchangedPolls = 0;
|
||||
// Changed files go to changedFilesInLastPoll queue
|
||||
@@ -311,12 +306,12 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
function addToPollingIntervalQueue(file: WatchedFile, pollingInterval: PollingInterval) {
|
||||
function addToPollingIntervalQueue(file: WatchedFileWithUnchangedPolls, pollingInterval: PollingInterval) {
|
||||
pollingIntervalQueue(pollingInterval).push(file);
|
||||
scheduleNextPollIfNotAlreadyScheduled(pollingInterval);
|
||||
}
|
||||
|
||||
function addChangedFileToLowPollingIntervalQueue(file: WatchedFile) {
|
||||
function addChangedFileToLowPollingIntervalQueue(file: WatchedFileWithUnchangedPolls) {
|
||||
changedFilesInLastPoll.push(file);
|
||||
scheduleNextPollIfNotAlreadyScheduled(PollingInterval.Low);
|
||||
}
|
||||
@@ -423,59 +418,50 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
export function createSingleFileWatcherPerName(
|
||||
watchFile: HostWatchFile,
|
||||
useCaseSensitiveFileNames: boolean
|
||||
): HostWatchFile {
|
||||
interface SingleFileWatcher {
|
||||
watcher: FileWatcher;
|
||||
refCount: number;
|
||||
}
|
||||
const cache = new Map<string, SingleFileWatcher>();
|
||||
const callbacksCache = createMultiMap<FileWatcherCallback>();
|
||||
interface SingleFileWatcher<T extends FileWatcherCallback | FsWatchCallback>{
|
||||
watcher: FileWatcher;
|
||||
callbacks: T[];
|
||||
}
|
||||
function createSingleWatcherPerName<T extends FileWatcherCallback | FsWatchCallback>(
|
||||
cache: Map<SingleFileWatcher<T>>,
|
||||
useCaseSensitiveFileNames: boolean,
|
||||
name: string,
|
||||
callback: T,
|
||||
createWatcher: (callback: T) => FileWatcher,
|
||||
): FileWatcher {
|
||||
const toCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
|
||||
const path = toCanonicalFileName(name);
|
||||
const existing = cache.get(path);
|
||||
if (existing) {
|
||||
existing.callbacks.push(callback);
|
||||
}
|
||||
else {
|
||||
cache.set(path, {
|
||||
watcher: createWatcher((
|
||||
// Cant infer types correctly so lets satisfy checker
|
||||
(param1: any, param2: never, param3: any) => cache.get(path)?.callbacks.slice().forEach(cb => cb(param1, param2, param3))
|
||||
) as T),
|
||||
callbacks: [callback]
|
||||
});
|
||||
}
|
||||
|
||||
return (fileName, callback, pollingInterval, options) => {
|
||||
const path = toCanonicalFileName(fileName);
|
||||
const existing = cache.get(path);
|
||||
if (existing) {
|
||||
existing.refCount++;
|
||||
return {
|
||||
close: () => {
|
||||
const watcher = cache.get(path);
|
||||
// Watcher is not expected to be undefined, but if it is normally its because
|
||||
// exception was thrown somewhere else and watch state is not what it should be
|
||||
if (!watcher) return;
|
||||
if (!orderedRemoveItem(watcher.callbacks, callback) || watcher.callbacks.length) return;
|
||||
cache.delete(path);
|
||||
closeFileWatcherOf(watcher);
|
||||
}
|
||||
else {
|
||||
cache.set(path, {
|
||||
watcher: watchFile(
|
||||
fileName,
|
||||
(fileName, eventKind, modifiedTime) => forEach(
|
||||
callbacksCache.get(path),
|
||||
cb => cb(fileName, eventKind, modifiedTime)
|
||||
),
|
||||
pollingInterval,
|
||||
options
|
||||
),
|
||||
refCount: 1
|
||||
});
|
||||
}
|
||||
callbacksCache.add(path, callback);
|
||||
|
||||
return {
|
||||
close: () => {
|
||||
const watcher = Debug.checkDefined(cache.get(path));
|
||||
callbacksCache.remove(path, callback);
|
||||
watcher.refCount--;
|
||||
if (watcher.refCount) return;
|
||||
cache.delete(path);
|
||||
closeFileWatcherOf(watcher);
|
||||
}
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if file status changed
|
||||
*/
|
||||
/*@internal*/
|
||||
export function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean {
|
||||
function onWatchedFileStat(watchedFile: WatchedFile, modifiedTime: Date): boolean {
|
||||
const oldTime = watchedFile.mtime.getTime();
|
||||
const newTime = modifiedTime.getTime();
|
||||
if (oldTime !== newTime) {
|
||||
@@ -512,8 +498,7 @@ namespace ts {
|
||||
curSysLog = logger;
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
export interface RecursiveDirectoryWatcherHost {
|
||||
interface RecursiveDirectoryWatcherHost {
|
||||
watchDirectory: HostWatchDirectory;
|
||||
useCaseSensitiveFileNames: boolean;
|
||||
getCurrentDirectory: System["getCurrentDirectory"];
|
||||
@@ -529,8 +514,7 @@ namespace ts {
|
||||
* that means if this is recursive watcher, watch the children directories as well
|
||||
* (eg on OS that dont support recursive watch using fs.watch use fs.watchFile)
|
||||
*/
|
||||
/*@internal*/
|
||||
export function createDirectoryWatcherSupportingRecursive({
|
||||
function createDirectoryWatcherSupportingRecursive({
|
||||
watchDirectory,
|
||||
useCaseSensitiveFileNames,
|
||||
getCurrentDirectory,
|
||||
@@ -792,8 +776,7 @@ namespace ts {
|
||||
Directory,
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
export function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback {
|
||||
function createFileWatcherCallback(callback: FsWatchCallback): FileWatcherCallback {
|
||||
return (_fileName, eventKind, modifiedTime) => callback(eventKind === FileWatcherEventKind.Changed ? "change" : "rename", "", modifiedTime);
|
||||
}
|
||||
|
||||
@@ -854,7 +837,7 @@ namespace ts {
|
||||
/*@internal*/
|
||||
export interface CreateSystemWatchFunctions {
|
||||
// Polling watch file
|
||||
pollingWatchFile: HostWatchFile;
|
||||
pollingWatchFileWorker: HostWatchFile;
|
||||
// For dynamic polling watch file
|
||||
getModifiedTime: NonNullable<System["getModifiedTime"]>;
|
||||
setTimeout: NonNullable<System["setTimeout"]>;
|
||||
@@ -878,7 +861,7 @@ namespace ts {
|
||||
|
||||
/*@internal*/
|
||||
export function createSystemWatchFunctions({
|
||||
pollingWatchFile,
|
||||
pollingWatchFileWorker,
|
||||
getModifiedTime,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
@@ -896,6 +879,9 @@ namespace ts {
|
||||
inodeWatching,
|
||||
sysLog,
|
||||
}: CreateSystemWatchFunctions): { watchFile: HostWatchFile; watchDirectory: HostWatchDirectory; } {
|
||||
const pollingWatches = new Map<string, SingleFileWatcher<FileWatcherCallback>>();
|
||||
const fsWatches = new Map<string, SingleFileWatcher<FsWatchCallback>>();
|
||||
const fsWatchesRecursive = new Map<string, SingleFileWatcher<FsWatchCallback>>();
|
||||
let dynamicPollingWatchFile: HostWatchFile | undefined;
|
||||
let fixedChunkSizePollingWatchFile: HostWatchFile | undefined;
|
||||
let nonPollingWatchFile: HostWatchFile | undefined;
|
||||
@@ -968,7 +954,7 @@ namespace ts {
|
||||
// Use notifications from FS to watch with falling back to fs.watchFile
|
||||
generateWatchFileOptions(WatchFileKind.UseFsEventsOnParentDirectory, PollingWatchKind.PriorityInterval, options) :
|
||||
// Default to do not use fixed polling interval
|
||||
{ watchFile: defaultWatchFileKind?.() || WatchFileKind.FixedPollingInterval };
|
||||
{ watchFile: defaultWatchFileKind?.() || WatchFileKind.UseFsEvents };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1073,6 +1059,15 @@ namespace ts {
|
||||
}
|
||||
}
|
||||
|
||||
function pollingWatchFile(fileName: string, callback: FileWatcherCallback, pollingInterval: PollingInterval, options: WatchOptions | undefined) {
|
||||
return createSingleWatcherPerName(
|
||||
pollingWatches,
|
||||
useCaseSensitiveFileNames,
|
||||
fileName,
|
||||
callback,
|
||||
cb => pollingWatchFileWorker(fileName, cb, pollingInterval, options),
|
||||
);
|
||||
}
|
||||
function fsWatch(
|
||||
fileOrDirectory: string,
|
||||
entryKind: FileSystemEntryKind,
|
||||
@@ -1080,6 +1075,23 @@ namespace ts {
|
||||
recursive: boolean,
|
||||
fallbackPollingInterval: PollingInterval,
|
||||
fallbackOptions: WatchOptions | undefined
|
||||
): FileWatcher {
|
||||
return createSingleWatcherPerName(
|
||||
recursive ? fsWatchesRecursive : fsWatches,
|
||||
useCaseSensitiveFileNames,
|
||||
fileOrDirectory,
|
||||
callback,
|
||||
cb => fsWatchHandlingExistenceOnHost(fileOrDirectory, entryKind, cb, recursive, fallbackPollingInterval, fallbackOptions),
|
||||
);
|
||||
}
|
||||
|
||||
function fsWatchHandlingExistenceOnHost(
|
||||
fileOrDirectory: string,
|
||||
entryKind: FileSystemEntryKind,
|
||||
callback: FsWatchCallback,
|
||||
recursive: boolean,
|
||||
fallbackPollingInterval: PollingInterval,
|
||||
fallbackOptions: WatchOptions | undefined
|
||||
): FileWatcher {
|
||||
let lastDirectoryPartWithDirectorySeparator: string | undefined;
|
||||
let lastDirectoryPart: string | undefined;
|
||||
@@ -1445,7 +1457,7 @@ namespace ts {
|
||||
const fsSupportsRecursiveFsWatch = isNode4OrLater && (process.platform === "win32" || process.platform === "darwin");
|
||||
const getCurrentDirectory = memoize(() => process.cwd());
|
||||
const { watchFile, watchDirectory } = createSystemWatchFunctions({
|
||||
pollingWatchFile: createSingleFileWatcherPerName(fsWatchFileWorker, useCaseSensitiveFileNames),
|
||||
pollingWatchFileWorker: fsWatchFileWorker,
|
||||
getModifiedTime,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
|
||||
@@ -283,13 +283,11 @@ interface Array<T> { length: number; [n: number]: T; }`
|
||||
|
||||
export interface TestFileWatcher {
|
||||
cb: FileWatcherCallback;
|
||||
fileName: string;
|
||||
pollingInterval: PollingInterval;
|
||||
}
|
||||
|
||||
export interface TestFsWatcher {
|
||||
cb: FsWatchCallback;
|
||||
directoryName: string;
|
||||
inode: number | undefined;
|
||||
}
|
||||
|
||||
@@ -310,7 +308,6 @@ interface Array<T> { length: number; [n: number]: T; }`
|
||||
|
||||
export enum Tsc_WatchFile {
|
||||
DynamicPolling = "DynamicPriorityPolling",
|
||||
SingleFileWatcherPerName = "SingleFileWatcherPerName"
|
||||
}
|
||||
|
||||
export enum Tsc_WatchDirectory {
|
||||
@@ -388,12 +385,7 @@ interface Array<T> { length: number; [n: number]: T; }`
|
||||
// We dont have polling watch file
|
||||
// it is essentially fsWatch but lets get that separate from fsWatch and
|
||||
// into watchedFiles for easier testing
|
||||
pollingWatchFile: tscWatchFile === Tsc_WatchFile.SingleFileWatcherPerName ?
|
||||
createSingleFileWatcherPerName(
|
||||
this.watchFileWorker.bind(this),
|
||||
this.useCaseSensitiveFileNames
|
||||
) :
|
||||
this.watchFileWorker.bind(this),
|
||||
pollingWatchFileWorker: this.watchFileWorker.bind(this),
|
||||
getModifiedTime: this.getModifiedTime.bind(this),
|
||||
setTimeout: this.setTimeout.bind(this),
|
||||
clearTimeout: this.clearTimeout.bind(this),
|
||||
@@ -498,7 +490,7 @@ interface Array<T> { length: number; [n: number]: T; }`
|
||||
this.fs.get(getDirectoryPath(currentEntry.path))!.modifiedTime = this.now();
|
||||
if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) {
|
||||
const directoryFullPath = getDirectoryPath(currentEntry.fullPath);
|
||||
this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime, /*useFileNameInCallback*/ true);
|
||||
this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime);
|
||||
this.invokeFsWatchesCallbacks(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
|
||||
this.invokeRecursiveFsWatches(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
|
||||
}
|
||||
@@ -679,7 +671,7 @@ interface Array<T> { length: number; [n: number]: T; }`
|
||||
return createWatcher(
|
||||
this.watchedFiles,
|
||||
this.toFullPath(fileName),
|
||||
{ fileName, cb, pollingInterval }
|
||||
{ cb, pollingInterval }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -696,7 +688,6 @@ interface Array<T> { length: number; [n: number]: T; }`
|
||||
recursive ? this.fsWatchesRecursive : this.fsWatches,
|
||||
path,
|
||||
{
|
||||
directoryName: fileOrDirectory,
|
||||
cb,
|
||||
inode: this.inodes?.get(path)
|
||||
}
|
||||
@@ -705,8 +696,8 @@ interface Array<T> { length: number; [n: number]: T; }`
|
||||
return result;
|
||||
}
|
||||
|
||||
invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind, modifiedTime?: Date, useFileNameInCallback?: boolean) {
|
||||
invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(fileFullPath)), ({ cb, fileName }) => cb(useFileNameInCallback ? fileName : fileFullPath, eventKind, modifiedTime));
|
||||
invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind, modifiedTime: Date | undefined) {
|
||||
invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(fileFullPath)), ({ cb }) => cb(fileFullPath, eventKind, modifiedTime));
|
||||
}
|
||||
|
||||
private fsWatchCallback(map: MultiMap<Path, TestFsWatcher>, fullPath: string, eventName: "rename" | "change", modifiedTime: Date | undefined, entryFullPath: string | undefined, useTildeSuffix: boolean | undefined) {
|
||||
@@ -1050,11 +1041,11 @@ interface Array<T> { length: number; [n: number]: T; }`
|
||||
}
|
||||
|
||||
serializeWatches(baseline: string[] = []) {
|
||||
serializeMultiMap(baseline, "WatchedFiles", this.watchedFiles, ({ fileName, pollingInterval }) => ({ fileName, pollingInterval }));
|
||||
serializeMultiMap(baseline, "PolledWatches", this.watchedFiles);
|
||||
baseline.push("");
|
||||
serializeMultiMap(baseline, "FsWatches", this.fsWatches, serializeTestFsWatcher);
|
||||
serializeMultiMap(baseline, "FsWatches", this.fsWatches);
|
||||
baseline.push("");
|
||||
serializeMultiMap(baseline, "FsWatchesRecursive", this.fsWatchesRecursive, serializeTestFsWatcher);
|
||||
serializeMultiMap(baseline, "FsWatchesRecursive", this.fsWatchesRecursive);
|
||||
baseline.push("");
|
||||
return baseline;
|
||||
}
|
||||
@@ -1158,19 +1149,12 @@ interface Array<T> { length: number; [n: number]: T; }`
|
||||
}
|
||||
}
|
||||
|
||||
function serializeTestFsWatcher({ directoryName, inode }: TestFsWatcher) {
|
||||
return {
|
||||
directoryName,
|
||||
inode,
|
||||
};
|
||||
}
|
||||
|
||||
function serializeMultiMap<T, U>(baseline: string[], caption: string, multiMap: MultiMap<string, T>, valueMapper: (value: T) => U) {
|
||||
function serializeMultiMap<T>(baseline: string[], caption: string, multiMap: MultiMap<string, T>) {
|
||||
baseline.push(`${caption}::`);
|
||||
multiMap.forEach((values, key) => {
|
||||
baseline.push(`${key}:`);
|
||||
for (const value of values) {
|
||||
baseline.push(` ${JSON.stringify(valueMapper(value))}`);
|
||||
baseline.push(` ${JSON.stringify(value)}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,124 +1,111 @@
|
||||
namespace ts.tscWatch {
|
||||
describe("unittests:: tsbuildWatch:: watchEnvironment:: tsbuild:: watchMode:: with different watch environments", () => {
|
||||
describe("when watchFile can create multiple watchers per file", () => {
|
||||
verifyWatchFileOnMultipleProjects(/*singleWatchPerFile*/ false);
|
||||
});
|
||||
it("watchFile on same file multiple times because file is part of multiple projects", () => {
|
||||
const project = `${TestFSWithWatch.tsbuildProjectsLocation}/myproject`;
|
||||
let maxPkgs = 4;
|
||||
const configPath = `${project}/tsconfig.json`;
|
||||
const typing: File = {
|
||||
path: `${project}/typings/xterm.d.ts`,
|
||||
content: "export const typing = 10;"
|
||||
};
|
||||
|
||||
describe("when watchFile is single watcher per file", () => {
|
||||
verifyWatchFileOnMultipleProjects(
|
||||
/*singleWatchPerFile*/ true,
|
||||
arrayToMap(["TSC_WATCHFILE"], identity, () => TestFSWithWatch.Tsc_WatchFile.SingleFileWatcherPerName)
|
||||
);
|
||||
});
|
||||
|
||||
function verifyWatchFileOnMultipleProjects(singleWatchPerFile: boolean, environmentVariables?: ESMap<string, string>) {
|
||||
it("watchFile on same file multiple times because file is part of multiple projects", () => {
|
||||
const project = `${TestFSWithWatch.tsbuildProjectsLocation}/myproject`;
|
||||
let maxPkgs = 4;
|
||||
const configPath = `${project}/tsconfig.json`;
|
||||
const typing: File = {
|
||||
path: `${project}/typings/xterm.d.ts`,
|
||||
content: "export const typing = 10;"
|
||||
};
|
||||
|
||||
const allPkgFiles = pkgs(pkgFiles);
|
||||
const system = createWatchedSystem([libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project, environmentVariables });
|
||||
writePkgReferences(system);
|
||||
const { sys, baseline, oldSnap, cb, getPrograms } = createBaseline(system);
|
||||
const host = createSolutionBuilderWithWatchHostForBaseline(sys, cb);
|
||||
const solutionBuilder = createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true });
|
||||
solutionBuilder.build();
|
||||
runWatchBaseline({
|
||||
scenario: "watchEnvironment",
|
||||
subScenario: `same file in multiple projects${singleWatchPerFile ? " with single watcher per file" : ""}`,
|
||||
commandLineArgs: ["--b", "--w"],
|
||||
sys,
|
||||
baseline,
|
||||
oldSnap,
|
||||
getPrograms,
|
||||
changes: [
|
||||
{
|
||||
caption: "modify typing file",
|
||||
change: sys => sys.writeFile(typing.path, `${typing.content}export const typing1 = 10;`),
|
||||
timeouts: sys => {
|
||||
sys.checkTimeoutQueueLengthAndRun(1);
|
||||
checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout(sys);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Make change
|
||||
caption: "change pkg references",
|
||||
change: sys => {
|
||||
maxPkgs--;
|
||||
writePkgReferences(sys);
|
||||
},
|
||||
timeouts: checkSingleTimeoutQueueLengthAndRun,
|
||||
},
|
||||
{
|
||||
caption: "modify typing file",
|
||||
change: sys => sys.writeFile(typing.path, typing.content),
|
||||
timeouts: sys => {
|
||||
sys.checkTimeoutQueueLengthAndRun(1);
|
||||
checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout(sys);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Make change to remove all watches
|
||||
caption: "change pkg references to remove all watches",
|
||||
change: sys => {
|
||||
maxPkgs = 0;
|
||||
writePkgReferences(sys);
|
||||
},
|
||||
timeouts: checkSingleTimeoutQueueLengthAndRun,
|
||||
},
|
||||
{
|
||||
caption: "modify typing file",
|
||||
change: sys => sys.writeFile(typing.path, `${typing.content}export const typing1 = 10;`),
|
||||
timeouts: sys => sys.checkTimeoutQueueLength(0),
|
||||
},
|
||||
],
|
||||
watchOrSolution: solutionBuilder
|
||||
});
|
||||
|
||||
function flatArray<T>(arr: T[][]): readonly T[] {
|
||||
return flatMap(arr, identity);
|
||||
}
|
||||
function pkgs<T>(cb: (index: number) => T): T[] {
|
||||
const result: T[] = [];
|
||||
for (let index = 0; index < maxPkgs; index++) {
|
||||
result.push(cb(index));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function createPkgReference(index: number) {
|
||||
return { path: `./pkg${index}` };
|
||||
}
|
||||
function pkgFiles(index: number): File[] {
|
||||
return [
|
||||
{
|
||||
path: `${project}/pkg${index}/index.ts`,
|
||||
content: `export const pkg${index} = ${index};`
|
||||
},
|
||||
{
|
||||
path: `${project}/pkg${index}/tsconfig.json`,
|
||||
content: JSON.stringify({
|
||||
complerOptions: { composite: true },
|
||||
include: [
|
||||
"**/*.ts",
|
||||
"../typings/xterm.d.ts"
|
||||
]
|
||||
})
|
||||
const allPkgFiles = pkgs(pkgFiles);
|
||||
const system = createWatchedSystem([libFile, typing, ...flatArray(allPkgFiles)], { currentDirectory: project });
|
||||
writePkgReferences(system);
|
||||
const { sys, baseline, oldSnap, cb, getPrograms } = createBaseline(system);
|
||||
const host = createSolutionBuilderWithWatchHostForBaseline(sys, cb);
|
||||
const solutionBuilder = createSolutionBuilderWithWatch(host, ["tsconfig.json"], { watch: true, verbose: true });
|
||||
solutionBuilder.build();
|
||||
runWatchBaseline({
|
||||
scenario: "watchEnvironment",
|
||||
subScenario: `same file in multiple projects with single watcher per file`,
|
||||
commandLineArgs: ["--b", "--w"],
|
||||
sys,
|
||||
baseline,
|
||||
oldSnap,
|
||||
getPrograms,
|
||||
changes: [
|
||||
{
|
||||
caption: "modify typing file",
|
||||
change: sys => sys.writeFile(typing.path, `${typing.content}export const typing1 = 10;`),
|
||||
timeouts: sys => {
|
||||
sys.checkTimeoutQueueLengthAndRun(1);
|
||||
checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout(sys);
|
||||
}
|
||||
];
|
||||
}
|
||||
function writePkgReferences(system: TestFSWithWatch.TestServerHost) {
|
||||
system.writeFile(configPath, JSON.stringify({
|
||||
files: [],
|
||||
include: [],
|
||||
references: pkgs(createPkgReference)
|
||||
}));
|
||||
}
|
||||
},
|
||||
{
|
||||
// Make change
|
||||
caption: "change pkg references",
|
||||
change: sys => {
|
||||
maxPkgs--;
|
||||
writePkgReferences(sys);
|
||||
},
|
||||
timeouts: checkSingleTimeoutQueueLengthAndRun,
|
||||
},
|
||||
{
|
||||
caption: "modify typing file",
|
||||
change: sys => sys.writeFile(typing.path, typing.content),
|
||||
timeouts: sys => {
|
||||
sys.checkTimeoutQueueLengthAndRun(1);
|
||||
checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout(sys);
|
||||
}
|
||||
},
|
||||
{
|
||||
// Make change to remove all watches
|
||||
caption: "change pkg references to remove all watches",
|
||||
change: sys => {
|
||||
maxPkgs = 0;
|
||||
writePkgReferences(sys);
|
||||
},
|
||||
timeouts: checkSingleTimeoutQueueLengthAndRun,
|
||||
},
|
||||
{
|
||||
caption: "modify typing file",
|
||||
change: sys => sys.writeFile(typing.path, `${typing.content}export const typing1 = 10;`),
|
||||
timeouts: sys => sys.checkTimeoutQueueLength(0),
|
||||
},
|
||||
],
|
||||
watchOrSolution: solutionBuilder
|
||||
});
|
||||
}
|
||||
|
||||
function flatArray<T>(arr: T[][]): readonly T[] {
|
||||
return flatMap(arr, identity);
|
||||
}
|
||||
function pkgs<T>(cb: (index: number) => T): T[] {
|
||||
const result: T[] = [];
|
||||
for (let index = 0; index < maxPkgs; index++) {
|
||||
result.push(cb(index));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
function createPkgReference(index: number) {
|
||||
return { path: `./pkg${index}` };
|
||||
}
|
||||
function pkgFiles(index: number): File[] {
|
||||
return [
|
||||
{
|
||||
path: `${project}/pkg${index}/index.ts`,
|
||||
content: `export const pkg${index} = ${index};`
|
||||
},
|
||||
{
|
||||
path: `${project}/pkg${index}/tsconfig.json`,
|
||||
content: JSON.stringify({
|
||||
complerOptions: { composite: true },
|
||||
include: [
|
||||
"**/*.ts",
|
||||
"../typings/xterm.d.ts"
|
||||
]
|
||||
})
|
||||
}
|
||||
];
|
||||
}
|
||||
function writePkgReferences(system: TestFSWithWatch.TestServerHost) {
|
||||
system.writeFile(configPath, JSON.stringify({
|
||||
files: [],
|
||||
include: [],
|
||||
references: pkgs(createPkgReference)
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -499,33 +499,25 @@ namespace ts.projectSystem {
|
||||
});
|
||||
|
||||
describe("unittests:: tsserver:: watchEnvironment:: watchFile is single watcher per file", () => {
|
||||
function verifyWatchFile(scenario: string, environmentVariables?: ESMap<string, string>) {
|
||||
it(scenario, () => {
|
||||
const config: File = {
|
||||
path: `${tscWatch.projectRoot}/tsconfig.json`,
|
||||
content: JSON.stringify({
|
||||
compilerOptions: {
|
||||
composite: true,
|
||||
resolveJsonModule: true,
|
||||
},
|
||||
})
|
||||
};
|
||||
const index: File = {
|
||||
path: `${tscWatch.projectRoot}/index.ts`,
|
||||
content: `import * as tsconfig from "./tsconfig.json";`
|
||||
};
|
||||
const host = createServerHost([config, index, libFile], { environmentVariables });
|
||||
const session = createSession(host, { logger: createLoggerWithInMemoryLogs() });
|
||||
openFilesForSession([index], session);
|
||||
host.serializeWatches().forEach(b => session.logger.info(b));
|
||||
baselineTsserverLogs("watchEnvironment", scenario, session);
|
||||
});
|
||||
}
|
||||
|
||||
verifyWatchFile("when watchFile can create multiple watchers per file");
|
||||
verifyWatchFile(
|
||||
"when watchFile is single watcher per file",
|
||||
arrayToMap(["TSC_WATCHFILE"], identity, () => TestFSWithWatch.Tsc_WatchFile.SingleFileWatcherPerName)
|
||||
);
|
||||
it("when watchFile is single watcher per file", () => {
|
||||
const config: File = {
|
||||
path: `${tscWatch.projectRoot}/tsconfig.json`,
|
||||
content: JSON.stringify({
|
||||
compilerOptions: {
|
||||
composite: true,
|
||||
resolveJsonModule: true,
|
||||
},
|
||||
})
|
||||
};
|
||||
const index: File = {
|
||||
path: `${tscWatch.projectRoot}/index.ts`,
|
||||
content: `import * as tsconfig from "./tsconfig.json";`
|
||||
};
|
||||
const host = createServerHost([config, index, libFile]);
|
||||
const session = createSession(host, { logger: createLoggerWithInMemoryLogs() });
|
||||
openFilesForSession([index], session);
|
||||
host.serializeWatches().forEach(b => session.logger.info(b));
|
||||
baselineTsserverLogs("watchEnvironment", "when watchFile is single watcher per file", session);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -247,20 +247,6 @@ namespace ts.server {
|
||||
|
||||
// Override sys.write because fs.writeSync is not reliable on Node 4
|
||||
sys.write = (s: string) => writeMessage(sys.bufferFrom!(s, "utf8") as globalThis.Buffer);
|
||||
// REVIEW: for now this implementation uses polling.
|
||||
// The advantage of polling is that it works reliably
|
||||
// on all os and with network mounted files.
|
||||
// For 90 referenced files, the average time to detect
|
||||
// changes is 2*msInterval (by default 5 seconds).
|
||||
// The overhead of this is .04 percent (1/2500) with
|
||||
// average pause of < 1 millisecond (and max
|
||||
// pause less than 1.5 milliseconds); question is
|
||||
// do we anticipate reference sets in the 100s and
|
||||
// do we care about waiting 10-20 seconds to detect
|
||||
// changes for large reference sets? If so, do we want
|
||||
// to increase the chunk size or decrease the interval
|
||||
// time dynamically to match the large reference set?
|
||||
sys.defaultWatchFileKind = () => WatchFileKind.FixedChunkSizePolling;
|
||||
|
||||
/* eslint-disable no-restricted-globals */
|
||||
sys.setTimeout = setTimeout;
|
||||
|
||||
Reference in New Issue
Block a user