Open-Source Wikis

/

TypeScript

/

Systems

/

Watch and builder

microsoft/TypeScript

Watch and builder

How tsc --watch, tsserver, and tsc -b keep a long-lived compilation alive without re-doing work after every keystroke.

Source

File Lines Role
src/compiler/watch.ts 1,300 Watch-mode shared utilities; pretty-printed status messages
src/compiler/watchPublic.ts 1,800 createWatchProgram, the public watch entry
src/compiler/watchUtilities.ts 1,000+ Shared host helpers (cached file system, polling)
src/compiler/builder.ts 4,000+ effective Incremental file/affecting-files tracker, .tsbuildinfo
src/compiler/builderPublic.ts small createBuilderProgram factories
src/compiler/builderState.ts 800+ Per-file signature computation

Purpose

A Program is immutable and constructing one is expensive. Watch mode and tsserver need to:

  1. Detect changes — files added, modified, deleted. Both file watchers and polling are supported.
  2. Decide what to re-do — usually a tiny subset of the program (the changed file plus its dependents).
  3. Reuse old state — share parsed/bound source files with the next program.
  4. Persist incrementality.tsbuildinfo files store enough state for a subsequent process to resume incrementality after a restart.

Key abstractions

Symbol Role
createWatchProgram(host) The watch loop
createWatchCompilerHost(...) / createWatchCompilerHostOfFilesAndCompilerOptions(...) / createWatchCompilerHostOfConfigFile(...) Host factories — the host owns file system, watchers, error reporting
WatchOptions Per-tsconfig watch tuning (watchFile, watchDirectory, fallbackPolling, synchronousWatchDirectory, excludeDirectories, excludeFiles)
BuilderProgram A Program wrapper that tracks which files need re-emit
EmitAndSemanticDiagnosticsBuilderProgram The default — does both emit and semantic diagnostics incrementally
SemanticDiagnosticsBuilderProgram Type-check only, no emit (used by language service)
getSemanticDiagnosticsOfNextAffectedFile Pulls one affected file at a time so editors can stream results
.tsbuildinfo On-disk cache of file signatures, options, and reference graph

Watch loop

graph TD
    Start["createWatchProgram"] --> Build["build initial Program"]
    Build --> Emit["emit (or skip in --noEmit)"]
    Emit --> Wait["host.watchFile / watchDirectory"]
    Wait -->|change| Plan["plan rebuild"]
    Plan --> Reuse["pass oldProgram into createProgram"]
    Reuse --> Build
    Wait -->|tsconfig change| Reload["reload tsconfig"]
    Reload --> Build

Two layers of caching keep this fast:

  • The host's cachedDirectoryStructureHost memoises directory listings so readDirectory doesn't hit disk on each rebuild.
  • The BuilderProgram records, for each file, a signature (a hash of its emit-affecting shape). When file A's signature changes, only files that imported A's affected exports are flagged for rebuild.

Incremental signatures

builderState.ts computes per-file signatures — hashes derived from the emit output (.js text + .d.ts text) of a file. When a file changes:

  1. Re-parse, re-bind, re-check the file.
  2. Recompute its signature. If unchanged, no downstream file needs work — only the file itself was a leaf change.
  3. If the signature changed, walk the import graph and invalidate every file that depended on this one's exports.

This lets watchers cheaply distinguish between "edited a comment in a deeply-imported library file" (signature stable, no rebuild) and "added a new export" (rebuild every consumer).

.tsbuildinfo

When --incremental (or --composite) is set, the builder serialises its state to a .tsbuildinfo file at the end of compilation. The file contains:

  • The exact CompilerOptions used.
  • Per-file signatures.
  • The reference graph.
  • Open referenced project paths and their states (in solution-builder mode).

A subsequent tsc invocation reads .tsbuildinfo, validates that options haven't changed, and resumes incrementality. Even though the V8 process has died and no in-memory Program exists, the next run can avoid re-checking unchanged files.

Watch options

watchOptions in tsconfig.json (top-level, not under compilerOptions) lets users tune:

  • watchFileuseFsEvents, useFsEventsOnParentDirectory, priorityPollingInterval, dynamicPriorityPolling, fixedPollingInterval.
  • watchDirectory — recursive watching strategies.
  • fallbackPolling — when native watchers fail.
  • synchronousWatchDirectory — debugging aid.
  • excludeDirectories, excludeFiles — paths the watcher should ignore (e.g., dist/).

These exist because Node's native watchers behave differently across operating systems, network filesystems, virtualised drives, and very large directories. The WatchOptions enum is the team's accumulated knowledge of which strategies work where.

Integration points

  • tsc --watch calls createWatchProgram directly (via executeCommandLine).
  • tsserver builds its own per-project watch host on top of these primitives in src/server/editorServices.ts.
  • tsc -b (build mode) wraps multiple BuilderPrograms — see systems/build-mode.
  • The EmitResult of an incremental emit is consumed by editors that watch the on-disk output.

Entry points for modification

See systems/build-mode for the multi-project orchestration.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Watch and builder – TypeScript wiki | Factory