Wanna see something cool? Check out Angular Spotify 🎧

Upgrading to Angular 20: more AI gotcha than I expected

After finishing the Jira Clone upgrade to Angular 20, I opened Angular Spotify and saw it was still on Angular 17. Same problem, different repo.

Angular Spotify at Angular 17, target 20

No Angular upgrade happens in this post. Two things came up during planning that are worth writing about. Claude gave confident answers that turned out to be wrong once I pushed back:

  • It insisted RxJS 7 was required before Angular 18. It is not, RxJS 6 still works.
  • It cited a Sentry migration guide for a pattern that is not on that page. The actual reference is in the package README.

If you are using AI for a migration (which is very likely a yes), those two sections are the main point to watch out for.

1. Where Angular Spotify stands today

Angular Spotify has drifted more than Jira Clone had. Current state:

Package Current Notes
@angular/core 17.3.2 Target: 20.x
@nx/* 18.2.0 Nx already ahead of Angular, fine
@ngrx/* 17.0.1 Tracks Angular majors
ng-zorro-antd 15.1.0 Peer deps @angular/core@^15.0.1; highest-risk upgrade
rxjs 6.6.6 Last release 6.6.7 in 2021; see 3.3
zone.js 0.14.4 Fine for 17/18
@sentry/angular 7.49.0 Plus @sentry/[email protected], deprecated
jest 29.4.3 Plan to migrate to Vitest at Angular 20
eslint 8.57.0 Flat config + ESLint 9 at Angular 20
cypress 7.6.0 Listed as a dep, but no e2e tests exist
tailwindcss 3.3.2 Stay on v3 (lesson from Jira Clone Part 7)

On the surface it looks fine: Angular 17, Nx 18, NgRx 17. The issue is [email protected] where the peer deps only support @angular/core@^15.0.1 and we have been running Angular 17. Yarn tolerated the mismatch silently. If we were using npm, we would not have gone this far 😂

[email protected] also looked like a blocker. Claude said it was, it is not hehe. More on that in 3.3.

Going straight to Angular 20 means upgrading seven or eight packages across three Angular majors in one PR which is too risky. I am doing it as a series instead: 17 → 18 → 19 → 20, one major per PR, one post per step.

Before the Angular upgrade, there are four prep PRs to land first. That is what this post is about.

2. Kicking off Claude Code for planning

Claude Code planning prompt for Angular Spotify

Same opening move as Part 7 of Jira Clone. I started with the plan, not the code. First prompt was roughly: Plan Angular 20 migration. I also want to write a blog post. Read my existing post to match the style.

It was back-and-forth, not a one-shot plan. Claude asked questions, I picked options. A few key decisions:

  • Strategy: Big-bang 17 → 20 in one PR, or stepwise? Went with stepwise. Smaller PRs, easier rollback.
  • Blog post shape: One giant post or one per Angular upgrade? One per upgrade. Three upgrades in one post loses the per-step detail.
  • Timing: Write the post as a guide upfront, or as a post-mortem after? Post-mortem. You want to write what actually happened, not what you planned.

Then Claude audited the repo and gave the assessment: which packages block the Angular upgrade, and which are just debt to clean up along the way.

3. Four prep PRs: peer-dependency analysis

Before touching Angular, figure out which other packages are tied to the Angular version and which are not. For example, ng-zorro-antd is tied — it only supports Angular 15. rxjs is not — it works across Angular 17 through 20.

Four packages kept coming up: @sentry/angular, ng-zorro-antd, rxjs, and cypress. For each one, the question was: does this block the Angular upgrade, or is it just debt I want to clean up anyway? One command answers that: npm view <pkg>@<version> peerDependencies.

peerDependencies is how a package tells you which version of its dependencies it expects you to have. For example, ng-zorro-antd@15 expects @angular/core@^15.0.1 — meaning it will might work correctly if you run it with Angular 17 or 18.

After two corrections (more on those below), this is Claude’s final classification:

# PR Status What peer deps say
2.1 Sentry: drop @sentry/tracing, bump @sentry/angular to v8 Blocker @sentry/[email protected] caps @angular/core at <= 15.x
2.2 ng-zorro 15 → 17 Blocker ng-zorro-antd@15 caps @angular/core at ^15.0.1
2.3 RxJS 6 → 7 Non-blocker No peer-dep conflict, see 3.3
2.4 Cypress removal Non-blocker No peer-dep conflict

3.1 Sentry (blocker) · PR #112

$ npm view @sentry/[email protected] peerDependencies
{
  rxjs: '^6.5.5 || ^7.x',
  '@angular/core': '>= 10.x <= 15.x',      # caps at Angular 15
  '@angular/common': '>= 10.x <= 15.x',
  '@angular/router': '>= 10.x <= 15.x'
}

@sentry/[email protected] caps Angular at 15.x. We have been on Angular 17 for months because yarn allowed the mismatch.

There is also a second issue: @sentry/[email protected] has no peer deps so nothing warned, but it pulls @sentry/*@6.8 internals while @sentry/[email protected] pulls @sentry/*@7.49. We have been shipping two different Sentry versions in one bundle without knowing it.

3.1.a Gotcha 1: right pattern, wrong source

The Sentry prep PR looked small on paper. Two files: main.ts for the v8 init API, and web-shell.module.ts. I did not expect was a new block in web-shell.module.ts:

{ provide: Sentry.TraceService, deps: [Router] },
{
  provide: APP_INITIALIZER,
  useFactory: () => (): void => undefined,
  deps: [Sentry.TraceService],
  multi: true
}

Sentry TraceService and APP_INITIALIZER block added by Claude

I asked Claude why the block was there. The answer was confident and came with a URL:

It is the pattern Sentry’s v8 Angular migration guide recommends. See here: https://docs.sentry.io/platforms/javascript/guides/angular/migration/v7-to-v8/

Sounded ok, then I opened the link. There is nothing on that page about TraceService or APP_INITIALIZER.

So I pushed back: “I do not see Sentry.TraceService mentioned on that page, are you sure?” Claude then ran curl against the actual Sentry docs, grepped for the terms, and came back with the correction.

Claude citing the Sentry v7 to v8 migration guide as the source

There is actually a mention of TraceService and the APP_INITIALIZER pattern in the Sentry docs, but it is in the README at sentry-javascript/8.55.1/angular/README.md#tracing

TraceService pattern documented in the @sentry/angular package README

What I want to remember from this one.

  • I treated a URL as confirmation. The URL did not support the claim. A citation is only a citation if you open it. When an AI cites a URL, open the URL.
  • When an AI adds code that the original did not have, ask why.

3.2 ng-zorro (blocker) · PR #113

$ npm view [email protected] peerDependencies
{
  '@angular/core': '^15.0.1',              # caps at Angular 15
  '@angular/forms': '^15.0.1',
  '@angular/common': '^15.0.1',
  '@angular/router': '^15.0.1',
  '@angular/animations': '^15.0.1',
  '@angular/platform-browser': '^15.0.1'
}

Same as Sentry, but worse. ng-zorro-antd@15 pins Angular to ^15.0.1 and we are on Angular 17. It has been running on an unsupported Angular version the whole time.

This is the highest-risk PR. ng-zorro touches around several files: player controls, lyrics toggle, modals, toasts. Bumping 15 → 17 means two majors of breaking changes at once: theme tokens, form control API, modal service signatures, and the usual nz-select template quirks.

Why 17 and not 18? To keep the PR manageable. One ng-zorro major per Angular step: 15 → 17 now, then 17 → 18, 18 → 19, 19 → 20 alongside each Angular upgrade.

3.3 Gotcha 2: RxJS is not actually required for Angular 18 · PR #114

Claude’s first take was confident:

PR A, RxJS 6 → 7. Angular 18 requires it; doing it on 17 isolates RxJS breakage from Angular breakage.

Claude's initial claim that RxJS 7 is required for Angular 18

I almost accepted that. But something feel wrong, and to find out I asked for the actual npm view output. That is when it fell apart:

$ npm view @angular/[email protected] peerDependencies
{ rxjs: '^6.5.3 || ^7.4.0', 'zone.js': '~0.14.0' }

$ npm view @angular/[email protected] peerDependencies
{ rxjs: '^6.5.3 || ^7.4.0', 'zone.js': '~0.14.10' }

$ npm view @angular/[email protected] peerDependencies
{ rxjs: '^6.5.3 || ^7.4.0', 'zone.js': '~0.15.0' }

$ npm view @angular/[email protected] peerDependencies
{ rxjs: '^6.5.3 || ^7.4.0', 'zone.js': '~0.15.0' }

npm view output showing Angular 17 through 20 all accept RxJS 6 or 7

Angular 17 through 20 all accept rxjs@^6.5.3 || ^7.4.0. [email protected] is within that range. Claude made up the Angular 18 requirement.

RxJS 6 → 7 is not a blocker. Worth doing anyway since RxJS 6 has not had a release since 2021, .toPromise() is deprecated, and third-party Angular libs are dropping v6 support. But it does not gate the Angular upgrade.

I also grepped apps/ and libs/ for .toPromise( and got zero hits. This PR ends up being just a version upgrade.

Peer dependency analysis findings

What I want to remember from this one. Claude sounded sure. The confidence was not evidence. npm view took ten seconds and settled the question. Without pushing back, I would have shipped this PR based on a made-up requirement.

3.4 Cypress (non-blocker) · PR #115

$ npm view [email protected] peerDependencies
(none)

Cypress has no peer deps. No Angular conflict.

But I checked what the repo actually has:

  • No apps/*-e2e folder
  • No cypress.config.ts or cypress.json outside node_modules/
  • No *.cy.ts spec files anywhere in apps/ or libs/
  • cypress is in package.json, downloaded on every yarn install, and nothing uses it

This is not an upgrade. It is a removal. The plan was updated.

4. Four subagents, one parallel run

Two blockers, two non-blockers, nothing depends on anything else. Good fit for running all four in parallel.

Four subagents running in parallel

The approach:

  1. Commit the plan and briefs to main first. Every subagent’s worktree starts from the same docs.
  2. Spawn four Claude Code subagents in parallel, one per PR, each in its own git worktree so they do not step on each other.
  3. Each subagent reads its brief, executes, validates, and commits on its branch. No pushes, no PRs. I review before merging.
  4. Merge in order: 2.1 → 2.2 → 2.3 → 2.4. Blockers first. package.json and yarn.lock conflicts are expected, so each PR rebases onto main after the previous one lands.

The briefs are at docs/plans/prep/2.1-sentry-v8.md through 2.4-cypress-remove.md. Each one is self-contained with the scope, current state, exact commands, and a validation checklist. A subagent can start cold without reading anything else.

In the Jira Clone series I ran everything sequentially in one Claude Code session. Here, four sessions run on four branches at the same time.

5. Source

6. Two lessons

Both cases had the same pattern: a confident answer, close to right but not quite, with a plausible-looking source. Both only surfaced because I pushed back.

Three things I am keeping from this:

  1. Run npm view <pkg>@<version> peerDependencies yourself occasionally. Do not accept “requires X” without verifying.
  2. When an AI cites a URL, open it.

This is not an argument against using Claude Code for migrations. Both mistakes were caught in the back-and-forth. The problem is not that the AI makes things up. It is that you accept them without checking.

Pushing back matters more as AI gets better at sounding confident. I wish AI would push back more often 😂

Published 19 Apr 2026

Read more

 — How to hide Agent View by default on startup in Antigravity
 — Upgrade to Angular 20 from Angular 13 - Part 7: Angular 20 with Claude Code
 — Fuzzy search everywhere on macOS with fzf and fd
 — Speed up zsh startup by lazy loading nvm
 — Upgrade to Angular 20 from Angular 13 - Part 6: Angular 19 with Claude Code