Repository navigation
[heft-jest-plugin] Fix watch mode failing on Jest 30 - #6200
farzaneh-wm wants to merge 3 commits into
Conversation
|
@microsoft-github-policy-service agree company="Warner Bros. Discovery" |
There was a problem hiding this comment.
🟡 Changes recommended
Critical watch-mode issues remain: exclusions do not filter watcher notifications, and roots and coverage exclusions use unnormalized Jest configuration, risking missed changes, repeated runs, and false success after test failures.
2 open issues
What changed in this PR
This PR addresses #6199 by replacing the Heft Jest plugin’s reliance on private Jest modules with public runCLI calls, restoring watch-mode support for Jest 30.
Changes:
- Moves watch orchestration to Heft, using file timestamps under Jest roots to trigger full-suite reruns.
- Adds path resolution and traversal exclusions for dependencies, temporary files, and coverage output.
- Removes obsolete watch-patching helpers and adds a patch-release change entry.
| File | Description |
|---|---|
heft-plugins/heft-jest-plugin/src/TerminalWritableStream.ts |
Removes the output stream wrapper used by the old watch patches. |
heft-plugins/heft-jest-plugin/src/JestPlugin.ts |
Replaces internal Jest patches with Heft-managed input tracking and public API execution. |
common/changes/@rushstack/heft-jest-plugin/farzaneh-wm-fix-jest30-watch_2026-11-11-17-11.json |
Records the Jest 30 watch-mode fix for release. |
Review effort: Balanced
…ep excluded folders unwatched
There was a problem hiding this comment.
🟡 Changes recommended
Watch mode misses new tests beside excluded folders, and preset normalization can evaluate environment-dependent configuration before NODE_ENV is set to test, causing incorrect roots or failures.
2 open issues
2 resolved since last review
Review effort: Balanced
…ks, skip VCS folders, and add tests


Summary
Fixes #6199
Since
@rushstack/heft-jest-pluginmoved to Jest 30,heft test-watch(andheft run-watchwith a Jest test phase) fails as soon as the test phase starts:In watch mode, the plugin monkey-patched two internal modules of
@jest/core(build/watch.jsandbuild/runJest.js) so that Heft could drive Jest's own watch loop. Jest 30 ships@jest/coreas a single bundle, and itsexportsonly expose.and./package.json, so those modules no longer exist and the patch can't be applied.This PR removes the patching. Heft now owns the watch loop, and each incremental run calls Jest's public
runCLIAPI, the same entry point thatheft testalready uses.Details
On each incremental run,
JestPluginnow:readConfigsfromjest-config, so that presets and defaults are applied.NODE_ENVis set first, because JavaScript presets may depend on it.roots, and reads their modification times through Heft'swatchFs, so that Heft schedules another incremental run when any of them change. This logic lives in the newsrc/JestWatchInputs.ts.runCLIif any file was added, deleted or modified since the previous run. Otherwise it printsNo pending test runs., as before.What counts as a test input, matching what Jest's own watch mode ignores:
watchPathIgnorePatternsormodulePathIgnorePatternsare skipped, as arenode_modulesand the.git,.hgand.slfolders.cacheDirectory), each project'scacheDirectory, and thecoverageDirectory. Without this, a root that contains them, such as the project folder, would rerun the tests every time Jest wrote its cache or coverage.rootDir.Other implementation notes:
watchGlobAsync. Itschangedflag comes from the watcher's cached file times, and those don't yet include thelibfiles that TypeScript emitted a few milliseconds earlier in the build phase. As a result, the tests only ran on a second pass. Reading the times throughwatchFs.statSyncruns the tests in the same pass.<N> Jest test(s) failed in the previous run, and no test inputs have changed sinceinstead of reporting success. If Jest throws, the next pass reruns the tests.NODE_ENVis restored in afinallyblock in bothheft testand watch mode. Previously it stayed set if Jest threw.TerminalWritableStream.ts(only used to capture the patched watch output), and thejestPluginSymbol/getJestPluginplumbing that let the patches find the plugin instance.JestPluginis@internal, and none of these appear in an API report.Behavior changes, also listed in the change file:
runCLIwithfindRelatedTests.watchPluginssetting are no longer used in watch mode, because Jest's own watch loop no longer runs.--find-related-testsnow works in watch mode. Its description inheft-plugin.jsonno longer says it's incompatible.Known limitation: Heft's watcher always watches a folder together with everything under it, and it doesn't support ignored paths. So a folder that contains an excluded or ignored path is not registered as a whole. Its files and its other subfolders are still tracked, but files or subfolders added directly to such a folder don't trigger a run. The default Heft rig roots (
<rootDir>/lib) aren't affected. The proper fix is to pass ignored paths through Heft'sWatchFileSystemAdapterto watchpack, which supports them. That changes@rushstack/heft's public API, so I'd like to do it in a follow-up PR.Alternative considered: detecting Jest 30 and failing with a clear error message. I chose to restore the feature instead. Calling
runCLIwithwatch/watchAllisn't an option, because it returns a promise that never resolves (see the discussion in the review thread).Performance: each incremental run reads the normalized config, plus the folder listing and modification time of every file under the Jest
roots. These are usually small output folders such aslib.How it was tested
New unit tests, run with
heft testinheft-plugins/heft-jest-plugin(21 tests in total pass, and the build has no lint warnings):JestWatchInputs.test.tscovers:node_modules,.git, excluded folders, and ignored files and foldersJestPluginWatch.test.tsmocks@jest/coreandjest-configand covers:NODE_ENVistestwhile the config is read, and is restored even after Jest throws, after which the tests run againManual testing with
heft test-watch --cleanon macOS, Jest 30.2.0, Node 22.23.4:build-tests/heft-jest-preset-testwith its normal config (rootslib), I checked these cases:Cannot find moduleerror..d.tsfile) reported the previous failure again.NODE_ENVistest, andNODE_ENVunset in the shell, the tests ran and passed.watchPathIgnorePatterns: ["<rootDir>/generated/"]and the roots set to the project folder, writing files intogenerated/triggered no run, and an edit triggered exactly one rerun in 75 seconds.Not tested: Windows, apart from CI.