diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 56d132fc925b4..507997c346de3 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -24,14 +24,14 @@ namespace ts { * Make `elements` into a `NodeArray`. If `elements` is `undefined`, returns an empty `NodeArray`. */ export function createNodeArray(elements?: ReadonlyArray, hasTrailingComma?: boolean): NodeArray { - if (elements) { + if (!elements || elements === emptyArray) { + elements = []; + } + else { if (isNodeArray(elements)) { return elements; } } - else { - elements = []; - } const array = >elements; array.pos = -1; diff --git a/src/compiler/sys.ts b/src/compiler/sys.ts index e3c304afb8f36..641ee86dd4f4d 100644 --- a/src/compiler/sys.ts +++ b/src/compiler/sys.ts @@ -334,9 +334,10 @@ namespace ts { /*@internal*/ export interface RecursiveDirectoryWatcherHost { watchDirectory: HostWatchDirectory; - getAccessileSortedChildDirectories(path: string): ReadonlyArray; + getAccessibleSortedChildDirectories(path: string): ReadonlyArray; directoryExists(dir: string): boolean; filePathComparer: Comparer; + realpath(s: string): string; } /** @@ -392,9 +393,14 @@ namespace ts { function watchChildDirectories(parentDir: string, existingChildWatches: ChildWatches, callback: DirectoryWatcherCallback): ChildWatches { let newChildWatches: DirectoryWatcher[] | undefined; enumerateInsertsAndDeletes( - host.directoryExists(parentDir) ? host.getAccessileSortedChildDirectories(parentDir) : emptyArray, + host.directoryExists(parentDir) ? mapDefined(host.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 host.filePathComparer(childFullName, host.realpath(childFullName)) === Comparison.EqualTo ? childFullName : undefined; + }) : emptyArray, existingChildWatches, - (child, childWatcher) => host.filePathComparer(getNormalizedAbsolutePath(child, parentDir), childWatcher.dirName), + (child, childWatcher) => host.filePathComparer(child, childWatcher.dirName), createAndAddChildDirectoryWatcher, closeFileWatcher, addChildDirectoryWatcher @@ -406,7 +412,7 @@ namespace ts { * Create new childDirectoryWatcher and add it to the new ChildDirectoryWatcher list */ function createAndAddChildDirectoryWatcher(childName: string) { - const result = createDirectoryWatcher(getNormalizedAbsolutePath(childName, parentDir), callback); + const result = createDirectoryWatcher(childName, callback); addChildDirectoryWatcher(result); } @@ -594,14 +600,7 @@ namespace ts { exit(exitCode?: number): void { process.exit(exitCode); }, - realpath(path: string): string { - try { - return _fs.realpathSync(path); - } - catch { - return path; - } - }, + realpath, debugMode: some(process.execArgv, arg => /^--(inspect|debug)(-brk)?(=\d+)?$/i.test(arg)), tryEnableSourceMapsForHost() { try { @@ -682,8 +681,9 @@ namespace ts { const watchDirectoryRecursively = createRecursiveDirectoryWatcher({ filePathComparer: useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, directoryExists, - getAccessileSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, - watchDirectory + getAccessibleSortedChildDirectories: path => getAccessibleFileSystemEntries(path).directories, + watchDirectory, + realpath }); return (directoryName, callback, recursive) => { @@ -1026,6 +1026,15 @@ namespace ts { return filter(_fs.readdirSync(path), dir => fileSystemEntryExists(combinePaths(path, dir), FileSystemEntryKind.Directory)); } + function realpath(path: string): string { + try { + return _fs.realpathSync(path); + } + catch { + return path; + } + } + function getModifiedTime(path: string) { try { return _fs.statSync(path).mtime; diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 313ba9d9029ec..999201029ded3 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -2355,9 +2355,53 @@ declare module "fs" { verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory); }); - // it("uses non recursive dynamic polling when renaming file in subfolder", () => { - // verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.DynamicPolling); - // }); + it("uses non recursive dynamic polling when renaming file in subfolder", () => { + verifyRenamingFileInSubFolder(TestFSWithWatch.Tsc_WatchDirectory.DynamicPolling); + }); + + it("when there are symlinks to folders in recursive folders", () => { + const cwd = "/home/user/projects/myproject"; + const file1: FileOrFolder = { + path: `${cwd}/src/file.ts`, + content: `import * as a from "a"` + }; + const tsconfig: FileOrFolder = { + path: `${cwd}/tsconfig.json`, + content: `{ "compilerOptions": { "extendedDiagnostics": true, "traceResolution": true }}` + }; + const realA: FileOrFolder = { + path: `${cwd}/node_modules/reala/index.d.ts`, + content: `export {}` + }; + const realB: FileOrFolder = { + path: `${cwd}/node_modules/realb/index.d.ts`, + content: `export {}` + }; + const symLinkA: FileOrFolder = { + path: `${cwd}/node_modules/a`, + symLink: `${cwd}/node_modules/reala` + }; + const symLinkB: FileOrFolder = { + path: `${cwd}/node_modules/b`, + symLink: `${cwd}/node_modules/realb` + }; + const symLinkBInA: FileOrFolder = { + path: `${cwd}/node_modules/reala/node_modules/b`, + symLink: `${cwd}/node_modules/b` + }; + const symLinkAInB: FileOrFolder = { + path: `${cwd}/node_modules/realb/node_modules/a`, + symLink: `${cwd}/node_modules/a` + }; + const files = [file1, tsconfig, realA, realB, symLinkA, symLinkB, symLinkBInA, symLinkAInB]; + const environmentVariables = createMap(); + environmentVariables.set("TSC_WATCHDIRECTORY", TestFSWithWatch.Tsc_WatchDirectory.NonRecursiveWatchDirectory); + const host = createWatchedSystem(files, { environmentVariables, currentDirectory: cwd }); + createWatchOfConfigFile("tsconfig.json", host); + checkWatchedDirectories(host, emptyArray, /*recursive*/ true); + checkWatchedDirectories(host, [cwd, `${cwd}/node_modules`, `${cwd}/node_modules/@types`, `${cwd}/node_modules/reala`, `${cwd}/node_modules/realb`, + `${cwd}/node_modules/reala/node_modules`, `${cwd}/node_modules/realb/node_modules`, `${cwd}/src`], /*recursive*/ false); + }); }); }); } diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 7a49b8e78f680..81232fb63f4ef 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -88,7 +88,7 @@ interface Array {}` } interface Folder extends FSEntry { - entries: FSEntry[]; + entries: SortedArray; } interface SymLink extends FSEntry { @@ -276,12 +276,14 @@ interface Array {}` DynamicPolling = "RecursiveDirectoryUsingDynamicPriorityPolling" } + const timeIncrements = 1000; export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, ModuleResolutionHost { args: string[] = []; private readonly output: string[] = []; private fs: Map = createMap(); + private time = timeIncrements; getCanonicalFileName: (s: string) => string; private toPath: (f: string) => Path; private timeoutCallbacks = new Callbacks(); @@ -310,18 +312,20 @@ interface Array {}` const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchFile(directory, () => cb(directory), PollingInterval.Medium); this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ directoryExists: path => this.directoryExists(path), - getAccessileSortedChildDirectories: path => this.getDirectories(path), + getAccessibleSortedChildDirectories: path => this.getDirectories(path), filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, - watchDirectory + watchDirectory, + realpath: s => this.realpath(s) }); } else if (tscWatchDirectory === Tsc_WatchDirectory.NonRecursiveWatchDirectory) { const watchDirectory: HostWatchDirectory = (directory, cb) => this.watchDirectory(directory, fileName => cb(fileName), /*recursive*/ false); this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ directoryExists: path => this.directoryExists(path), - getAccessileSortedChildDirectories: path => this.getDirectories(path), + getAccessibleSortedChildDirectories: path => this.getDirectories(path), filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, - watchDirectory + watchDirectory, + realpath: s => this.realpath(s) }); } else if (tscWatchDirectory === Tsc_WatchDirectory.DynamicPolling) { @@ -329,9 +333,10 @@ interface Array {}` const watchDirectory: HostWatchDirectory = (directory, cb) => watchFile(directory, () => cb(directory), PollingInterval.Medium); this.customRecursiveWatchDirectory = createRecursiveDirectoryWatcher({ directoryExists: path => this.directoryExists(path), - getAccessileSortedChildDirectories: path => this.getDirectories(path), + getAccessibleSortedChildDirectories: path => this.getDirectories(path), filePathComparer: this.useCaseSensitiveFileNames ? compareStringsCaseSensitive : compareStringsCaseInsensitive, - watchDirectory + watchDirectory, + realpath: s => this.realpath(s) }); } } @@ -355,6 +360,11 @@ interface Array {}` return s; } + private now() { + this.time += timeIncrements; + return new Date(this.time); + } + reloadFS(fileOrFolderList: ReadonlyArray, options?: Partial) { const mapNewLeaves = createMap(); const isNewFs = this.fs.size === 0; @@ -381,8 +391,8 @@ interface Array {}` } else { currentEntry.content = fileOrDirectory.content; - currentEntry.modifiedTime = new Date(); - this.fs.get(getDirectoryPath(currentEntry.path)).modifiedTime = new Date(); + currentEntry.modifiedTime = this.now(); + this.fs.get(getDirectoryPath(currentEntry.path)).modifiedTime = this.now(); if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) { this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath); } @@ -406,7 +416,7 @@ interface Array {}` } else { // Folder update: Nothing to do. - currentEntry.modifiedTime = new Date(); + currentEntry.modifiedTime = this.now(); } } } @@ -512,8 +522,8 @@ interface Array {}` } private addFileOrFolderInFolder(folder: Folder, fileOrDirectory: File | Folder | SymLink, ignoreWatch?: boolean) { - folder.entries.push(fileOrDirectory); - folder.modifiedTime = new Date(); + insertSorted(folder.entries, fileOrDirectory, (a, b) => compareStringsCaseSensitive(getBaseFileName(a.path), getBaseFileName(b.path))); + folder.modifiedTime = this.now(); this.fs.set(fileOrDirectory.path, fileOrDirectory); if (ignoreWatch) { @@ -528,7 +538,7 @@ interface Array {}` const baseFolder = this.fs.get(basePath) as Folder; if (basePath !== fileOrDirectory.path) { Debug.assert(!!baseFolder); - baseFolder.modifiedTime = new Date(); + baseFolder.modifiedTime = this.now(); filterMutate(baseFolder.entries, entry => entry !== fileOrDirectory); } this.fs.delete(fileOrDirectory.path); @@ -603,7 +613,7 @@ interface Array {}` return { path: this.toPath(fullPath), fullPath, - modifiedTime: new Date() + modifiedTime: this.now() }; } @@ -622,7 +632,7 @@ interface Array {}` private toFolder(path: string): Folder { const folder = this.toFsEntry(path) as Folder; - folder.entries = []; + folder.entries = [] as SortedArray; return folder; } @@ -642,7 +652,7 @@ interface Array {}` const realpath = this.realpath(path); if (path !== realpath) { - return this.getRealFsEntry(isFsEntry, realpath as Path); + return this.getRealFsEntry(isFsEntry, this.toPath(realpath)); } return undefined;