From de1b363ec1741bd06a07b824a12e3a0e4ef94d5f Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Wed, 18 Mar 2026 13:21:29 -0500 Subject: [PATCH 01/30] initial impl --- internal/execute/watcher.go | 203 +++++++++++++++++++++++++----------- 1 file changed, 143 insertions(+), 60 deletions(-) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 10e09d85e8c..1c79d98ce67 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -3,15 +3,52 @@ package execute import ( "fmt" "reflect" + "slices" "time" + "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" "github.com/microsoft/typescript-go/internal/execute/incremental" "github.com/microsoft/typescript-go/internal/execute/tsc" "github.com/microsoft/typescript-go/internal/tsoptions" + "github.com/microsoft/typescript-go/internal/vfs" ) +const watchDebounceWait = 250 * time.Millisecond + +type trackingFS struct { + inner vfs.FS + seenFiles collections.SyncSet[string] +} + +func (fs *trackingFS) ReadFile(path string) (string, bool) { + fs.seenFiles.Add(path) + return fs.inner.ReadFile(path) +} + +func (fs *trackingFS) FileExists(path string) bool { + fs.seenFiles.Add(path) + return fs.inner.FileExists(path) +} +func (fs *trackingFS) UseCaseSensitiveFileNames() bool { return fs.inner.UseCaseSensitiveFileNames() } +func (fs *trackingFS) WriteFile(path string, data string) error { + return fs.inner.WriteFile(path, data) +} +func (fs *trackingFS) Remove(path string) error { return fs.inner.Remove(path) } +func (fs *trackingFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { + return fs.inner.Chtimes(path, aTime, mTime) +} +func (fs *trackingFS) DirectoryExists(path string) bool { return fs.inner.DirectoryExists(path) } +func (fs *trackingFS) GetAccessibleEntries(path string) vfs.Entries { + return fs.inner.GetAccessibleEntries(path) +} +func (fs *trackingFS) Stat(path string) vfs.FileInfo { return fs.inner.Stat(path) } +func (fs *trackingFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { + return fs.inner.WalkDir(root, walkFn) +} +func (fs *trackingFS) Realpath(path string) string { return fs.inner.Realpath(path) } + type Watcher struct { sys tsc.System configFileName string @@ -21,13 +58,17 @@ type Watcher struct { reportErrorSummary tsc.DiagnosticsReporter testing tsc.CommandLineTesting - host compiler.CompilerHost - program *incremental.Program - prevModified map[string]time.Time - configModified bool + program *incremental.Program + extendedConfigCache *tsc.ExtendedConfigCache + configModified bool + + watchState map[string]time.Time } -var _ tsc.Watcher = (*Watcher)(nil) +var ( + _ tsc.Watcher = (*Watcher)(nil) + _ vfs.FS = (*trackingFS)(nil) +) func createWatcher( sys tsc.System, @@ -44,7 +85,6 @@ func createWatcher( reportDiagnostic: reportDiagnostic, reportErrorSummary: reportErrorSummary, testing: testing, - // reportWatchStatus: createWatchStatusReporter(sys, configParseResult.CompilerOptions().Pretty), } if configParseResult.ConfigFile != nil { w.configFileName = configParseResult.ConfigFile.SourceFile.FileName() @@ -53,51 +93,69 @@ func createWatcher( } func (w *Watcher) start() { - w.host = compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), nil, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) - w.program = incremental.ReadBuildInfoProgram(w.config, incremental.NewBuildInfoReader(w.host), w.host) + w.extendedConfigCache = &tsc.ExtendedConfigCache{} + host := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) + w.program = incremental.ReadBuildInfoProgram(w.config, incremental.NewBuildInfoReader(host), host) + + w.doBuild() if w.testing == nil { - watchInterval := w.config.ParsedConfig.WatchOptions.WatchInterval() for { + time.Sleep(w.pollInterval()) w.DoCycle() - time.Sleep(watchInterval) } - } else { - // Initial compilation in test mode - w.DoCycle() } } func (w *Watcher) DoCycle() { - // if this function is updated, make sure to update `RunWatchCycle` in export_test.go as needed - if w.hasErrorsInTsConfig() { // these are unrecoverable errors--report them and do not build return } - // updateProgram() + if w.watchState != nil && !w.configModified && !w.hasWatchedFilesChanged() { + if w.testing != nil { + w.testing.OnProgram(w.program) + } + return + } + + if w.testing == nil { + w.refreshWatchState() + settledAt := w.sys.Now() + for w.sys.Now().Sub(settledAt) < watchDebounceWait { + time.Sleep(w.pollInterval()) + if w.hasWatchedFilesChanged() { + w.refreshWatchState() + settledAt = w.sys.Now() + } + } + } + + w.doBuild() +} + +func (w *Watcher) doBuild() { + tfs := &trackingFS{inner: w.sys.FS()} + host := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), tfs, w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) + w.program = incremental.NewProgram(compiler.NewProgram(compiler.ProgramOptions{ Config: w.config, - Host: w.host, + Host: host, }), w.program, nil, w.testing != nil) - if w.hasBeenModified(w.program.GetProgram()) { - fmt.Fprintln(w.sys.Writer(), "build starting at", w.sys.Now().Format("03:04:05 PM")) - timeStart := w.sys.Now() - w.compileAndEmit() - fmt.Fprintf(w.sys.Writer(), "build finished in %.3fs\n", w.sys.Now().Sub(timeStart).Seconds()) - } else { - // print something??? - // fmt.Fprintln(w.sys.Writer(), "no changes detected at ", w.sys.Now()) - } + fmt.Fprintln(w.sys.Writer(), "build starting at", w.sys.Now().Format("03:04:05 PM")) + timeStart := w.sys.Now() + w.compileAndEmit() + w.buildWatchState(tfs) + w.configModified = false + fmt.Fprintf(w.sys.Writer(), "build finished in %.3fs\n", w.sys.Now().Sub(timeStart).Seconds()) + if w.testing != nil { w.testing.OnProgram(w.program) } } func (w *Watcher) compileAndEmit() { - // !!! output/error reporting is currently the same as non-watch mode - // diagnostics, emitResult, exitStatus := tsc.EmitFilesAndReportErrors(tsc.EmitInput{ Sys: w.sys, ProgramLike: w.program, @@ -111,10 +169,8 @@ func (w *Watcher) compileAndEmit() { } func (w *Watcher) hasErrorsInTsConfig() bool { - // only need to check and reparse tsconfig options/update host if we are watching a config file extendedConfigCache := &tsc.ExtendedConfigCache{} if w.configFileName != "" { - // !!! need to check that this merges compileroptions correctly. This differs from non-watch, since we allow overriding of previous options configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(w.configFileName, w.compilerOptionsFromCommandLine, nil, w.sys, extendedConfigCache) if len(errors) > 0 { for _, e := range errors { @@ -122,46 +178,73 @@ func (w *Watcher) hasErrorsInTsConfig() bool { } return true } - // CompilerOptions contain fields which should not be compared; clone to get a copy without those set. - if !reflect.DeepEqual(w.config.CompilerOptions().Clone(), configParseResult.CompilerOptions().Clone()) { - // fmt.Fprintln(w.sys.Writer(), "build triggered due to config change") + if !reflect.DeepEqual(w.config.CompilerOptions().Clone(), configParseResult.CompilerOptions().Clone()) || + !slices.Equal(w.config.FileNames(), configParseResult.FileNames()) { w.configModified = true } w.config = configParseResult } - w.host = compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) + w.extendedConfigCache = extendedConfigCache return false } -func (w *Watcher) hasBeenModified(program *compiler.Program) bool { - // checks watcher's snapshot against program file modified times - currState := map[string]time.Time{} - filesModified := w.configModified - for _, sourceFile := range program.SourceFiles() { - fileName := sourceFile.FileName() - s := w.sys.FS().Stat(fileName) - if s == nil { - // do nothing; if file is in program.SourceFiles() but is not found when calling Stat, file has been very recently deleted. - // deleted files are handled outside of this loop - continue - } - currState[fileName] = s.ModTime() - if !filesModified { - if currState[fileName] != w.prevModified[fileName] { - // fmt.Fprint(w.sys.Writer(), "build triggered from ", fileName, ": ", w.prevModified[fileName], " -> ", currState[fileName], "\n") - filesModified = true +func (w *Watcher) hasWatchedFilesChanged() bool { + for path, oldMt := range w.watchState { + s := w.sys.FS().Stat(path) + if oldMt.IsZero() { + if s != nil { + return true + } + } else { + if s == nil || s.ModTime() != oldMt { + return true } - // catch cases where no files are modified, but some were deleted - delete(w.prevModified, fileName) } } - if !filesModified && len(w.prevModified) > 0 { - // fmt.Fprintln(w.sys.Writer(), "build triggered due to deleted file") - filesModified = true + return false +} + +func (w *Watcher) buildWatchState(tfs *trackingFS) { + w.watchState = make(map[string]time.Time) + tfs.seenFiles.Range(func(fn string) bool { + if s := w.sys.FS().Stat(fn); s != nil { + w.watchState[fn] = s.ModTime() + } else { + w.watchState[fn] = time.Time{} + } + return true + }) +} + +func (w *Watcher) refreshWatchState() { + for path := range w.watchState { + if s := w.sys.FS().Stat(path); s != nil { + w.watchState[path] = s.ModTime() + } else { + w.watchState[path] = time.Time{} + } } - w.prevModified = currState +} - // reset state for next cycle - w.configModified = false - return filesModified +func (w *Watcher) pollInterval() time.Duration { + return w.config.ParsedConfig.WatchOptions.WatchInterval() +} + +// Testing helpers — exported for use by test packages. + +func (w *Watcher) HasWatchedFilesChanged() bool { + return w.hasWatchedFilesChanged() +} + +func (w *Watcher) RefreshWatchState() { + w.refreshWatchState() +} + +func (w *Watcher) WatchStateLen() int { + return len(w.watchState) +} + +func (w *Watcher) WatchStateHas(path string) bool { + _, ok := w.watchState[path] + return ok } From 208b2b6d7c5e566e8a8fe9a40098f19693765c5b Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:11:03 -0500 Subject: [PATCH 02/30] extensive testing and fixes --- internal/execute/tsctests/tscwatch_test.go | 399 ++++++++++++++++++ internal/execute/watcher.go | 83 +++- ...etects-at-types-package-installed-later.js | 78 ++++ .../watch-detects-change-in-symlinked-file.js | 74 ++++ ...to-previously-non-existent-include-path.js | 68 +++ ...leted-and-new-file-added-simultaneously.js | 76 ++++ ...h-detects-file-renamed-and-renamed-back.js | 113 +++++ .../watch-detects-import-path-restructured.js | 76 ++++ ...atch-detects-imported-directory-removed.js | 86 ++++ ...ts-imported-file-added-in-new-directory.js | 75 ++++ ...s-module-going-missing-then-coming-back.js | 107 +++++ ...etects-new-file-resolving-failed-import.js | 75 ++++ ...atch-detects-node-modules-package-added.js | 76 ++++ ...ch-detects-node-modules-package-removed.js | 78 ++++ ...-detects-package-json-main-field-edited.js | 71 ++++ .../watch-detects-scoped-package-installed.js | 74 ++++ .../watch-handles-file-rapidly-recreated.js | 73 ++++ .../watch-handles-tsconfig-deleted.js | 206 +++++++++ ...les-tsconfig-with-extends-base-modified.js | 70 +++ .../watch-rebuilds-when-file-is-modified.js | 67 +++ ...ch-rebuilds-when-source-file-is-deleted.js | 86 ++++ ...when-tsconfig-include-pattern-adds-file.js | 73 ++++ ...n-tsconfig-is-modified-to-change-strict.js | 77 ++++ .../watch-skips-build-when-no-files-change.js | 58 +++ ...config-is-touched-but-content-unchanged.js | 59 +++ ...-with-tsconfig-files-list-entry-deleted.js | 67 +++ 26 files changed, 2424 insertions(+), 21 deletions(-) create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-at-types-package-installed-later.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-change-in-symlinked-file.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-deleted-and-new-file-added-simultaneously.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-import-path-restructured.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-directory-removed.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-file-added-in-new-directory.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-module-going-missing-then-coming-back.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-resolving-failed-import.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-added.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-removed.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-main-field-edited.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-scoped-package-installed.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-file-rapidly-recreated.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-file-is-modified.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-source-file-is-deleted.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-include-pattern-adds-file.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-modified-to-change-strict.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-build-when-no-files-change.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-rebuild-when-tsconfig-is-touched-but-content-unchanged.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-files-list-entry-deleted.js diff --git a/internal/execute/tsctests/tscwatch_test.go b/internal/execute/tsctests/tscwatch_test.go index e577131250b..a633578bd04 100644 --- a/internal/execute/tsctests/tscwatch_test.go +++ b/internal/execute/tsctests/tscwatch_test.go @@ -3,6 +3,8 @@ package tsctests import ( "strings" "testing" + + "github.com/microsoft/typescript-go/internal/vfs/vfstest" ) func TestWatch(t *testing.T) { @@ -23,6 +25,403 @@ func TestWatch(t *testing.T) { }, commandLineArgs: []string{"--watch", "--incremental"}, }, + { + subScenario: "watch skips build when no files change", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `const x: number = 1;`, + "/home/src/workspaces/project/tsconfig.json": "{}", + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + noChange, + }, + }, + { + subScenario: "watch rebuilds when file is modified", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `const x: number = 1;`, + "/home/src/workspaces/project/tsconfig.json": "{}", + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("modify file", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/index.ts", `const x: number = 2;`) + }), + }, + }, + { + subScenario: "watch rebuilds when source file is deleted", + files: FileMap{ + "/home/src/workspaces/project/a.ts": `import { b } from "./b";`, + "/home/src/workspaces/project/b.ts": `export const b = 1;`, + "/home/src/workspaces/project/tsconfig.json": "{}", + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + { + caption: "delete imported file", + edit: func(sys *TestSys) { + sys.removeNoError("/home/src/workspaces/project/b.ts") + }, + expectedDiff: "incremental resolves to .js output from prior build (TS7016) while clean build cannot find module at all (TS2307)", + }, + }, + }, + { + subScenario: "watch detects new file resolving failed import", + files: FileMap{ + "/home/src/workspaces/project/a.ts": `import { b } from "./b";`, + "/home/src/workspaces/project/tsconfig.json": "{}", + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("create missing file", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/b.ts", `export const b = 1;`) + }), + }, + }, + // Directory-level change detection via imports + { + subScenario: "watch detects imported file added in new directory", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import { util } from "./lib/util";`, + "/home/src/workspaces/project/tsconfig.json": "{}", + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("create directory and imported file", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/lib/util.ts", `export const util = "hello";`) + }), + }, + }, + { + subScenario: "watch detects imported directory removed", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import { util } from "./lib/util";`, + "/home/src/workspaces/project/lib/util.ts": `export const util = "hello";`, + "/home/src/workspaces/project/tsconfig.json": "{}", + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + { + caption: "remove directory with imported file", + edit: func(sys *TestSys) { + sys.removeNoError("/home/src/workspaces/project/lib/util.ts") + }, + expectedDiff: "incremental resolves to .js output from prior build (TS7016) while clean build cannot find module at all (TS2307)", + }, + }, + }, + { + subScenario: "watch detects import path restructured", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import { util } from "./lib/util";`, + "/home/src/workspaces/project/lib/util.ts": `export const util = "v1";`, + "/home/src/workspaces/project/tsconfig.json": "{}", + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("move file to new path and update import", func(sys *TestSys) { + sys.removeNoError("/home/src/workspaces/project/lib/util.ts") + sys.writeFileNoError("/home/src/workspaces/project/src/util.ts", `export const util = "v2";`) + sys.writeFileNoError("/home/src/workspaces/project/index.ts", `import { util } from "./src/util";`) + }), + }, + }, + // tsconfig include/exclude change detection + { + subScenario: "watch rebuilds when tsconfig include pattern adds file", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `const x = 1;`, + "/home/src/workspaces/project/tsconfig.json": `{ + "compilerOptions": {}, + "include": ["*.ts"] +}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("widen include pattern to add src dir", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/src/extra.ts", `export const extra = 2;`) + sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", `{ + "compilerOptions": {}, + "include": ["*.ts", "src/**/*.ts"] +}`) + }), + }, + }, + { + subScenario: "watch rebuilds when tsconfig is modified to change strict", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `const x = null; const y: string = x;`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("enable strict mode", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", `{"compilerOptions": {"strict": true}}`) + }), + }, + }, + // Path resolution: tsconfig include pointing to non-existent directory + { + subScenario: "watch detects file added to previously non-existent include path", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `const x = 1;`, + "/home/src/workspaces/project/tsconfig.json": `{ + "compilerOptions": {}, + "include": ["index.ts", "src/**/*.ts"] +}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("create src dir with ts file matching include", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/src/helper.ts", `export const helper = "added";`) + }), + }, + }, + // Path resolution: import from non-existent node_modules package + { + subScenario: "watch detects node modules package added", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("install package in node_modules", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/package.json", `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`) + sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.js", `exports.lib = "hello";`) + sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts", `export declare const lib: string;`) + }), + }, + }, + // Path resolution: node_modules package removed + { + subScenario: "watch detects node modules package removed", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + "/home/src/workspaces/project/node_modules/mylib/package.json": `{"name": "mylib", "main": "index.js", "types": "index.d.ts"}`, + "/home/src/workspaces/project/node_modules/mylib/index.js": `exports.lib = "hello";`, + "/home/src/workspaces/project/node_modules/mylib/index.d.ts": `export declare const lib: string;`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + { + caption: "remove node_modules package", + edit: func(sys *TestSys) { + sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.d.ts") + sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/index.js") + sys.removeNoError("/home/src/workspaces/project/node_modules/mylib/package.json") + }, + }, + }, + }, + // Config file lifecycle + { + subScenario: "watch handles tsconfig deleted", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `const x = 1;`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + { + caption: "delete tsconfig", + expectedDiff: "incremental reports config read error while clean build without tsconfig prints usage help", + edit: func(sys *TestSys) { + sys.removeNoError("/home/src/workspaces/project/tsconfig.json") + }, + }, + }, + }, + { + subScenario: "watch handles tsconfig with extends base modified", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `const x: number = 1;`, + "/home/src/workspaces/project/base.json": `{ + "compilerOptions": { "strict": false } +}`, + "/home/src/workspaces/project/tsconfig.json": `{ + "extends": "./base.json" +}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("modify base config to enable strict", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/base.json", `{ + "compilerOptions": { "strict": true } +}`) + }), + }, + }, + { + subScenario: "watch skips rebuild when tsconfig is touched but content unchanged", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `const x = 1;`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("touch tsconfig without changing content", func(sys *TestSys) { + content := sys.readFileNoError("/home/src/workspaces/project/tsconfig.json") + sys.writeFileNoError("/home/src/workspaces/project/tsconfig.json", content) + }), + }, + }, + { + subScenario: "watch with tsconfig files list entry deleted", + files: FileMap{ + "/home/src/workspaces/project/a.ts": `export const a = 1;`, + "/home/src/workspaces/project/b.ts": `export const b = 2;`, + "/home/src/workspaces/project/tsconfig.json": `{ + "compilerOptions": {}, + "files": ["a.ts", "b.ts"] +}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("delete file listed in files array", func(sys *TestSys) { + sys.removeNoError("/home/src/workspaces/project/b.ts") + }), + }, + }, + // Module resolution & dependencies + { + subScenario: "watch detects module going missing then coming back", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import { util } from "./util";`, + "/home/src/workspaces/project/util.ts": `export const util = "v1";`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + { + caption: "delete util module", + edit: func(sys *TestSys) { + sys.removeNoError("/home/src/workspaces/project/util.ts") + }, + expectedDiff: "incremental resolves to .js output from prior build while clean build cannot find module", + }, + newTscEdit("recreate util module with new content", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/util.ts", `export const util = "v2";`) + }), + }, + }, + { + subScenario: "watch detects scoped package installed", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import { lib } from "@scope/mylib";`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("install scoped package", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/node_modules/@scope/mylib/package.json", `{"name": "@scope/mylib", "types": "index.d.ts"}`) + sys.writeFileNoError("/home/src/workspaces/project/node_modules/@scope/mylib/index.d.ts", `export declare const lib: string;`) + }), + }, + }, + { + subScenario: "watch detects package json main field edited", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + "/home/src/workspaces/project/node_modules/mylib/package.json": `{"name": "mylib", "types": "old.d.ts"}`, + "/home/src/workspaces/project/node_modules/mylib/old.d.ts": `export declare const lib: number;`, + "/home/src/workspaces/project/node_modules/mylib/new.d.ts": `export declare const lib: string;`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("change package.json types field", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/node_modules/mylib/package.json", `{"name": "mylib", "types": "new.d.ts"}`) + }), + }, + }, + { + subScenario: "watch detects at-types package installed later", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import * as lib from "untyped-lib";`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + "/home/src/workspaces/project/node_modules/untyped-lib/index.js": `module.exports = {};`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("install @types for the library", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/node_modules/@types/untyped-lib/index.d.ts", `declare module "untyped-lib" { export const value: string; }`) + sys.writeFileNoError("/home/src/workspaces/project/node_modules/@types/untyped-lib/package.json", `{"name": "@types/untyped-lib", "types": "index.d.ts"}`) + }), + }, + }, + // File operations + { + subScenario: "watch detects file renamed and renamed back", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import { helper } from "./helper";`, + "/home/src/workspaces/project/helper.ts": `export const helper = 1;`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + { + caption: "rename helper to helper2", + edit: func(sys *TestSys) { + sys.renameFileNoError("/home/src/workspaces/project/helper.ts", "/home/src/workspaces/project/helper2.ts") + }, + expectedDiff: "incremental resolves to .js output from prior build while clean build cannot find module", + }, + newTscEdit("rename back to helper", func(sys *TestSys) { + sys.renameFileNoError("/home/src/workspaces/project/helper2.ts", "/home/src/workspaces/project/helper.ts") + }), + }, + }, + { + subScenario: "watch detects file deleted and new file added simultaneously", + files: FileMap{ + "/home/src/workspaces/project/a.ts": `import { b } from "./b";`, + "/home/src/workspaces/project/b.ts": `export const b = 1;`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("delete b.ts and create c.ts with updated import", func(sys *TestSys) { + sys.removeNoError("/home/src/workspaces/project/b.ts") + sys.writeFileNoError("/home/src/workspaces/project/c.ts", `export const c = 2;`) + sys.writeFileNoError("/home/src/workspaces/project/a.ts", `import { c } from "./c";`) + }), + }, + }, + { + subScenario: "watch handles file rapidly recreated", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import { val } from "./data";`, + "/home/src/workspaces/project/data.ts": `export const val = "original";`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("delete and immediately recreate with new content", func(sys *TestSys) { + sys.removeNoError("/home/src/workspaces/project/data.ts") + sys.writeFileNoError("/home/src/workspaces/project/data.ts", `export const val = "recreated";`) + }), + }, + }, + // Symlinks + { + subScenario: "watch detects change in symlinked file", + files: FileMap{ + "/home/src/workspaces/project/index.ts": `import { shared } from "./link";`, + "/home/src/workspaces/shared/index.ts": `export const shared = "v1";`, + "/home/src/workspaces/project/link.ts": vfstest.Symlink("/home/src/workspaces/shared/index.ts"), + "/home/src/workspaces/project/tsconfig.json": `{}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("modify symlink target", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/shared/index.ts", `export const shared = "v2";`) + }), + }, + }, } for _, test := range testCases { diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 1c79d98ce67..0c359441ad0 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -39,8 +39,14 @@ func (fs *trackingFS) Remove(path string) error { return fs.inner.Remove(path) } func (fs *trackingFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { return fs.inner.Chtimes(path, aTime, mTime) } -func (fs *trackingFS) DirectoryExists(path string) bool { return fs.inner.DirectoryExists(path) } + +func (fs *trackingFS) DirectoryExists(path string) bool { + fs.seenFiles.Add(path) + return fs.inner.DirectoryExists(path) +} + func (fs *trackingFS) GetAccessibleEntries(path string) vfs.Entries { + fs.seenFiles.Add(path) return fs.inner.GetAccessibleEntries(path) } func (fs *trackingFS) Stat(path string) vfs.FileInfo { return fs.inner.Stat(path) } @@ -49,6 +55,11 @@ func (fs *trackingFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { } func (fs *trackingFS) Realpath(path string) string { return fs.inner.Realpath(path) } +type watchEntry struct { + modTime time.Time + exists bool +} + type Watcher struct { sys tsc.System configFileName string @@ -61,8 +72,9 @@ type Watcher struct { program *incremental.Program extendedConfigCache *tsc.ExtendedConfigCache configModified bool + configHasErrors bool - watchState map[string]time.Time + watchState map[string]watchEntry } var ( @@ -138,13 +150,14 @@ func (w *Watcher) doBuild() { tfs := &trackingFS{inner: w.sys.FS()} host := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), tfs, w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) + fmt.Fprintln(w.sys.Writer(), "build starting at", w.sys.Now().Format("03:04:05 PM")) + timeStart := w.sys.Now() + w.program = incremental.NewProgram(compiler.NewProgram(compiler.ProgramOptions{ Config: w.config, Host: host, }), w.program, nil, w.testing != nil) - fmt.Fprintln(w.sys.Writer(), "build starting at", w.sys.Now().Format("03:04:05 PM")) - timeStart := w.sys.Now() w.compileAndEmit() w.buildWatchState(tfs) w.configModified = false @@ -169,34 +182,56 @@ func (w *Watcher) compileAndEmit() { } func (w *Watcher) hasErrorsInTsConfig() bool { + if w.configFileName == "" { + return false + } + + // Skip re-parsing if the config file hasn't changed since last check. + if w.watchState != nil { + if entry, ok := w.watchState[w.configFileName]; ok { + s := w.sys.FS().Stat(w.configFileName) + unchanged := false + if !entry.exists { + unchanged = s == nil + } else { + unchanged = s != nil && s.ModTime().Equal(entry.modTime) + } + if unchanged { + return w.configHasErrors + } + } + } + extendedConfigCache := &tsc.ExtendedConfigCache{} - if w.configFileName != "" { - configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(w.configFileName, w.compilerOptionsFromCommandLine, nil, w.sys, extendedConfigCache) - if len(errors) > 0 { + configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(w.configFileName, w.compilerOptionsFromCommandLine, nil, w.sys, extendedConfigCache) + if len(errors) > 0 { + if !w.configHasErrors { for _, e := range errors { w.reportDiagnostic(e) } - return true + w.configHasErrors = true } - if !reflect.DeepEqual(w.config.CompilerOptions().Clone(), configParseResult.CompilerOptions().Clone()) || - !slices.Equal(w.config.FileNames(), configParseResult.FileNames()) { - w.configModified = true - } - w.config = configParseResult + return true + } + w.configHasErrors = false + if !reflect.DeepEqual(w.config.CompilerOptions().Clone(), configParseResult.CompilerOptions().Clone()) || + !slices.Equal(w.config.FileNames(), configParseResult.FileNames()) { + w.configModified = true } + w.config = configParseResult w.extendedConfigCache = extendedConfigCache return false } func (w *Watcher) hasWatchedFilesChanged() bool { - for path, oldMt := range w.watchState { + for path, old := range w.watchState { s := w.sys.FS().Stat(path) - if oldMt.IsZero() { + if !old.exists { if s != nil { return true } } else { - if s == nil || s.ModTime() != oldMt { + if s == nil || !s.ModTime().Equal(old.modTime) { return true } } @@ -205,12 +240,12 @@ func (w *Watcher) hasWatchedFilesChanged() bool { } func (w *Watcher) buildWatchState(tfs *trackingFS) { - w.watchState = make(map[string]time.Time) + w.watchState = make(map[string]watchEntry) tfs.seenFiles.Range(func(fn string) bool { if s := w.sys.FS().Stat(fn); s != nil { - w.watchState[fn] = s.ModTime() + w.watchState[fn] = watchEntry{modTime: s.ModTime(), exists: true} } else { - w.watchState[fn] = time.Time{} + w.watchState[fn] = watchEntry{exists: false} } return true }) @@ -219,9 +254,9 @@ func (w *Watcher) buildWatchState(tfs *trackingFS) { func (w *Watcher) refreshWatchState() { for path := range w.watchState { if s := w.sys.FS().Stat(path); s != nil { - w.watchState[path] = s.ModTime() + w.watchState[path] = watchEntry{modTime: s.ModTime(), exists: true} } else { - w.watchState[path] = time.Time{} + w.watchState[path] = watchEntry{exists: false} } } } @@ -248,3 +283,9 @@ func (w *Watcher) WatchStateHas(path string) bool { _, ok := w.watchState[path] return ok } + +func (w *Watcher) DebugWatchState(fn func(path string, modTime time.Time, exists bool)) { + for path, entry := range w.watchState { + fn(path, entry.modTime, entry.exists) + } +} diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-at-types-package-installed-later.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-at-types-package-installed-later.js new file mode 100644 index 00000000000..9637aacea1e --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-at-types-package-installed-later.js @@ -0,0 +1,78 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +import * as lib from "untyped-lib"; +//// [/home/src/workspaces/project/node_modules/untyped-lib/index.js] *new* +module.exports = {}; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +index.ts:1:22 - error TS7016: Could not find a declaration file for module 'untyped-lib'. '/home/src/workspaces/project/node_modules/untyped-lib/index.js' implicitly has an 'any' type. + +1 import * as lib from "untyped-lib"; +   ~~~~~~~~~~~~~ + + +Found 1 error in index.ts:1 + +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +export {}; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: install @types for the library +//// [/home/src/workspaces/project/node_modules/@types/untyped-lib/index.d.ts] *new* +declare module "untyped-lib" { export const value: string; } +//// [/home/src/workspaces/project/node_modules/@types/untyped-lib/package.json] *new* +{"name": "@types/untyped-lib", "types": "index.d.ts"} + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *rewrite with same content* + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/node_modules/@types/untyped-lib/index.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(used version) /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +(used version) /home/src/workspaces/project/node_modules/@types/untyped-lib/index.d.ts +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-change-in-symlinked-file.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-change-in-symlinked-file.js new file mode 100644 index 00000000000..cc5f9033902 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-change-in-symlinked-file.js @@ -0,0 +1,74 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +import { shared } from "./link"; +//// [/home/src/workspaces/project/link.ts] -> /home/src/workspaces/shared/index.ts *new* +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} +//// [/home/src/workspaces/shared/index.ts] *new* +export const shared = "v1"; + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +export {}; + +//// [/home/src/workspaces/project/link.js] *new* +export const shared = "v1"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/link.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: modify symlink target +//// [/home/src/workspaces/shared/index.ts] *modified* +export const shared = "v2"; + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *rewrite with same content* +//// [/home/src/workspaces/project/link.js] *modified* +export const shared = "v2"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/link.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/link.ts +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js new file mode 100644 index 00000000000..d339fc37347 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js @@ -0,0 +1,68 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +const x = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{ + "compilerOptions": {}, + "include": ["index.ts", "src/**/*.ts"] +} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +"use strict"; +const x = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: create src dir with ts file matching include +//// [/home/src/workspaces/project/src/helper.ts] *new* +export const helper = "added"; + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/src/helper.js] *new* +export const helper = "added"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/src/helper.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/src/helper.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-deleted-and-new-file-added-simultaneously.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-deleted-and-new-file-added-simultaneously.js new file mode 100644 index 00000000000..ddd109d3713 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-deleted-and-new-file-added-simultaneously.js @@ -0,0 +1,76 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/a.ts] *new* +import { b } from "./b"; +//// [/home/src/workspaces/project/b.ts] *new* +export const b = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/a.js] *new* +export {}; + +//// [/home/src/workspaces/project/b.js] *new* +export const b = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/b.ts +*refresh* /home/src/workspaces/project/a.ts +Signatures:: + + +Edit [0]:: delete b.ts and create c.ts with updated import +//// [/home/src/workspaces/project/a.ts] *modified* +import { c } from "./c"; +//// [/home/src/workspaces/project/b.ts] *deleted* +//// [/home/src/workspaces/project/c.ts] *new* +export const c = 2; + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/a.js] *rewrite with same content* +//// [/home/src/workspaces/project/c.js] *new* +export const c = 2; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/c.ts +*refresh* /home/src/workspaces/project/a.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/c.ts +(computed .d.ts) /home/src/workspaces/project/a.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js new file mode 100644 index 00000000000..22d830a3c8b --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js @@ -0,0 +1,113 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/helper.ts] *new* +export const helper = 1; +//// [/home/src/workspaces/project/index.ts] *new* +import { helper } from "./helper"; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/helper.js] *new* +export const helper = 1; + +//// [/home/src/workspaces/project/index.js] *new* +export {}; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/helper.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: rename helper to helper2 +//// [/home/src/workspaces/project/helper.ts] *deleted* +//// [/home/src/workspaces/project/helper2.ts] *new* +export const helper = 1; + + +Output:: +build starting at HH:MM:SS AM +index.ts:1:24 - error TS7016: Could not find a declaration file for module './helper'. '/home/src/workspaces/project/helper.js' implicitly has an 'any' type. + +1 import { helper } from "./helper"; +   ~~~~~~~~~~ + + +Found 1 error in index.ts:1 + +build finished in d.ddds +//// [/home/src/workspaces/project/helper2.js] *new* +export const helper = 1; + +//// [/home/src/workspaces/project/index.js] *rewrite with same content* + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/helper2.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/helper2.ts +(computed .d.ts) /home/src/workspaces/project/index.ts + + +Diff:: incremental resolves to .js output from prior build while clean build cannot find module +--- nonIncremental.output.txt ++++ incremental.output.txt +@@ -1,4 +1,4 @@ +-index.ts:1:24 - error TS2307: Cannot find module './helper' or its corresponding type declarations. ++index.ts:1:24 - error TS7016: Could not find a declaration file for module './helper'. '/home/src/workspaces/project/helper.js' implicitly has an 'any' type. + + 1 import { helper } from "./helper"; +    ~~~~~~~~~~ + +Edit [1]:: rename back to helper +//// [/home/src/workspaces/project/helper.ts] *new* +export const helper = 1; +//// [/home/src/workspaces/project/helper2.ts] *deleted* + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/helper.js] *rewrite with same content* +//// [/home/src/workspaces/project/index.js] *rewrite with same content* + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/helper.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/helper.ts +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-import-path-restructured.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-import-path-restructured.js new file mode 100644 index 00000000000..78cc8b7cc0e --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-import-path-restructured.js @@ -0,0 +1,76 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +import { util } from "./lib/util"; +//// [/home/src/workspaces/project/lib/util.ts] *new* +export const util = "v1"; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +export {}; + +//// [/home/src/workspaces/project/lib/util.js] *new* +export const util = "v1"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/lib/util.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: move file to new path and update import +//// [/home/src/workspaces/project/index.ts] *modified* +import { util } from "./src/util"; +//// [/home/src/workspaces/project/lib/util.ts] *deleted* +//// [/home/src/workspaces/project/src/util.ts] *new* +export const util = "v2"; + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *rewrite with same content* +//// [/home/src/workspaces/project/src/util.js] *new* +export const util = "v2"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/src/util.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/src/util.ts +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-directory-removed.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-directory-removed.js new file mode 100644 index 00000000000..37557659c60 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-directory-removed.js @@ -0,0 +1,86 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +import { util } from "./lib/util"; +//// [/home/src/workspaces/project/lib/util.ts] *new* +export const util = "hello"; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +export {}; + +//// [/home/src/workspaces/project/lib/util.js] *new* +export const util = "hello"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/lib/util.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: remove directory with imported file +//// [/home/src/workspaces/project/lib/util.ts] *deleted* + + +Output:: +build starting at HH:MM:SS AM +index.ts:1:22 - error TS7016: Could not find a declaration file for module './lib/util'. '/home/src/workspaces/project/lib/util.js' implicitly has an 'any' type. + +1 import { util } from "./lib/util"; +   ~~~~~~~~~~~~ + + +Found 1 error in index.ts:1 + +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *rewrite with same content* + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/index.ts + + +Diff:: incremental resolves to .js output from prior build (TS7016) while clean build cannot find module at all (TS2307) +--- nonIncremental.output.txt ++++ incremental.output.txt +@@ -1,4 +1,4 @@ +-index.ts:1:22 - error TS2307: Cannot find module './lib/util' or its corresponding type declarations. ++index.ts:1:22 - error TS7016: Could not find a declaration file for module './lib/util'. '/home/src/workspaces/project/lib/util.js' implicitly has an 'any' type. + + 1 import { util } from "./lib/util"; +    ~~~~~~~~~~~~ \ No newline at end of file diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-file-added-in-new-directory.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-file-added-in-new-directory.js new file mode 100644 index 00000000000..54ec48b04b0 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-file-added-in-new-directory.js @@ -0,0 +1,75 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +import { util } from "./lib/util"; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +index.ts:1:22 - error TS2307: Cannot find module './lib/util' or its corresponding type declarations. + +1 import { util } from "./lib/util"; +   ~~~~~~~~~~~~ + + +Found 1 error in index.ts:1 + +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +export {}; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: create directory and imported file +//// [/home/src/workspaces/project/lib/util.ts] *new* +export const util = "hello"; + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *rewrite with same content* +//// [/home/src/workspaces/project/lib/util.js] *new* +export const util = "hello"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/lib/util.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/lib/util.ts +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-module-going-missing-then-coming-back.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-module-going-missing-then-coming-back.js new file mode 100644 index 00000000000..644e05ed13f --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-module-going-missing-then-coming-back.js @@ -0,0 +1,107 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +import { util } from "./util"; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} +//// [/home/src/workspaces/project/util.ts] *new* +export const util = "v1"; + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +export {}; + +//// [/home/src/workspaces/project/util.js] *new* +export const util = "v1"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/util.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: delete util module +//// [/home/src/workspaces/project/util.ts] *deleted* + + +Output:: +build starting at HH:MM:SS AM +index.ts:1:22 - error TS7016: Could not find a declaration file for module './util'. '/home/src/workspaces/project/util.js' implicitly has an 'any' type. + +1 import { util } from "./util"; +   ~~~~~~~~ + + +Found 1 error in index.ts:1 + +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *rewrite with same content* + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/index.ts + + +Diff:: incremental resolves to .js output from prior build while clean build cannot find module +--- nonIncremental.output.txt ++++ incremental.output.txt +@@ -1,4 +1,4 @@ +-index.ts:1:22 - error TS2307: Cannot find module './util' or its corresponding type declarations. ++index.ts:1:22 - error TS7016: Could not find a declaration file for module './util'. '/home/src/workspaces/project/util.js' implicitly has an 'any' type. + + 1 import { util } from "./util"; +    ~~~~~~~~ + +Edit [1]:: recreate util module with new content +//// [/home/src/workspaces/project/util.ts] *new* +export const util = "v2"; + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *rewrite with same content* +//// [/home/src/workspaces/project/util.js] *modified* +export const util = "v2"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/util.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/util.ts +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-resolving-failed-import.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-resolving-failed-import.js new file mode 100644 index 00000000000..e4c464f492c --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-resolving-failed-import.js @@ -0,0 +1,75 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/a.ts] *new* +import { b } from "./b"; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +a.ts:1:19 - error TS2307: Cannot find module './b' or its corresponding type declarations. + +1 import { b } from "./b"; +   ~~~~~ + + +Found 1 error in a.ts:1 + +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/a.js] *new* +export {}; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/a.ts +Signatures:: + + +Edit [0]:: create missing file +//// [/home/src/workspaces/project/b.ts] *new* +export const b = 1; + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/a.js] *rewrite with same content* +//// [/home/src/workspaces/project/b.js] *new* +export const b = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/b.ts +*refresh* /home/src/workspaces/project/a.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/b.ts +(computed .d.ts) /home/src/workspaces/project/a.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-added.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-added.js new file mode 100644 index 00000000000..a75b2e3502a --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-added.js @@ -0,0 +1,76 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +import { lib } from "mylib"; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +index.ts:1:21 - error TS2307: Cannot find module 'mylib' or its corresponding type declarations. + +1 import { lib } from "mylib"; +   ~~~~~~~ + + +Found 1 error in index.ts:1 + +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +export {}; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: install package in node_modules +//// [/home/src/workspaces/project/node_modules/mylib/index.d.ts] *new* +export declare const lib: string; +//// [/home/src/workspaces/project/node_modules/mylib/index.js] *new* +exports.lib = "hello"; +//// [/home/src/workspaces/project/node_modules/mylib/package.json] *new* +{"name": "mylib", "main": "index.js", "types": "index.d.ts"} + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *rewrite with same content* + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/node_modules/mylib/index.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(used version) /home/src/workspaces/project/node_modules/mylib/index.d.ts +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-removed.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-removed.js new file mode 100644 index 00000000000..4df389ce46c --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-removed.js @@ -0,0 +1,78 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +import { lib } from "mylib"; +//// [/home/src/workspaces/project/node_modules/mylib/index.d.ts] *new* +export declare const lib: string; +//// [/home/src/workspaces/project/node_modules/mylib/index.js] *new* +exports.lib = "hello"; +//// [/home/src/workspaces/project/node_modules/mylib/package.json] *new* +{"name": "mylib", "main": "index.js", "types": "index.d.ts"} +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +export {}; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/node_modules/mylib/index.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: remove node_modules package +//// [/home/src/workspaces/project/node_modules/mylib/index.d.ts] *deleted* +//// [/home/src/workspaces/project/node_modules/mylib/index.js] *deleted* +//// [/home/src/workspaces/project/node_modules/mylib/package.json] *deleted* + + +Output:: +build starting at HH:MM:SS AM +index.ts:1:21 - error TS2307: Cannot find module 'mylib' or its corresponding type declarations. + +1 import { lib } from "mylib"; +   ~~~~~~~ + + +Found 1 error in index.ts:1 + +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *rewrite with same content* + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-main-field-edited.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-main-field-edited.js new file mode 100644 index 00000000000..2ed714c0ff0 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-main-field-edited.js @@ -0,0 +1,71 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +import { lib } from "mylib"; +//// [/home/src/workspaces/project/node_modules/mylib/new.d.ts] *new* +export declare const lib: string; +//// [/home/src/workspaces/project/node_modules/mylib/old.d.ts] *new* +export declare const lib: number; +//// [/home/src/workspaces/project/node_modules/mylib/package.json] *new* +{"name": "mylib", "types": "old.d.ts"} +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +export {}; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/node_modules/mylib/old.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: change package.json types field +//// [/home/src/workspaces/project/node_modules/mylib/package.json] *modified* +{"name": "mylib", "types": "new.d.ts"} + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *rewrite with same content* + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/node_modules/mylib/new.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(used version) /home/src/workspaces/project/node_modules/mylib/new.d.ts +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-scoped-package-installed.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-scoped-package-installed.js new file mode 100644 index 00000000000..1121205be03 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-scoped-package-installed.js @@ -0,0 +1,74 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +import { lib } from "@scope/mylib"; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +index.ts:1:21 - error TS2307: Cannot find module '@scope/mylib' or its corresponding type declarations. + +1 import { lib } from "@scope/mylib"; +   ~~~~~~~~~~~~~~ + + +Found 1 error in index.ts:1 + +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +export {}; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: install scoped package +//// [/home/src/workspaces/project/node_modules/@scope/mylib/index.d.ts] *new* +export declare const lib: string; +//// [/home/src/workspaces/project/node_modules/@scope/mylib/package.json] *new* +{"name": "@scope/mylib", "types": "index.d.ts"} + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *rewrite with same content* + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/node_modules/@scope/mylib/index.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(used version) /home/src/workspaces/project/node_modules/@scope/mylib/index.d.ts +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-file-rapidly-recreated.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-file-rapidly-recreated.js new file mode 100644 index 00000000000..d063a5aef0d --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-file-rapidly-recreated.js @@ -0,0 +1,73 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/data.ts] *new* +export const val = "original"; +//// [/home/src/workspaces/project/index.ts] *new* +import { val } from "./data"; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/data.js] *new* +export const val = "original"; + +//// [/home/src/workspaces/project/index.js] *new* +export {}; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/data.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: delete and immediately recreate with new content +//// [/home/src/workspaces/project/data.ts] *modified* +export const val = "recreated"; + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/data.js] *modified* +export const val = "recreated"; + +//// [/home/src/workspaces/project/index.js] *rewrite with same content* + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/data.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/data.ts +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js new file mode 100644 index 00000000000..f32c3028685 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js @@ -0,0 +1,206 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +const x = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +"use strict"; +const x = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: delete tsconfig +//// [/home/src/workspaces/project/tsconfig.json] *deleted* + + +Output:: +error TS5083: Cannot read file '/home/src/workspaces/project/tsconfig.json'. + + + +Diff:: incremental reports config read error while clean build without tsconfig prints usage help +--- nonIncremental.output.txt ++++ incremental.output.txt +@@ -1,144 +1,1 @@ +-Version FakeTSVersion +-tsc: The TypeScript Compiler - Version FakeTSVersion +- +-COMMON COMMANDS +- +- tsc +- Compiles the current project (tsconfig.json in the working directory.) +- +- tsc app.ts util.ts +- Ignoring tsconfig.json, compiles the specified files with default compiler options. +- +- tsc -b +- Build a composite project in the working directory. +- +- tsc --init +- Creates a tsconfig.json with the recommended settings in the working directory. +- +- tsc -p ./path/to/tsconfig.json +- Compiles the TypeScript project located at the specified path. +- +- tsc --help --all +- An expanded version of this information, showing all possible compiler options +- +- tsc --noEmit +- tsc --target esnext +- Compiles the current project, with additional settings. +- +-COMMAND LINE FLAGS +- +---help, -h +-Print this message. +- +---watch, -w +-Watch input files. +- +---all +-Show all compiler options. +- +---version, -v +-Print the compiler's version. +- +---init +-Initializes a TypeScript project and creates a tsconfig.json file. +- +---project, -p +-Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'. +- +---showConfig +-Print the final configuration instead of building. +- +---ignoreConfig +-Ignore the tsconfig found and build with commandline options and files. +- +---build, -b +-Build one or more projects and their dependencies, if out of date +- +-COMMON COMPILER OPTIONS +- +---pretty +-Enable color and formatting in TypeScript's output to make compiler errors easier to read. +-type: boolean +-default: true +- +---declaration, -d +-Generate .d.ts files from TypeScript and JavaScript files in your project. +-type: boolean +-default: `false`, unless `composite` is set +- +---declarationMap +-Create sourcemaps for d.ts files. +-type: boolean +-default: false +- +---emitDeclarationOnly +-Only output d.ts files and not JavaScript files. +-type: boolean +-default: false +- +---sourceMap +-Create source map files for emitted JavaScript files. +-type: boolean +-default: false +- +---noEmit +-Disable emitting files from a compilation. +-type: boolean +-default: false +- +---target, -t +-Set the JavaScript language version for emitted JavaScript and include compatible library declarations. +-one of: es6/es2015, es2016, es2017, es2018, es2019, es2020, es2021, es2022, es2023, es2024, es2025, esnext +-default: es2025 +- +---module, -m +-Specify what module code is generated. +-one of: commonjs, amd, system, umd, es6/es2015, es2020, es2022, esnext, node16, node18, node20, nodenext, preserve +-default: undefined +- +---lib +-Specify a set of bundled library declaration files that describe the target runtime environment. +-one or more: es5, es6/es2015, es7/es2016, es2017, es2018, es2019, es2020, es2021, es2022, es2023, es2024, es2025, esnext, dom, dom.iterable, dom.asynciterable, webworker, webworker.importscripts, webworker.iterable, webworker.asynciterable, scripthost, es2015.core, es2015.collection, es2015.generator, es2015.iterable, es2015.promise, es2015.proxy, es2015.reflect, es2015.symbol, es2015.symbol.wellknown, es2016.array.include, es2016.intl, es2017.arraybuffer, es2017.date, es2017.object, es2017.sharedmemory, es2017.string, es2017.intl, es2017.typedarrays, es2018.asyncgenerator, es2018.asynciterable/esnext.asynciterable, es2018.intl, es2018.promise, es2018.regexp, es2019.array, es2019.object, es2019.string, es2019.symbol/esnext.symbol, es2019.intl, es2020.bigint/esnext.bigint, es2020.date, es2020.promise, es2020.sharedmemory, es2020.string, es2020.symbol.wellknown, es2020.intl, es2020.number, es2021.promise, es2021.string, es2021.weakref/esnext.weakref, es2021.intl, es2022.array, es2022.error, es2022.intl, es2022.object, es2022.string, es2022.regexp, es2023.array, es2023.collection, es2023.intl, es2024.arraybuffer, es2024.collection, es2024.object/esnext.object, es2024.promise, es2024.regexp/esnext.regexp, es2024.sharedmemory, es2024.string/esnext.string, es2025.collection, es2025.float16/esnext.float16, es2025.intl, es2025.iterator/esnext.iterator, es2025.promise/esnext.promise, es2025.regexp, esnext.array, esnext.collection, esnext.date, esnext.decorators, esnext.disposable, esnext.error, esnext.intl, esnext.sharedmemory, esnext.temporal, esnext.typedarrays, decorators, decorators.legacy +-default: undefined +- +---allowJs +-Allow JavaScript files to be a part of your program. Use the 'checkJs' option to get errors from these files. +-type: boolean +-default: `false`, unless `checkJs` is set +- +---checkJs +-Enable error reporting in type-checked JavaScript files. +-type: boolean +-default: false +- +---jsx +-Specify what JSX code is generated. +-one of: preserve, react-native, react-jsx, react-jsxdev, react +-default: undefined +- +---outFile +-Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. +- +---outDir +-Specify an output folder for all emitted files. +- +---removeComments +-Disable emitting comments. +-type: boolean +-default: false +- +---strict +-Enable all strict type-checking options. +-type: boolean +-default: true +- +---types +-Specify type package names to be included without being referenced in a source file. +- +---esModuleInterop +-Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. +-type: boolean +-default: true +- +-You can learn about all of the compiler options at https://aka.ms/tsc +- ++error TS5083: Cannot read file '/home/src/workspaces/project/tsconfig.json'. \ No newline at end of file diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js new file mode 100644 index 00000000000..0063d01ff50 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js @@ -0,0 +1,70 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/base.json] *new* +{ + "compilerOptions": { "strict": false } +} +//// [/home/src/workspaces/project/index.ts] *new* +const x: number = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{ + "extends": "./base.json" +} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +"use strict"; +const x = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: modify base config to enable strict +//// [/home/src/workspaces/project/base.json] *modified* +{ + "compilerOptions": { "strict": true } +} + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-file-is-modified.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-file-is-modified.js new file mode 100644 index 00000000000..a77087e8ab6 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-file-is-modified.js @@ -0,0 +1,67 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +const x: number = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +"use strict"; +const x = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: modify file +//// [/home/src/workspaces/project/index.ts] *modified* +const x: number = 2; + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/index.js] *modified* +"use strict"; +const x = 2; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/index.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-source-file-is-deleted.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-source-file-is-deleted.js new file mode 100644 index 00000000000..4884b222a80 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-source-file-is-deleted.js @@ -0,0 +1,86 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/a.ts] *new* +import { b } from "./b"; +//// [/home/src/workspaces/project/b.ts] *new* +export const b = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/a.js] *new* +export {}; + +//// [/home/src/workspaces/project/b.js] *new* +export const b = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/b.ts +*refresh* /home/src/workspaces/project/a.ts +Signatures:: + + +Edit [0]:: delete imported file +//// [/home/src/workspaces/project/b.ts] *deleted* + + +Output:: +build starting at HH:MM:SS AM +a.ts:1:19 - error TS7016: Could not find a declaration file for module './b'. '/home/src/workspaces/project/b.js' implicitly has an 'any' type. + +1 import { b } from "./b"; +   ~~~~~ + + +Found 1 error in a.ts:1 + +build finished in d.ddds +//// [/home/src/workspaces/project/a.js] *rewrite with same content* + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/a.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/a.ts + + +Diff:: incremental resolves to .js output from prior build (TS7016) while clean build cannot find module at all (TS2307) +--- nonIncremental.output.txt ++++ incremental.output.txt +@@ -1,4 +1,4 @@ +-a.ts:1:19 - error TS2307: Cannot find module './b' or its corresponding type declarations. ++a.ts:1:19 - error TS7016: Could not find a declaration file for module './b'. '/home/src/workspaces/project/b.js' implicitly has an 'any' type. + + 1 import { b } from "./b"; +    ~~~~~ \ No newline at end of file diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-include-pattern-adds-file.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-include-pattern-adds-file.js new file mode 100644 index 00000000000..0d620e762a0 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-include-pattern-adds-file.js @@ -0,0 +1,73 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +const x = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{ + "compilerOptions": {}, + "include": ["*.ts"] +} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +"use strict"; +const x = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: widen include pattern to add src dir +//// [/home/src/workspaces/project/src/extra.ts] *new* +export const extra = 2; +//// [/home/src/workspaces/project/tsconfig.json] *modified* +{ + "compilerOptions": {}, + "include": ["*.ts", "src/**/*.ts"] +} + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/src/extra.js] *new* +export const extra = 2; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/src/extra.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/src/extra.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-modified-to-change-strict.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-modified-to-change-strict.js new file mode 100644 index 00000000000..1efa1bed4e8 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-modified-to-change-strict.js @@ -0,0 +1,77 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +const x = null; const y: string = x; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +index.ts:1:23 - error TS2322: Type 'null' is not assignable to type 'string'. + +1 const x = null; const y: string = x; +   ~ + + +Found 1 error in index.ts:1 + +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +"use strict"; +const x = null; +const y = x; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: enable strict mode +//// [/home/src/workspaces/project/tsconfig.json] *modified* +{"compilerOptions": {"strict": true}} + + +Output:: +build starting at HH:MM:SS AM +index.ts:1:23 - error TS2322: Type 'null' is not assignable to type 'string'. + +1 const x = null; const y: string = x; +   ~ + + +Found 1 error in index.ts:1 + +build finished in d.ddds + +tsconfig.json:: +SemanticDiagnostics:: +Signatures:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-build-when-no-files-change.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-build-when-no-files-change.js new file mode 100644 index 00000000000..25aaa126077 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-build-when-no-files-change.js @@ -0,0 +1,58 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +const x: number = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +"use strict"; +const x = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: no change + + +Output:: + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-rebuild-when-tsconfig-is-touched-but-content-unchanged.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-rebuild-when-tsconfig-is-touched-but-content-unchanged.js new file mode 100644 index 00000000000..2b42d08ddc2 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-rebuild-when-tsconfig-is-touched-but-content-unchanged.js @@ -0,0 +1,59 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/index.ts] *new* +const x = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/index.js] *new* +"use strict"; +const x = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: + + +Edit [0]:: touch tsconfig without changing content +//// [/home/src/workspaces/project/tsconfig.json] *mTime changed* + + +Output:: + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/index.ts +Signatures:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-files-list-entry-deleted.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-files-list-entry-deleted.js new file mode 100644 index 00000000000..ac79ed1e6e3 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-files-list-entry-deleted.js @@ -0,0 +1,67 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/a.ts] *new* +export const a = 1; +//// [/home/src/workspaces/project/b.ts] *new* +export const b = 2; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{ + "compilerOptions": {}, + "files": ["a.ts", "b.ts"] +} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/a.js] *new* +export const a = 1; + +//// [/home/src/workspaces/project/b.js] *new* +export const b = 2; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/a.ts +*refresh* /home/src/workspaces/project/b.ts +Signatures:: + + +Edit [0]:: delete file listed in files array +//// [/home/src/workspaces/project/b.ts] *deleted* + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds + +tsconfig.json:: +SemanticDiagnostics:: +Signatures:: From 7c564ba3f19b804d38fc32d9cf520e23060ec4fd Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Tue, 24 Mar 2026 17:45:53 -0500 Subject: [PATCH 03/30] removed unnecessary code and updated tests --- internal/execute/tsctests/tscwatch_test.go | 2 +- internal/execute/watcher.go | 16 ---------------- ...andles-tsconfig-with-extends-base-modified.js | 13 +++++++++++-- 3 files changed, 12 insertions(+), 19 deletions(-) diff --git a/internal/execute/tsctests/tscwatch_test.go b/internal/execute/tsctests/tscwatch_test.go index a633578bd04..f499c6283a0 100644 --- a/internal/execute/tsctests/tscwatch_test.go +++ b/internal/execute/tsctests/tscwatch_test.go @@ -238,7 +238,7 @@ func TestWatch(t *testing.T) { { subScenario: "watch handles tsconfig with extends base modified", files: FileMap{ - "/home/src/workspaces/project/index.ts": `const x: number = 1;`, + "/home/src/workspaces/project/index.ts": `const x = null; const y: string = x;`, "/home/src/workspaces/project/base.json": `{ "compilerOptions": { "strict": false } }`, diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 0c359441ad0..a1994dbc3aa 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -186,22 +186,6 @@ func (w *Watcher) hasErrorsInTsConfig() bool { return false } - // Skip re-parsing if the config file hasn't changed since last check. - if w.watchState != nil { - if entry, ok := w.watchState[w.configFileName]; ok { - s := w.sys.FS().Stat(w.configFileName) - unchanged := false - if !entry.exists { - unchanged = s == nil - } else { - unchanged = s != nil && s.ModTime().Equal(entry.modTime) - } - if unchanged { - return w.configHasErrors - } - } - } - extendedConfigCache := &tsc.ExtendedConfigCache{} configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(w.configFileName, w.compilerOptionsFromCommandLine, nil, w.sys, extendedConfigCache) if len(errors) > 0 { diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js index 0063d01ff50..82e002da61c 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js @@ -6,7 +6,7 @@ Input:: "compilerOptions": { "strict": false } } //// [/home/src/workspaces/project/index.ts] *new* -const x: number = 1; +const x = null; const y: string = x; //// [/home/src/workspaces/project/tsconfig.json] *new* { "extends": "./base.json" @@ -42,7 +42,8 @@ interface Symbol { declare const console: { log(msg: any): void; }; //// [/home/src/workspaces/project/index.js] *new* "use strict"; -const x = 1; +const x = null; +const y = x; tsconfig.json:: @@ -61,6 +62,14 @@ Edit [0]:: modify base config to enable strict Output:: build starting at HH:MM:SS AM +index.ts:1:23 - error TS2322: Type 'null' is not assignable to type 'string'. + +1 const x = null; const y: string = x; +   ~ + + +Found 1 error in index.ts:1 + build finished in d.ddds tsconfig.json:: From 95cb142279d6f7fb0dea621371636dd04c201cd8 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:31:47 -0500 Subject: [PATCH 04/30] addressed copilot comments --- internal/execute/tsctests/tscwatch_test.go | 2 +- internal/execute/watcher.go | 4 +--- ...ed.js => watch-detects-package-json-types-field-edited.js} | 0 3 files changed, 2 insertions(+), 4 deletions(-) rename testdata/baselines/reference/tscWatch/commandLineWatch/{watch-detects-package-json-main-field-edited.js => watch-detects-package-json-types-field-edited.js} (100%) diff --git a/internal/execute/tsctests/tscwatch_test.go b/internal/execute/tsctests/tscwatch_test.go index f499c6283a0..2d945e2bd4f 100644 --- a/internal/execute/tsctests/tscwatch_test.go +++ b/internal/execute/tsctests/tscwatch_test.go @@ -323,7 +323,7 @@ func TestWatch(t *testing.T) { }, }, { - subScenario: "watch detects package json main field edited", + subScenario: "watch detects package json types field edited", files: FileMap{ "/home/src/workspaces/project/index.ts": `import { lib } from "mylib";`, "/home/src/workspaces/project/tsconfig.json": `{}`, diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index a1994dbc3aa..3b4fea74557 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -3,7 +3,6 @@ package execute import ( "fmt" "reflect" - "slices" "time" "github.com/microsoft/typescript-go/internal/collections" @@ -198,8 +197,7 @@ func (w *Watcher) hasErrorsInTsConfig() bool { return true } w.configHasErrors = false - if !reflect.DeepEqual(w.config.CompilerOptions().Clone(), configParseResult.CompilerOptions().Clone()) || - !slices.Equal(w.config.FileNames(), configParseResult.FileNames()) { + if !reflect.DeepEqual(w.config.ParsedConfig, configParseResult.ParsedConfig) { w.configModified = true } w.config = configParseResult diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-main-field-edited.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-types-field-edited.js similarity index 100% rename from testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-main-field-edited.js rename to testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-types-field-edited.js From bcdaabcaf514c440e06ea4181d3fb819d2ae4715 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Tue, 24 Mar 2026 18:33:22 -0500 Subject: [PATCH 05/30] forced configmodified true --- internal/execute/watcher.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 3b4fea74557..8e0afabf99d 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -196,6 +196,9 @@ func (w *Watcher) hasErrorsInTsConfig() bool { } return true } + if w.configHasErrors { + w.configModified = true + } w.configHasErrors = false if !reflect.DeepEqual(w.config.ParsedConfig, configParseResult.ParsedConfig) { w.configModified = true From fec40581428fa65ca711e8317acf0ccabbebe1c8 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Wed, 25 Mar 2026 10:41:16 -0500 Subject: [PATCH 06/30] added coarse diff checking for tsconfig --- internal/execute/watcher.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 8e0afabf99d..9dfdcf173b4 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -72,6 +72,7 @@ type Watcher struct { extendedConfigCache *tsc.ExtendedConfigCache configModified bool configHasErrors bool + lastConfigMtime time.Time watchState map[string]watchEntry } @@ -185,6 +186,12 @@ func (w *Watcher) hasErrorsInTsConfig() bool { return false } + if !w.configHasErrors { + if s := w.sys.FS().Stat(w.configFileName); s != nil && s.ModTime().Equal(w.lastConfigMtime) { + return false + } + } + extendedConfigCache := &tsc.ExtendedConfigCache{} configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(w.configFileName, w.compilerOptionsFromCommandLine, nil, w.sys, extendedConfigCache) if len(errors) > 0 { @@ -200,6 +207,9 @@ func (w *Watcher) hasErrorsInTsConfig() bool { w.configModified = true } w.configHasErrors = false + if s := w.sys.FS().Stat(w.configFileName); s != nil { + w.lastConfigMtime = s.ModTime() + } if !reflect.DeepEqual(w.config.ParsedConfig, configParseResult.ParsedConfig) { w.configModified = true } From 5ef0db6f330745e304661571bafc595bf018b458 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:17:41 -0500 Subject: [PATCH 07/30] added wildcard includes and seperated program building logic for lsp integration --- internal/execute/tsctests/tscwatch_test.go | 32 ++- internal/execute/watcher.go | 210 +++++++++++------- ...to-previously-non-existent-include-path.js | 12 +- ...h-detects-file-renamed-and-renamed-back.js | 9 +- ...-new-file-in-existing-include-directory.js | 69 ++++++ ...onfig-is-touched-but-content-unchanged.js} | 4 +- 6 files changed, 242 insertions(+), 94 deletions(-) create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js rename testdata/baselines/reference/tscWatch/commandLineWatch/{watch-skips-rebuild-when-tsconfig-is-touched-but-content-unchanged.js => watch-rebuilds-when-tsconfig-is-touched-but-content-unchanged.js} (92%) diff --git a/internal/execute/tsctests/tscwatch_test.go b/internal/execute/tsctests/tscwatch_test.go index 2d945e2bd4f..1ebf27fea99 100644 --- a/internal/execute/tsctests/tscwatch_test.go +++ b/internal/execute/tsctests/tscwatch_test.go @@ -174,9 +174,33 @@ func TestWatch(t *testing.T) { }, commandLineArgs: []string{"--watch"}, edits: []*tscEdit{ - newTscEdit("create src dir with ts file matching include", func(sys *TestSys) { - sys.writeFileNoError("/home/src/workspaces/project/src/helper.ts", `export const helper = "added";`) - }), + { + caption: "create src dir with ts file matching include", + edit: func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/src/helper.ts", `export const helper = "added";`) + }, + expectedDiff: "incremental skips emit for new unreferenced file", + }, + }, + }, + { + subScenario: "watch detects new file in existing include directory", + files: FileMap{ + "/home/src/workspaces/project/src/a.ts": `export const a = 1;`, + "/home/src/workspaces/project/tsconfig.json": `{ + "compilerOptions": {}, + "include": ["src/**/*.ts"] +}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + { + caption: "add new file to existing src directory", + edit: func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/src/b.ts", `export const b = 2;`) + }, + expectedDiff: "incremental skips emit for new unreferenced file", + }, }, }, // Path resolution: import from non-existent node_modules package @@ -256,7 +280,7 @@ func TestWatch(t *testing.T) { }, }, { - subScenario: "watch skips rebuild when tsconfig is touched but content unchanged", + subScenario: "watch rebuilds when tsconfig is touched but content unchanged", files: FileMap{ "/home/src/workspaces/project/index.ts": `const x = 1;`, "/home/src/workspaces/project/tsconfig.json": `{}`, diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 9dfdcf173b4..2565b7389c7 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -54,11 +54,93 @@ func (fs *trackingFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { } func (fs *trackingFS) Realpath(path string) string { return fs.inner.Realpath(path) } -type watchEntry struct { +type WatchEntry struct { modTime time.Time exists bool } +type FileWatcher struct { + fs vfs.FS + pollInterval time.Duration + testing bool + callback func() + watchState map[string]WatchEntry +} + +func newFileWatcher(fs vfs.FS, pollInterval time.Duration, testing bool, callback func()) *FileWatcher { + return &FileWatcher{ + fs: fs, + pollInterval: pollInterval, + testing: testing, + callback: callback, + } +} + +func (fw *FileWatcher) updateWatchedFiles(tfs *trackingFS) { + fw.watchState = make(map[string]WatchEntry) + tfs.seenFiles.Range(func(fn string) bool { + if s := fw.fs.Stat(fn); s != nil { + fw.watchState[fn] = WatchEntry{modTime: s.ModTime(), exists: true} + } else { + fw.watchState[fn] = WatchEntry{exists: false} + } + return true + }) +} + +func (fw *FileWatcher) WaitForSettled(now func() time.Time) { + if fw.testing { + return + } + current := fw.currentState() + settledAt := now() + for now().Sub(settledAt) < watchDebounceWait { + time.Sleep(fw.pollInterval) + if fw.HasChanges(current) { + current = fw.currentState() + settledAt = now() + } + } +} + +func (fw *FileWatcher) currentState() map[string]WatchEntry { + state := make(map[string]WatchEntry, len(fw.watchState)) + for path := range fw.watchState { + if s := fw.fs.Stat(path); s != nil { + state[path] = WatchEntry{modTime: s.ModTime(), exists: true} + } else { + state[path] = WatchEntry{exists: false} + } + } + return state +} + +func (fw *FileWatcher) HasChanges(baseline map[string]WatchEntry) bool { + for path, old := range baseline { + s := fw.fs.Stat(path) + if !old.exists { + if s != nil { + return true + } + } else { + if s == nil || !s.ModTime().Equal(old.modTime) { + return true + } + } + } + return false +} + +func (fw *FileWatcher) Run(now func() time.Time) { + for { + time.Sleep(fw.pollInterval) + if fw.watchState == nil || fw.HasChanges(fw.watchState) { + fw.WaitForSettled(now) + fw.callback() + } + } +} + type Watcher struct { sys tsc.System configFileName string @@ -72,9 +154,9 @@ type Watcher struct { extendedConfigCache *tsc.ExtendedConfigCache configModified bool configHasErrors bool - lastConfigMtime time.Time + configFilePaths []string - watchState map[string]watchEntry + fileWatcher *FileWatcher } var ( @@ -101,6 +183,12 @@ func createWatcher( if configParseResult.ConfigFile != nil { w.configFileName = configParseResult.ConfigFile.SourceFile.FileName() } + w.fileWatcher = newFileWatcher( + sys.FS(), + w.config.ParsedConfig.WatchOptions.WatchInterval(), + testing != nil, + w.DoCycle, + ) return w } @@ -109,40 +197,28 @@ func (w *Watcher) start() { host := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) w.program = incremental.ReadBuildInfoProgram(w.config, incremental.NewBuildInfoReader(host), host) + if w.configFileName != "" { + w.configFilePaths = append([]string{w.configFileName}, w.config.ExtendedSourceFiles()...) + } + w.doBuild() if w.testing == nil { - for { - time.Sleep(w.pollInterval()) - w.DoCycle() - } + w.fileWatcher.Run(w.sys.Now) } } func (w *Watcher) DoCycle() { if w.hasErrorsInTsConfig() { - // these are unrecoverable errors--report them and do not build return } - if w.watchState != nil && !w.configModified && !w.hasWatchedFilesChanged() { + if w.fileWatcher.watchState != nil && !w.configModified && !w.fileWatcher.HasChanges(w.fileWatcher.watchState) { if w.testing != nil { w.testing.OnProgram(w.program) } return } - if w.testing == nil { - w.refreshWatchState() - settledAt := w.sys.Now() - for w.sys.Now().Sub(settledAt) < watchDebounceWait { - time.Sleep(w.pollInterval()) - if w.hasWatchedFilesChanged() { - w.refreshWatchState() - settledAt = w.sys.Now() - } - } - } - w.doBuild() } @@ -159,7 +235,16 @@ func (w *Watcher) doBuild() { }), w.program, nil, w.testing != nil) w.compileAndEmit() - w.buildWatchState(tfs) + if w.config.ConfigFile != nil { + for dir := range w.config.WildcardDirectories() { + tfs.seenFiles.Add(dir) + } + } + for _, path := range w.configFilePaths { + tfs.seenFiles.Add(path) + } + w.fileWatcher.updateWatchedFiles(tfs) + w.fileWatcher.pollInterval = w.config.ParsedConfig.WatchOptions.WatchInterval() w.configModified = false fmt.Fprintf(w.sys.Writer(), "build finished in %.3fs\n", w.sys.Now().Sub(timeStart).Seconds()) @@ -186,8 +271,25 @@ func (w *Watcher) hasErrorsInTsConfig() bool { return false } - if !w.configHasErrors { - if s := w.sys.FS().Stat(w.configFileName); s != nil && s.ModTime().Equal(w.lastConfigMtime) { + if !w.configHasErrors && len(w.configFilePaths) > 0 { + changed := false + for _, path := range w.configFilePaths { + if old, ok := w.fileWatcher.watchState[path]; ok { + s := w.sys.FS().Stat(path) + if !old.exists { + if s != nil { + changed = true + break + } + } else { + if s == nil || !s.ModTime().Equal(old.modTime) { + changed = true + break + } + } + } + } + if !changed { return false } } @@ -207,9 +309,7 @@ func (w *Watcher) hasErrorsInTsConfig() bool { w.configModified = true } w.configHasErrors = false - if s := w.sys.FS().Stat(w.configFileName); s != nil { - w.lastConfigMtime = s.ModTime() - } + w.configFilePaths = append([]string{w.configFileName}, configParseResult.ExtendedSourceFiles()...) if !reflect.DeepEqual(w.config.ParsedConfig, configParseResult.ParsedConfig) { w.configModified = true } @@ -218,69 +318,23 @@ func (w *Watcher) hasErrorsInTsConfig() bool { return false } -func (w *Watcher) hasWatchedFilesChanged() bool { - for path, old := range w.watchState { - s := w.sys.FS().Stat(path) - if !old.exists { - if s != nil { - return true - } - } else { - if s == nil || !s.ModTime().Equal(old.modTime) { - return true - } - } - } - return false -} - -func (w *Watcher) buildWatchState(tfs *trackingFS) { - w.watchState = make(map[string]watchEntry) - tfs.seenFiles.Range(func(fn string) bool { - if s := w.sys.FS().Stat(fn); s != nil { - w.watchState[fn] = watchEntry{modTime: s.ModTime(), exists: true} - } else { - w.watchState[fn] = watchEntry{exists: false} - } - return true - }) -} - -func (w *Watcher) refreshWatchState() { - for path := range w.watchState { - if s := w.sys.FS().Stat(path); s != nil { - w.watchState[path] = watchEntry{modTime: s.ModTime(), exists: true} - } else { - w.watchState[path] = watchEntry{exists: false} - } - } -} - -func (w *Watcher) pollInterval() time.Duration { - return w.config.ParsedConfig.WatchOptions.WatchInterval() -} - -// Testing helpers — exported for use by test packages. +// Testing helpers — exported for use by test packages func (w *Watcher) HasWatchedFilesChanged() bool { - return w.hasWatchedFilesChanged() -} - -func (w *Watcher) RefreshWatchState() { - w.refreshWatchState() + return w.fileWatcher.HasChanges(w.fileWatcher.watchState) } func (w *Watcher) WatchStateLen() int { - return len(w.watchState) + return len(w.fileWatcher.watchState) } func (w *Watcher) WatchStateHas(path string) bool { - _, ok := w.watchState[path] + _, ok := w.fileWatcher.watchState[path] return ok } func (w *Watcher) DebugWatchState(fn func(path string, modTime time.Time, exists bool)) { - for path, entry := range w.watchState { + for path, entry := range w.fileWatcher.watchState { fn(path, entry.modTime, entry.exists) } } diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js index d339fc37347..b3017b1de24 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js @@ -57,12 +57,14 @@ export const helper = "added"; Output:: build starting at HH:MM:SS AM build finished in d.ddds -//// [/home/src/workspaces/project/src/helper.js] *new* -export const helper = "added"; - tsconfig.json:: SemanticDiagnostics:: -*refresh* /home/src/workspaces/project/src/helper.ts Signatures:: -(computed .d.ts) /home/src/workspaces/project/src/helper.ts + + +Diff:: incremental skips emit for new unreferenced file +--- nonIncremental /home/src/workspaces/project/src/helper.js ++++ incremental /home/src/workspaces/project/src/helper.js +@@ -1,1 +0,0 @@ +-export const helper = "added"; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js index 22d830a3c8b..0b7d34824d9 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js @@ -68,21 +68,20 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 build finished in d.ddds -//// [/home/src/workspaces/project/helper2.js] *new* -export const helper = 1; - //// [/home/src/workspaces/project/index.js] *rewrite with same content* tsconfig.json:: SemanticDiagnostics:: -*refresh* /home/src/workspaces/project/helper2.ts *refresh* /home/src/workspaces/project/index.ts Signatures:: -(computed .d.ts) /home/src/workspaces/project/helper2.ts (computed .d.ts) /home/src/workspaces/project/index.ts Diff:: incremental resolves to .js output from prior build while clean build cannot find module +--- nonIncremental /home/src/workspaces/project/helper2.js ++++ incremental /home/src/workspaces/project/helper2.js +@@ -1,1 +0,0 @@ +-export const helper = 1; --- nonIncremental.output.txt +++ incremental.output.txt @@ -1,4 +1,4 @@ diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js new file mode 100644 index 00000000000..06453e759ce --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js @@ -0,0 +1,69 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/src/a.ts] *new* +export const a = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{ + "compilerOptions": {}, + "include": ["src/**/*.ts"] +} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/src/a.js] *new* +export const a = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/src/a.ts +Signatures:: + + +Edit [0]:: add new file to existing src directory +//// [/home/src/workspaces/project/src/b.ts] *new* +export const b = 2; + + +Output:: + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/src/a.ts +Signatures:: + + +Diff:: incremental skips emit for new unreferenced file +--- nonIncremental /home/src/workspaces/project/src/b.js ++++ incremental /home/src/workspaces/project/src/b.js +@@ -1,1 +0,0 @@ +-export const b = 2; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-rebuild-when-tsconfig-is-touched-but-content-unchanged.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-touched-but-content-unchanged.js similarity index 92% rename from testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-rebuild-when-tsconfig-is-touched-but-content-unchanged.js rename to testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-touched-but-content-unchanged.js index 2b42d08ddc2..8813ad18197 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-rebuild-when-tsconfig-is-touched-but-content-unchanged.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-touched-but-content-unchanged.js @@ -51,9 +51,9 @@ Edit [0]:: touch tsconfig without changing content Output:: +build starting at HH:MM:SS AM +build finished in d.ddds tsconfig.json:: SemanticDiagnostics:: -*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*refresh* /home/src/workspaces/project/index.ts Signatures:: From 1fa230ff56f176003889a552629040e5a36951e9 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:38:10 -0500 Subject: [PATCH 08/30] moved wildcard logic and switched to cachedvfs --- internal/execute/watcher.go | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 2565b7389c7..8cf331d4fc7 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -12,6 +12,7 @@ import ( "github.com/microsoft/typescript-go/internal/execute/tsc" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/cachedvfs" ) const watchDebounceWait = 250 * time.Millisecond @@ -223,9 +224,19 @@ func (w *Watcher) DoCycle() { } func (w *Watcher) doBuild() { - tfs := &trackingFS{inner: w.sys.FS()} + cached := cachedvfs.From(w.sys.FS()) + tfs := &trackingFS{inner: cached} host := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), tfs, w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) + if w.config.ConfigFile != nil { + for dir := range w.config.WildcardDirectories() { + tfs.seenFiles.Add(dir) + } + } + for _, path := range w.configFilePaths { + tfs.seenFiles.Add(path) + } + fmt.Fprintln(w.sys.Writer(), "build starting at", w.sys.Now().Format("03:04:05 PM")) timeStart := w.sys.Now() @@ -235,14 +246,7 @@ func (w *Watcher) doBuild() { }), w.program, nil, w.testing != nil) w.compileAndEmit() - if w.config.ConfigFile != nil { - for dir := range w.config.WildcardDirectories() { - tfs.seenFiles.Add(dir) - } - } - for _, path := range w.configFilePaths { - tfs.seenFiles.Add(path) - } + cached.DisableAndClearCache() w.fileWatcher.updateWatchedFiles(tfs) w.fileWatcher.pollInterval = w.config.ParsedConfig.WatchOptions.WatchInterval() w.configModified = false From bab9ed09f14d77c5a69f0b85d2ee4cb9de14b840 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Wed, 25 Mar 2026 14:42:58 -0500 Subject: [PATCH 09/30] accepted baseline --- .../tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js index f32c3028685..f7bd02d666b 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js @@ -154,7 +154,7 @@ Diff:: incremental reports config read error while clean build without tsconfig - ---module, -m -Specify what module code is generated. --one of: commonjs, amd, system, umd, es6/es2015, es2020, es2022, esnext, node16, node18, node20, nodenext, preserve +-one of: commonjs, es6/es2015, es2020, es2022, esnext, node16, node18, node20, nodenext, preserve -default: undefined - ---lib From bc5873a3e6a851b48bd6482d7b0827774db71a31 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:12:57 -0500 Subject: [PATCH 10/30] added basic ast caching --- internal/execute/watcher.go | 59 +++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 8cf331d4fc7..ee7f7aa5523 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -5,12 +5,15 @@ import ( "reflect" "time" + "github.com/microsoft/typescript-go/internal/ast" "github.com/microsoft/typescript-go/internal/collections" "github.com/microsoft/typescript-go/internal/compiler" "github.com/microsoft/typescript-go/internal/core" + "github.com/microsoft/typescript-go/internal/diagnostics" "github.com/microsoft/typescript-go/internal/execute/incremental" "github.com/microsoft/typescript-go/internal/execute/tsc" "github.com/microsoft/typescript-go/internal/tsoptions" + "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/cachedvfs" ) @@ -142,6 +145,51 @@ func (fw *FileWatcher) Run(now func() time.Time) { } } +type cachedSourceFile struct { + file *ast.SourceFile + modTime time.Time +} + +type watchCompilerHost struct { + inner compiler.CompilerHost + cache *collections.SyncMap[tspath.Path, *cachedSourceFile] +} + +var _ compiler.CompilerHost = (*watchCompilerHost)(nil) + +func (h *watchCompilerHost) FS() vfs.FS { return h.inner.FS() } +func (h *watchCompilerHost) DefaultLibraryPath() string { return h.inner.DefaultLibraryPath() } +func (h *watchCompilerHost) GetCurrentDirectory() string { return h.inner.GetCurrentDirectory() } +func (h *watchCompilerHost) Trace(msg *diagnostics.Message, args ...any) { + h.inner.Trace(msg, args...) +} +func (h *watchCompilerHost) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { + return h.inner.GetResolvedProjectReference(fileName, path) +} + +func (h *watchCompilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile { + if cached, ok := h.cache.Load(opts.Path); ok { + info := h.inner.FS().Stat(opts.FileName) + if info != nil && info.ModTime().Equal(cached.modTime) { + return cached.file + } + } + + file := h.inner.GetSourceFile(opts) + if file != nil { + info := h.inner.FS().Stat(opts.FileName) + if info != nil { + h.cache.Store(opts.Path, &cachedSourceFile{ + file: file, + modTime: info.ModTime(), + }) + } + } else { + h.cache.Delete(opts.Path) + } + return file +} + type Watcher struct { sys tsc.System configFileName string @@ -157,7 +205,8 @@ type Watcher struct { configHasErrors bool configFilePaths []string - fileWatcher *FileWatcher + sourceFileCache *collections.SyncMap[tspath.Path, *cachedSourceFile] + fileWatcher *FileWatcher } var ( @@ -180,6 +229,7 @@ func createWatcher( reportDiagnostic: reportDiagnostic, reportErrorSummary: reportErrorSummary, testing: testing, + sourceFileCache: &collections.SyncMap[tspath.Path, *cachedSourceFile]{}, } if configParseResult.ConfigFile != nil { w.configFileName = configParseResult.ConfigFile.SourceFile.FileName() @@ -224,9 +274,14 @@ func (w *Watcher) DoCycle() { } func (w *Watcher) doBuild() { + if w.configModified { + w.sourceFileCache = &collections.SyncMap[tspath.Path, *cachedSourceFile]{} + } + cached := cachedvfs.From(w.sys.FS()) tfs := &trackingFS{inner: cached} - host := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), tfs, w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) + innerHost := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), tfs, w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) + host := &watchCompilerHost{inner: innerHost, cache: w.sourceFileCache} if w.config.ConfigFile != nil { for dir := range w.config.WildcardDirectories() { From c676f362378cb8d3f2b875545b579a2880e62120 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Thu, 26 Mar 2026 14:18:38 -0500 Subject: [PATCH 11/30] fixed panic #3015 and formatting issues --- internal/execute/watcher.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index ee7f7aa5523..28879659876 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -157,12 +157,13 @@ type watchCompilerHost struct { var _ compiler.CompilerHost = (*watchCompilerHost)(nil) -func (h *watchCompilerHost) FS() vfs.FS { return h.inner.FS() } -func (h *watchCompilerHost) DefaultLibraryPath() string { return h.inner.DefaultLibraryPath() } -func (h *watchCompilerHost) GetCurrentDirectory() string { return h.inner.GetCurrentDirectory() } +func (h *watchCompilerHost) FS() vfs.FS { return h.inner.FS() } +func (h *watchCompilerHost) DefaultLibraryPath() string { return h.inner.DefaultLibraryPath() } +func (h *watchCompilerHost) GetCurrentDirectory() string { return h.inner.GetCurrentDirectory() } func (h *watchCompilerHost) Trace(msg *diagnostics.Message, args ...any) { h.inner.Trace(msg, args...) } + func (h *watchCompilerHost) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { return h.inner.GetResolvedProjectReference(fileName, path) } @@ -317,6 +318,7 @@ func (w *Watcher) compileAndEmit() { Sys: w.sys, ProgramLike: w.program, Program: w.program.GetProgram(), + Config: w.config, ReportDiagnostic: w.reportDiagnostic, ReportErrorSummary: w.reportErrorSummary, Writer: w.sys.Writer(), From ddc2738fb1e6fd2922a16f388a3cb2db6d991f68 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:20:31 -0500 Subject: [PATCH 12/30] added further cache eviction --- internal/execute/watcher.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 28879659876..29a8eb0a8a2 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -306,6 +306,15 @@ func (w *Watcher) doBuild() { w.fileWatcher.updateWatchedFiles(tfs) w.fileWatcher.pollInterval = w.config.ParsedConfig.WatchOptions.WatchInterval() w.configModified = false + + programFiles := w.program.GetProgram().FilesByPath() + w.sourceFileCache.Range(func(path tspath.Path, _ *cachedSourceFile) bool { + if _, ok := programFiles[path]; !ok { + w.sourceFileCache.Delete(path) + } + return true + }) + fmt.Fprintf(w.sys.Writer(), "build finished in %.3fs\n", w.sys.Now().Sub(timeStart).Seconds()) if w.testing != nil { From aa81e0a78e2f8f26128fd9e84a8e084af421b851 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Fri, 27 Mar 2026 13:57:55 -0500 Subject: [PATCH 13/30] added wildcard recursive walking and testing --- internal/execute/tsctests/tscwatch_test.go | 69 ++++++++++++-- internal/execute/watcher.go | 78 ++++++++++++++-- ...tiple-new-subdirectories-simultaneously.js | 82 +++++++++++++++++ ...s-file-added-in-new-nested-subdirectory.js | 67 ++++++++++++++ ...to-previously-non-existent-include-path.js | 12 +-- ...h-detects-file-renamed-and-renamed-back.js | 9 +- ...sted-subdirectory-removed-and-recreated.js | 91 +++++++++++++++++++ ...-new-file-in-existing-include-directory.js | 16 ++-- 8 files changed, 388 insertions(+), 36 deletions(-) create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-multiple-new-subdirectories-simultaneously.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-new-nested-subdirectory.js create mode 100644 testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-nested-subdirectory-removed-and-recreated.js diff --git a/internal/execute/tsctests/tscwatch_test.go b/internal/execute/tsctests/tscwatch_test.go index 1ebf27fea99..44e041f5a60 100644 --- a/internal/execute/tsctests/tscwatch_test.go +++ b/internal/execute/tsctests/tscwatch_test.go @@ -174,13 +174,9 @@ func TestWatch(t *testing.T) { }, commandLineArgs: []string{"--watch"}, edits: []*tscEdit{ - { - caption: "create src dir with ts file matching include", - edit: func(sys *TestSys) { - sys.writeFileNoError("/home/src/workspaces/project/src/helper.ts", `export const helper = "added";`) - }, - expectedDiff: "incremental skips emit for new unreferenced file", - }, + newTscEdit("create src dir with ts file matching include", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/src/helper.ts", `export const helper = "added";`) + }), }, }, { @@ -190,17 +186,70 @@ func TestWatch(t *testing.T) { "/home/src/workspaces/project/tsconfig.json": `{ "compilerOptions": {}, "include": ["src/**/*.ts"] +}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("add new file to existing src directory", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/src/b.ts", `export const b = 2;`) + }), + }, + }, + // Wildcard include: nested subdirectory detection + { + subScenario: "watch detects file added in new nested subdirectory", + files: FileMap{ + "/home/src/workspaces/project/src/a.ts": `export const a = 1;`, + "/home/src/workspaces/project/tsconfig.json": `{ + "compilerOptions": {}, + "include": ["src/**/*.ts"] +}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("create nested dir with ts file", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/src/deep/nested/util.ts", `export const util = "nested";`) + }), + }, + }, + { + subScenario: "watch detects file added in multiple new subdirectories simultaneously", + files: FileMap{ + "/home/src/workspaces/project/src/a.ts": `export const a = 1;`, + "/home/src/workspaces/project/tsconfig.json": `{ + "compilerOptions": {}, + "include": ["src/**/*.ts"] +}`, + }, + commandLineArgs: []string{"--watch"}, + edits: []*tscEdit{ + newTscEdit("create multiple new subdirs with files", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/src/models/user.ts", `export interface User { name: string; }`) + sys.writeFileNoError("/home/src/workspaces/project/src/utils/format.ts", `export function format(s: string): string { return s.trim(); }`) + }), + }, + }, + { + subScenario: "watch detects nested subdirectory removed and recreated", + files: FileMap{ + "/home/src/workspaces/project/src/lib/helper.ts": `export const helper = "v1";`, + "/home/src/workspaces/project/tsconfig.json": `{ + "compilerOptions": {}, + "include": ["src/**/*.ts"] }`, }, commandLineArgs: []string{"--watch"}, edits: []*tscEdit{ { - caption: "add new file to existing src directory", + caption: "remove nested dir", + expectedDiff: "incremental has prior state and does not report no-inputs error", edit: func(sys *TestSys) { - sys.writeFileNoError("/home/src/workspaces/project/src/b.ts", `export const b = 2;`) + sys.removeNoError("/home/src/workspaces/project/src/lib/helper.ts") }, - expectedDiff: "incremental skips emit for new unreferenced file", }, + newTscEdit("recreate nested dir with new content", func(sys *TestSys) { + sys.writeFileNoError("/home/src/workspaces/project/src/lib/helper.ts", `export const helper = "v2";`) + }), }, }, // Path resolution: import from non-existent node_modules package diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 29a8eb0a8a2..aa6f26d2b68 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -3,6 +3,7 @@ package execute import ( "fmt" "reflect" + "slices" "time" "github.com/microsoft/typescript-go/internal/ast" @@ -64,11 +65,12 @@ type WatchEntry struct { } type FileWatcher struct { - fs vfs.FS - pollInterval time.Duration - testing bool - callback func() - watchState map[string]WatchEntry + fs vfs.FS + pollInterval time.Duration + testing bool + callback func() + watchState map[string]WatchEntry + wildcardDirectories map[string]bool // dir path -> recursive flag } func newFileWatcher(fs vfs.FS, pollInterval time.Duration, testing bool, callback func()) *FileWatcher { @@ -90,6 +92,22 @@ func (fw *FileWatcher) updateWatchedFiles(tfs *trackingFS) { } return true }) + for dir, recursive := range fw.wildcardDirectories { + if !recursive { + continue + } + fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + if _, ok := fw.watchState[path]; !ok { + if s := fw.fs.Stat(path); s != nil { + fw.watchState[path] = WatchEntry{modTime: s.ModTime(), exists: true} + } + } + return nil + }) + } } func (fw *FileWatcher) WaitForSettled(now func() time.Time) { @@ -116,6 +134,22 @@ func (fw *FileWatcher) currentState() map[string]WatchEntry { state[path] = WatchEntry{exists: false} } } + for dir, recursive := range fw.wildcardDirectories { + if !recursive { + continue + } + fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + if _, ok := state[path]; !ok { + if s := fw.fs.Stat(path); s != nil { + state[path] = WatchEntry{modTime: s.ModTime(), exists: true} + } + } + return nil + }) + } return state } @@ -132,6 +166,25 @@ func (fw *FileWatcher) HasChanges(baseline map[string]WatchEntry) bool { } } } + for dir, recursive := range fw.wildcardDirectories { + if !recursive { + continue + } + found := false + fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + if _, ok := baseline[path]; !ok { + found = true + return vfs.SkipAll + } + return nil + }) + if found { + return true + } + } return false } @@ -265,6 +318,14 @@ func (w *Watcher) DoCycle() { return } if w.fileWatcher.watchState != nil && !w.configModified && !w.fileWatcher.HasChanges(w.fileWatcher.watchState) { + if w.config.ConfigFile != nil && len(w.config.WildcardDirectories()) > 0 { + updated := w.config.ReloadFileNamesOfParsedCommandLine(w.sys.FS()) + if !slices.Equal(w.config.FileNames(), updated.FileNames()) { + w.config = updated + w.doBuild() + return + } + } if w.testing != nil { w.testing.OnProgram(w.program) } @@ -285,9 +346,14 @@ func (w *Watcher) doBuild() { host := &watchCompilerHost{inner: innerHost, cache: w.sourceFileCache} if w.config.ConfigFile != nil { - for dir := range w.config.WildcardDirectories() { + wildcardDirs := w.config.WildcardDirectories() + for dir := range wildcardDirs { tfs.seenFiles.Add(dir) } + w.fileWatcher.wildcardDirectories = wildcardDirs + if len(wildcardDirs) > 0 { + w.config = w.config.ReloadFileNamesOfParsedCommandLine(w.sys.FS()) + } } for _, path := range w.configFilePaths { tfs.seenFiles.Add(path) diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-multiple-new-subdirectories-simultaneously.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-multiple-new-subdirectories-simultaneously.js new file mode 100644 index 00000000000..b3053504306 --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-multiple-new-subdirectories-simultaneously.js @@ -0,0 +1,82 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/src/a.ts] *new* +export const a = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{ + "compilerOptions": {}, + "include": ["src/**/*.ts"] +} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/src/a.js] *new* +export const a = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/src/a.ts +Signatures:: + + +Edit [0]:: create multiple new subdirs with files +//// [/home/src/workspaces/project/src/models/user.ts] *new* +export interface User { name: string; } +//// [/home/src/workspaces/project/src/utils/format.ts] *new* +export function format(s: string): string { return s.trim(); } + + +Output:: +build starting at HH:MM:SS AM +src/utils/format.ts:1:54 - error TS2339: Property 'trim' does not exist on type 'string'. + +1 export function format(s: string): string { return s.trim(); } +   ~~~~ + + +Found 1 error in src/utils/format.ts:1 + +build finished in d.ddds +//// [/home/src/workspaces/project/src/models/user.js] *new* +export {}; + +//// [/home/src/workspaces/project/src/utils/format.js] *new* +export function format(s) { return s.trim(); } + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/src/models/user.ts +*refresh* /home/src/workspaces/project/src/utils/format.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/src/models/user.ts +(computed .d.ts) /home/src/workspaces/project/src/utils/format.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-new-nested-subdirectory.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-new-nested-subdirectory.js new file mode 100644 index 00000000000..0d6c71ad8ea --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-new-nested-subdirectory.js @@ -0,0 +1,67 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/src/a.ts] *new* +export const a = 1; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{ + "compilerOptions": {}, + "include": ["src/**/*.ts"] +} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/src/a.js] *new* +export const a = 1; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/src/a.ts +Signatures:: + + +Edit [0]:: create nested dir with ts file +//// [/home/src/workspaces/project/src/deep/nested/util.ts] *new* +export const util = "nested"; + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/src/deep/nested/util.js] *new* +export const util = "nested"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/src/deep/nested/util.ts +Signatures:: +(computed .d.ts) /home/src/workspaces/project/src/deep/nested/util.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js index b3017b1de24..d339fc37347 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js @@ -57,14 +57,12 @@ export const helper = "added"; Output:: build starting at HH:MM:SS AM build finished in d.ddds +//// [/home/src/workspaces/project/src/helper.js] *new* +export const helper = "added"; + tsconfig.json:: SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/src/helper.ts Signatures:: - - -Diff:: incremental skips emit for new unreferenced file ---- nonIncremental /home/src/workspaces/project/src/helper.js -+++ incremental /home/src/workspaces/project/src/helper.js -@@ -1,1 +0,0 @@ --export const helper = "added"; +(computed .d.ts) /home/src/workspaces/project/src/helper.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js index 0b7d34824d9..22d830a3c8b 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js @@ -68,20 +68,21 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 build finished in d.ddds +//// [/home/src/workspaces/project/helper2.js] *new* +export const helper = 1; + //// [/home/src/workspaces/project/index.js] *rewrite with same content* tsconfig.json:: SemanticDiagnostics:: +*refresh* /home/src/workspaces/project/helper2.ts *refresh* /home/src/workspaces/project/index.ts Signatures:: +(computed .d.ts) /home/src/workspaces/project/helper2.ts (computed .d.ts) /home/src/workspaces/project/index.ts Diff:: incremental resolves to .js output from prior build while clean build cannot find module ---- nonIncremental /home/src/workspaces/project/helper2.js -+++ incremental /home/src/workspaces/project/helper2.js -@@ -1,1 +0,0 @@ --export const helper = 1; --- nonIncremental.output.txt +++ incremental.output.txt @@ -1,4 +1,4 @@ diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-nested-subdirectory-removed-and-recreated.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-nested-subdirectory-removed-and-recreated.js new file mode 100644 index 00000000000..729a1c8fdcd --- /dev/null +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-nested-subdirectory-removed-and-recreated.js @@ -0,0 +1,91 @@ +currentDirectory::/home/src/workspaces/project +useCaseSensitiveFileNames::true +Input:: +//// [/home/src/workspaces/project/src/lib/helper.ts] *new* +export const helper = "v1"; +//// [/home/src/workspaces/project/tsconfig.json] *new* +{ + "compilerOptions": {}, + "include": ["src/**/*.ts"] +} + +tsgo --watch +ExitStatus:: Success +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* +/// +interface Boolean {} +interface Function {} +interface CallableFunction {} +interface NewableFunction {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array { length: number; [n: number]: T; } +interface ReadonlyArray {} +interface SymbolConstructor { + (desc?: string | number): symbol; + for(name: string): symbol; + readonly toStringTag: symbol; +} +declare var Symbol: SymbolConstructor; +interface Symbol { + readonly [Symbol.toStringTag]: string; +} +declare const console: { log(msg: any): void; }; +//// [/home/src/workspaces/project/src/lib/helper.js] *new* +export const helper = "v1"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/src/lib/helper.ts +Signatures:: + + +Edit [0]:: remove nested dir +//// [/home/src/workspaces/project/src/lib/helper.ts] *deleted* + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds + +tsconfig.json:: +SemanticDiagnostics:: +Signatures:: + + +Diff:: incremental has prior state and does not report no-inputs error +--- nonIncremental.output.txt ++++ incremental.output.txt +@@ -1,4 +0,0 @@ +-error TS18003: No inputs were found in config file '/home/src/workspaces/project/tsconfig.json'. Specified 'include' paths were '["src/**/*.ts"]' and 'exclude' paths were '[]'. +- +-Found 1 error. +- + +Edit [1]:: recreate nested dir with new content +//// [/home/src/workspaces/project/src/lib/helper.ts] *new* +export const helper = "v2"; + + +Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/src/lib/helper.js] *modified* +export const helper = "v2"; + + +tsconfig.json:: +SemanticDiagnostics:: +*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +*refresh* /home/src/workspaces/project/src/lib/helper.ts +Signatures:: +(used version) /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts +(computed .d.ts) /home/src/workspaces/project/src/lib/helper.ts diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js index 06453e759ce..f7423c19fd1 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js @@ -54,16 +54,14 @@ export const b = 2; Output:: +build starting at HH:MM:SS AM +build finished in d.ddds +//// [/home/src/workspaces/project/src/b.js] *new* +export const b = 2; + tsconfig.json:: SemanticDiagnostics:: -*refresh* /home/src/tslibs/TS/Lib/lib.es2025.full.d.ts -*refresh* /home/src/workspaces/project/src/a.ts +*refresh* /home/src/workspaces/project/src/b.ts Signatures:: - - -Diff:: incremental skips emit for new unreferenced file ---- nonIncremental /home/src/workspaces/project/src/b.js -+++ incremental /home/src/workspaces/project/src/b.js -@@ -1,1 +0,0 @@ --export const b = 2; +(computed .d.ts) /home/src/workspaces/project/src/b.ts From 0c2d3569fed15a8926b349f95e3872c8ed096cb8 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Fri, 27 Mar 2026 14:45:30 -0500 Subject: [PATCH 14/30] minor caching and error reporting fixes --- internal/execute/tsctests/tscwatch_test.go | 2 +- internal/execute/watcher.go | 33 ++++++++++++---------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/internal/execute/tsctests/tscwatch_test.go b/internal/execute/tsctests/tscwatch_test.go index 44e041f5a60..fabe775d025 100644 --- a/internal/execute/tsctests/tscwatch_test.go +++ b/internal/execute/tsctests/tscwatch_test.go @@ -565,7 +565,7 @@ func noEmitWatchTestInput( } func newTscEdit(name string, edit func(sys *TestSys)) *tscEdit { - return &tscEdit{name, []string{}, edit, ""} + return &tscEdit{caption: name, edit: edit} } func TestTscNoEmitWatch(t *testing.T) { diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index aa6f26d2b68..df97ee97334 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -53,7 +53,12 @@ func (fs *trackingFS) GetAccessibleEntries(path string) vfs.Entries { fs.seenFiles.Add(path) return fs.inner.GetAccessibleEntries(path) } -func (fs *trackingFS) Stat(path string) vfs.FileInfo { return fs.inner.Stat(path) } + +func (fs *trackingFS) Stat(path string) vfs.FileInfo { + fs.seenFiles.Add(path) + return fs.inner.Stat(path) +} + func (fs *trackingFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { return fs.inner.WalkDir(root, walkFn) } @@ -96,7 +101,7 @@ func (fw *FileWatcher) updateWatchedFiles(tfs *trackingFS) { if !recursive { continue } - fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { + _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { if err != nil || !d.IsDir() { return nil } @@ -138,7 +143,7 @@ func (fw *FileWatcher) currentState() map[string]WatchEntry { if !recursive { continue } - fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { + _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { if err != nil || !d.IsDir() { return nil } @@ -171,7 +176,7 @@ func (fw *FileWatcher) HasChanges(baseline map[string]WatchEntry) bool { continue } found := false - fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { + _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { if err != nil || !d.IsDir() { return nil } @@ -229,9 +234,9 @@ func (h *watchCompilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast. } } + info := h.inner.FS().Stat(opts.FileName) file := h.inner.GetSourceFile(opts) if file != nil { - info := h.inner.FS().Stat(opts.FileName) if info != nil { h.cache.Store(opts.Path, &cachedSourceFile{ file: file, @@ -306,7 +311,7 @@ func (w *Watcher) start() { w.configFilePaths = append([]string{w.configFileName}, w.config.ExtendedSourceFiles()...) } - w.doBuild() + w.doBuild(false) if w.testing == nil { w.fileWatcher.Run(w.sys.Now) @@ -322,7 +327,7 @@ func (w *Watcher) DoCycle() { updated := w.config.ReloadFileNamesOfParsedCommandLine(w.sys.FS()) if !slices.Equal(w.config.FileNames(), updated.FileNames()) { w.config = updated - w.doBuild() + w.doBuild(true) return } } @@ -332,10 +337,10 @@ func (w *Watcher) DoCycle() { return } - w.doBuild() + w.doBuild(false) } -func (w *Watcher) doBuild() { +func (w *Watcher) doBuild(fileNamesReloaded bool) { if w.configModified { w.sourceFileCache = &collections.SyncMap[tspath.Path, *cachedSourceFile]{} } @@ -351,7 +356,7 @@ func (w *Watcher) doBuild() { tfs.seenFiles.Add(dir) } w.fileWatcher.wildcardDirectories = wildcardDirs - if len(wildcardDirs) > 0 { + if len(wildcardDirs) > 0 && !fileNamesReloaded { w.config = w.config.ReloadFileNamesOfParsedCommandLine(w.sys.FS()) } } @@ -433,12 +438,10 @@ func (w *Watcher) hasErrorsInTsConfig() bool { extendedConfigCache := &tsc.ExtendedConfigCache{} configParseResult, errors := tsoptions.GetParsedCommandLineOfConfigFile(w.configFileName, w.compilerOptionsFromCommandLine, nil, w.sys, extendedConfigCache) if len(errors) > 0 { - if !w.configHasErrors { - for _, e := range errors { - w.reportDiagnostic(e) - } - w.configHasErrors = true + for _, e := range errors { + w.reportDiagnostic(e) } + w.configHasErrors = true return true } if w.configHasErrors { From 3b56aa4cf0d6ce3b3f673b62010fd25cb8e884b1 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:25:56 -0500 Subject: [PATCH 15/30] fixed double caching --- internal/execute/watcher.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index df97ee97334..1f9a06dce26 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -227,14 +227,14 @@ func (h *watchCompilerHost) GetResolvedProjectReference(fileName string, path ts } func (h *watchCompilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile { + info := h.inner.FS().Stat(opts.FileName) + if cached, ok := h.cache.Load(opts.Path); ok { - info := h.inner.FS().Stat(opts.FileName) if info != nil && info.ModTime().Equal(cached.modTime) { return cached.file } } - info := h.inner.FS().Stat(opts.FileName) file := h.inner.GetSourceFile(opts) if file != nil { if info != nil { From 6b19e2f0f21c9b09b244d34a4a7fb0154d0e91e4 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:33:45 -0500 Subject: [PATCH 16/30] walkdir and wildcard improvements --- internal/execute/watcher.go | 66 ++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 26 deletions(-) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 1f9a06dce26..fdfdfaa2adb 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -3,7 +3,6 @@ package execute import ( "fmt" "reflect" - "slices" "time" "github.com/microsoft/typescript-go/internal/ast" @@ -60,13 +59,18 @@ func (fs *trackingFS) Stat(path string) vfs.FileInfo { } func (fs *trackingFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { - return fs.inner.WalkDir(root, walkFn) + fs.seenFiles.Add(root) + return fs.inner.WalkDir(root, func(path string, d vfs.DirEntry, err error) error { + fs.seenFiles.Add(path) + return walkFn(path, d, err) + }) } func (fs *trackingFS) Realpath(path string) string { return fs.inner.Realpath(path) } type WatchEntry struct { - modTime time.Time - exists bool + modTime time.Time + exists bool + childCount int // -1 if not tracked } type FileWatcher struct { @@ -75,7 +79,7 @@ type FileWatcher struct { testing bool callback func() watchState map[string]WatchEntry - wildcardDirectories map[string]bool // dir path -> recursive flag + wildcardDirectories map[string]bool } func newFileWatcher(fs vfs.FS, pollInterval time.Duration, testing bool, callback func()) *FileWatcher { @@ -91,9 +95,9 @@ func (fw *FileWatcher) updateWatchedFiles(tfs *trackingFS) { fw.watchState = make(map[string]WatchEntry) tfs.seenFiles.Range(func(fn string) bool { if s := fw.fs.Stat(fn); s != nil { - fw.watchState[fn] = WatchEntry{modTime: s.ModTime(), exists: true} + fw.watchState[fn] = WatchEntry{modTime: s.ModTime(), exists: true, childCount: -1} } else { - fw.watchState[fn] = WatchEntry{exists: false} + fw.watchState[fn] = WatchEntry{exists: false, childCount: -1} } return true }) @@ -105,9 +109,14 @@ func (fw *FileWatcher) updateWatchedFiles(tfs *trackingFS) { if err != nil || !d.IsDir() { return nil } - if _, ok := fw.watchState[path]; !ok { + entries := fw.fs.GetAccessibleEntries(path) + count := len(entries.Files) + len(entries.Directories) + if existing, ok := fw.watchState[path]; ok { + existing.childCount = count + fw.watchState[path] = existing + } else { if s := fw.fs.Stat(path); s != nil { - fw.watchState[path] = WatchEntry{modTime: s.ModTime(), exists: true} + fw.watchState[path] = WatchEntry{modTime: s.ModTime(), exists: true, childCount: count} } } return nil @@ -134,9 +143,9 @@ func (fw *FileWatcher) currentState() map[string]WatchEntry { state := make(map[string]WatchEntry, len(fw.watchState)) for path := range fw.watchState { if s := fw.fs.Stat(path); s != nil { - state[path] = WatchEntry{modTime: s.ModTime(), exists: true} + state[path] = WatchEntry{modTime: s.ModTime(), exists: true, childCount: -1} } else { - state[path] = WatchEntry{exists: false} + state[path] = WatchEntry{exists: false, childCount: -1} } } for dir, recursive := range fw.wildcardDirectories { @@ -147,9 +156,14 @@ func (fw *FileWatcher) currentState() map[string]WatchEntry { if err != nil || !d.IsDir() { return nil } - if _, ok := state[path]; !ok { + entries := fw.fs.GetAccessibleEntries(path) + count := len(entries.Files) + len(entries.Directories) + if existing, ok := state[path]; ok { + existing.childCount = count + state[path] = existing + } else { if s := fw.fs.Stat(path); s != nil { - state[path] = WatchEntry{modTime: s.ModTime(), exists: true} + state[path] = WatchEntry{modTime: s.ModTime(), exists: true, childCount: count} } } return nil @@ -180,10 +194,18 @@ func (fw *FileWatcher) HasChanges(baseline map[string]WatchEntry) bool { if err != nil || !d.IsDir() { return nil } - if _, ok := baseline[path]; !ok { + entry, ok := baseline[path] + if !ok { found = true return vfs.SkipAll } + if entry.childCount >= 0 { + entries := fw.fs.GetAccessibleEntries(path) + if len(entries.Files)+len(entries.Directories) != entry.childCount { + found = true + return vfs.SkipAll + } + } return nil }) if found { @@ -311,7 +333,7 @@ func (w *Watcher) start() { w.configFilePaths = append([]string{w.configFileName}, w.config.ExtendedSourceFiles()...) } - w.doBuild(false) + w.doBuild() if w.testing == nil { w.fileWatcher.Run(w.sys.Now) @@ -323,24 +345,16 @@ func (w *Watcher) DoCycle() { return } if w.fileWatcher.watchState != nil && !w.configModified && !w.fileWatcher.HasChanges(w.fileWatcher.watchState) { - if w.config.ConfigFile != nil && len(w.config.WildcardDirectories()) > 0 { - updated := w.config.ReloadFileNamesOfParsedCommandLine(w.sys.FS()) - if !slices.Equal(w.config.FileNames(), updated.FileNames()) { - w.config = updated - w.doBuild(true) - return - } - } if w.testing != nil { w.testing.OnProgram(w.program) } return } - w.doBuild(false) + w.doBuild() } -func (w *Watcher) doBuild(fileNamesReloaded bool) { +func (w *Watcher) doBuild() { if w.configModified { w.sourceFileCache = &collections.SyncMap[tspath.Path, *cachedSourceFile]{} } @@ -356,7 +370,7 @@ func (w *Watcher) doBuild(fileNamesReloaded bool) { tfs.seenFiles.Add(dir) } w.fileWatcher.wildcardDirectories = wildcardDirs - if len(wildcardDirs) > 0 && !fileNamesReloaded { + if len(wildcardDirs) > 0 { w.config = w.config.ReloadFileNamesOfParsedCommandLine(w.sys.FS()) } } From e53264739e4eb0e6f89694e39cbab90c4242a605 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:56:46 -0500 Subject: [PATCH 17/30] updated logging --- internal/execute/watcher.go | 21 ++++++++++++------- ...etects-at-types-package-installed-later.js | 12 +++++++---- .../watch-detects-change-in-symlinked-file.js | 12 +++++++---- ...tiple-new-subdirectories-simultaneously.js | 12 +++++++---- ...s-file-added-in-new-nested-subdirectory.js | 12 +++++++---- ...to-previously-non-existent-include-path.js | 12 +++++++---- ...leted-and-new-file-added-simultaneously.js | 12 +++++++---- ...h-detects-file-renamed-and-renamed-back.js | 18 ++++++++++------ .../watch-detects-import-path-restructured.js | 12 +++++++---- ...atch-detects-imported-directory-removed.js | 12 +++++++---- ...ts-imported-file-added-in-new-directory.js | 12 +++++++---- ...s-module-going-missing-then-coming-back.js | 18 ++++++++++------ ...sted-subdirectory-removed-and-recreated.js | 18 ++++++++++------ ...-new-file-in-existing-include-directory.js | 12 +++++++---- ...etects-new-file-resolving-failed-import.js | 12 +++++++---- ...atch-detects-node-modules-package-added.js | 12 +++++++---- ...ch-detects-node-modules-package-removed.js | 12 +++++++---- ...detects-package-json-types-field-edited.js | 12 +++++++---- .../watch-detects-scoped-package-installed.js | 12 +++++++---- .../watch-handles-file-rapidly-recreated.js | 12 +++++++---- .../watch-handles-tsconfig-deleted.js | 6 ++++-- ...les-tsconfig-with-extends-base-modified.js | 12 +++++++---- .../watch-rebuilds-when-file-is-modified.js | 12 +++++++---- ...ch-rebuilds-when-source-file-is-deleted.js | 12 +++++++---- ...when-tsconfig-include-pattern-adds-file.js | 12 +++++++---- ...n-tsconfig-is-modified-to-change-strict.js | 12 +++++++---- ...config-is-touched-but-content-unchanged.js | 12 +++++++---- .../watch-skips-build-when-no-files-change.js | 6 ++++-- .../watch-with-no-tsconfig.js | 6 ++++-- .../watch-with-tsconfig-and-incremental.js | 6 ++++-- ...-with-tsconfig-files-list-entry-deleted.js | 12 +++++++---- 31 files changed, 249 insertions(+), 126 deletions(-) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index fdfdfaa2adb..80fb1673ebd 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -1,7 +1,6 @@ package execute import ( - "fmt" "reflect" "time" @@ -278,6 +277,7 @@ type Watcher struct { compilerOptionsFromCommandLine *core.CompilerOptions reportDiagnostic tsc.DiagnosticReporter reportErrorSummary tsc.DiagnosticsReporter + reportWatchStatus tsc.DiagnosticReporter testing tsc.CommandLineTesting program *incremental.Program @@ -309,6 +309,7 @@ func createWatcher( compilerOptionsFromCommandLine: compilerOptionsFromCommandLine, reportDiagnostic: reportDiagnostic, reportErrorSummary: reportErrorSummary, + reportWatchStatus: tsc.CreateWatchStatusReporter(sys, configParseResult.Locale(), configParseResult.CompilerOptions(), testing), testing: testing, sourceFileCache: &collections.SyncMap[tspath.Path, *cachedSourceFile]{}, } @@ -333,6 +334,7 @@ func (w *Watcher) start() { w.configFilePaths = append([]string{w.configFileName}, w.config.ExtendedSourceFiles()...) } + w.reportWatchStatus(ast.NewCompilerDiagnostic(diagnostics.Starting_compilation_in_watch_mode)) w.doBuild() if w.testing == nil { @@ -351,6 +353,7 @@ func (w *Watcher) DoCycle() { return } + w.reportWatchStatus(ast.NewCompilerDiagnostic(diagnostics.File_change_detected_Starting_incremental_compilation)) w.doBuild() } @@ -378,15 +381,12 @@ func (w *Watcher) doBuild() { tfs.seenFiles.Add(path) } - fmt.Fprintln(w.sys.Writer(), "build starting at", w.sys.Now().Format("03:04:05 PM")) - timeStart := w.sys.Now() - w.program = incremental.NewProgram(compiler.NewProgram(compiler.ProgramOptions{ Config: w.config, Host: host, }), w.program, nil, w.testing != nil) - w.compileAndEmit() + result := w.compileAndEmit() cached.DisableAndClearCache() w.fileWatcher.updateWatchedFiles(tfs) w.fileWatcher.pollInterval = w.config.ParsedConfig.WatchOptions.WatchInterval() @@ -400,15 +400,20 @@ func (w *Watcher) doBuild() { return true }) - fmt.Fprintf(w.sys.Writer(), "build finished in %.3fs\n", w.sys.Now().Sub(timeStart).Seconds()) + errorCount := len(result.Diagnostics) + if errorCount == 1 { + w.reportWatchStatus(ast.NewCompilerDiagnostic(diagnostics.Found_1_error_Watching_for_file_changes)) + } else { + w.reportWatchStatus(ast.NewCompilerDiagnostic(diagnostics.Found_0_errors_Watching_for_file_changes, errorCount)) + } if w.testing != nil { w.testing.OnProgram(w.program) } } -func (w *Watcher) compileAndEmit() { - tsc.EmitFilesAndReportErrors(tsc.EmitInput{ +func (w *Watcher) compileAndEmit() tsc.CompileAndEmitResult { + return tsc.EmitFilesAndReportErrors(tsc.EmitInput{ Sys: w.sys, ProgramLike: w.program, Program: w.program.GetProgram(), diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-at-types-package-installed-later.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-at-types-package-installed-later.js index 9637aacea1e..01adb0dc40d 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-at-types-package-installed-later.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-at-types-package-installed-later.js @@ -11,7 +11,8 @@ module.exports = {}; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] Starting compilation in watch mode... + index.ts:1:22 - error TS7016: Could not find a declaration file for module 'untyped-lib'. '/home/src/workspaces/project/node_modules/untyped-lib/index.js' implicitly has an 'any' type. 1 import * as lib from "untyped-lib"; @@ -20,7 +21,8 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -63,8 +65,10 @@ declare module "untyped-lib" { export const value: string; } Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *rewrite with same content* tsconfig.json:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-change-in-symlinked-file.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-change-in-symlinked-file.js index cc5f9033902..5aa1b5ad18e 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-change-in-symlinked-file.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-change-in-symlinked-file.js @@ -12,8 +12,10 @@ export const shared = "v1"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -58,8 +60,10 @@ export const shared = "v2"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *rewrite with same content* //// [/home/src/workspaces/project/link.js] *modified* export const shared = "v2"; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-multiple-new-subdirectories-simultaneously.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-multiple-new-subdirectories-simultaneously.js index b3053504306..6d26fe6cc46 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-multiple-new-subdirectories-simultaneously.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-multiple-new-subdirectories-simultaneously.js @@ -12,8 +12,10 @@ export const a = 1; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -56,7 +58,8 @@ export function format(s: string): string { return s.trim(); } Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + src/utils/format.ts:1:54 - error TS2339: Property 'trim' does not exist on type 'string'. 1 export function format(s: string): string { return s.trim(); } @@ -65,7 +68,8 @@ build starting at HH:MM:SS AM Found 1 error in src/utils/format.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/workspaces/project/src/models/user.js] *new* export {}; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-new-nested-subdirectory.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-new-nested-subdirectory.js index 0d6c71ad8ea..71da18e80e1 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-new-nested-subdirectory.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-in-new-nested-subdirectory.js @@ -12,8 +12,10 @@ export const a = 1; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -54,8 +56,10 @@ export const util = "nested"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/src/deep/nested/util.js] *new* export const util = "nested"; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js index d339fc37347..9130ae0e4bf 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-added-to-previously-non-existent-include-path.js @@ -12,8 +12,10 @@ const x = 1; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -55,8 +57,10 @@ export const helper = "added"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/src/helper.js] *new* export const helper = "added"; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-deleted-and-new-file-added-simultaneously.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-deleted-and-new-file-added-simultaneously.js index ddd109d3713..b9f2d5c8481 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-deleted-and-new-file-added-simultaneously.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-deleted-and-new-file-added-simultaneously.js @@ -11,8 +11,10 @@ export const b = 1; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -60,8 +62,10 @@ export const c = 2; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/a.js] *rewrite with same content* //// [/home/src/workspaces/project/c.js] *new* export const c = 2; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js index 22d830a3c8b..0496d95a210 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-file-renamed-and-renamed-back.js @@ -11,8 +11,10 @@ import { helper } from "./helper"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -58,7 +60,8 @@ export const helper = 1; Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + index.ts:1:24 - error TS7016: Could not find a declaration file for module './helper'. '/home/src/workspaces/project/helper.js' implicitly has an 'any' type. 1 import { helper } from "./helper"; @@ -67,7 +70,8 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/workspaces/project/helper2.js] *new* export const helper = 1; @@ -99,8 +103,10 @@ export const helper = 1; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/helper.js] *rewrite with same content* //// [/home/src/workspaces/project/index.js] *rewrite with same content* diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-import-path-restructured.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-import-path-restructured.js index 78cc8b7cc0e..3211131af98 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-import-path-restructured.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-import-path-restructured.js @@ -11,8 +11,10 @@ export const util = "v1"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -60,8 +62,10 @@ export const util = "v2"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *rewrite with same content* //// [/home/src/workspaces/project/src/util.js] *new* export const util = "v2"; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-directory-removed.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-directory-removed.js index 37557659c60..5c7c582f53b 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-directory-removed.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-directory-removed.js @@ -11,8 +11,10 @@ export const util = "hello"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -56,7 +58,8 @@ Edit [0]:: remove directory with imported file Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + index.ts:1:22 - error TS7016: Could not find a declaration file for module './lib/util'. '/home/src/workspaces/project/lib/util.js' implicitly has an 'any' type. 1 import { util } from "./lib/util"; @@ -65,7 +68,8 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *rewrite with same content* tsconfig.json:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-file-added-in-new-directory.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-file-added-in-new-directory.js index 54ec48b04b0..e003eba9749 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-file-added-in-new-directory.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-imported-file-added-in-new-directory.js @@ -9,7 +9,8 @@ import { util } from "./lib/util"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] Starting compilation in watch mode... + index.ts:1:22 - error TS2307: Cannot find module './lib/util' or its corresponding type declarations. 1 import { util } from "./lib/util"; @@ -18,7 +19,8 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -59,8 +61,10 @@ export const util = "hello"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *rewrite with same content* //// [/home/src/workspaces/project/lib/util.js] *new* export const util = "hello"; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-module-going-missing-then-coming-back.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-module-going-missing-then-coming-back.js index 644e05ed13f..5e3ddd32d8e 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-module-going-missing-then-coming-back.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-module-going-missing-then-coming-back.js @@ -11,8 +11,10 @@ export const util = "v1"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -56,7 +58,8 @@ Edit [0]:: delete util module Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + index.ts:1:22 - error TS7016: Could not find a declaration file for module './util'. '/home/src/workspaces/project/util.js' implicitly has an 'any' type. 1 import { util } from "./util"; @@ -65,7 +68,8 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *rewrite with same content* tsconfig.json:: @@ -91,8 +95,10 @@ export const util = "v2"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *rewrite with same content* //// [/home/src/workspaces/project/util.js] *modified* export const util = "v2"; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-nested-subdirectory-removed-and-recreated.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-nested-subdirectory-removed-and-recreated.js index 729a1c8fdcd..8627df293ba 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-nested-subdirectory-removed-and-recreated.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-nested-subdirectory-removed-and-recreated.js @@ -12,8 +12,10 @@ export const helper = "v1"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -53,8 +55,10 @@ Edit [0]:: remove nested dir Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -76,8 +80,10 @@ export const helper = "v2"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/src/lib/helper.js] *modified* export const helper = "v2"; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js index f7423c19fd1..fb174d116d7 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-in-existing-include-directory.js @@ -12,8 +12,10 @@ export const a = 1; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -54,8 +56,10 @@ export const b = 2; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/src/b.js] *new* export const b = 2; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-resolving-failed-import.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-resolving-failed-import.js index e4c464f492c..c5115bfe0f5 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-resolving-failed-import.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-new-file-resolving-failed-import.js @@ -9,7 +9,8 @@ import { b } from "./b"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] Starting compilation in watch mode... + a.ts:1:19 - error TS2307: Cannot find module './b' or its corresponding type declarations. 1 import { b } from "./b"; @@ -18,7 +19,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -59,8 +61,10 @@ export const b = 1; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/a.js] *rewrite with same content* //// [/home/src/workspaces/project/b.js] *new* export const b = 1; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-added.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-added.js index a75b2e3502a..2c08f665024 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-added.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-added.js @@ -9,7 +9,8 @@ import { lib } from "mylib"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] Starting compilation in watch mode... + index.ts:1:21 - error TS2307: Cannot find module 'mylib' or its corresponding type declarations. 1 import { lib } from "mylib"; @@ -18,7 +19,8 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -63,8 +65,10 @@ exports.lib = "hello"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *rewrite with same content* tsconfig.json:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-removed.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-removed.js index 4df389ce46c..744b4cfbcf5 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-removed.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-node-modules-package-removed.js @@ -15,8 +15,10 @@ exports.lib = "hello"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -59,7 +61,8 @@ Edit [0]:: remove node_modules package Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + index.ts:1:21 - error TS2307: Cannot find module 'mylib' or its corresponding type declarations. 1 import { lib } from "mylib"; @@ -68,7 +71,8 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *rewrite with same content* tsconfig.json:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-types-field-edited.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-types-field-edited.js index 2ed714c0ff0..9f8e5d02a37 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-types-field-edited.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-package-json-types-field-edited.js @@ -15,8 +15,10 @@ export declare const lib: number; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -58,8 +60,10 @@ Edit [0]:: change package.json types field Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *rewrite with same content* tsconfig.json:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-scoped-package-installed.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-scoped-package-installed.js index 1121205be03..9f17d930f1a 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-scoped-package-installed.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-detects-scoped-package-installed.js @@ -9,7 +9,8 @@ import { lib } from "@scope/mylib"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] Starting compilation in watch mode... + index.ts:1:21 - error TS2307: Cannot find module '@scope/mylib' or its corresponding type declarations. 1 import { lib } from "@scope/mylib"; @@ -18,7 +19,8 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -61,8 +63,10 @@ export declare const lib: string; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *rewrite with same content* tsconfig.json:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-file-rapidly-recreated.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-file-rapidly-recreated.js index d063a5aef0d..3cf8b77fd36 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-file-rapidly-recreated.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-file-rapidly-recreated.js @@ -11,8 +11,10 @@ import { val } from "./data"; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -57,8 +59,10 @@ export const val = "recreated"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/data.js] *modified* export const val = "recreated"; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js index f7bd02d666b..e6cdcb8b522 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js @@ -9,8 +9,10 @@ const x = 1; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js index 82e002da61c..4abbf691989 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-with-extends-base-modified.js @@ -15,8 +15,10 @@ const x = null; const y: string = x; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -61,7 +63,8 @@ Edit [0]:: modify base config to enable strict Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + index.ts:1:23 - error TS2322: Type 'null' is not assignable to type 'string'. 1 const x = null; const y: string = x; @@ -70,7 +73,8 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-file-is-modified.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-file-is-modified.js index a77087e8ab6..ad0d4250854 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-file-is-modified.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-file-is-modified.js @@ -9,8 +9,10 @@ const x: number = 1; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -52,8 +54,10 @@ const x: number = 2; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/index.js] *modified* "use strict"; const x = 2; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-source-file-is-deleted.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-source-file-is-deleted.js index 4884b222a80..9f72fb77745 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-source-file-is-deleted.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-source-file-is-deleted.js @@ -11,8 +11,10 @@ export const b = 1; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -56,7 +58,8 @@ Edit [0]:: delete imported file Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + a.ts:1:19 - error TS7016: Could not find a declaration file for module './b'. '/home/src/workspaces/project/b.js' implicitly has an 'any' type. 1 import { b } from "./b"; @@ -65,7 +68,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/workspaces/project/a.js] *rewrite with same content* tsconfig.json:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-include-pattern-adds-file.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-include-pattern-adds-file.js index 0d620e762a0..959ae56259e 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-include-pattern-adds-file.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-include-pattern-adds-file.js @@ -12,8 +12,10 @@ const x = 1; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -60,8 +62,10 @@ export const extra = 2; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/src/extra.js] *new* export const extra = 2; diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-modified-to-change-strict.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-modified-to-change-strict.js index 1efa1bed4e8..0e5e0cce9ae 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-modified-to-change-strict.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-modified-to-change-strict.js @@ -9,7 +9,8 @@ const x = null; const y: string = x; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] Starting compilation in watch mode... + index.ts:1:23 - error TS2322: Type 'null' is not assignable to type 'string'. 1 const x = null; const y: string = x; @@ -18,7 +19,8 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -61,7 +63,8 @@ Edit [0]:: enable strict mode Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + index.ts:1:23 - error TS2322: Type 'null' is not assignable to type 'string'. 1 const x = null; const y: string = x; @@ -70,7 +73,8 @@ build starting at HH:MM:SS AM Found 1 error in index.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-touched-but-content-unchanged.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-touched-but-content-unchanged.js index 8813ad18197..a9a3639536a 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-touched-but-content-unchanged.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-rebuilds-when-tsconfig-is-touched-but-content-unchanged.js @@ -9,8 +9,10 @@ const x = 1; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -51,8 +53,10 @@ Edit [0]:: touch tsconfig without changing content Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-build-when-no-files-change.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-build-when-no-files-change.js index 25aaa126077..d79f8a92954 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-build-when-no-files-change.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-skips-build-when-no-files-change.js @@ -9,8 +9,10 @@ const x: number = 1; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-no-tsconfig.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-no-tsconfig.js index 71e272910cb..f85233069c8 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-no-tsconfig.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-no-tsconfig.js @@ -7,8 +7,10 @@ Input:: tsgo index.ts --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-and-incremental.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-and-incremental.js index c32265cec30..1bbfe39551f 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-and-incremental.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-and-incremental.js @@ -9,8 +9,10 @@ Input:: tsgo --watch --incremental ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-files-list-entry-deleted.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-files-list-entry-deleted.js index ac79ed1e6e3..622e5fb3ffc 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-files-list-entry-deleted.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-with-tsconfig-files-list-entry-deleted.js @@ -14,8 +14,10 @@ export const b = 2; tsgo --watch ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -59,8 +61,10 @@ Edit [0]:: delete file listed in files array Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: From 7ad6575fb7e4c9b0e882f64070575c538003e399 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:46:10 -0500 Subject: [PATCH 18/30] accepted baselines --- .../Parse-watch-interval-option.js | 6 ++- .../noEmit/dts-errors-without-dts-enabled.js | 42 ++++++++++++------- .../reference/tscWatch/noEmit/dts-errors.js | 42 ++++++++++++------- .../tscWatch/noEmit/semantic-errors.js | 42 ++++++++++++------- .../tscWatch/noEmit/syntax-errors.js | 42 ++++++++++++------- 5 files changed, 116 insertions(+), 58 deletions(-) diff --git a/testdata/baselines/reference/tscWatch/commandLine/Parse-watch-interval-option.js b/testdata/baselines/reference/tscWatch/commandLine/Parse-watch-interval-option.js index 4542c5c6a19..c4931ed7907 100644 --- a/testdata/baselines/reference/tscWatch/commandLine/Parse-watch-interval-option.js +++ b/testdata/baselines/reference/tscWatch/commandLine/Parse-watch-interval-option.js @@ -14,8 +14,10 @@ export const a = 1 tsgo -w --watchInterval 1000 ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} diff --git a/testdata/baselines/reference/tscWatch/noEmit/dts-errors-without-dts-enabled.js b/testdata/baselines/reference/tscWatch/noEmit/dts-errors-without-dts-enabled.js index 38d0ffcb6d6..8c7009954f9 100644 --- a/testdata/baselines/reference/tscWatch/noEmit/dts-errors-without-dts-enabled.js +++ b/testdata/baselines/reference/tscWatch/noEmit/dts-errors-without-dts-enabled.js @@ -13,8 +13,10 @@ const a = class { private p = 10; }; tsgo -w ExitStatus:: Success Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] Starting compilation in watch mode... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -52,8 +54,10 @@ const a = "hello"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -73,8 +77,10 @@ Edit [1]:: emit after fixing error Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/a.js] *new* "use strict"; const a = "hello"; @@ -96,8 +102,10 @@ Edit [2]:: no emit run after fixing error Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -110,8 +118,10 @@ const a = class { private p = 10; }; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -131,8 +141,10 @@ Edit [4]:: emit when error Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/a.js] *modified* "use strict"; const a = class { @@ -156,8 +168,10 @@ Edit [5]:: no emit run when error Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: diff --git a/testdata/baselines/reference/tscWatch/noEmit/dts-errors.js b/testdata/baselines/reference/tscWatch/noEmit/dts-errors.js index 18e4be0d7cb..135de179f59 100644 --- a/testdata/baselines/reference/tscWatch/noEmit/dts-errors.js +++ b/testdata/baselines/reference/tscWatch/noEmit/dts-errors.js @@ -14,7 +14,8 @@ const a = class { private p = 10; }; tsgo -w ExitStatus:: Success Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] Starting compilation in watch mode... + a.ts:1:7 - error TS4094: Property 'p' of exported anonymous class type may not be private or protected. 1 const a = class { private p = 10; }; @@ -27,7 +28,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -65,8 +67,10 @@ const a = "hello"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -86,8 +90,10 @@ Edit [1]:: emit after fixing error Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/a.d.ts] *new* declare const a = "hello"; @@ -112,8 +118,10 @@ Edit [2]:: no emit run after fixing error Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -126,7 +134,8 @@ const a = class { private p = 10; }; Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + a.ts:1:7 - error TS4094: Property 'p' of exported anonymous class type may not be private or protected. 1 const a = class { private p = 10; }; @@ -139,7 +148,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -159,7 +169,8 @@ Edit [4]:: emit when error Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + a.ts:1:7 - error TS4094: Property 'p' of exported anonymous class type may not be private or protected. 1 const a = class { private p = 10; }; @@ -172,7 +183,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/workspaces/project/a.d.ts] *modified* declare const a: { new (): { @@ -203,7 +215,8 @@ Edit [5]:: no emit run when error Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + a.ts:1:7 - error TS4094: Property 'p' of exported anonymous class type may not be private or protected. 1 const a = class { private p = 10; }; @@ -216,7 +229,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: diff --git a/testdata/baselines/reference/tscWatch/noEmit/semantic-errors.js b/testdata/baselines/reference/tscWatch/noEmit/semantic-errors.js index cf568870a8c..dbee71768b2 100644 --- a/testdata/baselines/reference/tscWatch/noEmit/semantic-errors.js +++ b/testdata/baselines/reference/tscWatch/noEmit/semantic-errors.js @@ -13,7 +13,8 @@ const a: number = "hello" tsgo -w ExitStatus:: Success Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] Starting compilation in watch mode... + a.ts:1:7 - error TS2322: Type 'string' is not assignable to type 'number'. 1 const a: number = "hello" @@ -22,7 +23,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -60,8 +62,10 @@ const a = "hello"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -81,8 +85,10 @@ Edit [1]:: emit after fixing error Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/a.js] *new* "use strict"; const a = "hello"; @@ -104,8 +110,10 @@ Edit [2]:: no emit run after fixing error Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -118,7 +126,8 @@ const a: number = "hello" Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + a.ts:1:7 - error TS2322: Type 'string' is not assignable to type 'number'. 1 const a: number = "hello" @@ -127,7 +136,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -147,7 +157,8 @@ Edit [4]:: emit when error Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + a.ts:1:7 - error TS2322: Type 'string' is not assignable to type 'number'. 1 const a: number = "hello" @@ -156,7 +167,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/workspaces/project/a.js] *rewrite with same content* tsconfig.json:: @@ -175,7 +187,8 @@ Edit [5]:: no emit run when error Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + a.ts:1:7 - error TS2322: Type 'string' is not assignable to type 'number'. 1 const a: number = "hello" @@ -184,7 +197,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: diff --git a/testdata/baselines/reference/tscWatch/noEmit/syntax-errors.js b/testdata/baselines/reference/tscWatch/noEmit/syntax-errors.js index 8f2b4abde33..b7d623008b5 100644 --- a/testdata/baselines/reference/tscWatch/noEmit/syntax-errors.js +++ b/testdata/baselines/reference/tscWatch/noEmit/syntax-errors.js @@ -13,7 +13,8 @@ const a = "hello tsgo -w ExitStatus:: Success Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] Starting compilation in watch mode... + a.ts:1:17 - error TS1002: Unterminated string literal. 1 const a = "hello @@ -22,7 +23,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/tslibs/TS/Lib/lib.es2025.full.d.ts] *Lib* /// interface Boolean {} @@ -60,8 +62,10 @@ const a = "hello"; Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -81,8 +85,10 @@ Edit [1]:: emit after fixing error Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + //// [/home/src/workspaces/project/a.js] *new* "use strict"; const a = "hello"; @@ -104,8 +110,10 @@ Edit [2]:: no emit run after fixing error Output:: -build starting at HH:MM:SS AM -build finished in d.ddds +[HH:MM:SS AM] File change detected. Starting incremental compilation... + +[HH:MM:SS AM] Found 0 errors. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -118,7 +126,8 @@ const a = "hello Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + a.ts:1:17 - error TS1002: Unterminated string literal. 1 const a = "hello @@ -127,7 +136,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: @@ -145,7 +155,8 @@ Edit [4]:: emit when error Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + a.ts:1:17 - error TS1002: Unterminated string literal. 1 const a = "hello @@ -154,7 +165,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + //// [/home/src/workspaces/project/a.js] *modified* "use strict"; const a = "hello; @@ -178,7 +190,8 @@ Edit [5]:: no emit run when error Output:: -build starting at HH:MM:SS AM +[HH:MM:SS AM] File change detected. Starting incremental compilation... + a.ts:1:17 - error TS1002: Unterminated string literal. 1 const a = "hello @@ -187,7 +200,8 @@ build starting at HH:MM:SS AM Found 1 error in a.ts:1 -build finished in d.ddds +[HH:MM:SS AM] Found 1 error. Watching for file changes. + tsconfig.json:: SemanticDiagnostics:: From 90048b7c2d7913c09619ef72519c010ce9c94223 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:51:00 -0500 Subject: [PATCH 19/30] abstracted trackingfs to seperate file --- internal/execute/watcher.go | 109 ++++++------------------ internal/vfs/trackingvfs/trackingvfs.go | 76 +++++++++++++++++ 2 files changed, 103 insertions(+), 82 deletions(-) create mode 100644 internal/vfs/trackingvfs/trackingvfs.go diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 80fb1673ebd..8d464079769 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -15,69 +15,17 @@ import ( "github.com/microsoft/typescript-go/internal/tspath" "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/cachedvfs" + "github.com/microsoft/typescript-go/internal/vfs/trackingvfs" ) const watchDebounceWait = 250 * time.Millisecond -type trackingFS struct { - inner vfs.FS - seenFiles collections.SyncSet[string] -} - -func (fs *trackingFS) ReadFile(path string) (string, bool) { - fs.seenFiles.Add(path) - return fs.inner.ReadFile(path) -} - -func (fs *trackingFS) FileExists(path string) bool { - fs.seenFiles.Add(path) - return fs.inner.FileExists(path) -} -func (fs *trackingFS) UseCaseSensitiveFileNames() bool { return fs.inner.UseCaseSensitiveFileNames() } -func (fs *trackingFS) WriteFile(path string, data string) error { - return fs.inner.WriteFile(path, data) -} -func (fs *trackingFS) Remove(path string) error { return fs.inner.Remove(path) } -func (fs *trackingFS) Chtimes(path string, aTime time.Time, mTime time.Time) error { - return fs.inner.Chtimes(path, aTime, mTime) -} - -func (fs *trackingFS) DirectoryExists(path string) bool { - fs.seenFiles.Add(path) - return fs.inner.DirectoryExists(path) -} - -func (fs *trackingFS) GetAccessibleEntries(path string) vfs.Entries { - fs.seenFiles.Add(path) - return fs.inner.GetAccessibleEntries(path) -} - -func (fs *trackingFS) Stat(path string) vfs.FileInfo { - fs.seenFiles.Add(path) - return fs.inner.Stat(path) -} - -func (fs *trackingFS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { - fs.seenFiles.Add(root) - return fs.inner.WalkDir(root, func(path string, d vfs.DirEntry, err error) error { - fs.seenFiles.Add(path) - return walkFn(path, d, err) - }) -} -func (fs *trackingFS) Realpath(path string) string { return fs.inner.Realpath(path) } - -type WatchEntry struct { - modTime time.Time - exists bool - childCount int // -1 if not tracked -} - type FileWatcher struct { fs vfs.FS pollInterval time.Duration testing bool callback func() - watchState map[string]WatchEntry + watchState map[string]trackingvfs.WatchEntry wildcardDirectories map[string]bool } @@ -90,13 +38,13 @@ func newFileWatcher(fs vfs.FS, pollInterval time.Duration, testing bool, callbac } } -func (fw *FileWatcher) updateWatchedFiles(tfs *trackingFS) { - fw.watchState = make(map[string]WatchEntry) - tfs.seenFiles.Range(func(fn string) bool { +func (fw *FileWatcher) updateWatchedFiles(tfs *trackingvfs.FS) { + fw.watchState = make(map[string]trackingvfs.WatchEntry) + tfs.SeenFiles.Range(func(fn string) bool { if s := fw.fs.Stat(fn); s != nil { - fw.watchState[fn] = WatchEntry{modTime: s.ModTime(), exists: true, childCount: -1} + fw.watchState[fn] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} } else { - fw.watchState[fn] = WatchEntry{exists: false, childCount: -1} + fw.watchState[fn] = trackingvfs.WatchEntry{Exists: false, ChildCount: -1} } return true }) @@ -111,11 +59,11 @@ func (fw *FileWatcher) updateWatchedFiles(tfs *trackingFS) { entries := fw.fs.GetAccessibleEntries(path) count := len(entries.Files) + len(entries.Directories) if existing, ok := fw.watchState[path]; ok { - existing.childCount = count + existing.ChildCount = count fw.watchState[path] = existing } else { if s := fw.fs.Stat(path); s != nil { - fw.watchState[path] = WatchEntry{modTime: s.ModTime(), exists: true, childCount: count} + fw.watchState[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} } } return nil @@ -138,13 +86,13 @@ func (fw *FileWatcher) WaitForSettled(now func() time.Time) { } } -func (fw *FileWatcher) currentState() map[string]WatchEntry { - state := make(map[string]WatchEntry, len(fw.watchState)) +func (fw *FileWatcher) currentState() map[string]trackingvfs.WatchEntry { + state := make(map[string]trackingvfs.WatchEntry, len(fw.watchState)) for path := range fw.watchState { if s := fw.fs.Stat(path); s != nil { - state[path] = WatchEntry{modTime: s.ModTime(), exists: true, childCount: -1} + state[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} } else { - state[path] = WatchEntry{exists: false, childCount: -1} + state[path] = trackingvfs.WatchEntry{Exists: false, ChildCount: -1} } } for dir, recursive := range fw.wildcardDirectories { @@ -158,11 +106,11 @@ func (fw *FileWatcher) currentState() map[string]WatchEntry { entries := fw.fs.GetAccessibleEntries(path) count := len(entries.Files) + len(entries.Directories) if existing, ok := state[path]; ok { - existing.childCount = count + existing.ChildCount = count state[path] = existing } else { if s := fw.fs.Stat(path); s != nil { - state[path] = WatchEntry{modTime: s.ModTime(), exists: true, childCount: count} + state[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} } } return nil @@ -171,15 +119,15 @@ func (fw *FileWatcher) currentState() map[string]WatchEntry { return state } -func (fw *FileWatcher) HasChanges(baseline map[string]WatchEntry) bool { +func (fw *FileWatcher) HasChanges(baseline map[string]trackingvfs.WatchEntry) bool { for path, old := range baseline { s := fw.fs.Stat(path) - if !old.exists { + if !old.Exists { if s != nil { return true } } else { - if s == nil || !s.ModTime().Equal(old.modTime) { + if s == nil || !s.ModTime().Equal(old.ModTime) { return true } } @@ -198,9 +146,9 @@ func (fw *FileWatcher) HasChanges(baseline map[string]WatchEntry) bool { found = true return vfs.SkipAll } - if entry.childCount >= 0 { + if entry.ChildCount >= 0 { entries := fw.fs.GetAccessibleEntries(path) - if len(entries.Files)+len(entries.Directories) != entry.childCount { + if len(entries.Files)+len(entries.Directories) != entry.ChildCount { found = true return vfs.SkipAll } @@ -290,10 +238,7 @@ type Watcher struct { fileWatcher *FileWatcher } -var ( - _ tsc.Watcher = (*Watcher)(nil) - _ vfs.FS = (*trackingFS)(nil) -) +var _ tsc.Watcher = (*Watcher)(nil) func createWatcher( sys tsc.System, @@ -363,14 +308,14 @@ func (w *Watcher) doBuild() { } cached := cachedvfs.From(w.sys.FS()) - tfs := &trackingFS{inner: cached} + tfs := &trackingvfs.FS{Inner: cached} innerHost := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), tfs, w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) host := &watchCompilerHost{inner: innerHost, cache: w.sourceFileCache} if w.config.ConfigFile != nil { wildcardDirs := w.config.WildcardDirectories() for dir := range wildcardDirs { - tfs.seenFiles.Add(dir) + tfs.SeenFiles.Add(dir) } w.fileWatcher.wildcardDirectories = wildcardDirs if len(wildcardDirs) > 0 { @@ -378,7 +323,7 @@ func (w *Watcher) doBuild() { } } for _, path := range w.configFilePaths { - tfs.seenFiles.Add(path) + tfs.SeenFiles.Add(path) } w.program = incremental.NewProgram(compiler.NewProgram(compiler.ProgramOptions{ @@ -436,13 +381,13 @@ func (w *Watcher) hasErrorsInTsConfig() bool { for _, path := range w.configFilePaths { if old, ok := w.fileWatcher.watchState[path]; ok { s := w.sys.FS().Stat(path) - if !old.exists { + if !old.Exists { if s != nil { changed = true break } } else { - if s == nil || !s.ModTime().Equal(old.modTime) { + if s == nil || !s.ModTime().Equal(old.ModTime) { changed = true break } @@ -493,6 +438,6 @@ func (w *Watcher) WatchStateHas(path string) bool { func (w *Watcher) DebugWatchState(fn func(path string, modTime time.Time, exists bool)) { for path, entry := range w.fileWatcher.watchState { - fn(path, entry.modTime, entry.exists) + fn(path, entry.ModTime, entry.Exists) } } diff --git a/internal/vfs/trackingvfs/trackingvfs.go b/internal/vfs/trackingvfs/trackingvfs.go new file mode 100644 index 00000000000..4f6935574e7 --- /dev/null +++ b/internal/vfs/trackingvfs/trackingvfs.go @@ -0,0 +1,76 @@ +// Package trackingvfs provides a VFS wrapper that records every file path +// accessed during compilation. This allows watch mode to know exactly which +// files and directories the compiler depended on, including non-existent +// paths from failed module resolution. +package trackingvfs + +import ( + "time" + + "github.com/microsoft/typescript-go/internal/collections" + "github.com/microsoft/typescript-go/internal/vfs" +) + +// FS wraps a vfs.FS and records every path accessed via read-like operations. +// Write operations (WriteFile, Remove, Chtimes) are not tracked since they +// represent outputs, not dependencies. +type FS struct { + Inner vfs.FS + SeenFiles collections.SyncSet[string] +} + +var _ vfs.FS = (*FS)(nil) + +func (fs *FS) ReadFile(path string) (string, bool) { + fs.SeenFiles.Add(path) + return fs.Inner.ReadFile(path) +} + +func (fs *FS) FileExists(path string) bool { + fs.SeenFiles.Add(path) + return fs.Inner.FileExists(path) +} + +func (fs *FS) UseCaseSensitiveFileNames() bool { return fs.Inner.UseCaseSensitiveFileNames() } + +func (fs *FS) WriteFile(path string, data string) error { + return fs.Inner.WriteFile(path, data) +} + +func (fs *FS) Remove(path string) error { return fs.Inner.Remove(path) } + +func (fs *FS) Chtimes(path string, aTime time.Time, mTime time.Time) error { + return fs.Inner.Chtimes(path, aTime, mTime) +} + +func (fs *FS) DirectoryExists(path string) bool { + fs.SeenFiles.Add(path) + return fs.Inner.DirectoryExists(path) +} + +func (fs *FS) GetAccessibleEntries(path string) vfs.Entries { + fs.SeenFiles.Add(path) + return fs.Inner.GetAccessibleEntries(path) +} + +func (fs *FS) Stat(path string) vfs.FileInfo { + fs.SeenFiles.Add(path) + return fs.Inner.Stat(path) +} + +func (fs *FS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { + fs.SeenFiles.Add(root) + return fs.Inner.WalkDir(root, func(path string, d vfs.DirEntry, err error) error { + fs.SeenFiles.Add(path) + return walkFn(path, d, err) + }) +} + +func (fs *FS) Realpath(path string) string { return fs.Inner.Realpath(path) } + +// WatchEntry stores the observed state of a single path at snapshot time. +type WatchEntry struct { + ModTime time.Time + Exists bool + ChildCount int // -1 if not tracked +} From cf5891d9d955f81080129750c2c30585645a4ff9 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:54:10 -0500 Subject: [PATCH 20/30] abstracted filewatcher to separate file --- internal/execute/watcher.go | 177 ++---------------------------- internal/vfs/vfswatch/vfswatch.go | 164 +++++++++++++++++++++++++++ 2 files changed, 176 insertions(+), 165 deletions(-) create mode 100644 internal/vfs/vfswatch/vfswatch.go diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 8d464079769..f511daacb99 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -16,162 +16,9 @@ import ( "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/cachedvfs" "github.com/microsoft/typescript-go/internal/vfs/trackingvfs" + "github.com/microsoft/typescript-go/internal/vfs/vfswatch" ) -const watchDebounceWait = 250 * time.Millisecond - -type FileWatcher struct { - fs vfs.FS - pollInterval time.Duration - testing bool - callback func() - watchState map[string]trackingvfs.WatchEntry - wildcardDirectories map[string]bool -} - -func newFileWatcher(fs vfs.FS, pollInterval time.Duration, testing bool, callback func()) *FileWatcher { - return &FileWatcher{ - fs: fs, - pollInterval: pollInterval, - testing: testing, - callback: callback, - } -} - -func (fw *FileWatcher) updateWatchedFiles(tfs *trackingvfs.FS) { - fw.watchState = make(map[string]trackingvfs.WatchEntry) - tfs.SeenFiles.Range(func(fn string) bool { - if s := fw.fs.Stat(fn); s != nil { - fw.watchState[fn] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} - } else { - fw.watchState[fn] = trackingvfs.WatchEntry{Exists: false, ChildCount: -1} - } - return true - }) - for dir, recursive := range fw.wildcardDirectories { - if !recursive { - continue - } - _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { - if err != nil || !d.IsDir() { - return nil - } - entries := fw.fs.GetAccessibleEntries(path) - count := len(entries.Files) + len(entries.Directories) - if existing, ok := fw.watchState[path]; ok { - existing.ChildCount = count - fw.watchState[path] = existing - } else { - if s := fw.fs.Stat(path); s != nil { - fw.watchState[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} - } - } - return nil - }) - } -} - -func (fw *FileWatcher) WaitForSettled(now func() time.Time) { - if fw.testing { - return - } - current := fw.currentState() - settledAt := now() - for now().Sub(settledAt) < watchDebounceWait { - time.Sleep(fw.pollInterval) - if fw.HasChanges(current) { - current = fw.currentState() - settledAt = now() - } - } -} - -func (fw *FileWatcher) currentState() map[string]trackingvfs.WatchEntry { - state := make(map[string]trackingvfs.WatchEntry, len(fw.watchState)) - for path := range fw.watchState { - if s := fw.fs.Stat(path); s != nil { - state[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} - } else { - state[path] = trackingvfs.WatchEntry{Exists: false, ChildCount: -1} - } - } - for dir, recursive := range fw.wildcardDirectories { - if !recursive { - continue - } - _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { - if err != nil || !d.IsDir() { - return nil - } - entries := fw.fs.GetAccessibleEntries(path) - count := len(entries.Files) + len(entries.Directories) - if existing, ok := state[path]; ok { - existing.ChildCount = count - state[path] = existing - } else { - if s := fw.fs.Stat(path); s != nil { - state[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} - } - } - return nil - }) - } - return state -} - -func (fw *FileWatcher) HasChanges(baseline map[string]trackingvfs.WatchEntry) bool { - for path, old := range baseline { - s := fw.fs.Stat(path) - if !old.Exists { - if s != nil { - return true - } - } else { - if s == nil || !s.ModTime().Equal(old.ModTime) { - return true - } - } - } - for dir, recursive := range fw.wildcardDirectories { - if !recursive { - continue - } - found := false - _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { - if err != nil || !d.IsDir() { - return nil - } - entry, ok := baseline[path] - if !ok { - found = true - return vfs.SkipAll - } - if entry.ChildCount >= 0 { - entries := fw.fs.GetAccessibleEntries(path) - if len(entries.Files)+len(entries.Directories) != entry.ChildCount { - found = true - return vfs.SkipAll - } - } - return nil - }) - if found { - return true - } - } - return false -} - -func (fw *FileWatcher) Run(now func() time.Time) { - for { - time.Sleep(fw.pollInterval) - if fw.watchState == nil || fw.HasChanges(fw.watchState) { - fw.WaitForSettled(now) - fw.callback() - } - } -} - type cachedSourceFile struct { file *ast.SourceFile modTime time.Time @@ -235,7 +82,7 @@ type Watcher struct { configFilePaths []string sourceFileCache *collections.SyncMap[tspath.Path, *cachedSourceFile] - fileWatcher *FileWatcher + fileWatcher *vfswatch.FileWatcher } var _ tsc.Watcher = (*Watcher)(nil) @@ -261,7 +108,7 @@ func createWatcher( if configParseResult.ConfigFile != nil { w.configFileName = configParseResult.ConfigFile.SourceFile.FileName() } - w.fileWatcher = newFileWatcher( + w.fileWatcher = vfswatch.NewFileWatcher( sys.FS(), w.config.ParsedConfig.WatchOptions.WatchInterval(), testing != nil, @@ -291,7 +138,7 @@ func (w *Watcher) DoCycle() { if w.hasErrorsInTsConfig() { return } - if w.fileWatcher.watchState != nil && !w.configModified && !w.fileWatcher.HasChanges(w.fileWatcher.watchState) { + if w.fileWatcher.WatchState != nil && !w.configModified && !w.fileWatcher.HasChanges(w.fileWatcher.WatchState) { if w.testing != nil { w.testing.OnProgram(w.program) } @@ -317,7 +164,7 @@ func (w *Watcher) doBuild() { for dir := range wildcardDirs { tfs.SeenFiles.Add(dir) } - w.fileWatcher.wildcardDirectories = wildcardDirs + w.fileWatcher.WildcardDirectories = wildcardDirs if len(wildcardDirs) > 0 { w.config = w.config.ReloadFileNamesOfParsedCommandLine(w.sys.FS()) } @@ -333,8 +180,8 @@ func (w *Watcher) doBuild() { result := w.compileAndEmit() cached.DisableAndClearCache() - w.fileWatcher.updateWatchedFiles(tfs) - w.fileWatcher.pollInterval = w.config.ParsedConfig.WatchOptions.WatchInterval() + w.fileWatcher.UpdateWatchedFiles(tfs) + w.fileWatcher.PollInterval = w.config.ParsedConfig.WatchOptions.WatchInterval() w.configModified = false programFiles := w.program.GetProgram().FilesByPath() @@ -379,7 +226,7 @@ func (w *Watcher) hasErrorsInTsConfig() bool { if !w.configHasErrors && len(w.configFilePaths) > 0 { changed := false for _, path := range w.configFilePaths { - if old, ok := w.fileWatcher.watchState[path]; ok { + if old, ok := w.fileWatcher.WatchState[path]; ok { s := w.sys.FS().Stat(path) if !old.Exists { if s != nil { @@ -424,20 +271,20 @@ func (w *Watcher) hasErrorsInTsConfig() bool { // Testing helpers — exported for use by test packages func (w *Watcher) HasWatchedFilesChanged() bool { - return w.fileWatcher.HasChanges(w.fileWatcher.watchState) + return w.fileWatcher.HasChanges(w.fileWatcher.WatchState) } func (w *Watcher) WatchStateLen() int { - return len(w.fileWatcher.watchState) + return len(w.fileWatcher.WatchState) } func (w *Watcher) WatchStateHas(path string) bool { - _, ok := w.fileWatcher.watchState[path] + _, ok := w.fileWatcher.WatchState[path] return ok } func (w *Watcher) DebugWatchState(fn func(path string, modTime time.Time, exists bool)) { - for path, entry := range w.fileWatcher.watchState { + for path, entry := range w.fileWatcher.WatchState { fn(path, entry.ModTime, entry.Exists) } } diff --git a/internal/vfs/vfswatch/vfswatch.go b/internal/vfs/vfswatch/vfswatch.go new file mode 100644 index 00000000000..7883ff57dae --- /dev/null +++ b/internal/vfs/vfswatch/vfswatch.go @@ -0,0 +1,164 @@ +// This package implements a polling-based file watcher designed +// for use by both the CLI watcher and the language server. +package vfswatch + +import ( + "time" + + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/trackingvfs" +) + +const DebounceWait = 250 * time.Millisecond + +type FileWatcher struct { + fs vfs.FS + PollInterval time.Duration + testing bool + callback func() + WatchState map[string]trackingvfs.WatchEntry + WildcardDirectories map[string]bool +} + +func NewFileWatcher(fs vfs.FS, pollInterval time.Duration, testing bool, callback func()) *FileWatcher { + return &FileWatcher{ + fs: fs, + PollInterval: pollInterval, + testing: testing, + callback: callback, + } +} + +func (fw *FileWatcher) UpdateWatchedFiles(tfs *trackingvfs.FS) { + fw.WatchState = make(map[string]trackingvfs.WatchEntry) + tfs.SeenFiles.Range(func(fn string) bool { + if s := fw.fs.Stat(fn); s != nil { + fw.WatchState[fn] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} + } else { + fw.WatchState[fn] = trackingvfs.WatchEntry{Exists: false, ChildCount: -1} + } + return true + }) + for dir, recursive := range fw.WildcardDirectories { + if !recursive { + continue + } + _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + entries := fw.fs.GetAccessibleEntries(path) + count := len(entries.Files) + len(entries.Directories) + if existing, ok := fw.WatchState[path]; ok { + existing.ChildCount = count + fw.WatchState[path] = existing + } else { + if s := fw.fs.Stat(path); s != nil { + fw.WatchState[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} + } + } + return nil + }) + } +} + +func (fw *FileWatcher) WaitForSettled(now func() time.Time) { + if fw.testing { + return + } + current := fw.currentState() + settledAt := now() + for now().Sub(settledAt) < DebounceWait { + time.Sleep(fw.PollInterval) + if fw.HasChanges(current) { + current = fw.currentState() + settledAt = now() + } + } +} + +func (fw *FileWatcher) currentState() map[string]trackingvfs.WatchEntry { + state := make(map[string]trackingvfs.WatchEntry, len(fw.WatchState)) + for path := range fw.WatchState { + if s := fw.fs.Stat(path); s != nil { + state[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} + } else { + state[path] = trackingvfs.WatchEntry{Exists: false, ChildCount: -1} + } + } + for dir, recursive := range fw.WildcardDirectories { + if !recursive { + continue + } + _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + entries := fw.fs.GetAccessibleEntries(path) + count := len(entries.Files) + len(entries.Directories) + if existing, ok := state[path]; ok { + existing.ChildCount = count + state[path] = existing + } else { + if s := fw.fs.Stat(path); s != nil { + state[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} + } + } + return nil + }) + } + return state +} + +func (fw *FileWatcher) HasChanges(baseline map[string]trackingvfs.WatchEntry) bool { + for path, old := range baseline { + s := fw.fs.Stat(path) + if !old.Exists { + if s != nil { + return true + } + } else { + if s == nil || !s.ModTime().Equal(old.ModTime) { + return true + } + } + } + for dir, recursive := range fw.WildcardDirectories { + if !recursive { + continue + } + found := false + _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + entry, ok := baseline[path] + if !ok { + found = true + return vfs.SkipAll + } + if entry.ChildCount >= 0 { + entries := fw.fs.GetAccessibleEntries(path) + if len(entries.Files)+len(entries.Directories) != entry.ChildCount { + found = true + return vfs.SkipAll + } + } + return nil + }) + if found { + return true + } + } + return false +} + +func (fw *FileWatcher) Run(now func() time.Time) { + for { + time.Sleep(fw.PollInterval) + if fw.WatchState == nil || fw.HasChanges(fw.WatchState) { + fw.WaitForSettled(now) + fw.callback() + } + } +} From 6c953d92b6e5de737ca7690afea784bcae2a7ad0 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Tue, 31 Mar 2026 13:04:29 -0500 Subject: [PATCH 21/30] formatting fix --- internal/vfs/vfswatch/vfswatch.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/vfs/vfswatch/vfswatch.go b/internal/vfs/vfswatch/vfswatch.go index 7883ff57dae..35611d06598 100644 --- a/internal/vfs/vfswatch/vfswatch.go +++ b/internal/vfs/vfswatch/vfswatch.go @@ -1,4 +1,4 @@ -// This package implements a polling-based file watcher designed +// This package implements a polling-based file watcher designed // for use by both the CLI watcher and the language server. package vfswatch @@ -12,10 +12,10 @@ import ( const DebounceWait = 250 * time.Millisecond type FileWatcher struct { - fs vfs.FS - PollInterval time.Duration - testing bool - callback func() + fs vfs.FS + PollInterval time.Duration + testing bool + callback func() WatchState map[string]trackingvfs.WatchEntry WildcardDirectories map[string]bool } From 6cfdfacf52a91e6deeb84ad5db444059b07c6d8b Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:24:46 -0500 Subject: [PATCH 22/30] restructured for concurrency, embedded compilerhost, minor fixes --- internal/execute/watcher.go | 55 +++++--------------- internal/vfs/vfswatch/vfswatch.go | 84 ++++++++++++++++++++++++------- 2 files changed, 78 insertions(+), 61 deletions(-) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index f511daacb99..e19b656a90f 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -2,6 +2,7 @@ package execute import ( "reflect" + "sync" "time" "github.com/microsoft/typescript-go/internal/ast" @@ -13,7 +14,6 @@ import ( "github.com/microsoft/typescript-go/internal/execute/tsc" "github.com/microsoft/typescript-go/internal/tsoptions" "github.com/microsoft/typescript-go/internal/tspath" - "github.com/microsoft/typescript-go/internal/vfs" "github.com/microsoft/typescript-go/internal/vfs/cachedvfs" "github.com/microsoft/typescript-go/internal/vfs/trackingvfs" "github.com/microsoft/typescript-go/internal/vfs/vfswatch" @@ -25,25 +25,12 @@ type cachedSourceFile struct { } type watchCompilerHost struct { - inner compiler.CompilerHost + compiler.CompilerHost cache *collections.SyncMap[tspath.Path, *cachedSourceFile] } -var _ compiler.CompilerHost = (*watchCompilerHost)(nil) - -func (h *watchCompilerHost) FS() vfs.FS { return h.inner.FS() } -func (h *watchCompilerHost) DefaultLibraryPath() string { return h.inner.DefaultLibraryPath() } -func (h *watchCompilerHost) GetCurrentDirectory() string { return h.inner.GetCurrentDirectory() } -func (h *watchCompilerHost) Trace(msg *diagnostics.Message, args ...any) { - h.inner.Trace(msg, args...) -} - -func (h *watchCompilerHost) GetResolvedProjectReference(fileName string, path tspath.Path) *tsoptions.ParsedCommandLine { - return h.inner.GetResolvedProjectReference(fileName, path) -} - func (h *watchCompilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast.SourceFile { - info := h.inner.FS().Stat(opts.FileName) + info := h.CompilerHost.FS().Stat(opts.FileName) if cached, ok := h.cache.Load(opts.Path); ok { if info != nil && info.ModTime().Equal(cached.modTime) { @@ -51,7 +38,7 @@ func (h *watchCompilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast. } } - file := h.inner.GetSourceFile(opts) + file := h.CompilerHost.GetSourceFile(opts) if file != nil { if info != nil { h.cache.Store(opts.Path, &cachedSourceFile{ @@ -66,6 +53,7 @@ func (h *watchCompilerHost) GetSourceFile(opts ast.SourceFileParseOptions) *ast. } type Watcher struct { + mu sync.Mutex sys tsc.System configFileName string config *tsoptions.ParsedCommandLine @@ -135,10 +123,12 @@ func (w *Watcher) start() { } func (w *Watcher) DoCycle() { + w.mu.Lock() + defer w.mu.Unlock() if w.hasErrorsInTsConfig() { return } - if w.fileWatcher.WatchState != nil && !w.configModified && !w.fileWatcher.HasChanges(w.fileWatcher.WatchState) { + if !w.fileWatcher.WatchStateIsEmpty() && !w.configModified && !w.fileWatcher.HasChangesFromWatchState() { if w.testing != nil { w.testing.OnProgram(w.program) } @@ -157,14 +147,14 @@ func (w *Watcher) doBuild() { cached := cachedvfs.From(w.sys.FS()) tfs := &trackingvfs.FS{Inner: cached} innerHost := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), tfs, w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) - host := &watchCompilerHost{inner: innerHost, cache: w.sourceFileCache} + host := &watchCompilerHost{CompilerHost: innerHost, cache: w.sourceFileCache} if w.config.ConfigFile != nil { wildcardDirs := w.config.WildcardDirectories() for dir := range wildcardDirs { tfs.SeenFiles.Add(dir) } - w.fileWatcher.WildcardDirectories = wildcardDirs + w.fileWatcher.SetWildcardDirectories(wildcardDirs) if len(wildcardDirs) > 0 { w.config = w.config.ReloadFileNamesOfParsedCommandLine(w.sys.FS()) } @@ -181,7 +171,7 @@ func (w *Watcher) doBuild() { result := w.compileAndEmit() cached.DisableAndClearCache() w.fileWatcher.UpdateWatchedFiles(tfs) - w.fileWatcher.PollInterval = w.config.ParsedConfig.WatchOptions.WatchInterval() + w.fileWatcher.SetPollInterval(w.config.ParsedConfig.WatchOptions.WatchInterval()) w.configModified = false programFiles := w.program.GetProgram().FilesByPath() @@ -226,7 +216,7 @@ func (w *Watcher) hasErrorsInTsConfig() bool { if !w.configHasErrors && len(w.configFilePaths) > 0 { changed := false for _, path := range w.configFilePaths { - if old, ok := w.fileWatcher.WatchState[path]; ok { + if old, ok := w.fileWatcher.WatchStateEntry(path); ok { s := w.sys.FS().Stat(path) if !old.Exists { if s != nil { @@ -267,24 +257,3 @@ func (w *Watcher) hasErrorsInTsConfig() bool { w.extendedConfigCache = extendedConfigCache return false } - -// Testing helpers — exported for use by test packages - -func (w *Watcher) HasWatchedFilesChanged() bool { - return w.fileWatcher.HasChanges(w.fileWatcher.WatchState) -} - -func (w *Watcher) WatchStateLen() int { - return len(w.fileWatcher.WatchState) -} - -func (w *Watcher) WatchStateHas(path string) bool { - _, ok := w.fileWatcher.WatchState[path] - return ok -} - -func (w *Watcher) DebugWatchState(fn func(path string, modTime time.Time, exists bool)) { - for path, entry := range w.fileWatcher.WatchState { - fn(path, entry.ModTime, entry.Exists) - } -} diff --git a/internal/vfs/vfswatch/vfswatch.go b/internal/vfs/vfswatch/vfswatch.go index 35611d06598..febcbffe9ac 100644 --- a/internal/vfs/vfswatch/vfswatch.go +++ b/internal/vfs/vfswatch/vfswatch.go @@ -3,6 +3,7 @@ package vfswatch import ( + "sync" "time" "github.com/microsoft/typescript-go/internal/vfs" @@ -13,33 +14,61 @@ const DebounceWait = 250 * time.Millisecond type FileWatcher struct { fs vfs.FS - PollInterval time.Duration + pollInterval time.Duration testing bool callback func() - WatchState map[string]trackingvfs.WatchEntry - WildcardDirectories map[string]bool + watchState map[string]trackingvfs.WatchEntry + wildcardDirectories map[string]bool + mu sync.Mutex } func NewFileWatcher(fs vfs.FS, pollInterval time.Duration, testing bool, callback func()) *FileWatcher { return &FileWatcher{ fs: fs, - PollInterval: pollInterval, + pollInterval: pollInterval, testing: testing, callback: callback, } } +func (fw *FileWatcher) SetWildcardDirectories(dirs map[string]bool) { + fw.mu.Lock() + defer fw.mu.Unlock() + fw.wildcardDirectories = dirs +} + +func (fw *FileWatcher) SetPollInterval(d time.Duration) { + fw.mu.Lock() + defer fw.mu.Unlock() + fw.pollInterval = d +} + +func (fw *FileWatcher) WatchStateEntry(path string) (trackingvfs.WatchEntry, bool) { + fw.mu.Lock() + defer fw.mu.Unlock() + e, ok := fw.watchState[path] + return e, ok +} + +func (fw *FileWatcher) WatchStateIsEmpty() bool { + fw.mu.Lock() + defer fw.mu.Unlock() + return fw.watchState == nil +} + func (fw *FileWatcher) UpdateWatchedFiles(tfs *trackingvfs.FS) { - fw.WatchState = make(map[string]trackingvfs.WatchEntry) + fw.mu.Lock() + defer fw.mu.Unlock() + fw.watchState = make(map[string]trackingvfs.WatchEntry) tfs.SeenFiles.Range(func(fn string) bool { if s := fw.fs.Stat(fn); s != nil { - fw.WatchState[fn] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} + fw.watchState[fn] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} } else { - fw.WatchState[fn] = trackingvfs.WatchEntry{Exists: false, ChildCount: -1} + fw.watchState[fn] = trackingvfs.WatchEntry{Exists: false, ChildCount: -1} } return true }) - for dir, recursive := range fw.WildcardDirectories { + for dir, recursive := range fw.wildcardDirectories { if !recursive { continue } @@ -49,12 +78,12 @@ func (fw *FileWatcher) UpdateWatchedFiles(tfs *trackingvfs.FS) { } entries := fw.fs.GetAccessibleEntries(path) count := len(entries.Files) + len(entries.Directories) - if existing, ok := fw.WatchState[path]; ok { + if existing, ok := fw.watchState[path]; ok { existing.ChildCount = count - fw.WatchState[path] = existing + fw.watchState[path] = existing } else { if s := fw.fs.Stat(path); s != nil { - fw.WatchState[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} + fw.watchState[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} } } return nil @@ -68,8 +97,9 @@ func (fw *FileWatcher) WaitForSettled(now func() time.Time) { } current := fw.currentState() settledAt := now() + tick := min(fw.pollInterval, DebounceWait) for now().Sub(settledAt) < DebounceWait { - time.Sleep(fw.PollInterval) + time.Sleep(tick) if fw.HasChanges(current) { current = fw.currentState() settledAt = now() @@ -78,15 +108,19 @@ func (fw *FileWatcher) WaitForSettled(now func() time.Time) { } func (fw *FileWatcher) currentState() map[string]trackingvfs.WatchEntry { - state := make(map[string]trackingvfs.WatchEntry, len(fw.WatchState)) - for path := range fw.WatchState { + fw.mu.Lock() + watchState := fw.watchState + wildcardDirs := fw.wildcardDirectories + fw.mu.Unlock() + state := make(map[string]trackingvfs.WatchEntry, len(watchState)) + for path := range watchState { if s := fw.fs.Stat(path); s != nil { state[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} } else { state[path] = trackingvfs.WatchEntry{Exists: false, ChildCount: -1} } } - for dir, recursive := range fw.WildcardDirectories { + for dir, recursive := range wildcardDirs { if !recursive { continue } @@ -111,6 +145,9 @@ func (fw *FileWatcher) currentState() map[string]trackingvfs.WatchEntry { } func (fw *FileWatcher) HasChanges(baseline map[string]trackingvfs.WatchEntry) bool { + fw.mu.Lock() + wildcardDirs := fw.wildcardDirectories + fw.mu.Unlock() for path, old := range baseline { s := fw.fs.Stat(path) if !old.Exists { @@ -123,7 +160,7 @@ func (fw *FileWatcher) HasChanges(baseline map[string]trackingvfs.WatchEntry) bo } } } - for dir, recursive := range fw.WildcardDirectories { + for dir, recursive := range wildcardDirs { if !recursive { continue } @@ -153,10 +190,21 @@ func (fw *FileWatcher) HasChanges(baseline map[string]trackingvfs.WatchEntry) bo return false } +func (fw *FileWatcher) HasChangesFromWatchState() bool { + fw.mu.Lock() + ws := fw.watchState + fw.mu.Unlock() + return fw.HasChanges(ws) +} + func (fw *FileWatcher) Run(now func() time.Time) { for { - time.Sleep(fw.PollInterval) - if fw.WatchState == nil || fw.HasChanges(fw.WatchState) { + fw.mu.Lock() + interval := fw.pollInterval + ws := fw.watchState + fw.mu.Unlock() + time.Sleep(interval) + if ws == nil || fw.HasChanges(ws) { fw.WaitForSettled(now) fw.callback() } From a7d6ad057012fa004fa022bbde6d1b9a5715a4d1 Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Fri, 3 Apr 2026 15:24:18 -0700 Subject: [PATCH 23/30] Add concurrency fuzz/race tests for watcher Add two test files that expose data races in the watch CLI: - vfswatch_race_test.go: Tests FileWatcher concurrency directly. Hammers HasChanges, UpdateWatchedFiles, WildcardDirectories, and PollInterval from multiple goroutines. Includes fuzz tests for sequential and concurrent random operation sequences. - watcher_race_test.go: Tests full Watcher concurrency via the test infrastructure. Concurrent DoCycle calls, concurrent state reads, rapid config changes, and file churn during watch cycles. Running with -race detects hundreds of data race warnings across unprotected fields in FileWatcher (WatchState, WildcardDirectories, PollInterval) and Watcher (configModified, config, program, configHasErrors, configFilePaths, extendedConfigCache). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../execute/tsctests/watcher_race_test.go | 280 +++++++++++++ internal/vfs/vfswatch/vfswatch_race_test.go | 382 ++++++++++++++++++ 2 files changed, 662 insertions(+) create mode 100644 internal/execute/tsctests/watcher_race_test.go create mode 100644 internal/vfs/vfswatch/vfswatch_race_test.go diff --git a/internal/execute/tsctests/watcher_race_test.go b/internal/execute/tsctests/watcher_race_test.go new file mode 100644 index 00000000000..0e4eaab9d4a --- /dev/null +++ b/internal/execute/tsctests/watcher_race_test.go @@ -0,0 +1,280 @@ +package tsctests + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/microsoft/typescript-go/internal/execute" +) + +// createTestWatcher sets up a minimal project with a tsconfig and +// returns a Watcher ready for concurrent testing, plus the TestSys +// for file manipulation. +func createTestWatcher(t *testing.T) (*execute.Watcher, *TestSys) { + t.Helper() + input := &tscInput{ + files: FileMap{ + "/home/src/workspaces/project/a.ts": `const a: number = 1;`, + "/home/src/workspaces/project/b.ts": `import { a } from "./a"; export const b = a;`, + "/home/src/workspaces/project/tsconfig.json": `{}`, + }, + commandLineArgs: []string{"--watch"}, + } + sys := newTestSys(input, false) + result := execute.CommandLine(sys, []string{"--watch"}, sys) + if result.Watcher == nil { + t.Fatal("expected Watcher to be non-nil in watch mode") + } + w, ok := result.Watcher.(*execute.Watcher) + if !ok { + t.Fatalf("expected *execute.Watcher, got %T", result.Watcher) + } + return w, sys +} + +// TestWatcherConcurrentDoCycle calls DoCycle from multiple goroutines +// while modifying source files, exposing data races on Watcher fields +// such as configModified, program, config, and the underlying +// FileWatcher state. Run with -race to detect. +func TestWatcherConcurrentDoCycle(t *testing.T) { + t.Parallel() + w, sys := createTestWatcher(t) + + var wg sync.WaitGroup + + for i := 0; i < 8; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 10; j++ { + _ = sys.fsFromFileMap().WriteFile( + "/home/src/workspaces/project/a.ts", + fmt.Sprintf("const a: number = %d;", i*10+j), + ) + w.DoCycle() + } + }(i) + } + + wg.Wait() +} + +// TestWatcherDoCycleWithConcurrentStateReads calls DoCycle while +// other goroutines read watcher state through the exported test +// helper methods (HasWatchedFilesChanged, WatchStateLen, etc.). +func TestWatcherDoCycleWithConcurrentStateReads(t *testing.T) { + t.Parallel() + w, sys := createTestWatcher(t) + + var wg sync.WaitGroup + + // DoCycle goroutines + for i := 0; i < 4; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 15; j++ { + _ = sys.fsFromFileMap().WriteFile( + "/home/src/workspaces/project/a.ts", + fmt.Sprintf("const a: number = %d;", i*15+j), + ) + w.DoCycle() + } + }(i) + } + + // State reader goroutines + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 50; j++ { + _ = w.HasWatchedFilesChanged() + _ = w.WatchStateLen() + _ = w.WatchStateHas("/home/src/workspaces/project/a.ts") + w.DebugWatchState(func(path string, modTime time.Time, exists bool) {}) + } + }() + } + + wg.Wait() +} + +// TestWatcherConcurrentFileChangesAndDoCycle creates, modifies, and +// deletes files from multiple goroutines while DoCycle runs, testing +// races between FS mutations and watch state updates. +func TestWatcherConcurrentFileChangesAndDoCycle(t *testing.T) { + t.Parallel() + w, sys := createTestWatcher(t) + + var wg sync.WaitGroup + + // File creators + for i := 0; i < 4; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 20; j++ { + path := fmt.Sprintf("/home/src/workspaces/project/gen_%d_%d.ts", i, j) + _ = sys.fsFromFileMap().WriteFile(path, fmt.Sprintf("export const x%d_%d = %d;", i, j, j)) + } + }(i) + } + + // File deleters + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 20; j++ { + _ = sys.fsFromFileMap().Remove( + fmt.Sprintf("/home/src/workspaces/project/gen_0_%d.ts", j), + ) + } + }() + + // DoCycle callers + for i := 0; i < 4; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 10; j++ { + w.DoCycle() + } + }() + } + + wg.Wait() +} + +// TestWatcherRapidConfigChanges modifies tsconfig.json rapidly from +// multiple goroutines while DoCycle runs, testing races on +// config-related fields (configModified, configHasErrors, +// configFilePaths, config, extendedConfigCache). +func TestWatcherRapidConfigChanges(t *testing.T) { + t.Parallel() + w, sys := createTestWatcher(t) + + var wg sync.WaitGroup + + configs := []string{ + `{}`, + `{"compilerOptions": {"strict": true}}`, + `{"compilerOptions": {"target": "ES2020"}}`, + `{"compilerOptions": {"noEmit": true}}`, + } + + // Config modifiers + DoCycle + for i := 0; i < 3; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 10; j++ { + _ = sys.fsFromFileMap().WriteFile( + "/home/src/workspaces/project/tsconfig.json", + configs[(i+j)%len(configs)], + ) + w.DoCycle() + } + }(i) + } + + // Concurrent source file modifications + for i := 0; i < 2; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 15; j++ { + _ = sys.fsFromFileMap().WriteFile( + "/home/src/workspaces/project/a.ts", + fmt.Sprintf("const a: number = %d;", i*15+j), + ) + w.DoCycle() + } + }(i) + } + + // State readers + for i := 0; i < 4; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 30; j++ { + _ = w.HasWatchedFilesChanged() + _ = w.WatchStateLen() + } + }() + } + + wg.Wait() +} + +// TestWatcherConcurrentDoCycleNoChanges calls DoCycle from many +// goroutines when no files have changed, testing the early-return +// path where WatchState is read and HasChanges is called. This path +// reads WatchState and WildcardDirectories without synchronization. +func TestWatcherConcurrentDoCycleNoChanges(t *testing.T) { + t.Parallel() + w, _ := createTestWatcher(t) + + var wg sync.WaitGroup + + for i := 0; i < 16; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 50; j++ { + w.DoCycle() + } + }() + } + + wg.Wait() +} + +// TestWatcherAlternatingModifyAndDoCycle alternates between modifying +// a file and calling DoCycle from different goroutines, creating a +// realistic scenario where the file watcher detects changes mid-cycle. +func TestWatcherAlternatingModifyAndDoCycle(t *testing.T) { + t.Parallel() + w, sys := createTestWatcher(t) + + var wg sync.WaitGroup + + // Writer goroutine: continuously modifies files + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + _ = sys.fsFromFileMap().WriteFile( + "/home/src/workspaces/project/a.ts", + fmt.Sprintf("const a: number = %d;", j), + ) + } + }() + + // Multiple DoCycle goroutines + for i := 0; i < 4; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 25; j++ { + w.DoCycle() + } + }() + } + + // State reader goroutines + for i := 0; i < 4; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + _ = w.HasWatchedFilesChanged() + } + }() + } + + wg.Wait() +} diff --git a/internal/vfs/vfswatch/vfswatch_race_test.go b/internal/vfs/vfswatch/vfswatch_race_test.go new file mode 100644 index 00000000000..ea9608ecf85 --- /dev/null +++ b/internal/vfs/vfswatch/vfswatch_race_test.go @@ -0,0 +1,382 @@ +package vfswatch_test + +import ( + "fmt" + "sync" + "testing" + "time" + + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/trackingvfs" + "github.com/microsoft/typescript-go/internal/vfs/vfstest" + "github.com/microsoft/typescript-go/internal/vfs/vfswatch" +) + +func newTestFS() vfs.FS { + return vfstest.FromMap(map[string]string{ + "/src/a.ts": "const a = 1;", + "/src/b.ts": "const b = 2;", + "/src/c.ts": "const c = 3;", + "/src/sub/d.ts": "const d = 4;", + "/tsconfig.json": `{}`, + }, true) +} + +func newWatcherWithState(fs vfs.FS) *vfswatch.FileWatcher { + fw := vfswatch.NewFileWatcher(fs, 10*time.Millisecond, true, func() {}) + tfs := &trackingvfs.FS{Inner: fs} + tfs.SeenFiles.Add("/src/a.ts") + tfs.SeenFiles.Add("/src/b.ts") + tfs.SeenFiles.Add("/src/c.ts") + tfs.SeenFiles.Add("/src/sub/d.ts") + tfs.SeenFiles.Add("/tsconfig.json") + fw.UpdateWatchedFiles(tfs) + return fw +} + +// TestRaceHasChangesVsUpdateWatchedFiles tests for data races between +// concurrent HasChanges reads and UpdateWatchedFiles writes on the +// WatchState map. The WatchState field is a plain map with no +// synchronization; concurrent access should be detected by -race. +func TestRaceHasChangesVsUpdateWatchedFiles(t *testing.T) { + t.Parallel() + fs := newTestFS() + fw := newWatcherWithState(fs) + + var wg sync.WaitGroup + + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 200; j++ { + ws := fw.WatchState + if ws != nil { + fw.HasChanges(ws) + } + } + }() + } + + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + tfs := &trackingvfs.FS{Inner: fs} + tfs.SeenFiles.Add("/src/a.ts") + tfs.SeenFiles.Add("/src/b.ts") + fw.UpdateWatchedFiles(tfs) + } + }() + } + + wg.Wait() +} + +// TestRaceWildcardDirectoriesAccess tests for data races when +// WildcardDirectories is read internally by HasChanges while being +// replaced concurrently. WildcardDirectories is a plain map assigned +// directly on the struct with no synchronization. +func TestRaceWildcardDirectoriesAccess(t *testing.T) { + t.Parallel() + fs := newTestFS() + fw := newWatcherWithState(fs) + fw.WildcardDirectories = map[string]bool{"/src": true} + + var wg sync.WaitGroup + + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 200; j++ { + ws := fw.WatchState + if ws != nil { + fw.HasChanges(ws) + } + } + }() + } + + for i := 0; i < 5; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + fw.WildcardDirectories = map[string]bool{"/src": true} + } + }() + } + + wg.Wait() +} + +// TestRacePollIntervalAccess tests for data races on the PollInterval +// field when it is read and written from multiple goroutines. +func TestRacePollIntervalAccess(t *testing.T) { + t.Parallel() + fs := newTestFS() + fw := newWatcherWithState(fs) + + var wg sync.WaitGroup + + for i := 0; i < 10; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 500; j++ { + _ = fw.PollInterval + } + }() + } + + for i := 0; i < 5; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 200; j++ { + fw.PollInterval = time.Duration(i*200+j) * time.Millisecond + } + }(i) + } + + wg.Wait() +} + +// TestRaceMixedOperations hammers all FileWatcher operations +// concurrently: HasChanges, UpdateWatchedFiles, FS mutations, +// WildcardDirectories writes, and PollInterval writes. +func TestRaceMixedOperations(t *testing.T) { + t.Parallel() + fs := newTestFS() + fw := newWatcherWithState(fs) + fw.WildcardDirectories = map[string]bool{"/src": true} + + var wg sync.WaitGroup + + // HasChanges readers + for i := 0; i < 8; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 100; j++ { + ws := fw.WatchState + if ws != nil { + fw.HasChanges(ws) + } + } + }() + } + + // UpdateWatchedFiles writers + for i := 0; i < 4; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 50; j++ { + tfs := &trackingvfs.FS{Inner: fs} + tfs.SeenFiles.Add("/src/a.ts") + tfs.SeenFiles.Add(fmt.Sprintf("/src/new_%d_%d.ts", i, j)) + fw.UpdateWatchedFiles(tfs) + } + }(i) + } + + // FS modifiers + for i := 0; i < 4; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 50; j++ { + path := fmt.Sprintf("/src/gen_%d_%d.ts", i, j) + _ = fs.WriteFile(path, fmt.Sprintf("const x = %d;", j)) + if j%3 == 0 { + _ = fs.Remove(path) + } + } + }(i) + } + + // WildcardDirectories writers + for i := 0; i < 2; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 50; j++ { + fw.WildcardDirectories = map[string]bool{"/src": true} + } + }() + } + + // PollInterval writers + for i := 0; i < 2; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 100; j++ { + fw.PollInterval = time.Duration(50+j) * time.Millisecond + } + }(i) + } + + wg.Wait() +} + +// TestRaceUpdateWithConcurrentFileModifications creates and deletes +// files on the FS while UpdateWatchedFiles is scanning the same FS, +// testing for races between the FS walker and concurrent mutations. +func TestRaceUpdateWithConcurrentFileModifications(t *testing.T) { + t.Parallel() + fs := newTestFS() + fw := newWatcherWithState(fs) + fw.WildcardDirectories = map[string]bool{"/src": true} + + var wg sync.WaitGroup + + // Rapid file creation/deletion + for i := 0; i < 6; i++ { + wg.Add(1) + go func(i int) { + defer wg.Done() + for j := 0; j < 100; j++ { + path := fmt.Sprintf("/src/churn_%d_%d.ts", i, j) + _ = fs.WriteFile(path, fmt.Sprintf("export const v = %d;", j)) + _ = fs.Remove(path) + } + }(i) + } + + // Concurrent UpdateWatchedFiles (walks the FS tree via WildcardDirectories) + for i := 0; i < 4; i++ { + wg.Add(1) + go func() { + defer wg.Done() + for j := 0; j < 50; j++ { + tfs := &trackingvfs.FS{Inner: fs} + tfs.SeenFiles.Add("/src/a.ts") + tfs.SeenFiles.Add("/tsconfig.json") + fw.UpdateWatchedFiles(tfs) + } + }() + } + + wg.Wait() +} + +// FuzzFileWatcherOperations fuzzes random sequences of file operations +// and watcher state management to find panics and edge cases. +// Run with -race to also detect data races. +func FuzzFileWatcherOperations(f *testing.F) { + f.Add([]byte{0, 1, 2, 3, 0, 1, 2, 3}) + f.Add([]byte{2, 2, 2, 0, 0, 1, 3, 3}) + f.Add([]byte{3, 3, 3, 3, 0, 0, 0, 0}) + f.Add([]byte{4, 4, 4, 0, 2, 1, 3, 2}) + f.Add([]byte{5, 5, 5, 5, 5, 5, 5, 5}) + f.Add([]byte{0, 0, 0, 0, 0, 0, 0, 0}) + f.Add([]byte{1, 1, 1, 1, 1, 1, 1, 1}) + + f.Fuzz(func(t *testing.T, ops []byte) { + if len(ops) == 0 { + return + } + + fs := newTestFS() + fw := newWatcherWithState(fs) + + files := []string{"/src/a.ts", "/src/b.ts", "/src/c.ts", "/src/new.ts", "/src/sub/new.ts"} + + for i, op := range ops { + path := files[i%len(files)] + + switch op % 6 { + case 0: // Write/modify a file + _ = fs.WriteFile(path, fmt.Sprintf("const x = %d;", i)) + case 1: // Remove a file + _ = fs.Remove(path) + case 2: // Check for changes against current state + ws := fw.WatchState + if ws != nil { + fw.HasChanges(ws) + } + case 3: // Rebuild watch state + tfs := &trackingvfs.FS{Inner: fs} + for _, f := range files { + tfs.SeenFiles.Add(f) + } + fw.UpdateWatchedFiles(tfs) + case 4: // Set wildcard directories and check for changes + fw.WildcardDirectories = map[string]bool{"/src": true} + ws := fw.WatchState + if ws != nil { + fw.HasChanges(ws) + } + case 5: // Modify PollInterval + fw.PollInterval = time.Duration(i*10) * time.Millisecond + } + } + }) +} + +// FuzzFileWatcherConcurrent is a fuzz test that runs random operations +// from multiple goroutines to find concurrency bugs. +func FuzzFileWatcherConcurrent(f *testing.F) { + f.Add([]byte{0, 1, 2, 3, 4, 5, 0, 1, 2, 3, 4, 5}) + f.Add([]byte{0, 0, 0, 3, 3, 3, 2, 2, 2, 1, 1, 1}) + f.Add([]byte{2, 3, 2, 3, 2, 3, 0, 0, 0, 0, 0, 0}) + + f.Fuzz(func(t *testing.T, ops []byte) { + if len(ops) < 4 { + return + } + + fs := newTestFS() + fw := newWatcherWithState(fs) + fw.WildcardDirectories = map[string]bool{"/src": true} + + files := []string{"/src/a.ts", "/src/b.ts", "/src/c.ts", "/src/new.ts"} + + // Split ops into chunks for different goroutines + chunkSize := len(ops) / 2 + if chunkSize == 0 { + chunkSize = 1 + } + + var wg sync.WaitGroup + + for start := 0; start < len(ops); start += chunkSize { + end := start + chunkSize + if end > len(ops) { + end = len(ops) + } + chunk := ops[start:end] + + wg.Add(1) + go func(chunk []byte, goroutineID int) { + defer wg.Done() + for i, op := range chunk { + path := files[(goroutineID*len(chunk)+i)%len(files)] + switch op % 5 { + case 0: + _ = fs.WriteFile(path, fmt.Sprintf("const g%d = %d;", goroutineID, i)) + case 1: + _ = fs.Remove(path) + case 2: + ws := fw.WatchState + if ws != nil { + fw.HasChanges(ws) + } + case 3: + tfs := &trackingvfs.FS{Inner: fs} + tfs.SeenFiles.Add(path) + fw.UpdateWatchedFiles(tfs) + case 4: + fw.WildcardDirectories = map[string]bool{"/src": true} + } + } + }(chunk, start/chunkSize) + } + + wg.Wait() + }) +} From 03a34cc0070a8ae12e67c23145e68d688fbcf673 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:47:20 -0500 Subject: [PATCH 24/30] updated race tests for new structure --- .../execute/tsctests/watcher_race_test.go | 15 +++--- internal/vfs/vfswatch/vfswatch_race_test.go | 54 +++++++------------ 2 files changed, 25 insertions(+), 44 deletions(-) diff --git a/internal/execute/tsctests/watcher_race_test.go b/internal/execute/tsctests/watcher_race_test.go index 0e4eaab9d4a..113b9a0dd58 100644 --- a/internal/execute/tsctests/watcher_race_test.go +++ b/internal/execute/tsctests/watcher_race_test.go @@ -4,7 +4,6 @@ import ( "fmt" "sync" "testing" - "time" "github.com/microsoft/typescript-go/internal/execute" ) @@ -91,10 +90,10 @@ func TestWatcherDoCycleWithConcurrentStateReads(t *testing.T) { go func() { defer wg.Done() for j := 0; j < 50; j++ { - _ = w.HasWatchedFilesChanged() - _ = w.WatchStateLen() - _ = w.WatchStateHas("/home/src/workspaces/project/a.ts") - w.DebugWatchState(func(path string, modTime time.Time, exists bool) {}) + w.DoCycle() + w.DoCycle() + w.DoCycle() + w.DoCycle() } }() } @@ -201,8 +200,8 @@ func TestWatcherRapidConfigChanges(t *testing.T) { go func() { defer wg.Done() for j := 0; j < 30; j++ { - _ = w.HasWatchedFilesChanged() - _ = w.WatchStateLen() + w.DoCycle() + w.DoCycle() } }() } @@ -271,7 +270,7 @@ func TestWatcherAlternatingModifyAndDoCycle(t *testing.T) { go func() { defer wg.Done() for j := 0; j < 100; j++ { - _ = w.HasWatchedFilesChanged() + w.DoCycle() } }() } diff --git a/internal/vfs/vfswatch/vfswatch_race_test.go b/internal/vfs/vfswatch/vfswatch_race_test.go index ea9608ecf85..093b9f5e0ce 100644 --- a/internal/vfs/vfswatch/vfswatch_race_test.go +++ b/internal/vfs/vfswatch/vfswatch_race_test.go @@ -50,10 +50,7 @@ func TestRaceHasChangesVsUpdateWatchedFiles(t *testing.T) { go func() { defer wg.Done() for j := 0; j < 200; j++ { - ws := fw.WatchState - if ws != nil { - fw.HasChanges(ws) - } + fw.HasChangesFromWatchState() } }() } @@ -82,7 +79,7 @@ func TestRaceWildcardDirectoriesAccess(t *testing.T) { t.Parallel() fs := newTestFS() fw := newWatcherWithState(fs) - fw.WildcardDirectories = map[string]bool{"/src": true} + fw.SetWildcardDirectories(map[string]bool{"/src": true}) var wg sync.WaitGroup @@ -91,10 +88,7 @@ func TestRaceWildcardDirectoriesAccess(t *testing.T) { go func() { defer wg.Done() for j := 0; j < 200; j++ { - ws := fw.WatchState - if ws != nil { - fw.HasChanges(ws) - } + fw.HasChangesFromWatchState() } }() } @@ -104,7 +98,7 @@ func TestRaceWildcardDirectoriesAccess(t *testing.T) { go func() { defer wg.Done() for j := 0; j < 100; j++ { - fw.WildcardDirectories = map[string]bool{"/src": true} + fw.SetWildcardDirectories(map[string]bool{"/src": true}) } }() } @@ -126,7 +120,7 @@ func TestRacePollIntervalAccess(t *testing.T) { go func() { defer wg.Done() for j := 0; j < 500; j++ { - _ = fw.PollInterval + fw.HasChangesFromWatchState() } }() } @@ -136,7 +130,7 @@ func TestRacePollIntervalAccess(t *testing.T) { go func(i int) { defer wg.Done() for j := 0; j < 200; j++ { - fw.PollInterval = time.Duration(i*200+j) * time.Millisecond + fw.SetPollInterval(time.Duration(i*200+j) * time.Millisecond) } }(i) } @@ -151,7 +145,7 @@ func TestRaceMixedOperations(t *testing.T) { t.Parallel() fs := newTestFS() fw := newWatcherWithState(fs) - fw.WildcardDirectories = map[string]bool{"/src": true} + fw.SetWildcardDirectories(map[string]bool{"/src": true}) var wg sync.WaitGroup @@ -161,10 +155,7 @@ func TestRaceMixedOperations(t *testing.T) { go func() { defer wg.Done() for j := 0; j < 100; j++ { - ws := fw.WatchState - if ws != nil { - fw.HasChanges(ws) - } + fw.HasChangesFromWatchState() } }() } @@ -204,7 +195,7 @@ func TestRaceMixedOperations(t *testing.T) { go func() { defer wg.Done() for j := 0; j < 50; j++ { - fw.WildcardDirectories = map[string]bool{"/src": true} + fw.SetWildcardDirectories(map[string]bool{"/src": true}) } }() } @@ -215,7 +206,7 @@ func TestRaceMixedOperations(t *testing.T) { go func(i int) { defer wg.Done() for j := 0; j < 100; j++ { - fw.PollInterval = time.Duration(50+j) * time.Millisecond + fw.SetPollInterval(time.Duration(50+j) * time.Millisecond) } }(i) } @@ -230,7 +221,7 @@ func TestRaceUpdateWithConcurrentFileModifications(t *testing.T) { t.Parallel() fs := newTestFS() fw := newWatcherWithState(fs) - fw.WildcardDirectories = map[string]bool{"/src": true} + fw.SetWildcardDirectories(map[string]bool{"/src": true}) var wg sync.WaitGroup @@ -295,10 +286,7 @@ func FuzzFileWatcherOperations(f *testing.F) { case 1: // Remove a file _ = fs.Remove(path) case 2: // Check for changes against current state - ws := fw.WatchState - if ws != nil { - fw.HasChanges(ws) - } + fw.HasChangesFromWatchState() case 3: // Rebuild watch state tfs := &trackingvfs.FS{Inner: fs} for _, f := range files { @@ -306,13 +294,10 @@ func FuzzFileWatcherOperations(f *testing.F) { } fw.UpdateWatchedFiles(tfs) case 4: // Set wildcard directories and check for changes - fw.WildcardDirectories = map[string]bool{"/src": true} - ws := fw.WatchState - if ws != nil { - fw.HasChanges(ws) - } + fw.SetWildcardDirectories(map[string]bool{"/src": true}) + fw.HasChangesFromWatchState() case 5: // Modify PollInterval - fw.PollInterval = time.Duration(i*10) * time.Millisecond + fw.SetPollInterval(time.Duration(i*10) * time.Millisecond) } } }) @@ -332,7 +317,7 @@ func FuzzFileWatcherConcurrent(f *testing.F) { fs := newTestFS() fw := newWatcherWithState(fs) - fw.WildcardDirectories = map[string]bool{"/src": true} + fw.SetWildcardDirectories(map[string]bool{"/src": true}) files := []string{"/src/a.ts", "/src/b.ts", "/src/c.ts", "/src/new.ts"} @@ -362,16 +347,13 @@ func FuzzFileWatcherConcurrent(f *testing.F) { case 1: _ = fs.Remove(path) case 2: - ws := fw.WatchState - if ws != nil { - fw.HasChanges(ws) - } + fw.HasChangesFromWatchState() case 3: tfs := &trackingvfs.FS{Inner: fs} tfs.SeenFiles.Add(path) fw.UpdateWatchedFiles(tfs) case 4: - fw.WildcardDirectories = map[string]bool{"/src": true} + fw.SetWildcardDirectories(map[string]bool{"/src": true}) } } }(chunk, start/chunkSize) From 6f793edb56b9b5d1756f7d1388b88877233b7918 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:19:56 -0500 Subject: [PATCH 25/30] fixed linting for tests --- .../execute/tsctests/watcher_race_test.go | 96 +++++++--------- internal/vfs/vfswatch/vfswatch_race_test.go | 105 +++++++----------- 2 files changed, 83 insertions(+), 118 deletions(-) diff --git a/internal/execute/tsctests/watcher_race_test.go b/internal/execute/tsctests/watcher_race_test.go index 113b9a0dd58..bad718b2d10 100644 --- a/internal/execute/tsctests/watcher_race_test.go +++ b/internal/execute/tsctests/watcher_race_test.go @@ -43,11 +43,11 @@ func TestWatcherConcurrentDoCycle(t *testing.T) { var wg sync.WaitGroup - for i := 0; i < 8; i++ { + for i := range 8 { wg.Add(1) go func(i int) { defer wg.Done() - for j := 0; j < 10; j++ { + for j := range 10 { _ = sys.fsFromFileMap().WriteFile( "/home/src/workspaces/project/a.ts", fmt.Sprintf("const a: number = %d;", i*10+j), @@ -70,11 +70,11 @@ func TestWatcherDoCycleWithConcurrentStateReads(t *testing.T) { var wg sync.WaitGroup // DoCycle goroutines - for i := 0; i < 4; i++ { + for i := range 4 { wg.Add(1) go func(i int) { defer wg.Done() - for j := 0; j < 15; j++ { + for j := range 15 { _ = sys.fsFromFileMap().WriteFile( "/home/src/workspaces/project/a.ts", fmt.Sprintf("const a: number = %d;", i*15+j), @@ -85,17 +85,15 @@ func TestWatcherDoCycleWithConcurrentStateReads(t *testing.T) { } // State reader goroutines - for i := 0; i < 8; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 50; j++ { + for range 8 { + wg.Go(func() { + for range 50 { w.DoCycle() w.DoCycle() w.DoCycle() w.DoCycle() } - }() + }) } wg.Wait() @@ -111,11 +109,11 @@ func TestWatcherConcurrentFileChangesAndDoCycle(t *testing.T) { var wg sync.WaitGroup // File creators - for i := 0; i < 4; i++ { + for i := range 4 { wg.Add(1) go func(i int) { defer wg.Done() - for j := 0; j < 20; j++ { + for j := range 20 { path := fmt.Sprintf("/home/src/workspaces/project/gen_%d_%d.ts", i, j) _ = sys.fsFromFileMap().WriteFile(path, fmt.Sprintf("export const x%d_%d = %d;", i, j, j)) } @@ -123,25 +121,21 @@ func TestWatcherConcurrentFileChangesAndDoCycle(t *testing.T) { } // File deleters - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 20; j++ { + wg.Go(func() { + for j := range 20 { _ = sys.fsFromFileMap().Remove( fmt.Sprintf("/home/src/workspaces/project/gen_0_%d.ts", j), ) } - }() + }) // DoCycle callers - for i := 0; i < 4; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 10; j++ { + for range 4 { + wg.Go(func() { + for range 10 { w.DoCycle() } - }() + }) } wg.Wait() @@ -165,11 +159,11 @@ func TestWatcherRapidConfigChanges(t *testing.T) { } // Config modifiers + DoCycle - for i := 0; i < 3; i++ { + for i := range 3 { wg.Add(1) go func(i int) { defer wg.Done() - for j := 0; j < 10; j++ { + for j := range 10 { _ = sys.fsFromFileMap().WriteFile( "/home/src/workspaces/project/tsconfig.json", configs[(i+j)%len(configs)], @@ -180,11 +174,11 @@ func TestWatcherRapidConfigChanges(t *testing.T) { } // Concurrent source file modifications - for i := 0; i < 2; i++ { + for i := range 2 { wg.Add(1) go func(i int) { defer wg.Done() - for j := 0; j < 15; j++ { + for j := range 15 { _ = sys.fsFromFileMap().WriteFile( "/home/src/workspaces/project/a.ts", fmt.Sprintf("const a: number = %d;", i*15+j), @@ -195,15 +189,13 @@ func TestWatcherRapidConfigChanges(t *testing.T) { } // State readers - for i := 0; i < 4; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 30; j++ { + for range 4 { + wg.Go(func() { + for range 30 { w.DoCycle() w.DoCycle() } - }() + }) } wg.Wait() @@ -219,14 +211,12 @@ func TestWatcherConcurrentDoCycleNoChanges(t *testing.T) { var wg sync.WaitGroup - for i := 0; i < 16; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 50; j++ { + for range 16 { + wg.Go(func() { + for range 50 { w.DoCycle() } - }() + }) } wg.Wait() @@ -242,37 +232,31 @@ func TestWatcherAlternatingModifyAndDoCycle(t *testing.T) { var wg sync.WaitGroup // Writer goroutine: continuously modifies files - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 100; j++ { + wg.Go(func() { + for j := range 100 { _ = sys.fsFromFileMap().WriteFile( "/home/src/workspaces/project/a.ts", fmt.Sprintf("const a: number = %d;", j), ) } - }() + }) // Multiple DoCycle goroutines - for i := 0; i < 4; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 25; j++ { + for range 4 { + wg.Go(func() { + for range 25 { w.DoCycle() } - }() + }) } // State reader goroutines - for i := 0; i < 4; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 100; j++ { + for range 4 { + wg.Go(func() { + for range 100 { w.DoCycle() } - }() + }) } wg.Wait() diff --git a/internal/vfs/vfswatch/vfswatch_race_test.go b/internal/vfs/vfswatch/vfswatch_race_test.go index 093b9f5e0ce..5fdd82f3ba3 100644 --- a/internal/vfs/vfswatch/vfswatch_race_test.go +++ b/internal/vfs/vfswatch/vfswatch_race_test.go @@ -45,27 +45,23 @@ func TestRaceHasChangesVsUpdateWatchedFiles(t *testing.T) { var wg sync.WaitGroup - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 200; j++ { + for range 10 { + wg.Go(func() { + for range 200 { fw.HasChangesFromWatchState() } - }() + }) } - for i := 0; i < 5; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 100; j++ { + for range 5 { + wg.Go(func() { + for range 100 { tfs := &trackingvfs.FS{Inner: fs} tfs.SeenFiles.Add("/src/a.ts") tfs.SeenFiles.Add("/src/b.ts") fw.UpdateWatchedFiles(tfs) } - }() + }) } wg.Wait() @@ -83,24 +79,20 @@ func TestRaceWildcardDirectoriesAccess(t *testing.T) { var wg sync.WaitGroup - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 200; j++ { + for range 10 { + wg.Go(func() { + for range 200 { fw.HasChangesFromWatchState() } - }() + }) } - for i := 0; i < 5; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 100; j++ { + for range 5 { + wg.Go(func() { + for range 100 { fw.SetWildcardDirectories(map[string]bool{"/src": true}) } - }() + }) } wg.Wait() @@ -115,21 +107,19 @@ func TestRacePollIntervalAccess(t *testing.T) { var wg sync.WaitGroup - for i := 0; i < 10; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 500; j++ { + for range 10 { + wg.Go(func() { + for range 500 { fw.HasChangesFromWatchState() } - }() + }) } - for i := 0; i < 5; i++ { + for i := range 5 { wg.Add(1) go func(i int) { defer wg.Done() - for j := 0; j < 200; j++ { + for j := range 200 { fw.SetPollInterval(time.Duration(i*200+j) * time.Millisecond) } }(i) @@ -150,22 +140,20 @@ func TestRaceMixedOperations(t *testing.T) { var wg sync.WaitGroup // HasChanges readers - for i := 0; i < 8; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 100; j++ { + for range 8 { + wg.Go(func() { + for range 100 { fw.HasChangesFromWatchState() } - }() + }) } // UpdateWatchedFiles writers - for i := 0; i < 4; i++ { + for i := range 4 { wg.Add(1) go func(i int) { defer wg.Done() - for j := 0; j < 50; j++ { + for j := range 50 { tfs := &trackingvfs.FS{Inner: fs} tfs.SeenFiles.Add("/src/a.ts") tfs.SeenFiles.Add(fmt.Sprintf("/src/new_%d_%d.ts", i, j)) @@ -175,11 +163,11 @@ func TestRaceMixedOperations(t *testing.T) { } // FS modifiers - for i := 0; i < 4; i++ { + for i := range 4 { wg.Add(1) go func(i int) { defer wg.Done() - for j := 0; j < 50; j++ { + for j := range 50 { path := fmt.Sprintf("/src/gen_%d_%d.ts", i, j) _ = fs.WriteFile(path, fmt.Sprintf("const x = %d;", j)) if j%3 == 0 { @@ -190,22 +178,20 @@ func TestRaceMixedOperations(t *testing.T) { } // WildcardDirectories writers - for i := 0; i < 2; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 50; j++ { + for range 2 { + wg.Go(func() { + for range 50 { fw.SetWildcardDirectories(map[string]bool{"/src": true}) } - }() + }) } // PollInterval writers - for i := 0; i < 2; i++ { + for i := range 2 { wg.Add(1) go func(i int) { defer wg.Done() - for j := 0; j < 100; j++ { + for j := range 100 { fw.SetPollInterval(time.Duration(50+j) * time.Millisecond) } }(i) @@ -226,11 +212,11 @@ func TestRaceUpdateWithConcurrentFileModifications(t *testing.T) { var wg sync.WaitGroup // Rapid file creation/deletion - for i := 0; i < 6; i++ { + for i := range 6 { wg.Add(1) go func(i int) { defer wg.Done() - for j := 0; j < 100; j++ { + for j := range 100 { path := fmt.Sprintf("/src/churn_%d_%d.ts", i, j) _ = fs.WriteFile(path, fmt.Sprintf("export const v = %d;", j)) _ = fs.Remove(path) @@ -239,17 +225,15 @@ func TestRaceUpdateWithConcurrentFileModifications(t *testing.T) { } // Concurrent UpdateWatchedFiles (walks the FS tree via WildcardDirectories) - for i := 0; i < 4; i++ { - wg.Add(1) - go func() { - defer wg.Done() - for j := 0; j < 50; j++ { + for range 4 { + wg.Go(func() { + for range 50 { tfs := &trackingvfs.FS{Inner: fs} tfs.SeenFiles.Add("/src/a.ts") tfs.SeenFiles.Add("/tsconfig.json") fw.UpdateWatchedFiles(tfs) } - }() + }) } wg.Wait() @@ -330,10 +314,7 @@ func FuzzFileWatcherConcurrent(f *testing.F) { var wg sync.WaitGroup for start := 0; start < len(ops); start += chunkSize { - end := start + chunkSize - if end > len(ops) { - end = len(ops) - } + end := min(start+chunkSize, len(ops)) chunk := ops[start:end] wg.Add(1) From efcbeb4d5d3eefd14de6d62f66adf86fc470609c Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Thu, 9 Apr 2026 11:42:16 -0500 Subject: [PATCH 26/30] created UpdateWatchState for code cleanliness --- internal/execute/watcher.go | 12 ++- internal/vfs/trackingvfs/trackingvfs.go | 7 -- internal/vfs/vfswatch/vfswatch.go | 64 ++++++++------- internal/vfs/vfswatch/vfswatch_race_test.go | 90 ++++++++------------- 4 files changed, 74 insertions(+), 99 deletions(-) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index e19b656a90f..f4192171a23 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -149,12 +149,12 @@ func (w *Watcher) doBuild() { innerHost := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), tfs, w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) host := &watchCompilerHost{CompilerHost: innerHost, cache: w.sourceFileCache} + var wildcardDirs map[string]bool if w.config.ConfigFile != nil { - wildcardDirs := w.config.WildcardDirectories() + wildcardDirs = w.config.WildcardDirectories() for dir := range wildcardDirs { tfs.SeenFiles.Add(dir) } - w.fileWatcher.SetWildcardDirectories(wildcardDirs) if len(wildcardDirs) > 0 { w.config = w.config.ReloadFileNamesOfParsedCommandLine(w.sys.FS()) } @@ -170,7 +170,13 @@ func (w *Watcher) doBuild() { result := w.compileAndEmit() cached.DisableAndClearCache() - w.fileWatcher.UpdateWatchedFiles(tfs) + + var watchPaths []string + tfs.SeenFiles.Range(func(fn string) bool { + watchPaths = append(watchPaths, fn) + return true + }) + w.fileWatcher.UpdateWatchState(watchPaths, wildcardDirs) w.fileWatcher.SetPollInterval(w.config.ParsedConfig.WatchOptions.WatchInterval()) w.configModified = false diff --git a/internal/vfs/trackingvfs/trackingvfs.go b/internal/vfs/trackingvfs/trackingvfs.go index 4f6935574e7..e79aabf41e9 100644 --- a/internal/vfs/trackingvfs/trackingvfs.go +++ b/internal/vfs/trackingvfs/trackingvfs.go @@ -67,10 +67,3 @@ func (fs *FS) WalkDir(root string, walkFn vfs.WalkDirFunc) error { } func (fs *FS) Realpath(path string) string { return fs.Inner.Realpath(path) } - -// WatchEntry stores the observed state of a single path at snapshot time. -type WatchEntry struct { - ModTime time.Time - Exists bool - ChildCount int // -1 if not tracked -} diff --git a/internal/vfs/vfswatch/vfswatch.go b/internal/vfs/vfswatch/vfswatch.go index febcbffe9ac..a0f7434c2b7 100644 --- a/internal/vfs/vfswatch/vfswatch.go +++ b/internal/vfs/vfswatch/vfswatch.go @@ -7,17 +7,22 @@ import ( "time" "github.com/microsoft/typescript-go/internal/vfs" - "github.com/microsoft/typescript-go/internal/vfs/trackingvfs" ) const DebounceWait = 250 * time.Millisecond +type WatchEntry struct { + ModTime time.Time + Exists bool + ChildCount int // -1 if not tracked +} + type FileWatcher struct { fs vfs.FS pollInterval time.Duration testing bool callback func() - watchState map[string]trackingvfs.WatchEntry + watchState map[string]WatchEntry wildcardDirectories map[string]bool mu sync.Mutex } @@ -31,19 +36,13 @@ func NewFileWatcher(fs vfs.FS, pollInterval time.Duration, testing bool, callbac } } -func (fw *FileWatcher) SetWildcardDirectories(dirs map[string]bool) { - fw.mu.Lock() - defer fw.mu.Unlock() - fw.wildcardDirectories = dirs -} - func (fw *FileWatcher) SetPollInterval(d time.Duration) { fw.mu.Lock() defer fw.mu.Unlock() fw.pollInterval = d } -func (fw *FileWatcher) WatchStateEntry(path string) (trackingvfs.WatchEntry, bool) { +func (fw *FileWatcher) WatchStateEntry(path string) (WatchEntry, bool) { fw.mu.Lock() defer fw.mu.Unlock() e, ok := fw.watchState[path] @@ -56,19 +55,19 @@ func (fw *FileWatcher) WatchStateIsEmpty() bool { return fw.watchState == nil } -func (fw *FileWatcher) UpdateWatchedFiles(tfs *trackingvfs.FS) { +func (fw *FileWatcher) UpdateWatchState(paths []string, wildcardDirs map[string]bool) { fw.mu.Lock() defer fw.mu.Unlock() - fw.watchState = make(map[string]trackingvfs.WatchEntry) - tfs.SeenFiles.Range(func(fn string) bool { + fw.watchState = make(map[string]WatchEntry, len(paths)) + for _, fn := range paths { if s := fw.fs.Stat(fn); s != nil { - fw.watchState[fn] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} + fw.watchState[fn] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} } else { - fw.watchState[fn] = trackingvfs.WatchEntry{Exists: false, ChildCount: -1} + fw.watchState[fn] = WatchEntry{Exists: false, ChildCount: -1} } - return true - }) - for dir, recursive := range fw.wildcardDirectories { + } + fw.wildcardDirectories = wildcardDirs + for dir, recursive := range wildcardDirs { if !recursive { continue } @@ -83,7 +82,7 @@ func (fw *FileWatcher) UpdateWatchedFiles(tfs *trackingvfs.FS) { fw.watchState[path] = existing } else { if s := fw.fs.Stat(path); s != nil { - fw.watchState[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} + fw.watchState[path] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} } } return nil @@ -95,29 +94,33 @@ func (fw *FileWatcher) WaitForSettled(now func() time.Time) { if fw.testing { return } + fw.mu.Lock() + wildcardDirs := fw.wildcardDirectories + pollInterval := fw.pollInterval + fw.mu.Unlock() current := fw.currentState() settledAt := now() - tick := min(fw.pollInterval, DebounceWait) + tick := min(pollInterval, DebounceWait) for now().Sub(settledAt) < DebounceWait { time.Sleep(tick) - if fw.HasChanges(current) { + if fw.hasChanges(current, wildcardDirs) { current = fw.currentState() settledAt = now() } } } -func (fw *FileWatcher) currentState() map[string]trackingvfs.WatchEntry { +func (fw *FileWatcher) currentState() map[string]WatchEntry { fw.mu.Lock() watchState := fw.watchState wildcardDirs := fw.wildcardDirectories fw.mu.Unlock() - state := make(map[string]trackingvfs.WatchEntry, len(watchState)) + state := make(map[string]WatchEntry, len(watchState)) for path := range watchState { if s := fw.fs.Stat(path); s != nil { - state[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} + state[path] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} } else { - state[path] = trackingvfs.WatchEntry{Exists: false, ChildCount: -1} + state[path] = WatchEntry{Exists: false, ChildCount: -1} } } for dir, recursive := range wildcardDirs { @@ -135,7 +138,7 @@ func (fw *FileWatcher) currentState() map[string]trackingvfs.WatchEntry { state[path] = existing } else { if s := fw.fs.Stat(path); s != nil { - state[path] = trackingvfs.WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} + state[path] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} } } return nil @@ -144,10 +147,7 @@ func (fw *FileWatcher) currentState() map[string]trackingvfs.WatchEntry { return state } -func (fw *FileWatcher) HasChanges(baseline map[string]trackingvfs.WatchEntry) bool { - fw.mu.Lock() - wildcardDirs := fw.wildcardDirectories - fw.mu.Unlock() +func (fw *FileWatcher) hasChanges(baseline map[string]WatchEntry, wildcardDirs map[string]bool) bool { for path, old := range baseline { s := fw.fs.Stat(path) if !old.Exists { @@ -193,8 +193,9 @@ func (fw *FileWatcher) HasChanges(baseline map[string]trackingvfs.WatchEntry) bo func (fw *FileWatcher) HasChangesFromWatchState() bool { fw.mu.Lock() ws := fw.watchState + wildcardDirs := fw.wildcardDirectories fw.mu.Unlock() - return fw.HasChanges(ws) + return fw.hasChanges(ws, wildcardDirs) } func (fw *FileWatcher) Run(now func() time.Time) { @@ -202,9 +203,10 @@ func (fw *FileWatcher) Run(now func() time.Time) { fw.mu.Lock() interval := fw.pollInterval ws := fw.watchState + wildcardDirs := fw.wildcardDirectories fw.mu.Unlock() time.Sleep(interval) - if ws == nil || fw.HasChanges(ws) { + if ws == nil || fw.hasChanges(ws, wildcardDirs) { fw.WaitForSettled(now) fw.callback() } diff --git a/internal/vfs/vfswatch/vfswatch_race_test.go b/internal/vfs/vfswatch/vfswatch_race_test.go index 5fdd82f3ba3..b1004fbc2a6 100644 --- a/internal/vfs/vfswatch/vfswatch_race_test.go +++ b/internal/vfs/vfswatch/vfswatch_race_test.go @@ -7,11 +7,18 @@ import ( "time" "github.com/microsoft/typescript-go/internal/vfs" - "github.com/microsoft/typescript-go/internal/vfs/trackingvfs" "github.com/microsoft/typescript-go/internal/vfs/vfstest" "github.com/microsoft/typescript-go/internal/vfs/vfswatch" ) +var defaultPaths = []string{ + "/src/a.ts", + "/src/b.ts", + "/src/c.ts", + "/src/sub/d.ts", + "/tsconfig.json", +} + func newTestFS() vfs.FS { return vfstest.FromMap(map[string]string{ "/src/a.ts": "const a = 1;", @@ -24,21 +31,14 @@ func newTestFS() vfs.FS { func newWatcherWithState(fs vfs.FS) *vfswatch.FileWatcher { fw := vfswatch.NewFileWatcher(fs, 10*time.Millisecond, true, func() {}) - tfs := &trackingvfs.FS{Inner: fs} - tfs.SeenFiles.Add("/src/a.ts") - tfs.SeenFiles.Add("/src/b.ts") - tfs.SeenFiles.Add("/src/c.ts") - tfs.SeenFiles.Add("/src/sub/d.ts") - tfs.SeenFiles.Add("/tsconfig.json") - fw.UpdateWatchedFiles(tfs) + fw.UpdateWatchState(defaultPaths, nil) return fw } -// TestRaceHasChangesVsUpdateWatchedFiles tests for data races between -// concurrent HasChanges reads and UpdateWatchedFiles writes on the -// WatchState map. The WatchState field is a plain map with no -// synchronization; concurrent access should be detected by -race. -func TestRaceHasChangesVsUpdateWatchedFiles(t *testing.T) { +// TestRaceHasChangesVsUpdateWatchState tests for data races between +// concurrent HasChanges reads and UpdateWatchState writes on the +// WatchState map. +func TestRaceHasChangesVsUpdateWatchState(t *testing.T) { t.Parallel() fs := newTestFS() fw := newWatcherWithState(fs) @@ -56,10 +56,7 @@ func TestRaceHasChangesVsUpdateWatchedFiles(t *testing.T) { for range 5 { wg.Go(func() { for range 100 { - tfs := &trackingvfs.FS{Inner: fs} - tfs.SeenFiles.Add("/src/a.ts") - tfs.SeenFiles.Add("/src/b.ts") - fw.UpdateWatchedFiles(tfs) + fw.UpdateWatchState([]string{"/src/a.ts", "/src/b.ts"}, nil) } }) } @@ -69,13 +66,12 @@ func TestRaceHasChangesVsUpdateWatchedFiles(t *testing.T) { // TestRaceWildcardDirectoriesAccess tests for data races when // WildcardDirectories is read internally by HasChanges while being -// replaced concurrently. WildcardDirectories is a plain map assigned -// directly on the struct with no synchronization. +// replaced concurrently via UpdateWatchState. func TestRaceWildcardDirectoriesAccess(t *testing.T) { t.Parallel() fs := newTestFS() fw := newWatcherWithState(fs) - fw.SetWildcardDirectories(map[string]bool{"/src": true}) + fw.UpdateWatchState(defaultPaths, map[string]bool{"/src": true}) var wg sync.WaitGroup @@ -90,7 +86,7 @@ func TestRaceWildcardDirectoriesAccess(t *testing.T) { for range 5 { wg.Go(func() { for range 100 { - fw.SetWildcardDirectories(map[string]bool{"/src": true}) + fw.UpdateWatchState(defaultPaths, map[string]bool{"/src": true}) } }) } @@ -129,13 +125,13 @@ func TestRacePollIntervalAccess(t *testing.T) { } // TestRaceMixedOperations hammers all FileWatcher operations -// concurrently: HasChanges, UpdateWatchedFiles, FS mutations, -// WildcardDirectories writes, and PollInterval writes. +// concurrently: HasChanges, UpdateWatchState, FS mutations, +// and PollInterval writes. func TestRaceMixedOperations(t *testing.T) { t.Parallel() fs := newTestFS() fw := newWatcherWithState(fs) - fw.SetWildcardDirectories(map[string]bool{"/src": true}) + fw.UpdateWatchState(defaultPaths, map[string]bool{"/src": true}) var wg sync.WaitGroup @@ -148,16 +144,14 @@ func TestRaceMixedOperations(t *testing.T) { }) } - // UpdateWatchedFiles writers + // UpdateWatchState writers for i := range 4 { wg.Add(1) go func(i int) { defer wg.Done() for j := range 50 { - tfs := &trackingvfs.FS{Inner: fs} - tfs.SeenFiles.Add("/src/a.ts") - tfs.SeenFiles.Add(fmt.Sprintf("/src/new_%d_%d.ts", i, j)) - fw.UpdateWatchedFiles(tfs) + paths := []string{"/src/a.ts", fmt.Sprintf("/src/new_%d_%d.ts", i, j)} + fw.UpdateWatchState(paths, map[string]bool{"/src": true}) } }(i) } @@ -177,15 +171,6 @@ func TestRaceMixedOperations(t *testing.T) { }(i) } - // WildcardDirectories writers - for range 2 { - wg.Go(func() { - for range 50 { - fw.SetWildcardDirectories(map[string]bool{"/src": true}) - } - }) - } - // PollInterval writers for i := range 2 { wg.Add(1) @@ -201,13 +186,13 @@ func TestRaceMixedOperations(t *testing.T) { } // TestRaceUpdateWithConcurrentFileModifications creates and deletes -// files on the FS while UpdateWatchedFiles is scanning the same FS, +// files on the FS while UpdateWatchState is scanning the same FS, // testing for races between the FS walker and concurrent mutations. func TestRaceUpdateWithConcurrentFileModifications(t *testing.T) { t.Parallel() fs := newTestFS() fw := newWatcherWithState(fs) - fw.SetWildcardDirectories(map[string]bool{"/src": true}) + fw.UpdateWatchState(defaultPaths, map[string]bool{"/src": true}) var wg sync.WaitGroup @@ -224,14 +209,11 @@ func TestRaceUpdateWithConcurrentFileModifications(t *testing.T) { }(i) } - // Concurrent UpdateWatchedFiles (walks the FS tree via WildcardDirectories) + // Concurrent UpdateWatchState (walks the FS tree via WildcardDirectories) for range 4 { wg.Go(func() { for range 50 { - tfs := &trackingvfs.FS{Inner: fs} - tfs.SeenFiles.Add("/src/a.ts") - tfs.SeenFiles.Add("/tsconfig.json") - fw.UpdateWatchedFiles(tfs) + fw.UpdateWatchState([]string{"/src/a.ts", "/tsconfig.json"}, map[string]bool{"/src": true}) } }) } @@ -272,13 +254,9 @@ func FuzzFileWatcherOperations(f *testing.F) { case 2: // Check for changes against current state fw.HasChangesFromWatchState() case 3: // Rebuild watch state - tfs := &trackingvfs.FS{Inner: fs} - for _, f := range files { - tfs.SeenFiles.Add(f) - } - fw.UpdateWatchedFiles(tfs) + fw.UpdateWatchState(files, nil) case 4: // Set wildcard directories and check for changes - fw.SetWildcardDirectories(map[string]bool{"/src": true}) + fw.UpdateWatchState(files, map[string]bool{"/src": true}) fw.HasChangesFromWatchState() case 5: // Modify PollInterval fw.SetPollInterval(time.Duration(i*10) * time.Millisecond) @@ -301,7 +279,7 @@ func FuzzFileWatcherConcurrent(f *testing.F) { fs := newTestFS() fw := newWatcherWithState(fs) - fw.SetWildcardDirectories(map[string]bool{"/src": true}) + fw.UpdateWatchState(defaultPaths, map[string]bool{"/src": true}) files := []string{"/src/a.ts", "/src/b.ts", "/src/c.ts", "/src/new.ts"} @@ -322,7 +300,7 @@ func FuzzFileWatcherConcurrent(f *testing.F) { defer wg.Done() for i, op := range chunk { path := files[(goroutineID*len(chunk)+i)%len(files)] - switch op % 5 { + switch op % 4 { case 0: _ = fs.WriteFile(path, fmt.Sprintf("const g%d = %d;", goroutineID, i)) case 1: @@ -330,11 +308,7 @@ func FuzzFileWatcherConcurrent(f *testing.F) { case 2: fw.HasChangesFromWatchState() case 3: - tfs := &trackingvfs.FS{Inner: fs} - tfs.SeenFiles.Add(path) - fw.UpdateWatchedFiles(tfs) - case 4: - fw.SetWildcardDirectories(map[string]bool{"/src": true}) + fw.UpdateWatchState([]string{path}, map[string]bool{"/src": true}) } } }(chunk, start/chunkSize) From 8680e2f2c3e41214dd0b13ef83df98f962483ba1 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:09:44 -0500 Subject: [PATCH 27/30] fixed directory walking --- internal/execute/watcher.go | 35 +++++------ internal/vfs/vfswatch/vfswatch.go | 101 ++++++++++++++---------------- 2 files changed, 64 insertions(+), 72 deletions(-) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index f4192171a23..95b8790a593 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -170,13 +170,7 @@ func (w *Watcher) doBuild() { result := w.compileAndEmit() cached.DisableAndClearCache() - - var watchPaths []string - tfs.SeenFiles.Range(func(fn string) bool { - watchPaths = append(watchPaths, fn) - return true - }) - w.fileWatcher.UpdateWatchState(watchPaths, wildcardDirs) + w.fileWatcher.UpdateWatchState(tfs.SeenFiles.ToSlice(), wildcardDirs) w.fileWatcher.SetPollInterval(w.config.ParsedConfig.WatchOptions.WatchInterval()) w.configModified = false @@ -222,18 +216,21 @@ func (w *Watcher) hasErrorsInTsConfig() bool { if !w.configHasErrors && len(w.configFilePaths) > 0 { changed := false for _, path := range w.configFilePaths { - if old, ok := w.fileWatcher.WatchStateEntry(path); ok { - s := w.sys.FS().Stat(path) - if !old.Exists { - if s != nil { - changed = true - break - } - } else { - if s == nil || !s.ModTime().Equal(old.ModTime) { - changed = true - break - } + old, ok := w.fileWatcher.WatchStateEntry(path) + if !ok { + changed = true + break + } + s := w.sys.FS().Stat(path) + if !old.Exists { + if s != nil { + changed = true + break + } + } else { + if s == nil || !s.ModTime().Equal(old.ModTime) { + changed = true + break } } } diff --git a/internal/vfs/vfswatch/vfswatch.go b/internal/vfs/vfswatch/vfswatch.go index a0f7434c2b7..88ad3925b74 100644 --- a/internal/vfs/vfswatch/vfswatch.go +++ b/internal/vfs/vfswatch/vfswatch.go @@ -56,38 +56,11 @@ func (fw *FileWatcher) WatchStateIsEmpty() bool { } func (fw *FileWatcher) UpdateWatchState(paths []string, wildcardDirs map[string]bool) { + state := snapshotPaths(fw.fs, paths, wildcardDirs) fw.mu.Lock() defer fw.mu.Unlock() - fw.watchState = make(map[string]WatchEntry, len(paths)) - for _, fn := range paths { - if s := fw.fs.Stat(fn); s != nil { - fw.watchState[fn] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} - } else { - fw.watchState[fn] = WatchEntry{Exists: false, ChildCount: -1} - } - } + fw.watchState = state fw.wildcardDirectories = wildcardDirs - for dir, recursive := range wildcardDirs { - if !recursive { - continue - } - _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { - if err != nil || !d.IsDir() { - return nil - } - entries := fw.fs.GetAccessibleEntries(path) - count := len(entries.Files) + len(entries.Directories) - if existing, ok := fw.watchState[path]; ok { - existing.ChildCount = count - fw.watchState[path] = existing - } else { - if s := fw.fs.Stat(path); s != nil { - fw.watchState[path] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} - } - } - return nil - }) - } } func (fw *FileWatcher) WaitForSettled(now func() time.Time) { @@ -115,38 +88,65 @@ func (fw *FileWatcher) currentState() map[string]WatchEntry { watchState := fw.watchState wildcardDirs := fw.wildcardDirectories fw.mu.Unlock() - state := make(map[string]WatchEntry, len(watchState)) + paths := make([]string, 0, len(watchState)) for path := range watchState { - if s := fw.fs.Stat(path); s != nil { - state[path] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} + paths = append(paths, path) + } + return snapshotPaths(fw.fs, paths, wildcardDirs) +} + +func snapshotPaths(fs vfs.FS, paths []string, wildcardDirs map[string]bool) map[string]WatchEntry { + state := make(map[string]WatchEntry, len(paths)) + for _, fn := range paths { + if s := fs.Stat(fn); s != nil { + state[fn] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} } else { - state[path] = WatchEntry{Exists: false, ChildCount: -1} + state[fn] = WatchEntry{Exists: false, ChildCount: -1} } } for dir, recursive := range wildcardDirs { if !recursive { + snapshotDirEntry(fs, state, dir) continue } - _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { + _ = fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { if err != nil || !d.IsDir() { return nil } - entries := fw.fs.GetAccessibleEntries(path) - count := len(entries.Files) + len(entries.Directories) - if existing, ok := state[path]; ok { - existing.ChildCount = count - state[path] = existing - } else { - if s := fw.fs.Stat(path); s != nil { - state[path] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} - } - } + snapshotDirEntry(fs, state, path) return nil }) } return state } +func snapshotDirEntry(fs vfs.FS, state map[string]WatchEntry, dir string) { + entries := fs.GetAccessibleEntries(dir) + count := len(entries.Files) + len(entries.Directories) + if existing, ok := state[dir]; ok { + existing.ChildCount = count + state[dir] = existing + } else { + if s := fs.Stat(dir); s != nil { + state[dir] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} + } + } +} + +func dirChanged(fs vfs.FS, baseline map[string]WatchEntry, dir string) bool { + entry, ok := baseline[dir] + if !ok { + return true + } + if entry.ChildCount >= 0 { + entries := fs.GetAccessibleEntries(dir) + if len(entries.Files)+len(entries.Directories) != entry.ChildCount { + return true + } + } + return false +} + func (fw *FileWatcher) hasChanges(baseline map[string]WatchEntry, wildcardDirs map[string]bool) bool { for path, old := range baseline { s := fw.fs.Stat(path) @@ -162,6 +162,9 @@ func (fw *FileWatcher) hasChanges(baseline map[string]WatchEntry, wildcardDirs m } for dir, recursive := range wildcardDirs { if !recursive { + if dirChanged(fw.fs, baseline, dir) { + return true + } continue } found := false @@ -169,18 +172,10 @@ func (fw *FileWatcher) hasChanges(baseline map[string]WatchEntry, wildcardDirs m if err != nil || !d.IsDir() { return nil } - entry, ok := baseline[path] - if !ok { + if dirChanged(fw.fs, baseline, path) { found = true return vfs.SkipAll } - if entry.ChildCount >= 0 { - entries := fw.fs.GetAccessibleEntries(path) - if len(entries.Files)+len(entries.Directories) != entry.ChildCount { - found = true - return vfs.SkipAll - } - } return nil }) if found { From b072ec3182aafef61bca245d57c9cbc63e6a88ec Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:57:33 -0500 Subject: [PATCH 28/30] fixed rename issue with child hashing --- internal/vfs/vfswatch/vfswatch.go | 37 ++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/internal/vfs/vfswatch/vfswatch.go b/internal/vfs/vfswatch/vfswatch.go index 88ad3925b74..0336ca7a70d 100644 --- a/internal/vfs/vfswatch/vfswatch.go +++ b/internal/vfs/vfswatch/vfswatch.go @@ -3,6 +3,8 @@ package vfswatch import ( + "hash/fnv" + "slices" "sync" "time" @@ -12,9 +14,9 @@ import ( const DebounceWait = 250 * time.Millisecond type WatchEntry struct { - ModTime time.Time - Exists bool - ChildCount int // -1 if not tracked + ModTime time.Time + Exists bool + ChildrenHash uint64 // 0 if not tracked } type FileWatcher struct { @@ -99,9 +101,9 @@ func snapshotPaths(fs vfs.FS, paths []string, wildcardDirs map[string]bool) map[ state := make(map[string]WatchEntry, len(paths)) for _, fn := range paths { if s := fs.Stat(fn); s != nil { - state[fn] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: -1} + state[fn] = WatchEntry{ModTime: s.ModTime(), Exists: true} } else { - state[fn] = WatchEntry{Exists: false, ChildCount: -1} + state[fn] = WatchEntry{Exists: false} } } for dir, recursive := range wildcardDirs { @@ -122,25 +124,40 @@ func snapshotPaths(fs vfs.FS, paths []string, wildcardDirs map[string]bool) map[ func snapshotDirEntry(fs vfs.FS, state map[string]WatchEntry, dir string) { entries := fs.GetAccessibleEntries(dir) - count := len(entries.Files) + len(entries.Directories) + h := hashEntries(entries) if existing, ok := state[dir]; ok { - existing.ChildCount = count + existing.ChildrenHash = h state[dir] = existing } else { if s := fs.Stat(dir); s != nil { - state[dir] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildCount: count} + state[dir] = WatchEntry{ModTime: s.ModTime(), Exists: true, ChildrenHash: h} } } } +// hashEntries returns a hash of the sorted file and directory names +// within a directory. This detects adds, deletes, and renames. +func hashEntries(entries vfs.Entries) uint64 { + names := make([]string, 0, len(entries.Files)+len(entries.Directories)) + names = append(names, entries.Files...) + names = append(names, entries.Directories...) + slices.Sort(names) + h := fnv.New64a() + for _, name := range names { + h.Write([]byte(name)) + h.Write([]byte{0}) + } + return h.Sum64() +} + func dirChanged(fs vfs.FS, baseline map[string]WatchEntry, dir string) bool { entry, ok := baseline[dir] if !ok { return true } - if entry.ChildCount >= 0 { + if entry.ChildrenHash != 0 { entries := fs.GetAccessibleEntries(dir) - if len(entries.Files)+len(entries.Directories) != entry.ChildCount { + if hashEntries(entries) != entry.ChildrenHash { return true } } From 5ea61a140b050a31dd6042da0dcb2b764d62e366 Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:59:50 -0500 Subject: [PATCH 29/30] minor fixes --- .../execute/tsctests/watcher_race_test.go | 9 +- internal/execute/watcher.go | 14 +- internal/vfs/vfswatch/vfswatch.go | 50 +++++-- internal/vfs/vfswatch/vfswatch_bench_test.go | 135 ++++++++++++++++++ .../watch-handles-tsconfig-deleted.js | 2 + 5 files changed, 193 insertions(+), 17 deletions(-) create mode 100644 internal/vfs/vfswatch/vfswatch_bench_test.go diff --git a/internal/execute/tsctests/watcher_race_test.go b/internal/execute/tsctests/watcher_race_test.go index bad718b2d10..90206d498ca 100644 --- a/internal/execute/tsctests/watcher_race_test.go +++ b/internal/execute/tsctests/watcher_race_test.go @@ -60,9 +60,9 @@ func TestWatcherConcurrentDoCycle(t *testing.T) { wg.Wait() } -// TestWatcherDoCycleWithConcurrentStateReads calls DoCycle while -// other goroutines read watcher state through the exported test -// helper methods (HasWatchedFilesChanged, WatchStateLen, etc.). +// TestWatcherDoCycleWithConcurrentStateReads calls DoCycle from +// multiple goroutines, some modifying files and some not, to test +// concurrent access to all Watcher and FileWatcher state. func TestWatcherDoCycleWithConcurrentStateReads(t *testing.T) { t.Parallel() w, sys := createTestWatcher(t) @@ -203,8 +203,7 @@ func TestWatcherRapidConfigChanges(t *testing.T) { // TestWatcherConcurrentDoCycleNoChanges calls DoCycle from many // goroutines when no files have changed, testing the early-return -// path where WatchState is read and HasChanges is called. This path -// reads WatchState and WildcardDirectories without synchronization. +// path where WatchState is read and HasChanges is called. func TestWatcherConcurrentDoCycleNoChanges(t *testing.T) { t.Parallel() w, _ := createTestWatcher(t) diff --git a/internal/execute/watcher.go b/internal/execute/watcher.go index 95b8790a593..a47945dd4f1 100644 --- a/internal/execute/watcher.go +++ b/internal/execute/watcher.go @@ -106,6 +106,7 @@ func createWatcher( } func (w *Watcher) start() { + w.mu.Lock() w.extendedConfigCache = &tsc.ExtendedConfigCache{} host := compiler.NewCompilerHost(w.sys.GetCurrentDirectory(), w.sys.FS(), w.sys.DefaultLibraryPath(), w.extendedConfigCache, getTraceFromSys(w.sys, w.config.Locale(), w.testing)) w.program = incremental.ReadBuildInfoProgram(w.config, incremental.NewBuildInfoReader(host), host) @@ -116,6 +117,7 @@ func (w *Watcher) start() { w.reportWatchStatus(ast.NewCompilerDiagnostic(diagnostics.Starting_compilation_in_watch_mode)) w.doBuild() + w.mu.Unlock() if w.testing == nil { w.fileWatcher.Run(w.sys.Now) @@ -125,10 +127,10 @@ func (w *Watcher) start() { func (w *Watcher) DoCycle() { w.mu.Lock() defer w.mu.Unlock() - if w.hasErrorsInTsConfig() { + if w.recheckTsConfig() { return } - if !w.fileWatcher.WatchStateIsEmpty() && !w.configModified && !w.fileWatcher.HasChangesFromWatchState() { + if !w.fileWatcher.WatchStateUninitialized() && !w.configModified && !w.fileWatcher.HasChangesFromWatchState() { if w.testing != nil { w.testing.OnProgram(w.program) } @@ -208,7 +210,7 @@ func (w *Watcher) compileAndEmit() tsc.CompileAndEmitResult { }) } -func (w *Watcher) hasErrorsInTsConfig() bool { +func (w *Watcher) recheckTsConfig() bool { if w.configFileName == "" { return false } @@ -246,6 +248,12 @@ func (w *Watcher) hasErrorsInTsConfig() bool { w.reportDiagnostic(e) } w.configHasErrors = true + errorCount := len(errors) + if errorCount == 1 { + w.reportWatchStatus(ast.NewCompilerDiagnostic(diagnostics.Found_1_error_Watching_for_file_changes)) + } else { + w.reportWatchStatus(ast.NewCompilerDiagnostic(diagnostics.Found_0_errors_Watching_for_file_changes, errorCount)) + } return true } if w.configHasErrors { diff --git a/internal/vfs/vfswatch/vfswatch.go b/internal/vfs/vfswatch/vfswatch.go index 0336ca7a70d..20e414abad3 100644 --- a/internal/vfs/vfswatch/vfswatch.go +++ b/internal/vfs/vfswatch/vfswatch.go @@ -11,7 +11,7 @@ import ( "github.com/microsoft/typescript-go/internal/vfs" ) -const DebounceWait = 250 * time.Millisecond +const debounceWait = 250 * time.Millisecond type WatchEntry struct { ModTime time.Time @@ -51,7 +51,7 @@ func (fw *FileWatcher) WatchStateEntry(path string) (WatchEntry, bool) { return e, ok } -func (fw *FileWatcher) WatchStateIsEmpty() bool { +func (fw *FileWatcher) WatchStateUninitialized() bool { fw.mu.Lock() defer fw.mu.Unlock() return fw.watchState == nil @@ -75,8 +75,8 @@ func (fw *FileWatcher) WaitForSettled(now func() time.Time) { fw.mu.Unlock() current := fw.currentState() settledAt := now() - tick := min(pollInterval, DebounceWait) - for now().Sub(settledAt) < DebounceWait { + tick := min(pollInterval, debounceWait) + for now().Sub(settledAt) < debounceWait { time.Sleep(tick) if fw.hasChanges(current, wildcardDirs) { current = fw.currentState() @@ -90,18 +90,40 @@ func (fw *FileWatcher) currentState() map[string]WatchEntry { watchState := fw.watchState wildcardDirs := fw.wildcardDirectories fw.mu.Unlock() - paths := make([]string, 0, len(watchState)) - for path := range watchState { - paths = append(paths, path) + state := make(map[string]WatchEntry, len(watchState)) + for fn := range watchState { + if s := fw.fs.Stat(fn); s != nil { + state[fn] = WatchEntry{ModTime: s.ModTime(), Exists: true} + } else { + state[fn] = WatchEntry{Exists: false} + } } - return snapshotPaths(fw.fs, paths, wildcardDirs) + for dir, recursive := range wildcardDirs { + if !recursive { + snapshotDirEntry(fw.fs, state, dir) + continue + } + _ = fw.fs.WalkDir(dir, func(path string, d vfs.DirEntry, err error) error { + if err != nil || !d.IsDir() { + return nil + } + snapshotDirEntry(fw.fs, state, path) + return nil + }) + } + return state } func snapshotPaths(fs vfs.FS, paths []string, wildcardDirs map[string]bool) map[string]WatchEntry { state := make(map[string]WatchEntry, len(paths)) for _, fn := range paths { if s := fs.Stat(fn); s != nil { - state[fn] = WatchEntry{ModTime: s.ModTime(), Exists: true} + entry := WatchEntry{ModTime: s.ModTime(), Exists: true} + if s.IsDir() { + entries := fs.GetAccessibleEntries(fn) + entry.ChildrenHash = hashEntries(entries) + } + state[fn] = entry } else { state[fn] = WatchEntry{Exists: false} } @@ -175,6 +197,12 @@ func (fw *FileWatcher) hasChanges(baseline map[string]WatchEntry, wildcardDirs m if s == nil || !s.ModTime().Equal(old.ModTime) { return true } + if old.ChildrenHash != 0 { + entries := fw.fs.GetAccessibleEntries(path) + if hashEntries(entries) != old.ChildrenHash { + return true + } + } } } for dir, recursive := range wildcardDirs { @@ -202,6 +230,10 @@ func (fw *FileWatcher) hasChanges(baseline map[string]WatchEntry, wildcardDirs m return false } +// HasChangesFromWatchState compares the current filesystem against the +// stored watch state. Safe for concurrent use: watchState and +// wildcardDirectories are snapshotted under lock; the maps themselves +// are never mutated after creation (UpdateWatchState replaces them). func (fw *FileWatcher) HasChangesFromWatchState() bool { fw.mu.Lock() ws := fw.watchState diff --git a/internal/vfs/vfswatch/vfswatch_bench_test.go b/internal/vfs/vfswatch/vfswatch_bench_test.go new file mode 100644 index 00000000000..7a604f4936f --- /dev/null +++ b/internal/vfs/vfswatch/vfswatch_bench_test.go @@ -0,0 +1,135 @@ +package vfswatch_test + +import ( + "fmt" + "testing" + "time" + + "github.com/microsoft/typescript-go/internal/vfs" + "github.com/microsoft/typescript-go/internal/vfs/vfstest" + "github.com/microsoft/typescript-go/internal/vfs/vfswatch" +) + +func makeLargeFS(nFiles int, nDirs int) vfs.FS { + files := map[string]string{ + "/tsconfig.json": `{}`, + } + for i := range nDirs { + dir := fmt.Sprintf("/src/dir%d", i) + for j := range nFiles / nDirs { + files[fmt.Sprintf("%s/file%d.ts", dir, j)] = fmt.Sprintf("export const x%d_%d = %d;", i, j, i*100+j) + } + } + return vfstest.FromMap(files, true) +} + +func makePaths(nFiles int, nDirs int) []string { + paths := make([]string, 0, nFiles+nDirs+1) + paths = append(paths, "/tsconfig.json") + for i := range nDirs { + dir := fmt.Sprintf("/src/dir%d", i) + paths = append(paths, dir) + for j := range nFiles / nDirs { + paths = append(paths, fmt.Sprintf("%s/file%d.ts", dir, j)) + } + } + return paths +} + +// BenchmarkUpdateWatchState measures the cost of snapshotting the filesystem. +func BenchmarkUpdateWatchState(b *testing.B) { + for _, size := range []struct { + files, dirs int + }{ + {50, 5}, + {500, 20}, + {2000, 50}, + } { + b.Run(fmt.Sprintf("files=%d_dirs=%d", size.files, size.dirs), func(b *testing.B) { + fs := makeLargeFS(size.files, size.dirs) + fw := vfswatch.NewFileWatcher(fs, 10*time.Millisecond, true, func() {}) + paths := makePaths(size.files, size.dirs) + wildcards := map[string]bool{"/src": true} + + b.ResetTimer() + b.ReportAllocs() + for range b.N { + fw.UpdateWatchState(paths, wildcards) + } + }) + } +} + +// BenchmarkHasChangesNoChange measures per-poll cost when nothing changed. +func BenchmarkHasChangesNoChange(b *testing.B) { + for _, size := range []struct { + files, dirs int + }{ + {50, 5}, + {500, 20}, + {2000, 50}, + } { + b.Run(fmt.Sprintf("files=%d_dirs=%d", size.files, size.dirs), func(b *testing.B) { + fs := makeLargeFS(size.files, size.dirs) + fw := vfswatch.NewFileWatcher(fs, 10*time.Millisecond, true, func() {}) + paths := makePaths(size.files, size.dirs) + wildcards := map[string]bool{"/src": true} + fw.UpdateWatchState(paths, wildcards) + + b.ResetTimer() + b.ReportAllocs() + for range b.N { + fw.HasChangesFromWatchState() + } + }) + } +} + +// BenchmarkHasChangesWithChange measures detection cost when one file changed. +func BenchmarkHasChangesWithChange(b *testing.B) { + for _, size := range []struct { + files, dirs int + }{ + {50, 5}, + {500, 20}, + {2000, 50}, + } { + b.Run(fmt.Sprintf("files=%d_dirs=%d", size.files, size.dirs), func(b *testing.B) { + fs := makeLargeFS(size.files, size.dirs) + fw := vfswatch.NewFileWatcher(fs, 10*time.Millisecond, true, func() {}) + paths := makePaths(size.files, size.dirs) + wildcards := map[string]bool{"/src": true} + fw.UpdateWatchState(paths, wildcards) + // Modify one file so hasChanges returns true + _ = fs.WriteFile("/src/dir0/file0.ts", "export const changed = true;") + + b.ResetTimer() + b.ReportAllocs() + for range b.N { + fw.HasChangesFromWatchState() + } + }) + } +} + +// BenchmarkHashEntries measures hashing cost for directory listings. +func BenchmarkHashEntries(b *testing.B) { + for _, nEntries := range []int{10, 100, 500} { + b.Run(fmt.Sprintf("entries=%d", nEntries), func(b *testing.B) { + files := map[string]string{} + for i := range nEntries { + files[fmt.Sprintf("/dir/file%d.ts", i)] = "x" + } + fs := vfstest.FromMap(files, true) + fw := vfswatch.NewFileWatcher(fs, 10*time.Millisecond, true, func() {}) + paths := []string{"/dir"} + fw.UpdateWatchState(paths, nil) + + b.ResetTimer() + b.ReportAllocs() + for range b.N { + fw.HasChangesFromWatchState() + } + }) + } +} diff --git a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js index e6cdcb8b522..1d1ac5045a6 100644 --- a/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js +++ b/testdata/baselines/reference/tscWatch/commandLineWatch/watch-handles-tsconfig-deleted.js @@ -54,6 +54,8 @@ Edit [0]:: delete tsconfig Output:: error TS5083: Cannot read file '/home/src/workspaces/project/tsconfig.json'. +[HH:MM:SS AM] Found 1 error. Watching for file changes. + From 709230e42e2e68e6af4e44bb331a0a82151ba0fb Mon Sep 17 00:00:00 2001 From: John Favret <64748847+johnfav03@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:49:34 -0500 Subject: [PATCH 30/30] changed hashing to xxh3, added file/dir prefix --- internal/vfs/vfswatch/vfswatch.go | 24 ++-- internal/vfs/vfswatch/vfswatch_bench_test.go | 135 ------------------- 2 files changed, 14 insertions(+), 145 deletions(-) delete mode 100644 internal/vfs/vfswatch/vfswatch_bench_test.go diff --git a/internal/vfs/vfswatch/vfswatch.go b/internal/vfs/vfswatch/vfswatch.go index 20e414abad3..dabfcaaf122 100644 --- a/internal/vfs/vfswatch/vfswatch.go +++ b/internal/vfs/vfswatch/vfswatch.go @@ -3,12 +3,12 @@ package vfswatch import ( - "hash/fnv" "slices" "sync" "time" "github.com/microsoft/typescript-go/internal/vfs" + "github.com/zeebo/xxh3" ) const debounceWait = 250 * time.Millisecond @@ -157,16 +157,20 @@ func snapshotDirEntry(fs vfs.FS, state map[string]WatchEntry, dir string) { } } -// hashEntries returns a hash of the sorted file and directory names -// within a directory. This detects adds, deletes, and renames. func hashEntries(entries vfs.Entries) uint64 { - names := make([]string, 0, len(entries.Files)+len(entries.Directories)) - names = append(names, entries.Files...) - names = append(names, entries.Directories...) - slices.Sort(names) - h := fnv.New64a() - for _, name := range names { - h.Write([]byte(name)) + dirs := slices.Clone(entries.Directories) + files := slices.Clone(entries.Files) + slices.Sort(dirs) + slices.Sort(files) + var h xxh3.Hasher + for _, name := range dirs { + h.WriteString("d:") + h.WriteString(name) + h.Write([]byte{0}) + } + for _, name := range files { + h.WriteString("f:") + h.WriteString(name) h.Write([]byte{0}) } return h.Sum64() diff --git a/internal/vfs/vfswatch/vfswatch_bench_test.go b/internal/vfs/vfswatch/vfswatch_bench_test.go deleted file mode 100644 index 7a604f4936f..00000000000 --- a/internal/vfs/vfswatch/vfswatch_bench_test.go +++ /dev/null @@ -1,135 +0,0 @@ -package vfswatch_test - -import ( - "fmt" - "testing" - "time" - - "github.com/microsoft/typescript-go/internal/vfs" - "github.com/microsoft/typescript-go/internal/vfs/vfstest" - "github.com/microsoft/typescript-go/internal/vfs/vfswatch" -) - -func makeLargeFS(nFiles int, nDirs int) vfs.FS { - files := map[string]string{ - "/tsconfig.json": `{}`, - } - for i := range nDirs { - dir := fmt.Sprintf("/src/dir%d", i) - for j := range nFiles / nDirs { - files[fmt.Sprintf("%s/file%d.ts", dir, j)] = fmt.Sprintf("export const x%d_%d = %d;", i, j, i*100+j) - } - } - return vfstest.FromMap(files, true) -} - -func makePaths(nFiles int, nDirs int) []string { - paths := make([]string, 0, nFiles+nDirs+1) - paths = append(paths, "/tsconfig.json") - for i := range nDirs { - dir := fmt.Sprintf("/src/dir%d", i) - paths = append(paths, dir) - for j := range nFiles / nDirs { - paths = append(paths, fmt.Sprintf("%s/file%d.ts", dir, j)) - } - } - return paths -} - -// BenchmarkUpdateWatchState measures the cost of snapshotting the filesystem. -func BenchmarkUpdateWatchState(b *testing.B) { - for _, size := range []struct { - files, dirs int - }{ - {50, 5}, - {500, 20}, - {2000, 50}, - } { - b.Run(fmt.Sprintf("files=%d_dirs=%d", size.files, size.dirs), func(b *testing.B) { - fs := makeLargeFS(size.files, size.dirs) - fw := vfswatch.NewFileWatcher(fs, 10*time.Millisecond, true, func() {}) - paths := makePaths(size.files, size.dirs) - wildcards := map[string]bool{"/src": true} - - b.ResetTimer() - b.ReportAllocs() - for range b.N { - fw.UpdateWatchState(paths, wildcards) - } - }) - } -} - -// BenchmarkHasChangesNoChange measures per-poll cost when nothing changed. -func BenchmarkHasChangesNoChange(b *testing.B) { - for _, size := range []struct { - files, dirs int - }{ - {50, 5}, - {500, 20}, - {2000, 50}, - } { - b.Run(fmt.Sprintf("files=%d_dirs=%d", size.files, size.dirs), func(b *testing.B) { - fs := makeLargeFS(size.files, size.dirs) - fw := vfswatch.NewFileWatcher(fs, 10*time.Millisecond, true, func() {}) - paths := makePaths(size.files, size.dirs) - wildcards := map[string]bool{"/src": true} - fw.UpdateWatchState(paths, wildcards) - - b.ResetTimer() - b.ReportAllocs() - for range b.N { - fw.HasChangesFromWatchState() - } - }) - } -} - -// BenchmarkHasChangesWithChange measures detection cost when one file changed. -func BenchmarkHasChangesWithChange(b *testing.B) { - for _, size := range []struct { - files, dirs int - }{ - {50, 5}, - {500, 20}, - {2000, 50}, - } { - b.Run(fmt.Sprintf("files=%d_dirs=%d", size.files, size.dirs), func(b *testing.B) { - fs := makeLargeFS(size.files, size.dirs) - fw := vfswatch.NewFileWatcher(fs, 10*time.Millisecond, true, func() {}) - paths := makePaths(size.files, size.dirs) - wildcards := map[string]bool{"/src": true} - fw.UpdateWatchState(paths, wildcards) - // Modify one file so hasChanges returns true - _ = fs.WriteFile("/src/dir0/file0.ts", "export const changed = true;") - - b.ResetTimer() - b.ReportAllocs() - for range b.N { - fw.HasChangesFromWatchState() - } - }) - } -} - -// BenchmarkHashEntries measures hashing cost for directory listings. -func BenchmarkHashEntries(b *testing.B) { - for _, nEntries := range []int{10, 100, 500} { - b.Run(fmt.Sprintf("entries=%d", nEntries), func(b *testing.B) { - files := map[string]string{} - for i := range nEntries { - files[fmt.Sprintf("/dir/file%d.ts", i)] = "x" - } - fs := vfstest.FromMap(files, true) - fw := vfswatch.NewFileWatcher(fs, 10*time.Millisecond, true, func() {}) - paths := []string{"/dir"} - fw.UpdateWatchState(paths, nil) - - b.ResetTimer() - b.ReportAllocs() - for range b.N { - fw.HasChangesFromWatchState() - } - }) - } -}