Skip to content

ng serve with SSR is broken in every git worktree since 22.1.0 (shared cache resolves deps_ssr outside the worktree) #33968

Description

@salemdar

Command

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.0.9

Description

Description

Since #33321 / PR #33519 ("Share persistent build cache across git worktrees"), @angular/build resolves the default persistent cache to the common git directory — i.e. the main repository root — for every git worktree.

For plain ng build this works as intended. But for an SSR app, ng serve also writes vite's SSR prebundles into that cache, at <main-repo>/.angular/cache/<version>/<project>/vite/deps_ssr/. Those prebundled files then contain bare imports (e.g. firebase-admin/data-connect) that Node resolves relative to the prebundle's own location — under the main repo. The worktree's node_modules is never consulted, so the import fails and every SSR request returns HTTP 500.

The result is that ng serve is unusable in any git worktree of an SSR project from 22.1.0 onward.

Two things worth emphasising, because they are easy to assume otherwise:

  1. This is not a concurrency problem. It is not about two worktrees building at the same time. A single worktree, with the shared cache freshly deleted and nothing else running, still fails on the very first request.
  2. ng build is unaffected, which makes this easy to miss in CI — only the dev-server SSR path breaks.

This is not a request to revert #33321. Sharing the cache is reasonable and the disk-space motivation there is real. The problem is specifically that deps_ssr is not relocatable the way the rest of the cache is: its contents encode a module-resolution root, so placing it outside the worktree breaks resolution.

Suggested fix

Keep the shared cache, but keep the vite deps_ssr (and likely deps) prebundles worktree-local, since they are position-dependent in a way the other cache entries are not. Alternatively, resolve SSR externals against the workspace root rather than the prebundle location.

A workaround exists but is not committable: setting an absolute cli.cache.path in angular.json short-circuits the git lookup and fixes it. It has to be absolute (a relative path is still redirected to the common git dir), so it is machine-specific and cannot be checked into a shared repo. There is also no environment variable for the cache path (NG_BUILD_CACHE_STORE only selects the lmdb/sqlite backend), so there is currently no portable way for a team to opt out.

Minimal Reproduction

An SSR app with a server-side dependency listed in externalDependencies — i.e. left unbundled, so it stays a bare import inside the SSR prebundle and has to be resolved from disk at request time. That is the condition that makes resolution sensitive to where the prebundle lives. Reproduced with firebase-admin, but the mechanism is not specific to it.

# 1. Create an SSR app in a git repo
ng new my-ssr-app --ssr
cd my-ssr-app
git init && git add -A && git commit -m "init"

# 2. Add a server-side dependency, imported from the SSR entry
npm i firebase-admin
# in src/server.ts:  import 'firebase-admin/app';

# 3. Mark it external so it is not bundled (angular.json, build options):
#    "externalDependencies": ["firebase-admin", "firebase-admin/app"]

git add -A && git commit -m "add external server dep"

# 4. Create a worktree and install there
git worktree add ../wt-test -b wt-test
cd ../wt-test
npm i

# 5. Serve from the worktree
ng serve
# open http://localhost:4200  ->  HTTP 500

Observed: the prebundle is written to <main-repo>/.angular/cache/<version>/<project>/vite/deps_ssr/, not to ../wt-test/.angular/cache/..., and the SSR request fails.

Expected: the app renders, as it does on 22.0.9.

The failure is most visible with a package manager that does not flatten node_modules (this was found under pnpm). With npm's hoisted layout a stray parent-directory node_modules may accidentally satisfy the lookup and mask the bug, so pnpm is the more reliable way to observe it.

To confirm the cause without a full reproduction, the resolution logic can be exercised directly:

const { normalizeCacheOptions } = require('@angular/build/src/utils/normalize-cache.js');
console.log(normalizeCacheOptions({}, '/path/to/worktree/apps/my-app').path);
// 22.0.9 -> /path/to/worktree/apps/my-app/.angular/cache/22.0.9
// 22.1.x -> /path/to/MAIN-REPO/.angular/cache/22.1.x

Note that on 22.1.x the worktree resolves to the repository root, while a non-worktree checkout of the same project resolves to apps/my-app/.angular/cache — so the two are not merely shared, they are different directories.

Exception or Error

Error: Cannot find module 'firebase-admin/data-connect' imported from
'/Users/me/Workspace/myrepo/.angular/cache/22.1.6/myapp/vite/deps_ssr/@scope_my-admin-sdk.js'
    at fetchModule (file:///Users/me/Workspace/myrepo/.claude/worktrees/my-worktree/node_modules/.pnpm/vite@8.1.5/node_modules/vite/...)

# Note the two different roots in that single error: the failing import lives under the **main repo** (`/myrepo/.angular/cache/...`), while vite itself is running from the **worktree** (`/myrepo/.../my-worktree/node_modules/...`).

Your Environment

@angular/build:   22.1.6 (runtime-verified); the responsible code is present in 22.1.0+
@angular/cli:     22.1.6
@angular/core:    22.1.4
@angular/ssr:     22.1.6
Node:             22.23.1
Package Manager:  pnpm 11.15.1
OS:               macOS 26.6.2 (darwin arm64)

# Verified not present on `@angular/build` 22.0.9. The `getCacheBasePath()` helper that introduces the git-worktree lookup first appears in `src/utils/normalize-cache.js` in 22.1.0.

Anything else relevant?

  • Introduced by Share persistent build cache across git worktrees #33321 → PR feat(@angular/build): share persistent build cache across git worktrees #33519 (merged 2026-07-07).

  • Bisected to the package level by diffing the shipped src/utils/normalize-cache.js between 22.0.9 (resolve(workspaceRoot, path), no git logic) and 22.1.0+ (getCacheBasePath(), which reads .gitgitdir:commondir and returns resolve(dirname(commonGitDir), cachePathSetting)).

  • A/B tested on one machine with the same commit and the same 22.1.6 install, changing only cli.cache.path:

    cli.cache.path cache location ng serve SSR
    default <main-repo>/.angular/cache/22.1.6 HTTP 500
    absolute, worktree-local worktree's own .angular/cache/22.1.6 HTTP 200, correct SSR output
  • Impact for monorepos that use worktrees per feature branch: local dev and any Playwright/e2e suite that needs a dev server are both blocked, so the only options today are pinning @angular/build / @angular/cli / @angular/ssr to ~22.0.9, or hand-writing a machine-specific absolute path into angular.json on every developer's machine.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions