Skip to content

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

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 qs → side-channel → object-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().

Activity

  1. added 3 commits that reference this issue on May 14, 2026
    b06849a
    cbad575
    93d3527
  2. angular-automatic-lock-bot commented on Jun 14, 2026

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  3. locked and limited conversation to collaborators on Jun 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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