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

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