Skip to content

fix(typescript): clear the pending program update when a build closes the watch program - #2025

Open
sdjayna wants to merge 3 commits into
rollup:masterfrom
sdjayna:fix/typescript-close-clears-update-timer
Open

sdjayna wants to merge 3 commits into
rollup:masterfrom
sdjayna:fix/typescript-close-clears-update-timer

Conversation

@sdjayna

@sdjayna sdjayna commented Sep 25, 2026 •

Copy link
Copy Markdown

Rollup Plugin Name: typescript

This PR contains:

  • bugfix
  • feature
  • refactor
  • documentation
  • other

Are tests included?

  • yes (bugfixes and features will not be merged without tests)
  • no

Breaking Changes?

  • yes (breaking changes will not be merged unless absolutely necessary)
  • no

List any relevant issue numbers:

nrwl/nx#36794, nrwl/nx#37196

Description

A one-shot rollup -c with this plugin sometimes prints created dist in 380ms and never exits. The plugin runs a TypeScript watch program for every build and, when Rollup is not in watch mode, closes it in buildEnd. TypeScript schedules program updates through host.setTimeout(updateProgramWithWatchStatus, 250, "timerToUpdateProgram"), and its close() closes the file watchers and clears the resolution timer but not that one (6.0.3; release-5.9 has the same close()). So a change to any watched path in the 250 ms before buildEnd (a source file, a tsconfig, a package.json, node_modules/@types, a directory of a failed lookup) leaves the timer armed; it fires after the close, updateProgramWithWatchStatus() runs synchronizeProgram() on the closed program and installs a new set of file, directory and missing-file watchers. builderProgram is already undefined, so nothing closes them again, and since Rollup's CLI does not call process.exit the process stays up. This is the rollup -c child that Nx's e2e suite finds alive minutes after its output (nrwl/nx#36794); nrwl/nx#37196 works around it on the Nx side by wrapping the typescript option.

The change is in createWatchProgram in src/watchProgram.ts: the host's setTimeout and clearTimeout are wrapped to keep the set of pending timers, and the returned program's close() cancels whatever is still pending after TypeScript's own close. Hosts without timers (TypeScript then updates synchronously) are left alone. Watch mode is unchanged: the program is only closed from closeWatcher, and a timer is only cancelled by a close.

Measured with dist/cjs/index.js from this branch dropped into a generated Nx React library (rollup -c rollup.config.cjs, TypeScript 6.0.3, Rollup 4.63.5, Node 26.7.0, macOS), one touch of the entry source file at a fixed offset from spawn; the process lives about 1,050 ms and buildEnd falls in the 650 to 850 ms band:

plugin touch offset runs still alive 20 s after created …
12.3.0 650 to 850 ms 30 11
12.3.0 900 to 1,600 ms 20 0
12.3.0 none 3 0
this branch 650 to 850 ms 30 0
this branch 900 to 1,050 ms 8 0
this branch none 3 0

Every hung process holds 192 FSEventWrap and 13 StatWatcher handles (process.getActiveResourcesInfo() on SIGUSR2); no run with this branch holds any.

Test: clears a pending program update when the watch program is closed uses fakeTypescript with a createWatchProgram that arms a timer through the host the way TypeScript does and records whether it fires after the build; it fails on master (the timer fires) and passes here. pnpm test in packages/typescript: 91 pass, 2 skipped (pre-existing). tsc --noEmit, eslint and prettier --check clean on the two changed files.

The root cause is TypeScript's; clearing timerToUpdateProgram in close() there would make the wrapper redundant, but the plugin has to run against every TypeScript its users have installed. Reported upstream as microsoft/TypeScript#64450.

… the watch program

TypeScript's `createWatchProgram().close()` stops the file watchers but does
not clear `timerToUpdateProgram`. The plugin closes the program in `buildEnd`
when Rollup is not in watch mode, so a change to any watched path in the last
250ms of the build leaves the timer armed; it fires after the close, rebuilds
the program with a full set of file watchers, and nothing closes those again.
`rollup -c` then prints its output and never exits.

Track the timers the program arms through the host and cancel the pending
ones when the program is closed. Watch mode is unchanged: the program is not
closed there until the watcher stops.
@sdjayna
sdjayna requested a review from shellscape as a code owner September 25, 2026 19:48
@greptile-apps

greptile-apps Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[Medium risk] Fixes timer cleanup in the TypeScript watch program.

The PR appears safe to merge; no outstanding finding or new actionable issue remains.

Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[TypeScript schedules update] --> B[Host tracks timer]
  B --> C[One-shot build closes program]
  C --> D[Cancel pending timer]
  D --> E[No update runs after close]
Loading

Reviews (2) · Last reviewed commit: "fix(typescript): forward timer arguments..."

Comment thread packages/typescript/test/test.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant