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
+[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module 'untyped-lib'. '/home/src/workspaces/project/node_modules/untyped-lib/index.js' implicitly has an 'any' type.
+
+[7m1[0m import * as lib from "untyped-lib";
+[7m [0m [91m ~~~~~~~~~~~~~[0m
+
+
+Found 1 error in index.ts[90m:1[0m
+
+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
+[96mindex.ts[0m:[93m1[0m:[93m24[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './helper'. '/home/src/workspaces/project/helper.js' implicitly has an 'any' type.
+
+[7m1[0m import { helper } from "./helper";
+[7m [0m [91m ~~~~~~~~~~[0m
+
+
+Found 1 error in index.ts[90m:1[0m
+
+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 @@
+-[96mindex.ts[0m:[93m1[0m:[93m24[0m - [91merror[0m[90m TS2307: [0mCannot find module './helper' or its corresponding type declarations.
++[96mindex.ts[0m:[93m1[0m:[93m24[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './helper'. '/home/src/workspaces/project/helper.js' implicitly has an 'any' type.
+
+ [7m1[0m import { helper } from "./helper";
+ [7m [0m [91m ~~~~~~~~~~[0m
+
+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
+[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './lib/util'. '/home/src/workspaces/project/lib/util.js' implicitly has an 'any' type.
+
+[7m1[0m import { util } from "./lib/util";
+[7m [0m [91m ~~~~~~~~~~~~[0m
+
+
+Found 1 error in index.ts[90m:1[0m
+
+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 @@
+-[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS2307: [0mCannot find module './lib/util' or its corresponding type declarations.
++[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './lib/util'. '/home/src/workspaces/project/lib/util.js' implicitly has an 'any' type.
+
+ [7m1[0m import { util } from "./lib/util";
+ [7m [0m [91m ~~~~~~~~~~~~[0m
\ 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
+[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS2307: [0mCannot find module './lib/util' or its corresponding type declarations.
+
+[7m1[0m import { util } from "./lib/util";
+[7m [0m [91m ~~~~~~~~~~~~[0m
+
+
+Found 1 error in index.ts[90m:1[0m
+
+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
+[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './util'. '/home/src/workspaces/project/util.js' implicitly has an 'any' type.
+
+[7m1[0m import { util } from "./util";
+[7m [0m [91m ~~~~~~~~[0m
+
+
+Found 1 error in index.ts[90m:1[0m
+
+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 @@
+-[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS2307: [0mCannot find module './util' or its corresponding type declarations.
++[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './util'. '/home/src/workspaces/project/util.js' implicitly has an 'any' type.
+
+ [7m1[0m import { util } from "./util";
+ [7m [0m [91m ~~~~~~~~[0m
+
+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
+[96ma.ts[0m:[93m1[0m:[93m19[0m - [91merror[0m[90m TS2307: [0mCannot find module './b' or its corresponding type declarations.
+
+[7m1[0m import { b } from "./b";
+[7m [0m [91m ~~~~~[0m
+
+
+Found 1 error in a.ts[90m:1[0m
+
+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
+[96mindex.ts[0m:[93m1[0m:[93m21[0m - [91merror[0m[90m TS2307: [0mCannot find module 'mylib' or its corresponding type declarations.
+
+[7m1[0m import { lib } from "mylib";
+[7m [0m [91m ~~~~~~~[0m
+
+
+Found 1 error in index.ts[90m:1[0m
+
+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
+[96mindex.ts[0m:[93m1[0m:[93m21[0m - [91merror[0m[90m TS2307: [0mCannot find module 'mylib' or its corresponding type declarations.
+
+[7m1[0m import { lib } from "mylib";
+[7m [0m [91m ~~~~~~~[0m
+
+
+Found 1 error in index.ts[90m:1[0m
+
+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
+[96mindex.ts[0m:[93m1[0m:[93m21[0m - [91merror[0m[90m TS2307: [0mCannot find module '@scope/mylib' or its corresponding type declarations.
+
+[7m1[0m import { lib } from "@scope/mylib";
+[7m [0m [91m ~~~~~~~~~~~~~~[0m
+
+
+Found 1 error in index.ts[90m:1[0m
+
+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::
+[91merror[0m[90m TS5083: [0mCannot 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
+-
+-[1mCOMMON COMMANDS[22m
+-
+- [94mtsc[39m
+- Compiles the current project (tsconfig.json in the working directory.)
+-
+- [94mtsc app.ts util.ts[39m
+- Ignoring tsconfig.json, compiles the specified files with default compiler options.
+-
+- [94mtsc -b[39m
+- Build a composite project in the working directory.
+-
+- [94mtsc --init[39m
+- Creates a tsconfig.json with the recommended settings in the working directory.
+-
+- [94mtsc -p ./path/to/tsconfig.json[39m
+- Compiles the TypeScript project located at the specified path.
+-
+- [94mtsc --help --all[39m
+- An expanded version of this information, showing all possible compiler options
+-
+- [94mtsc --noEmit[39m
+- [94mtsc --target esnext[39m
+- Compiles the current project, with additional settings.
+-
+-[1mCOMMAND LINE FLAGS[22m
+-
+-[94m--help, -h[39m
+-Print this message.
+-
+-[94m--watch, -w[39m
+-Watch input files.
+-
+-[94m--all[39m
+-Show all compiler options.
+-
+-[94m--version, -v[39m
+-Print the compiler's version.
+-
+-[94m--init[39m
+-Initializes a TypeScript project and creates a tsconfig.json file.
+-
+-[94m--project, -p[39m
+-Compile the project given the path to its configuration file, or to a folder with a 'tsconfig.json'.
+-
+-[94m--showConfig[39m
+-Print the final configuration instead of building.
+-
+-[94m--ignoreConfig[39m
+-Ignore the tsconfig found and build with commandline options and files.
+-
+-[94m--build, -b[39m
+-Build one or more projects and their dependencies, if out of date
+-
+-[1mCOMMON COMPILER OPTIONS[22m
+-
+-[94m--pretty[39m
+-Enable color and formatting in TypeScript's output to make compiler errors easier to read.
+-type: boolean
+-default: true
+-
+-[94m--declaration, -d[39m
+-Generate .d.ts files from TypeScript and JavaScript files in your project.
+-type: boolean
+-default: `false`, unless `composite` is set
+-
+-[94m--declarationMap[39m
+-Create sourcemaps for d.ts files.
+-type: boolean
+-default: false
+-
+-[94m--emitDeclarationOnly[39m
+-Only output d.ts files and not JavaScript files.
+-type: boolean
+-default: false
+-
+-[94m--sourceMap[39m
+-Create source map files for emitted JavaScript files.
+-type: boolean
+-default: false
+-
+-[94m--noEmit[39m
+-Disable emitting files from a compilation.
+-type: boolean
+-default: false
+-
+-[94m--target, -t[39m
+-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
+-
+-[94m--module, -m[39m
+-Specify what module code is generated.
+-one of: commonjs, amd, system, umd, es6/es2015, es2020, es2022, esnext, node16, node18, node20, nodenext, preserve
+-default: undefined
+-
+-[94m--lib[39m
+-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
+-
+-[94m--allowJs[39m
+-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
+-
+-[94m--checkJs[39m
+-Enable error reporting in type-checked JavaScript files.
+-type: boolean
+-default: false
+-
+-[94m--jsx[39m
+-Specify what JSX code is generated.
+-one of: preserve, react-native, react-jsx, react-jsxdev, react
+-default: undefined
+-
+-[94m--outFile[39m
+-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.
+-
+-[94m--outDir[39m
+-Specify an output folder for all emitted files.
+-
+-[94m--removeComments[39m
+-Disable emitting comments.
+-type: boolean
+-default: false
+-
+-[94m--strict[39m
+-Enable all strict type-checking options.
+-type: boolean
+-default: true
+-
+-[94m--types[39m
+-Specify type package names to be included without being referenced in a source file.
+-
+-[94m--esModuleInterop[39m
+-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
+-
++[91merror[0m[90m TS5083: [0mCannot 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
+[96ma.ts[0m:[93m1[0m:[93m19[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './b'. '/home/src/workspaces/project/b.js' implicitly has an 'any' type.
+
+[7m1[0m import { b } from "./b";
+[7m [0m [91m ~~~~~[0m
+
+
+Found 1 error in a.ts[90m:1[0m
+
+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 @@
+-[96ma.ts[0m:[93m1[0m:[93m19[0m - [91merror[0m[90m TS2307: [0mCannot find module './b' or its corresponding type declarations.
++[96ma.ts[0m:[93m1[0m:[93m19[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './b'. '/home/src/workspaces/project/b.js' implicitly has an 'any' type.
+
+ [7m1[0m import { b } from "./b";
+ [7m [0m [91m ~~~~~[0m
\ 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
+[96mindex.ts[0m:[93m1[0m:[93m23[0m - [91merror[0m[90m TS2322: [0mType 'null' is not assignable to type 'string'.
+
+[7m1[0m const x = null; const y: string = x;
+[7m [0m [91m ~[0m
+
+
+Found 1 error in index.ts[90m:1[0m
+
+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
+[96mindex.ts[0m:[93m1[0m:[93m23[0m - [91merror[0m[90m TS2322: [0mType 'null' is not assignable to type 'string'.
+
+[7m1[0m const x = null; const y: string = x;
+[7m [0m [91m ~[0m
+
+
+Found 1 error in index.ts[90m:1[0m
+
+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
+[96mindex.ts[0m:[93m1[0m:[93m23[0m - [91merror[0m[90m TS2322: [0mType 'null' is not assignable to type 'string'.
+
+[7m1[0m const x = null; const y: string = x;
+[7m [0m [91m ~[0m
+
+
+Found 1 error in index.ts[90m:1[0m
+
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[90m:1[0m
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
-
-[94m--module, -m[39m
-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
-
-[94m--lib[39m
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
+[96msrc/utils/format.ts[0m:[93m1[0m:[93m54[0m - [91merror[0m[90m TS2339: [0mProperty 'trim' does not exist on type 'string'.
+
+[7m1[0m export function format(s: string): string { return s.trim(); }
+[7m [0m [91m ~~~~[0m
+
+
+Found 1 error in src/utils/format.ts[90m:1[0m
+
+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[90m:1[0m
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 @@
+-[91merror[0m[90m TS18003: [0mNo 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module 'untyped-lib'. '/home/src/workspaces/project/node_modules/untyped-lib/index.js' implicitly has an 'any' type.
[7m1[0m import * as lib from "untyped-lib";
@@ -20,7 +21,8 @@ build starting at HH:MM:SS AM
Found 1 error in index.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96msrc/utils/format.ts[0m:[93m1[0m:[93m54[0m - [91merror[0m[90m TS2339: [0mProperty 'trim' does not exist on type 'string'.
[7m1[0m 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[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96mindex.ts[0m:[93m1[0m:[93m24[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './helper'. '/home/src/workspaces/project/helper.js' implicitly has an 'any' type.
[7m1[0m import { helper } from "./helper";
@@ -67,7 +70,8 @@ build starting at HH:MM:SS AM
Found 1 error in index.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './lib/util'. '/home/src/workspaces/project/lib/util.js' implicitly has an 'any' type.
[7m1[0m import { util } from "./lib/util";
@@ -65,7 +68,8 @@ build starting at HH:MM:SS AM
Found 1 error in index.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS2307: [0mCannot find module './lib/util' or its corresponding type declarations.
[7m1[0m import { util } from "./lib/util";
@@ -18,7 +19,8 @@ build starting at HH:MM:SS AM
Found 1 error in index.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96mindex.ts[0m:[93m1[0m:[93m22[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './util'. '/home/src/workspaces/project/util.js' implicitly has an 'any' type.
[7m1[0m import { util } from "./util";
@@ -65,7 +68,8 @@ build starting at HH:MM:SS AM
Found 1 error in index.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
[96ma.ts[0m:[93m1[0m:[93m19[0m - [91merror[0m[90m TS2307: [0mCannot find module './b' or its corresponding type declarations.
[7m1[0m import { b } from "./b";
@@ -18,7 +19,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
[96mindex.ts[0m:[93m1[0m:[93m21[0m - [91merror[0m[90m TS2307: [0mCannot find module 'mylib' or its corresponding type declarations.
[7m1[0m import { lib } from "mylib";
@@ -18,7 +19,8 @@ build starting at HH:MM:SS AM
Found 1 error in index.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96mindex.ts[0m:[93m1[0m:[93m21[0m - [91merror[0m[90m TS2307: [0mCannot find module 'mylib' or its corresponding type declarations.
[7m1[0m import { lib } from "mylib";
@@ -68,7 +71,8 @@ build starting at HH:MM:SS AM
Found 1 error in index.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
[96mindex.ts[0m:[93m1[0m:[93m21[0m - [91merror[0m[90m TS2307: [0mCannot find module '@scope/mylib' or its corresponding type declarations.
[7m1[0m import { lib } from "@scope/mylib";
@@ -18,7 +19,8 @@ build starting at HH:MM:SS AM
Found 1 error in index.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96mindex.ts[0m:[93m1[0m:[93m23[0m - [91merror[0m[90m TS2322: [0mType 'null' is not assignable to type 'string'.
[7m1[0m const x = null; const y: string = x;
@@ -70,7 +73,8 @@ build starting at HH:MM:SS AM
Found 1 error in index.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96ma.ts[0m:[93m1[0m:[93m19[0m - [91merror[0m[90m TS7016: [0mCould not find a declaration file for module './b'. '/home/src/workspaces/project/b.js' implicitly has an 'any' type.
[7m1[0m import { b } from "./b";
@@ -65,7 +68,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
[96mindex.ts[0m:[93m1[0m:[93m23[0m - [91merror[0m[90m TS2322: [0mType 'null' is not assignable to type 'string'.
[7m1[0m const x = null; const y: string = x;
@@ -18,7 +19,8 @@ build starting at HH:MM:SS AM
Found 1 error in index.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96mindex.ts[0m:[93m1[0m:[93m23[0m - [91merror[0m[90m TS2322: [0mType 'null' is not assignable to type 'string'.
[7m1[0m const x = null; const y: string = x;
@@ -70,7 +73,8 @@ build starting at HH:MM:SS AM
Found 1 error in index.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
[96ma.ts[0m:[93m1[0m:[93m7[0m - [91merror[0m[90m TS4094: [0mProperty 'p' of exported anonymous class type may not be private or protected.
[7m1[0m const a = class { private p = 10; };
@@ -27,7 +28,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96ma.ts[0m:[93m1[0m:[93m7[0m - [91merror[0m[90m TS4094: [0mProperty 'p' of exported anonymous class type may not be private or protected.
[7m1[0m const a = class { private p = 10; };
@@ -139,7 +148,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96ma.ts[0m:[93m1[0m:[93m7[0m - [91merror[0m[90m TS4094: [0mProperty 'p' of exported anonymous class type may not be private or protected.
[7m1[0m const a = class { private p = 10; };
@@ -172,7 +183,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96ma.ts[0m:[93m1[0m:[93m7[0m - [91merror[0m[90m TS4094: [0mProperty 'p' of exported anonymous class type may not be private or protected.
[7m1[0m const a = class { private p = 10; };
@@ -216,7 +229,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
[96ma.ts[0m:[93m1[0m:[93m7[0m - [91merror[0m[90m TS2322: [0mType 'string' is not assignable to type 'number'.
[7m1[0m const a: number = "hello"
@@ -22,7 +23,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96ma.ts[0m:[93m1[0m:[93m7[0m - [91merror[0m[90m TS2322: [0mType 'string' is not assignable to type 'number'.
[7m1[0m const a: number = "hello"
@@ -127,7 +136,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96ma.ts[0m:[93m1[0m:[93m7[0m - [91merror[0m[90m TS2322: [0mType 'string' is not assignable to type 'number'.
[7m1[0m const a: number = "hello"
@@ -156,7 +167,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96ma.ts[0m:[93m1[0m:[93m7[0m - [91merror[0m[90m TS2322: [0mType 'string' is not assignable to type 'number'.
[7m1[0m const a: number = "hello"
@@ -184,7 +197,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] Starting compilation in watch mode...
+
[96ma.ts[0m:[93m1[0m:[93m17[0m - [91merror[0m[90m TS1002: [0mUnterminated string literal.
[7m1[0m const a = "hello
@@ -22,7 +23,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96ma.ts[0m:[93m1[0m:[93m17[0m - [91merror[0m[90m TS1002: [0mUnterminated string literal.
[7m1[0m const a = "hello
@@ -127,7 +136,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96ma.ts[0m:[93m1[0m:[93m17[0m - [91merror[0m[90m TS1002: [0mUnterminated string literal.
[7m1[0m const a = "hello
@@ -154,7 +165,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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
+[2J[3J[H[[90mHH:MM:SS AM[0m] File change detected. Starting incremental compilation...
+
[96ma.ts[0m:[93m1[0m:[93m17[0m - [91merror[0m[90m TS1002: [0mUnterminated string literal.
[7m1[0m const a = "hello
@@ -187,7 +200,8 @@ build starting at HH:MM:SS AM
Found 1 error in a.ts[90m:1[0m
-build finished in d.ddds
+[[90mHH:MM:SS AM[0m] 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::
[91merror[0m[90m TS5083: [0mCannot read file '/home/src/workspaces/project/tsconfig.json'.
+[[90mHH:MM:SS AM[0m] 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()
- }
- })
- }
-}