Command
serve
Is this a regression?
The previous version in which this bug was not present was
No response
Description
Disclaimer: This issue was co-authored with claude code
Component HMR silently fails in Angular workspaces where the application is located in a subdirectory (e.g. projects/my-app/) rather than the workspace root.
The angular:component-update WebSocket event is correctly sent to the browser and the client-side import.meta.hot.on('angular:component-update', ...) listener is registered, but the update is never applied because the component IDs don't match between server and client.
Root cause: The HMR update ID is computed using different base directories on each side:
- Server (aot-compilation.ts): relative(host.getCurrentDirectory(), componentFilename) — getCurrentDirectory() returns the project directory (projects/management/), producing src/app/foo/foo.component.ts
- Client (compiler-cli, extractHmrMetatadata → getProjectRelativePath): uses rootDirs derived from compilerOptions.rootDir, which points to the workspace root, producing projects/management/src/app/foo/foo.component.ts
The encoded IDs never match, so d.id === id in the client-side handler always evaluates to false.
This is related to #32998 which describes the same symptom triggered by an explicit rootDir setting.
Fix: In aot-compilation.ts, resolve compilerOptions.rootDir relative to getCurrentDirectory() before computing the relative path:
const hmrBaseDir = compilerOptions.rootDir
? resolve(host.getCurrentDirectory(), compilerOptions.rootDir)
: host.getCurrentDirectory();
let relativePath = relative(hmrBaseDir, componentFilename);
Minimal Reproduction
Here is a repo with a minimal reproduction.
- ng new repro-workspace --no-create-application
- cd repro-workspace
- ng generate application my-app --project-root projects/my-app
- Move the project config into a per-project angular.json (to simulate a multi-project workspace where each project has its own angular.json):
- Clear projects in the root angular.json to {}
- Create projects/my-app/angular.json with the project definition, using paths relative to projects/my-app/ (i.e. "root": ".", "browser": "src/main.ts", "tsConfig": "tsconfig.app.json")
- Add "hmr": true and "liveReload": true to the serve options
- cd projects/my-app && npx ng serve my-app
- Modify src/app/app.component.html
- Observe terminal: Component update sent to client(s).
- Observe browser WebSocket: angular:component-update event received with ID starting with src%2Fapp%2F...
- Observe browser source code: client-side HMR listener registered with ID starting with projects%2Fmy-app%2Fsrc%2Fapp%2F...
- Expected: Component updates in the browser
- Actual: Nothing happens. The server-side ID (src/app/...) doesn't match the client-side ID (projects/my-app/src/app/...) because host.getCurrentDirectory() in aot-compilation.ts returns the project subdirectory, while the compiler's getProjectRelativePath resolves
against rootDirs (the workspace root via rootDir: "." in the root tsconfig.json).
The root cause is in packages/angular/build/src/tools/angular/compilation/aot-compilation.ts: the HMR update ID is computed as relative(host.getCurrentDirectory(), componentFilename), but the client-side ID uses getProjectRelativePath(fileName, rootDirs, compilerHost)
which resolves against compilerOptions.rootDir. When the project's angular.json is in a subdirectory, these produce different paths.
Exception or Error
Your Environment
_ _ ____ _ ___
/ \ _ __ __ _ _ _| | __ _ _ __ / ___| | |_ _|
/ △ \ | '_ \ / _` | | | | |/ _` | '__| | | | | | |
/ ___ \| | | | (_| | |_| | | (_| | | | |___| |___ | |
/_/ \_\_| |_|\__, |\__,_|_|\__,_|_| \____|_____|___|
|___/
Angular CLI : 21.2.6
Angular : 21.2.7
Node.js : 24.14.1
Package Manager : pnpm 10.33.0
Operating System : darwin arm64
┌───────────────────────────────┬───────────────────┬───────────────────┐
│ Package │ Installed Version │ Requested Version │
├───────────────────────────────┼───────────────────┼───────────────────┤
│ @angular-devkit/build-angular │ 21.2.6 │ 21.2.6 │
│ @angular-devkit/core │ 21.2.6 │ 21.2.6 │
│ @angular/build │ 21.2.6 │ 21.2.6 │
│ @angular/cli │ 21.2.6 │ 21.2.6 │
│ @angular/compiler │ 21.2.7 │ 21.2.7 │
│ @angular/compiler-cli │ 21.2.7 │ 21.2.7 │
│ @angular/core │ 21.2.7 │ 21.2.7 │
│ @angular/platform-browser │ 21.2.7 │ 21.2.7 │
│ ng-packagr │ 21.2.2 │ 21.2.2 │
│ typescript │ 5.9.3 │ 5.9.3 │
│ vitest │ 4.1.4 │ 4.1.4 │
│ zone.js │ 0.15.1 │ 0.15.1 │
└───────────────────────────────┴───────────────────┴───────────────────┘
Anything else relevant?
Tested on macOS Darwin 24.1.0. Package manager: pnpm.
Command
serve
Is this a regression?
The previous version in which this bug was not present was
No response
Description
Component HMR silently fails in Angular workspaces where the application is located in a subdirectory (e.g. projects/my-app/) rather than the workspace root.
The angular:component-update WebSocket event is correctly sent to the browser and the client-side import.meta.hot.on('angular:component-update', ...) listener is registered, but the update is never applied because the component IDs don't match between server and client.
Root cause: The HMR update ID is computed using different base directories on each side:
The encoded IDs never match, so d.id === id in the client-side handler always evaluates to false.
This is related to #32998 which describes the same symptom triggered by an explicit rootDir setting.
Fix: In aot-compilation.ts, resolve compilerOptions.rootDir relative to getCurrentDirectory() before computing the relative path:
Minimal Reproduction
Here is a repo with a minimal reproduction.
- Clear projects in the root angular.json to {}
- Create projects/my-app/angular.json with the project definition, using paths relative to projects/my-app/ (i.e. "root": ".", "browser": "src/main.ts", "tsConfig": "tsconfig.app.json")
- Add "hmr": true and "liveReload": true to the serve options
against rootDirs (the workspace root via rootDir: "." in the root tsconfig.json).
The root cause is in packages/angular/build/src/tools/angular/compilation/aot-compilation.ts: the HMR update ID is computed as relative(host.getCurrentDirectory(), componentFilename), but the client-side ID uses getProjectRelativePath(fileName, rootDirs, compilerHost)
which resolves against compilerOptions.rootDir. When the project's angular.json is in a subdirectory, these produce different paths.
Exception or Error
Your Environment
Anything else relevant?
Tested on macOS Darwin 24.1.0. Package manager: pnpm.