Conversation
… 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.
|
leosvelperez
pushed a commit
to nrwl/nx
that referenced
this pull request
Sep 29, 2026
…es its watch program (#37196) **Fixes the `react-package.test.ts` flake: a rollup build finishes, prints `created …`, and its process never exits, so the e2e times out at 300 s. It fails about one `master` run in 25 and has been failing PRs that have nothing to do with rollup.** **Cause: TypeScript leaves a program-update timer armed when `@rollup/plugin-typescript` closes its watch program at the end of the build. This change clears it; the hang went from 11 of 53 runs to 0 of 41.** ## Current Behavior ### What fails, and why it matters `e2e-react` fails with `Command timed out after 300s: build childlib…` while the output already ends in `created ../../dist/libs/childlib… in 1.4s`. The build is done; the process is still there. Every such run is a red CI for a change that did nothing wrong, and a re-run. | Failed because of it | When | |---|---| | `master` | 3 of the last 30 runs (24 Sep 13:39 and 13:44, 25 Sep 14:58); #36794 measured it at about 4% of `master` runs since August | | #37113 (daemon lockfile check) | main-linux, 25 Sep ([run 36133475462](https://github.com/nrwl/nx/actions/runs/36133475462)) | | #37085 (parallel cache restore) | main-linux, 18 Sep, then green on re-run | #36794 found from a CI dump that the `rollup -c` child is what stays alive after its last output, and added a handle dump to find out why. This PR is the why. ### Why the process does not exit 1. `withNx` adds [`@rollup/plugin-typescript`](https://github.com/nrwl/nx/blob/0d8fca1b245ce2700b240008140d3eaef49eebb1/packages/rollup/src/plugins/with-nx/with-nx.ts#L262). The plugin runs the TypeScript compiler as a **watch program** even for a one-shot build, and closes it in [`buildEnd`](https://github.com/rollup/plugins/blob/639f45638234c1c3fabfb13615c78bebaef89ef2/packages/typescript/src/index.ts#L196-L200). 2. A watch program reacts to file changes by arming a 250 ms timer ([`timerToUpdateProgram`](https://github.com/microsoft/TypeScript/blob/050880ce59e30b356b686bd3144efe24f875ebc8/src/compiler/watchPublic.ts#L881-L891)). TypeScript's [`close()`](https://github.com/microsoft/TypeScript/blob/050880ce59e30b356b686bd3144efe24f875ebc8/src/compiler/watchPublic.ts#L562-L598) stops the file watchers but does not cancel that timer. 3. So if any watched file changes in the last 250 ms of the build, the timer fires after the program is closed, rebuilds it, and creates a fresh set of file watchers that nothing will ever close. Every hung process we caught holds 192 `FSEventWrap` and 13 `StatWatcher` handles; a healthy one holds none. 4. Rollup's CLI does not call `process.exit`, so those watchers keep it alive. [`nx:run-commands`](https://github.com/nrwl/nx/blob/0d8fca1b245ce2700b240008140d3eaef49eebb1/packages/nx/src/executors/run-commands/running-tasks.ts#L457) waits for the child to close, the [e2e](https://github.com/nrwl/nx/blob/0d8fca1b245ce2700b240008140d3eaef49eebb1/e2e/utils/command-utils.ts#L477) waits for nx, and the test times out. On CI a build phase runs 5 to 12 s on a shared runner with the daemon recomputing the graph beside it, so the window for a stray file event is far wider than 250 ms. Which event lands in it on CI does not matter to the fix. ## Expected Behavior The process exits when the build is done. **The change:** `packages/rollup/src/plugins/with-nx/watch-program-timers.ts` (+51 lines) gives the plugin a TypeScript whose watch programs remember the timers they arm and cancel them on `close()`. `withNx` passes it as the plugin's `typescript` option (+2 lines). Watch mode is untouched: the program is never closed there. **Proof:** `rollup -c` on a generated React library, with one `touch` of a source file at a fixed offset. The process normally exits at about 1,050 ms, and `buildEnd` falls in the 650 to 850 ms band. | | runs | still alive 20 s after `created …` | |---|---:|---:| | `@nx/rollup` 23.2.1, touch at 650 to 850 ms | 30 | **11** | | `@nx/rollup` 23.2.1, touch later or none | 23 | 0 | | this branch, touch at 650 to 850 ms | 30 | **0** | | this branch, touch later or none | 11 | 0 | Tests: 4 new specs pass (timer cleared on close, timer still fires while open, host without timers untouched, module otherwise unchanged); `nx run rollup:test` 188 pass; lint and format clean. **Where the bug really lives:** in TypeScript's `close()`, and the plugin could also clear the timers itself (rollup/plugins#2025). This PR fixes every `withNx` build now, on every installed TypeScript, whichever of those lands. ## Steps to Reproduce 1. `npx create-nx-workspace proj --preset=apps --pm=npm`, `npm i -D @nx/react`, `nx g @nx/react:library libs/childlib --bundler=rollup --unitTestRunner=jest`, then `nx show projects` once to cache the graph. 2. In `libs/childlib`: `NX_TASK_TARGET_PROJECT=@proj/childlib NX_TASK_TARGET_TARGET=build NX_WORKSPACE_ROOT=$PWD/../.. npx rollup -c rollup.config.cjs & sleep 0.7; touch src/lib/childlib.tsx; wait` (try offsets between 0.65 and 0.85 s; about one run in three hangs). 3. `rollup` prints `created dist in …` and does not exit. With this change it exits at every offset. ## Related Issue(s) #36794 (diagnostics for this timeout). Runs it failed: #37113 ([36133475462](https://github.com/nrwl/nx/actions/runs/36133475462)), #37085 on 18 Sep.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rollup Plugin Name:
typescriptThis PR contains:
Are tests included?
Breaking Changes?
List any relevant issue numbers:
nrwl/nx#36794, nrwl/nx#37196
Description
A one-shot
rollup -cwith this plugin sometimes printscreated dist in 380msand never exits. The plugin runs a TypeScript watch program for every build and, when Rollup is not in watch mode, closes it inbuildEnd. TypeScript schedules program updates throughhost.setTimeout(updateProgramWithWatchStatus, 250, "timerToUpdateProgram"), and itsclose()closes the file watchers and clears the resolution timer but not that one (6.0.3;release-5.9has the sameclose()). So a change to any watched path in the 250 ms beforebuildEnd(a source file, atsconfig, apackage.json,node_modules/@types, a directory of a failed lookup) leaves the timer armed; it fires after the close,updateProgramWithWatchStatus()runssynchronizeProgram()on the closed program and installs a new set of file, directory and missing-file watchers.builderProgramis alreadyundefined, so nothing closes them again, and since Rollup's CLI does not callprocess.exitthe process stays up. This is therollup -cchild 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 thetypescriptoption.The change is in
createWatchPrograminsrc/watchProgram.ts: the host'ssetTimeoutandclearTimeoutare wrapped to keep the set of pending timers, and the returned program'sclose()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 fromcloseWatcher, and a timer is only cancelled by a close.Measured with
dist/cjs/index.jsfrom 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), onetouchof the entry source file at a fixed offset from spawn; the process lives about 1,050 ms andbuildEndfalls in the 650 to 850 ms band:created …Every hung process holds 192
FSEventWrapand 13StatWatcherhandles (process.getActiveResourcesInfo()onSIGUSR2); no run with this branch holds any.Test:
clears a pending program update when the watch program is closedusesfakeTypescriptwith acreateWatchProgramthat arms a timer through the host the way TypeScript does and records whether it fires after the build; it fails onmaster(the timer fires) and passes here.pnpm testinpackages/typescript: 91 pass, 2 skipped (pre-existing).tsc --noEmit,eslintandprettier --checkclean on the two changed files.The root cause is TypeScript's; clearing
timerToUpdatePrograminclose()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.