Skip to content

Dev server triggers spurious rebuild due to browser-field disabled files leaking into watchFiles #33160

@fipil

Description

@fipil

Issue: Dev server triggers spurious rebuild due to browser-field disabled files leaking into watchFiles

Command

serve

Is this a regression?

Yes — this did not occur with the Webpack-based builder (@angular-devkit/build-angular:browser). It is specific to the @angular-devkit/build-angular:application builder (esbuild/Vite).

Description

When running ng serve (or nx serve with the Angular dev-server), the application builds successfully, then immediately triggers "Changes detected. Rebuilding..." without any user file changes. This results in a full second rebuild on every serve startup, adding ~30–50 seconds of wasted time.

Root Cause

The issue is in isInternalBundlerFile() in bundler-context.ts. This function is responsible for filtering out non-real files from the watch file list. It currently handles (disabled): paths only when the suffix is a Node.js builtin module (e.g., fs, path, util):

// Current code (v19.2.25):
function isInternalBundlerFile(file: string): boolean {
  // ...
  const DISABLED_BUILTIN = '(disabled):';
  const disabledIndex = file.indexOf(DISABLED_BUILTIN);
  if (disabledIndex >= 0) {
    return builtinModules.includes(file.slice(disabledIndex + DISABLED_BUILTIN.length));
  }
  return false;
}

However, esbuild also generates (disabled): prefixed inputs for browser-field disabled files — i.e., when a third-party package has "browser": { "./some-file.js": false } in its package.json. In this case, esbuild records the input as:

(disabled):node_modules/object-inspect/util.inspect

Since "node_modules/object-inspect/util.inspect" is not in builtinModules, isInternalBundlerFile() returns false, and this non-existent path gets added to the watch file list.

The path is then joined with the workspace root to form:

P:\Projects\my-app\(disabled):node_modules\object-inspect\util.inspect

Watchpack (the underlying file watcher) detects that this file does not exist and reports it as "removed", triggering an immediate rebuild.

Specific package triggering this

object-inspect (v1.13.4) — a transitive dependency commonly pulled in via qsside-channelobject-inspect. Its package.json contains:

{
  "browser": {
    "./util.inspect.js": false
  }
}

And util.inspect.js contains:

module.exports = require('util').inspect;

This is a legitimate pattern — the package disables the Node.js util.inspect wrapper in browser environments. But esbuild's metafile input entry (disabled):node_modules/object-inspect/util.inspect is not properly filtered by the Angular builder.

Suggested Fix

Change isInternalBundlerFile() to treat any path containing (disabled): as internal, not just Node.js builtins:

function isInternalBundlerFile(file: string): boolean {
  if (file[0] === '<' && file.at(-1) === '>') {
    return true;
  }
  // Any (disabled): path is a virtual esbuild entry that doesn't exist on disk
  if (file.includes('(disabled):')) {
    return true;
  }
  return false;
}

This is safe because (disabled): is an esbuild-internal convention for files that have been stubbed out (whether builtins or browser-field disabled files). None of these paths exist on disk, so watching them is always incorrect.

Minimal Reproduction

  1. Create a new Angular 19 application using the application builder
  2. Install a package that depends on object-inspect (e.g., npm install qs)
  3. Import it somewhere in the app: import qs from 'qs';
  4. Run ng serve
  5. Observe:
    • Build completes successfully
    • "Watch mode enabled. Watching for file changes..." appears
    • Immediately followed by "Changes detected. Rebuilding..." (with no user action)

To confirm the cause, add this line before the for await loop in build-action.js:

require('node:fs').writeFileSync('watcher-debug.log', changes.toDebugString(), 'utf8');

The log will show:

{
  "added": [],
  "modified": [],
  "removed": [
    "P:\\path\\to\\project\\(disabled):node_modules\\object-inspect\\util.inspect"
  ]
}

Your Environment

Angular CLI: 19.2.25
Node: 20.12.2
Package Manager: npm 10.8.3
OS: win32 x64

Angular: 19.2.21

Package                          Version
---------------------------------------------------------
@angular-devkit/architect        0.1902.25
@angular-devkit/build-angular    19.2.25
@angular-devkit/core             19.2.25
@angular/build                   19.2.25
@angular/cli                     19.2.25

Anything else relevant?

  • The issue likely affects any package using the "browser": { "./file.js": false } pattern in package.json. object-inspect is just one common example.
  • The problem is Windows-specific in terms of symptoms (watchpack on Windows uses ReadDirectoryChangesW which is more sensitive), but the underlying bug (adding non-existent paths to the watch list) exists cross-platform.
  • A previous related issue #25197 was fixed via #26182 (ignoring dot-directories). This is a different manifestation of the same class of problem.
  • The relevant source file is: packages/angular/build/src/tools/esbuild/bundler-context.ts, function isInternalBundlerFile().

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions