From 04ce89c80446ccc483b6181783038c8a470b655e Mon Sep 17 00:00:00 2001 From: Nathan Shively-Sanders <293473+sandersn@users.noreply.github.com> Date: Tue, 10 May 2022 10:09:40 -0700 Subject: [PATCH] extract most test code from VirtualServerHost --- src/compiler/sys.ts | 6 +- src/harness/virtualFileSystemWithWatch.ts | 341 ++++++++++++++++- src/server/editorServices.ts | 15 +- .../tsserver/applyChangesToOpenFiles.ts | 4 +- src/testRunner/unittests/tsserver/helpers.ts | 25 -- src/tsserver/server.ts | 6 +- src/vfs/virtualFileSystemWithWatch.ts | 353 +++--------------- 7 files changed, 402 insertions(+), 348 deletions(-) diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index 6cdcc6a26b9..4b6e60d84d4 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -1064,7 +1064,11 @@ namespace ts { * patch writefile to create folder before writing the file */ /*@internal*/ - export function patchWriteFileEnsuringDirectory(sys: System) { + export function patchWriteFileEnsuringDirectory(sys: { + writeFile(path: string, data: string, writeByteOrderMark?: boolean): void; + directoryExists(path: string): boolean; + createDirectory(path: string): void; + }) { // patch writefile to create folder before writing the file const originalWriteFile = sys.writeFile; sys.writeFile = (path, data, writeBom) => diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index a109884e8f8..19f851414a2 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -53,6 +53,54 @@ interface Array { length: number; [n: number]: T; }` return host; } + class Callbacks { + private map: TimeOutCallback[] = []; + private nextId = 1; + + getNextId() { + return this.nextId; + } + + register(cb: (...args: any[]) => void, args: any[]) { + const timeoutId = this.nextId; + this.nextId++; + this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args); + return timeoutId; + } + + unregister(id: any) { + if (typeof id === "number") { + delete this.map[id]; + } + } + + count() { + let n = 0; + for (const _ in this.map) { + n++; + } + return n; + } + + invoke(invokeKey?: number) { + if (invokeKey) { + this.map[invokeKey](); + delete this.map[invokeKey]; + return; + } + + // Note: invoking a callback may result in new callbacks been queued, + // so do not clear the entire callback list regardless. Only remove the + // ones we have invoked. + for (const key in this.map) { + this.map[key](); + delete this.map[key]; + } + } + } + + type TimeOutCallback = () => any; + export function getDiffInKeys(map: ESMap, expectedKeys: readonly string[]) { if (map.size === expectedKeys.length) { return ""; @@ -226,26 +274,190 @@ interface Array { length: number; [n: number]: T; }` }; } + export const timeIncrements = 1000; + export class TestServerHost extends VirtualServerHost implements server.ServerHost { readonly screenClears: number[] = []; private readonly output: string[] = []; + private time = timeIncrements; + private timeoutCallbacks = new Callbacks(); + private immediateCallbacks = new Callbacks(); + private readonly environmentVariables?: ESMap; + public require: ((initialPath: string, moduleName: string) => RequireResult) | undefined; + private readonly tscWatchFile?: string; + private readonly tscWatchDirectory?: string; + private readonly runWithoutRecursiveWatches?: boolean; + runWithFallbackPolling: boolean; + public defaultWatchFileKind?: () => WatchFileKind | undefined; constructor( public withSafeList: boolean, - fileOrFolderorSymLinkList: readonly FileOrFolderOrSymLink[], + fileOrFolderOrSymLinkList: readonly FileOrFolderOrSymLink[], options: TestServerHostCreationParameters = {}) { - super( - fileOrFolderorSymLinkList.concat(withSafeList ? safeList : []), - { - ...options, - executingFilePath: options.executingFilePath || getExecutingFilePathFromLibFile() - }); + super({ + ...options, + executingFilePath: options.executingFilePath || getExecutingFilePathFromLibFile() + }); + const { environmentVariables, runWithoutRecursiveWatches, runWithFallbackPolling } = options; + fileOrFolderOrSymLinkList = fileOrFolderOrSymLinkList.concat(withSafeList ? safeList : []); + this.tscWatchFile = environmentVariables && environmentVariables.get("TSC_WATCHFILE"); + this.tscWatchDirectory = environmentVariables && environmentVariables.get("TSC_WATCHDIRECTORY"); + this.runWithoutRecursiveWatches = runWithoutRecursiveWatches; + this.runWithFallbackPolling = !!runWithFallbackPolling; + this.environmentVariables = environmentVariables; + this.init(); + this.reloadFS(fileOrFolderOrSymLinkList); } + + override init() { + const { watchFile, watchDirectory } = createSystemWatchFunctions({ + // We dont have polling watch file + // it is essentially fsWatch but lets get that separate from fsWatch and + // into watchedFiles for easier testing + pollingWatchFile: this.tscWatchFile === Tsc_WatchFile.SingleFileWatcherPerName ? + createSingleFileWatcherPerName( + this.watchFileWorker.bind(this), + this.useCaseSensitiveFileNames + ) : + this.watchFileWorker.bind(this), + getModifiedTime: this.getModifiedTime.bind(this), + setTimeout: this.setTimeout.bind(this), + clearTimeout: this.clearTimeout.bind(this), + fsWatch: this.fsWatch.bind(this), + fileExists: this.fileExists.bind(this), + useCaseSensitiveFileNames: this.useCaseSensitiveFileNames, + getCurrentDirectory: this.getCurrentDirectory.bind(this), + fsSupportsRecursiveFsWatch: this.tscWatchDirectory ? false : !this.runWithoutRecursiveWatches, + directoryExists: this.directoryExists.bind(this), + getAccessibleSortedChildDirectories: path => this.getDirectories(path), + realpath: this.realpath.bind(this), + tscWatchFile: this.tscWatchFile, + tscWatchDirectory: this.tscWatchDirectory, + defaultWatchFileKind: () => this.defaultWatchFileKind?.(), + }); + this.watchFile = watchFile; + this.watchDirectory = watchDirectory; + } + + private reloadFS(fileOrFolderOrSymLinkList: readonly FileOrFolderOrSymLink[], options?: Partial) { + Debug.assert(this.fs.size === 0); + const filesOrFoldersToLoad: readonly FileOrFolderOrSymLink[] = !this.windowsStyleRoot ? fileOrFolderOrSymLinkList : + fileOrFolderOrSymLinkList.map(f => { + const result = clone(f); + result.path = this.getHostSpecificPath(f.path); + return result; + }); + for (const fileOrDirectory of filesOrFoldersToLoad) { + const path = this.toFullPath(fileOrDirectory.path); + // If its a change + const currentEntry = this.fs.get(path); + if (currentEntry) { + if (isFsFile(currentEntry)) { + if (isFile(fileOrDirectory)) { + // Update file + if (currentEntry.content !== fileOrDirectory.content) { + this.modifyFile(fileOrDirectory.path, fileOrDirectory.content, options); + } + } + else { + // TODO: Changing from file => folder/Symlink + } + } + else if (isFsSymLink(currentEntry)) { + // TODO: update symlinks + } + else { + // Folder + if (isFile(fileOrDirectory)) { + // TODO: Changing from folder => file + } + else { + // Folder update: Nothing to do. + currentEntry.modifiedTime = this.now(); + this.invokeFsWatches(currentEntry.fullPath, "change"); + } + } + } + else { + this.ensureFileOrFolder(fileOrDirectory, options && options.ignoreWatchInvokedWithTriggerAsFileCreate); + } + } + } + + protected override fsWatch( + fileOrDirectory: string, + _entryKind: FileSystemEntryKind, + cb: FsWatchCallback, + recursive: boolean, + fallbackPollingInterval: PollingInterval, + fallbackOptions: WatchOptions | undefined): FileWatcher { + return this.runWithFallbackPolling ? + this.watchFile( + fileOrDirectory, + createFileWatcherCallback(cb), + fallbackPollingInterval, + fallbackOptions + ) : + createWatcher( + recursive ? this.fsWatchesRecursive : this.fsWatches, + this.toFullPath(fileOrDirectory), + { + directoryName: fileOrDirectory, + cb, + fallbackPollingInterval, + fallbackOptions + } + ); + } + + // Output is pretty + writeOutputIsTTY() { + return true; + } + + override now() { + this.time += timeIncrements; + return new Date(this.time); + } + + // TODO: record and invoke callbacks to simulate timer events + setTimeout(callback: TimeOutCallback, _time: number, ...args: any[]) { + return this.timeoutCallbacks.register(callback, args); + } + + getNextTimeoutId() { + return this.timeoutCallbacks.getNextId(); + } + + clearTimeout(timeoutId: any): void { + this.timeoutCallbacks.unregister(timeoutId); + } + + runQueuedTimeoutCallbacks(timeoutId?: number) { + try { + this.timeoutCallbacks.invoke(timeoutId); + } + catch (e) { + if (e.message === this.exitMessage) { + return; + } + throw e; + } + } + runQueuedImmediateCallbacks(checkCount?: number) { if (checkCount !== undefined) { assert.equal(this.immediateCallbacks.count(), checkCount); } - super.runQueuedImmediateCallbacks(); + this.immediateCallbacks.invoke(); + } + + setImmediate(callback: TimeOutCallback, _time: number, ...args: any[]) { + return this.immediateCallbacks.register(callback, args); + } + + clearImmediate(timeoutId: any): void { + this.immediateCallbacks.unregister(timeoutId); } clearScreen(): void { @@ -262,6 +474,115 @@ interface Array { length: number; [n: number]: T; }` assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`); } + renameFile(fileName: string, newFileName: string) { + const fullPath = getNormalizedAbsolutePath(fileName, this.currentDirectory); + const path = this.toPath(fullPath); + const file = this.fs.get(path) as FsFile; + Debug.assert(!!file); + + // Only remove the file + this.removeFileOrFolder(file, /*isRemoveableLeafFolder*/ false, /*isRenaming*/ true); + + // Add updated folder with new folder name + const newFullPath = getNormalizedAbsolutePath(newFileName, this.currentDirectory); + const newFile = this.toFsFile({ path: newFullPath, content: file.content }); + const newPath = newFile.path; + const basePath = getDirectoryPath(path); + Debug.assert(basePath !== path); + Debug.assert(basePath === getDirectoryPath(newPath)); + const baseFolder = this.fs.get(basePath) as FsFolder; + this.addFileOrFolderInFolder(baseFolder, newFile); + } + + renameFolder(folderName: string, newFolderName: string) { + const fullPath = getNormalizedAbsolutePath(folderName, this.currentDirectory); + const path = this.toPath(fullPath); + const folder = this.fs.get(path) as FsFolder; + Debug.assert(!!folder); + + // Only remove the folder + this.removeFileOrFolder(folder, /*isRemoveableLeafFolder*/ false, /*isRenaming*/ true); + + // Add updated folder with new folder name + const newFullPath = getNormalizedAbsolutePath(newFolderName, this.currentDirectory); + const newFolder = this.toFsFolder(newFullPath); + const newPath = newFolder.path; + const basePath = getDirectoryPath(path); + Debug.assert(basePath !== path); + Debug.assert(basePath === getDirectoryPath(newPath)); + const baseFolder = this.fs.get(basePath) as FsFolder; + this.addFileOrFolderInFolder(baseFolder, newFolder); + + // Invoke watches for files in the folder as deleted (from old path) + this.renameFolderEntries(folder, newFolder); + } + + private renameFolderEntries(oldFolder: FsFolder, newFolder: FsFolder) { + for (const entry of oldFolder.entries) { + this.fs.delete(entry.path); + this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Deleted); + + entry.fullPath = combinePaths(newFolder.fullPath, getBaseFileName(entry.fullPath)); + entry.path = this.toPath(entry.fullPath); + if (newFolder !== oldFolder) { + newFolder.entries.push(entry); + } + this.fs.set(entry.path, entry); + this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Created); + if (isFsFolder(entry)) { + this.renameFolderEntries(entry, entry); + } + } + } + + deleteFolder(folderPath: string, recursive?: boolean) { + const path = this.toFullPath(folderPath); + const currentEntry = this.fs.get(path) as FsFolder; + Debug.assert(isFsFolder(currentEntry)); + if (recursive && currentEntry.entries.length) { + const subEntries = currentEntry.entries.slice(); + subEntries.forEach(fsEntry => { + if (isFsFolder(fsEntry)) { + this.deleteFolder(fsEntry.fullPath, recursive); + } + else { + this.removeFileOrFolder(fsEntry, /*isRemoveableLeafFolder*/ false); + } + }); + } + this.removeFileOrFolder(currentEntry, /*isRemoveableLeafFolder*/ false); + } + + readFile(s: string): string | undefined { + const fsEntry = this.getRealFile(this.toFullPath(s)); + return fsEntry ? fsEntry.content : undefined; + } + + getFileSize(s: string) { + const path = this.toFullPath(s); + const entry = this.fs.get(path)!; + if (isFsFile(entry)) { + return entry.fileSize ? entry.fileSize : entry.content.length; + } + return undefined!; // TODO: GH#18217 + } + + createHash(s: string): string { + return `${generateDjb2Hash(s)}-${s}`; + } + + createSHA256Hash(s: string): string { + return sys.createSHA256Hash!(s); + } + + prependFile(path: string, content: string, options?: Partial): void { + this.modifyFile(path, content + this.readFile(path), options); + } + + appendFile(path: string, content: string, options?: Partial): void { + this.modifyFile(path, this.readFile(path) + content, options); + } + override write(message: string) { this.output.push(message); } @@ -324,6 +645,10 @@ interface Array { length: number; [n: number]: T; }` serializeMultiMap(baseline, "FsWatchesRecursive", this.fsWatchesRecursive, serializeTestFsWatcher); baseline.push(""); } + + override getEnvironmentVariable(name: string) { + return this.environmentVariables && this.environmentVariables.get(name) || ""; + } } function diffFsFile(baseline: string[], fsEntry: FsFile) { diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 72719fe465a..c5ad227fedf 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -761,7 +761,7 @@ namespace ts.server { readonly toCanonicalFileName: (f: string) => string; public readonly host: ServerHost; - public readonly fshost: ServerHost | undefined; // TODOO: Should be TestFSWithWatch.VirtualServerHost | undefined + public readonly fshost: VirtualFS.VirtualServerHost | undefined; public readonly logger: Logger; public readonly cancellationToken: HostCancellationToken; public readonly useSingleInferredProject: boolean; @@ -808,7 +808,7 @@ namespace ts.server { constructor(opts: ProjectServiceOptions) { this.host = opts.host; - this.fshost = opts.fshost; + this.fshost = opts.fshost as VirtualFS.VirtualServerHost | undefined; // TODO: OK as long as VirtualServerHost is a supertype of ServerHost this.logger = opts.logger; this.cancellationToken = opts.cancellationToken; this.useSingleInferredProject = opts.useSingleInferredProject; @@ -3702,21 +3702,20 @@ namespace ts.server { /* @internal */ updateFileSystem(updatedFiles: protocol.FileSystemRequestArgs[] | undefined, deletedFiles?: string[]) { Debug.assert(this.fshost); - const fs = this.fshost as VirtualFS.VirtualServerHost; if (updatedFiles) { for (const { file, fileContent } of updatedFiles) { - if (fs.fileExists(file)) { - fs.modifyFile(file, fileContent); + if (this.fshost.fileExists(file)) { + this.fshost.modifyFile(file, fileContent); } else { - fs.ensureFileOrFolder({ path: getDirectoryPath(file) }); - fs.writeFile(file, fileContent); + this.fshost.ensureFileOrFolder({ path: getDirectoryPath(file) }); + this.fshost.writeFile(file, fileContent); } } } if (deletedFiles) { for (const file of deletedFiles) { - fs.deleteFile(file, /*deleteEmptyParentFolders*/ true); + this.fshost.deleteFile(file, /*deleteEmptyParentFolders*/ true); } } } diff --git a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts index 6dd4a70eb55..f6ddec2b14f 100644 --- a/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts +++ b/src/testRunner/unittests/tsserver/applyChangesToOpenFiles.ts @@ -55,8 +55,8 @@ ${file.fileContent}`; } it("with updateFileSystem request", () => { // TODO: probably some other watcher tests, not sure what - const host = VirtualFS.createVirtualServerHost([], { executingFilePath: "/a/tsc.js" }); - const session = createVirtualFilesystemSession(host, host); + const host = VirtualFS.createVirtualServerHost({ executingFilePath: "/a/tsc.js" }); + const session = createSession(host, { fshost: host }); const files = [app, file1, file2, file3, config, lib]; session.executeCommandSeq({ command: protocol.CommandTypes.UpdateFileSystem, diff --git a/src/testRunner/unittests/tsserver/helpers.ts b/src/testRunner/unittests/tsserver/helpers.ts index 0bd38794194..304353cf905 100644 --- a/src/testRunner/unittests/tsserver/helpers.ts +++ b/src/testRunner/unittests/tsserver/helpers.ts @@ -414,31 +414,6 @@ namespace ts.projectSystem { } } - export function createVirtualFilesystemSession(host: server.ServerHost, fshost: VirtualFS.VirtualServerHost | undefined, opts: Partial = {}) { - if (opts.typingsInstaller === undefined) { - opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host); - } - - if (opts.eventHandler !== undefined) { - opts.canUseEvents = true; - } - - const sessionOptions: TestSessionOptions = { - host, - fshost, - cancellationToken: server.nullCancellationToken, - useSingleInferredProject: false, - useInferredProjectPerProjectRoot: false, - typingsInstaller: undefined!, // TODO: GH#18217 - byteLength: Utils.byteLength, - hrtime: process.hrtime, - logger: opts.logger || createHasErrorMessageLogger(), - canUseEvents: false - }; - - return new TestSession({ ...sessionOptions, ...opts }); - } - export interface TestProjectServiceOptions extends server.ProjectServiceOptions { logger: Logger; } diff --git a/src/tsserver/server.ts b/src/tsserver/server.ts index fad5b3f49d5..a3b36f64d75 100644 --- a/src/tsserver/server.ts +++ b/src/tsserver/server.ts @@ -58,7 +58,11 @@ namespace ts.server { console.warn = (...args) => logger.msg(args.length === 1 ? args[0] : args.join(", "), Msg.Err); console.error = (...args) => logger.msg(args.length === 1 ? args[0] : args.join(", "), Msg.Err); - const fshost = vfs ? VirtualFS.createVirtualServerHost([]) : sys as ServerHost; + const fshost = vfs ? VirtualFS.createVirtualServerHost([], { + useCaseSensitiveFileNames: sys.useCaseSensitiveFileNames, + executingFilePath: "", // TODO: "" is the default..maybe this should be vfs, vfs:// or . + newLine: sys.newLine, + }) : sys as ServerHost; startServer( { globalPlugins: findArgumentStringArray("--globalPlugins"), diff --git a/src/vfs/virtualFileSystemWithWatch.ts b/src/vfs/virtualFileSystemWithWatch.ts index 23cc85d90d8..9de22ae7ef8 100644 --- a/src/vfs/virtualFileSystemWithWatch.ts +++ b/src/vfs/virtualFileSystemWithWatch.ts @@ -6,17 +6,11 @@ namespace ts.VirtualFS { currentDirectory?: string; newLine?: string; windowsStyleRoot?: string; - environmentVariables?: ESMap; - runWithoutRecursiveWatches?: boolean; - runWithFallbackPolling?: boolean; } - export function createVirtualWatchedSystem(fileOrFolderList: readonly FileOrFolderOrSymLink[], params?: VirtualServerHostCreationParameters): VirtualServerHost { - return new VirtualServerHost(fileOrFolderList, params); - } - - export function createVirtualServerHost(fileOrFolderList: readonly FileOrFolderOrSymLink[], params?: VirtualServerHostCreationParameters): VirtualServerHost { - const host = new VirtualServerHost(fileOrFolderList, params); + export function createVirtualServerHost(params: VirtualServerHostCreationParameters): VirtualServerHost { + const host = new VirtualServerHost(params); + host.init(); // Just like sys, patch the host to use writeFile patchWriteFileEnsuringDirectory(host); return host; @@ -92,59 +86,11 @@ namespace ts.VirtualFS { } } - function createWatcher(map: MultiMap, path: Path, callback: T): FileWatcher { + export function createWatcher(map: MultiMap, path: Path, callback: T): FileWatcher { map.add(path, callback); return { close: () => map.remove(path, callback) }; } - class Callbacks { - private map: TimeOutCallback[] = []; - private nextId = 1; - - getNextId() { - return this.nextId; - } - - register(cb: (...args: any[]) => void, args: any[]) { - const timeoutId = this.nextId; - this.nextId++; - this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args); - return timeoutId; - } - - unregister(id: any) { - if (typeof id === "number") { - delete this.map[id]; - } - } - - count() { - let n = 0; - for (const _ in this.map) { - n++; - } - return n; - } - - invoke(invokeKey?: number) { - if (invokeKey) { - this.map[invokeKey](); - delete this.map[invokeKey]; - return; - } - - // Note: invoking a callback may result in new callbacks been queued, - // so do not clear the entire callback list regardless. Only remove the - // ones we have invoked. - for (const key in this.map) { - this.map[key](); - delete this.map[key]; - } - } - } - - type TimeOutCallback = () => any; - export interface VirtualFileWatcher { cb: FileWatcherCallback; fileName: string; @@ -178,7 +124,6 @@ namespace ts.VirtualFS { DynamicPolling = "RecursiveDirectoryUsingDynamicPriorityPolling" } - export const timeIncrements = 1000; /** * also implements {server.ServerHost} but that would create a circular dependency */ @@ -186,56 +131,41 @@ namespace ts.VirtualFS { args: string[] = []; protected fs: ESMap = new Map(); - private time = timeIncrements; getCanonicalFileName: (s: string) => string; - private toPath: (f: string) => Path; - protected timeoutCallbacks = new Callbacks(); - protected immediateCallbacks = new Callbacks(); + protected toPath: (f: string) => Path; readonly watchedFiles = createMultiMap(); readonly fsWatches = createMultiMap(); readonly fsWatchesRecursive = createMultiMap(); - runWithFallbackPolling: boolean; public readonly useCaseSensitiveFileNames: boolean; public readonly newLine: string; public readonly windowsStyleRoot?: string; - private readonly environmentVariables?: ESMap; private readonly executingFilePath: string; - private readonly currentDirectory: string; - public require: ((initialPath: string, moduleName: string) => RequireResult) | undefined; - public defaultWatchFileKind?: () => WatchFileKind | undefined; + protected readonly currentDirectory: string; public storeFilesChangingSignatureDuringEmit = true; - watchFile: HostWatchFile; - watchDirectory: HostWatchDirectory; - constructor( - fileOrFolderOrSymLinkList: readonly FileOrFolderOrSymLink[], - { - useCaseSensitiveFileNames, executingFilePath, currentDirectory, - newLine, windowsStyleRoot, environmentVariables, - runWithoutRecursiveWatches, runWithFallbackPolling - }: VirtualServerHostCreationParameters = { executingFilePath: "" }) { + watchFile!: HostWatchFile; + watchDirectory!: HostWatchDirectory; + + constructor({ + useCaseSensitiveFileNames, executingFilePath, currentDirectory, + newLine, windowsStyleRoot + }: VirtualServerHostCreationParameters) { this.useCaseSensitiveFileNames = !!useCaseSensitiveFileNames; this.newLine = newLine || "\n"; this.windowsStyleRoot = windowsStyleRoot; - this.environmentVariables = environmentVariables; currentDirectory = currentDirectory || "/"; this.getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames); this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName); this.executingFilePath = this.getHostSpecificPath(executingFilePath); this.currentDirectory = this.getHostSpecificPath(currentDirectory); - this.runWithFallbackPolling = !!runWithFallbackPolling; - const tscWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE"); - const tscWatchDirectory = this.environmentVariables && this.environmentVariables.get("TSC_WATCHDIRECTORY"); + } + + init() { const { watchFile, watchDirectory } = createSystemWatchFunctions({ // 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), + pollingWatchFile: this.watchFileWorker.bind(this), getModifiedTime: this.getModifiedTime.bind(this), setTimeout: this.setTimeout.bind(this), clearTimeout: this.clearTimeout.bind(this), @@ -243,67 +173,16 @@ namespace ts.VirtualFS { fileExists: this.fileExists.bind(this), useCaseSensitiveFileNames: this.useCaseSensitiveFileNames, getCurrentDirectory: this.getCurrentDirectory.bind(this), - fsSupportsRecursiveFsWatch: tscWatchDirectory ? false : !runWithoutRecursiveWatches, + fsSupportsRecursiveFsWatch: true, directoryExists: this.directoryExists.bind(this), getAccessibleSortedChildDirectories: path => this.getDirectories(path), realpath: this.realpath.bind(this), - tscWatchFile, - tscWatchDirectory, - defaultWatchFileKind: () => this.defaultWatchFileKind?.(), + tscWatchFile: undefined, + tscWatchDirectory: undefined, + defaultWatchFileKind: () => undefined, }); this.watchFile = watchFile; this.watchDirectory = watchDirectory; - this.reloadFS(fileOrFolderOrSymLinkList); - } - - private reloadFS(fileOrFolderOrSymLinkList: readonly FileOrFolderOrSymLink[], options?: Partial) { - Debug.assert(this.fs.size === 0); - const filesOrFoldersToLoad: readonly FileOrFolderOrSymLink[] = !this.windowsStyleRoot ? fileOrFolderOrSymLinkList : - fileOrFolderOrSymLinkList.map(f => { - const result = clone(f); - result.path = this.getHostSpecificPath(f.path); - return result; - }); - for (const fileOrDirectory of filesOrFoldersToLoad) { - const path = this.toFullPath(fileOrDirectory.path); - // If its a change - const currentEntry = this.fs.get(path); - if (currentEntry) { - if (isFsFile(currentEntry)) { - if (isFile(fileOrDirectory)) { - // Update file - if (currentEntry.content !== fileOrDirectory.content) { - this.modifyFile(fileOrDirectory.path, fileOrDirectory.content, options); - } - } - else { - // TODO: Changing from file => folder/Symlink - } - } - else if (isFsSymLink(currentEntry)) { - // TODO: update symlinks - } - else { - // Folder - if (isFile(fileOrDirectory)) { - // TODO: Changing from folder => file - } - else { - // Folder update: Nothing to do. - currentEntry.modifiedTime = this.now(); - this.invokeFsWatches(currentEntry.fullPath, "change"); - } - } - } - else { - this.ensureFileOrFolder(fileOrDirectory, options && options.ignoreWatchInvokedWithTriggerAsFileCreate); - } - } - } - - // Output is pretty - writeOutputIsTTY() { - return true; } getNewLine() { @@ -326,8 +205,7 @@ namespace ts.VirtualFS { } now() { - this.time += timeIncrements; - return new Date(this.time); + return new Date(); } modifyFile(filePath: string, content: string, options?: Partial) { @@ -357,67 +235,6 @@ namespace ts.VirtualFS { } } - renameFile(fileName: string, newFileName: string) { - const fullPath = getNormalizedAbsolutePath(fileName, this.currentDirectory); - const path = this.toPath(fullPath); - const file = this.fs.get(path) as FsFile; - Debug.assert(!!file); - - // Only remove the file - this.removeFileOrFolder(file, /*isRemoveableLeafFolder*/ false, /*isRenaming*/ true); - - // Add updated folder with new folder name - const newFullPath = getNormalizedAbsolutePath(newFileName, this.currentDirectory); - const newFile = this.toFsFile({ path: newFullPath, content: file.content }); - const newPath = newFile.path; - const basePath = getDirectoryPath(path); - Debug.assert(basePath !== path); - Debug.assert(basePath === getDirectoryPath(newPath)); - const baseFolder = this.fs.get(basePath) as FsFolder; - this.addFileOrFolderInFolder(baseFolder, newFile); - } - - renameFolder(folderName: string, newFolderName: string) { - const fullPath = getNormalizedAbsolutePath(folderName, this.currentDirectory); - const path = this.toPath(fullPath); - const folder = this.fs.get(path) as FsFolder; - Debug.assert(!!folder); - - // Only remove the folder - this.removeFileOrFolder(folder, /*isRemoveableLeafFolder*/ false, /*isRenaming*/ true); - - // Add updated folder with new folder name - const newFullPath = getNormalizedAbsolutePath(newFolderName, this.currentDirectory); - const newFolder = this.toFsFolder(newFullPath); - const newPath = newFolder.path; - const basePath = getDirectoryPath(path); - Debug.assert(basePath !== path); - Debug.assert(basePath === getDirectoryPath(newPath)); - const baseFolder = this.fs.get(basePath) as FsFolder; - this.addFileOrFolderInFolder(baseFolder, newFolder); - - // Invoke watches for files in the folder as deleted (from old path) - this.renameFolderEntries(folder, newFolder); - } - - private renameFolderEntries(oldFolder: FsFolder, newFolder: FsFolder) { - for (const entry of oldFolder.entries) { - this.fs.delete(entry.path); - this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Deleted); - - entry.fullPath = combinePaths(newFolder.fullPath, getBaseFileName(entry.fullPath)); - entry.path = this.toPath(entry.fullPath); - if (newFolder !== oldFolder) { - newFolder.entries.push(entry); - } - this.fs.set(entry.path, entry); - this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Created); - if (isFsFolder(entry)) { - this.renameFolderEntries(entry, entry); - } - } - } - ensureFileOrFolder(fileOrDirectoryOrSymLink: FileOrFolderOrSymLink, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean, ignoreParentWatch?: boolean) { if (isFile(fileOrDirectoryOrSymLink)) { const file = this.toFsFile(fileOrDirectoryOrSymLink); @@ -461,7 +278,7 @@ namespace ts.VirtualFS { return folder; } - private addFileOrFolderInFolder(folder: FsFolder, fileOrDirectory: FsFile | FsFolder | FsSymLink, ignoreWatch?: boolean) { + protected addFileOrFolderInFolder(folder: FsFolder, fileOrDirectory: FsFile | FsFolder | FsSymLink, ignoreWatch?: boolean) { if (!this.fs.has(fileOrDirectory.path)) { insertSorted(folder.entries, fileOrDirectory, (a, b) => compareStringsCaseSensitive(getBaseFileName(a.path), getBaseFileName(b.path))); } @@ -475,7 +292,7 @@ namespace ts.VirtualFS { this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed); } - private removeFileOrFolder(fileOrDirectory: FsFile | FsFolder | FsSymLink, isRemovableLeafFolder: boolean, isRenaming = false) { + protected removeFileOrFolder(fileOrDirectory: FsFile | FsFolder | FsSymLink, isRemovableLeafFolder: boolean, isRenaming = false) { const basePath = getDirectoryPath(fileOrDirectory.path); const baseFolder = this.fs.get(basePath) as FsFolder; if (basePath !== fileOrDirectory.path) { @@ -504,25 +321,7 @@ namespace ts.VirtualFS { this.removeFileOrFolder(currentEntry, deleteEmptyParentFolders); } - deleteFolder(folderPath: string, recursive?: boolean) { - const path = this.toFullPath(folderPath); - const currentEntry = this.fs.get(path) as FsFolder; - Debug.assert(isFsFolder(currentEntry)); - if (recursive && currentEntry.entries.length) { - const subEntries = currentEntry.entries.slice(); - subEntries.forEach(fsEntry => { - if (isFsFolder(fsEntry)) { - this.deleteFolder(fsEntry.fullPath, recursive); - } - else { - this.removeFileOrFolder(fsEntry, /*isRemoveableLeafFolder*/ false); - } - }); - } - this.removeFileOrFolder(currentEntry, /*isRemoveableLeafFolder*/ false); - } - - private watchFileWorker(fileName: string, cb: FileWatcherCallback, pollingInterval: PollingInterval) { + protected watchFileWorker(fileName: string, cb: FileWatcherCallback, pollingInterval: PollingInterval) { return createWatcher( this.watchedFiles, this.toFullPath(fileName), @@ -530,30 +329,23 @@ namespace ts.VirtualFS { ); } - private fsWatch( + protected fsWatch( fileOrDirectory: string, _entryKind: FileSystemEntryKind, cb: FsWatchCallback, recursive: boolean, fallbackPollingInterval: PollingInterval, fallbackOptions: WatchOptions | undefined): FileWatcher { - return this.runWithFallbackPolling ? - this.watchFile( - fileOrDirectory, - createFileWatcherCallback(cb), + return createWatcher( + recursive ? this.fsWatchesRecursive : this.fsWatches, + this.toFullPath(fileOrDirectory), + { + directoryName: fileOrDirectory, + cb, fallbackPollingInterval, fallbackOptions - ) : - createWatcher( - recursive ? this.fsWatchesRecursive : this.fsWatches, - this.toFullPath(fileOrDirectory), - { - directoryName: fileOrDirectory, - cb, - fallbackPollingInterval, - fallbackOptions - } - ); + } + ); } invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind, useFileNameInCallback?: boolean) { @@ -584,13 +376,13 @@ namespace ts.VirtualFS { } } - private invokeFsWatches(fullPath: string, eventName: "rename" | "change") { + protected invokeFsWatches(fullPath: string, eventName: "rename" | "change") { this.invokeFsWatchesCallbacks(fullPath, eventName); this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, fullPath); this.invokeRecursiveFsWatches(fullPath, eventName); } - private invokeFileAndFsWatches(fileOrFolderFullPath: string, eventKind: FileWatcherEventKind) { + protected invokeFileAndFsWatches(fileOrFolderFullPath: string, eventKind: FileWatcherEventKind) { this.invokeFileWatcher(fileOrFolderFullPath, eventKind); this.invokeFsWatches(fileOrFolderFullPath, eventKind === FileWatcherEventKind.Changed ? "change" : "rename"); } @@ -604,7 +396,7 @@ namespace ts.VirtualFS { }; } - private toFsFile(file: File): FsFile { + protected toFsFile(file: File): FsFile { const fsFile = this.toFsEntry(file.path) as FsFile; fsFile.content = file.content; fsFile.fileSize = file.fileSize; @@ -617,7 +409,7 @@ namespace ts.VirtualFS { return fsSymLink; } - private toFsFolder(path: string): FsFolder { + protected toFsFolder(path: string): FsFolder { const fsFolder = this.toFsEntry(path) as FsFolder; fsFolder.entries = [] as FSEntry[] as SortedArray; // https://github.com/Microsoft/TypeScript/issues/19873 return fsFolder; @@ -649,7 +441,7 @@ namespace ts.VirtualFS { return !!this.getRealFile(fsEntry.path, fsEntry); } - private getRealFile(path: Path, fsEntry?: FSEntry): FsFile | undefined { + protected getRealFile(path: Path, fsEntry?: FSEntry): FsFile | undefined { return this.getRealFsEntry(isFsFile, path, fsEntry); } @@ -672,15 +464,6 @@ namespace ts.VirtualFS { return (fsEntry && fsEntry.modifiedTime)!; // TODO: GH#18217 } - setModifiedTime(s: string, date: Date) { - const path = this.toFullPath(s); - const fsEntry = this.fs.get(path); - if (fsEntry) { - fsEntry.modifiedTime = date; - this.invokeFileAndFsWatches(fsEntry.fullPath, FileWatcherEventKind.Changed); - } - } - readFile(s: string): string | undefined { const fsEntry = this.getRealFile(this.toFullPath(s)); return fsEntry ? fsEntry.content : undefined; @@ -732,49 +515,21 @@ namespace ts.VirtualFS { }, path => this.realpath(path)); } - createHash(s: string): string { - return `${generateDjb2Hash(s)}-${s}`; + setTimeout(callback: (...args: any[]) => void, ms: number, ...args: any[]): any { + // tslint:disable-next-line:no-restricted-globals + setTimeout(callback, ms, ...args); } - createSHA256Hash(s: string): string { - return sys.createSHA256Hash!(s); + clearTimeout(_timeoutId: any): void { + throw new Error("clearTimeout Not implemented in virtual filesystem host."); } - // TOOD: record and invoke callbacks to simulate timer events - setTimeout(callback: TimeOutCallback, _time: number, ...args: any[]) { - return this.timeoutCallbacks.register(callback, args); + setImmediate(_callback: (...args: any[]) => void, ..._args: any[]): any { + throw new Error("setImmediate Not implemented in virtual filesystem host."); } - getNextTimeoutId() { - return this.timeoutCallbacks.getNextId(); - } - - clearTimeout(timeoutId: any): void { - this.timeoutCallbacks.unregister(timeoutId); - } - - runQueuedTimeoutCallbacks(timeoutId?: number) { - try { - this.timeoutCallbacks.invoke(timeoutId); - } - catch (e) { - if (e.message === this.exitMessage) { - return; - } - throw e; - } - } - - runQueuedImmediateCallbacks() { - this.immediateCallbacks.invoke(); - } - - setImmediate(callback: TimeOutCallback, _time: number, ...args: any[]) { - return this.immediateCallbacks.register(callback, args); - } - - clearImmediate(timeoutId: any): void { - this.immediateCallbacks.unregister(timeoutId); + clearImmediate(_timeoutId: any): void { + throw new Error("clearImmediate Not implemented in virtual filesystem host."); } createDirectory(directoryName: string, recursive?: boolean): void { @@ -819,14 +574,6 @@ namespace ts.VirtualFS { } } - prependFile(path: string, content: string, options?: Partial): void { - this.modifyFile(path, content + this.readFile(path), options); - } - - appendFile(path: string, content: string, options?: Partial): void { - this.modifyFile(path, this.readFile(path) + content, options); - } - write(_message: string) { // Do nothing } @@ -859,8 +606,8 @@ namespace ts.VirtualFS { throw new Error(this.exitMessage); } - getEnvironmentVariable(name: string) { - return this.environmentVariables && this.environmentVariables.get(name) || ""; + getEnvironmentVariable(_name: string) { + return ""; } } }