Skip to content

Watch mode (ng test, ng serve) stalls after "Watch mode enabled" when preserveSymlinks: true — chokidar scans entire workspace incl. node_modules (regression in 22.2.0) #34162

Description

@reifi

Command

  • test
  • serve

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

22.1.8

Description

(AI generated)

After updating @angular/build / @angular/cli from 22.1.8 to 22.2.0, ng test in watch mode (@angular/build:unit-test, Vitest runner) no longer starts running tests after the initial build when the build target has "preserveSymlinks": true. The output stops at:

Application bundle generation complete. [52.899 seconds]

Watch mode enabled. Watching for file changes...

With NG_TEST_LOG=1, VitestExecutor never logs Executing test run — the first build result is not handed to the executor. We stopped the process after more than 4 minutes of waiting. ng test --watch=false is unaffected (the full suite finishes in ~3 min including the build).

Setting "preserveSymlinks": false in the test build configuration makes Vitest start ~2 s after the build completes. Downgrading Vitest (5.0.1 → 4.1.11) makes no difference, so it is not Vitest-related.

Cause (from reading @angular/build 22.2.0 sources):

In src/builders/application/build-action.js, watch mode now calls setupWatcher() from src/utils/watcher.js before emitting the first build result:

  1. createWatcher() uses chokidar instead of @parcel/watcher whenever followSymlinks (= preserveSymlinks) is true.
  2. createChokidarWatcher() calls chokidar.watch(rootDir, …) on the entire workspace root (cwd: workspaceRoot), regardless of NG_BUILD_WATCH_ROOT.
  3. The **/node_modules/** ignore pattern is only added when shouldWatchRoot && !preserveSymlinks, so with preserveSymlinks: true chokidar also walks all of node_modules (≈106 000 files in our workspace).
  4. It then does await once(watcher, 'ready') (added in fix(@angular/build): ensure chokidar watcher is ready before returning #34122), i.e. it blocks until that full initial scan has completed — with followSymlinks: true on Windows.

In 22.1.8, build-action.js used tools/esbuild/watcher.js and only watched the files/directories from result.watchFiles (plus the project root only if NG_BUILD_WATCH_ROOT was set), with no wait for an initial scan, so startup was instant.

preserveSymlinks: true is a common setting for workspaces that npm link local libraries, so this effectively makes watch mode unusable for them.

Since ng serve and ng build --watch go through the same runEsBuildBuildAction / setupWatcher path, they are affected as well, see comment below.

Possible fixes:

  • Keep watching only watchFiles (and the root only with NG_BUILD_WATCH_ROOT), as in 22.1.8, for the chokidar path; or
  • ignore node_modules under the workspace root in the chokidar path as well — explicitly added watch files outside the root (e.g. npm link targets) would still be watched; or
  • don't block the first build result on chokidar's ready when the initial scan is large.

Minimal Reproduction

Verified by me (human): https://github.com/reifi/angular-preserve-symlinks-watch-repro

Reproducible with a freshly generated project:

  1. npx @angular/cli@22.2.0 new repro --defaults --test-runner=vitest
  2. In angular.json, add "preserveSymlinks": true to projects.repro.architect.build.options.
  3. Run ng test (watch mode; ng test --watch in a non-TTY shell).
  4. Observe: output stops after "Watch mode enabled. Watching for file changes..." for about 2 minutes before Vitest starts.

Measured on Windows 11, fresh project (≈18 000 files in node_modules), time from "Watch mode enabled" to the Vitest test summary:

preserveSymlinks Time to test summary Vitest run duration
not set (default) 6 s 5.3 s
true 123 s 7.0 s

The delay scales with the number of files under the workspace root. In a large application workspace (≈106 000 files in node_modules) tests had still not started after more than 4 minutes, when we stopped the process. ng test --watch=false is not affected.

Exception or Error

No error — the process hangs silently after:

Watch mode enabled. Watching for file changes...

Your Environment

_                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/


Angular CLI       : 22.2.0
Angular           : 22.2.0
Node.js           : 26.8.2
Package Manager   : npm 11.19.1
Operating System  : win32 x64

┌─────────────────────────────────┬───────────────────┬───────────────────┐
│ Package                         │ Installed Version │ Requested Version │
├─────────────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build                  │ 22.2.0            │ ^22.2.0           │
│ @angular/cdk                    │ 22.2.0            │ ^22.2.0           │
│ @angular/cli                    │ 22.2.0            │ ^22.2.0           │
│ @angular/common                 │ 22.2.0            │ ^22.2.0           │
│ @angular/compiler               │ 22.2.0            │ ^22.2.0           │
│ @angular/compiler-cli           │ 22.2.0            │ ^22.2.0           │
│ @angular/core                   │ 22.2.0            │ ^22.2.0           │
│ @angular/forms                  │ 22.2.0            │ ^22.2.0           │
│ @angular/google-maps            │ 22.2.0            │ ^22.2.0           │
│ @angular/localize               │ 22.2.0            │ ^22.2.0           │
│ @angular/material               │ 22.2.0            │ ^22.2.0           │
│ @angular/material-luxon-adapter │ 22.2.0            │ ^22.2.0           │
│ @angular/platform-browser       │ 22.2.0            │ ^22.2.0           │
│ @angular/router                 │ 22.2.0            │ ^22.2.0           │
│ @angular/upgrade                │ 22.2.0            │ ^22.2.0           │
│ rxjs                            │ 7.8.2             │ ^7.8.2            │
│ typescript                      │ 6.0.3             │ ^6.0.3            │
│ vitest                          │ 5.0.1             │ ^5.0.1            │
│ zone.js                         │ 0.16.3            │ ^0.16.3           │
└─────────────────────────────────┴───────────────────┴───────────────────┘

Anything else relevant?

Workaround: override "preserveSymlinks": false in the build configuration referenced by the unit-test buildTarget, e.g.

"configurations": {
  "test": {
    "preserveSymlinks": false,
    …
  }
}

The unit-test builder reads preserveSymlinks from the build target options (unit-test/options.js), so this only affects tests; ng serve / ng build keep preserveSymlinks: true for npm link.

Activity

  1. added this to the needsTriage milestone on Sep 24, 2026
  2. changed the title [-]`ng test` (watch mode) stalls after "Watch mode enabled" when `preserveSymlinks: true` — chokidar scans entire workspace incl. `node_modules` (regression in 22.2.0)[/-] [+]Watch mode (`ng test`, 'ng serve') stalls after "Watch mode enabled" when `preserveSymlinks: true` — chokidar scans entire workspace incl. `node_modules` (regression in 22.2.0)[/+] on Sep 24, 2026
  3. changed the title [-]Watch mode (`ng test`, 'ng serve') stalls after "Watch mode enabled" when `preserveSymlinks: true` — chokidar scans entire workspace incl. `node_modules` (regression in 22.2.0)[/-] [+]Watch mode (`ng test`, `ng serve`) stalls after "Watch mode enabled" when `preserveSymlinks: true` — chokidar scans entire workspace incl. `node_modules` (regression in 22.2.0)[/+] on Sep 24, 2026
  4. reifi commented on Sep 24, 2026

    @reifi
    Author

    Follow-up: ng serve is affected as well, not just ng test.

    The dev server only calls server.listen() (src/builders/dev-server/vite/index.js) after it receives the first result from buildApplicationInternal. That result is emitted only after setupWatcher() returns, which waits for chokidar's initial scan of the workspace root. Until then the terminal shows "Watch mode enabled. Watching for file changes..." and nothing is listening on the port, so the browser can't connect.

    Measured with the same fresh reproduction project (ng serve --port 4300, Windows 11), time from "Watch mode enabled" until the port answers HTTP:

    preserveSymlinks Port answers after
    false 1 s
    true 61 s

    (The scan time varies between runs — 61 s here vs. ~2 min for the earlier ng test measurement on the same project — presumably due to file system caching, but it is consistently far above the false baseline.)

    We first noticed this in our large application workspace (≈106 000 files in node_modules), where the dev server stayed unreachable after "Watch mode enabled".

    ng build --watch presumably behaves the same, since it goes through the same runEsBuildBuildAction / setupWatcher path (not measured).

    Workaround we use for now: a build configuration "no-preserve-symlinks": { "preserveSymlinks": false } appended to the dev-server --build-target, dropped only when serving with an npm link-ed library (accepting the slow start in that case).

  5. added 6 commits that reference this issue on Sep 24, 2026
    012b969
    baca17e
    a797fb2
    df9dfab
    3db30e3
    dcd5c67
  6. added a commit that references this issue on Sep 25, 2026
    5194b41
  7. added a commit that references this issue on Sep 25, 2026
    91b0947
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions