Repository navigation
tsc -b --watch doesn't rebuild when an imported file changes, unless incremental is on #64424
Description
🔎 Search Terms
tsc -b --watch, tsc --build --watch, imported file not rebuilt, incremental, tsbuildinfo
🕗 Version & Regression Information
- This is the behavior in every version I tried
I tried 6.0.3 and today's nightly (7.1.0-dev.20260924.1). In 7 it only happens without incremental or composite. In 6.0.3 it happens even with incremental.
⏯ Playground Link
No response
💻 Code
mkdir proj && cd proj
echo '{"compilerOptions":{"outDir":"out","module":"nodenext","target":"es2022"},"files":["index.ts"]}' > tsconfig.json
echo '{"type":"module"}' > package.json
echo 'import { s } from "./other.js"; export const x = s;' > index.ts
echo 'export const s = "AAA";' > other.ts
npx tsc -b --watch
# in another terminal, once the first build is done:
echo 'export const s = "BBB";' > other.ts🙁 Actual behavior
Editing other.ts does nothing, and out/other.js still has "AAA". Editing index.ts does rebuild.
Here's what I got with the same project:
tsc -b --watch: no rebuildtsc -b --watchwith"incremental": true: rebuildstsc -b --watchwith"composite": true: rebuildstsc --watch(no-b): rebuilds
Same thing if the imported file is outside include, like ../shared/s.ts.
🙂 Expected behavior
tsc -b --watch should rebuild when any file in the program changes, like it already does with incremental on, or like tsc --watch does.
Additional information about the issue
It looks like the build orchestrator only finds out about non-root files from the .tsbuildinfo, both when it checks events and when it picks directories to watch:
TypeScript/tsc/internal/execute/build/orchestrator.go
Lines 430 to 446 in df1a31e
if !rootChanged { task.buildInfoEntryMu.Lock() bi := task.buildInfoEntry task.buildInfoEntryMu.Unlock() if bi != nil && bi.buildInfo != nil { buildInfoDir := tspath.GetDirectoryPath(string(bi.path)) for _, fileName := range bi.buildInfo.FileNames { fp := o.toPath(o.resolveBuildInfoFileName(fileName, buildInfoDir)) if roots.Has(fp) { continue } if _, changed := normalizedPaths[fp]; changed { task.resetStatus() needsUpdate.Store(true) break } } TypeScript/tsc/internal/execute/build/orchestrator.go
Lines 576 to 593 in df1a31e
// Non-root dependency directories from buildinfo (e.g. node_modules .d.ts files). task.buildInfoEntryMu.Lock() bi := task.buildInfoEntry task.buildInfoEntryMu.Unlock() if bi != nil && bi.buildInfo != nil { buildInfoDir := tspath.GetDirectoryPath(string(bi.path)) roots := collections.NewSetFromItems(core.Map(task.resolved.FileNames(), o.toPath)...) for _, fileName := range bi.buildInfo.FileNames { absPath := o.host.FS().Realpath(o.resolveBuildInfoFileName(fileName, buildInfoDir)) fp := o.toPath(absPath) if roots.Has(fp) { continue } dir := tspath.GetDirectoryPath(absPath) if !desiredDirs.Covered(dir) && watchmanager.CanWatchDirectory(dir) { desiredDirs.Set(dir, false) } }
Without incremental or composite there's no buildinfo. So the change event for other.ts does come in (its folder is watched), but nothing picks it up and it gets dropped.
6.0 had the same gap, since its tsc -b only watched parsed.fileNames. So it's not a regression, but it's a bit surprising that in 7 the answer depends on incremental.
This came up in #64366, where I was asked to open a separate issue.
(I used an AI assistant to help write this up.)
Activity
[-]`tsc -b --watch` ignores edits to imported non-root files unless `incremental` or `composite` is set[/-][+]`tsc -b --watch` doesn't rebuild when an imported file changes, unless `incremental` is on[/+]andrewbranch commentedon Oct 29, 2026
This is a longstanding gap that has been previously closed as working as intended: #39700
I don't necessarily want to defend the legacy behavior, but this isn't a bug per se; it's a design change.