Audit of the Angular 20 to 21 upgrade plan

Every factual claim in angular-20-to-21-upgrade-plan.html was checked against the working tree, node_modules, the npm registry, the GitHub advisory database, and the Angular 21.0.0 release notes. This page records what held up, what is wrong, and what the plan does not cover.

repo: TopFansLite-WebFrontend branch: feature/release-0.0.23/16-17-18-19-20-21 audited: 2026-09-18

Verdict

The plan is sound. The sequencing (Firebase swap first on Angular 20, then framework, then libraries) is the right call, the checklist covers every breaking change in the 21.0.0 release notes, and every version number resolves on the registry. Two claims are wrong in a way that changes what you do in Phase 2, and there are a handful of inventory gaps that would have surfaced as surprises in Phase 4 or 6.

Verified correct

These claims were checked and hold. They are listed so the reader knows what does not need re-verifying.

  • Current state. Core 20.3.31, CLI 20.3.37, TypeScript 5.8.3, Node 24.5.0, npm 12.0.1, zone.js 0.15.1, Material and CDK 20.2.14, firebase 11.10.0 pulled in transitively by @angular/fire 20.0.1. Commits 64a1a3d259 and 35f314c117 exist with the described messages.
  • Every target version exists and is the newest in its 21 line. Core 21.2.23, CLI 21.2.24, Material 21.2.14, TypeScript 5.9.3, @angular-eslint 21.4.0, primeng 21.1.10, ngx-bootstrap 21.2.2, ng-select 21.8.2, @ngrx/store 21.1.1, ng-bootstrap 20.0.0, ngx-toastr 20.0.5, ngx-mask 21.1.0, ngx-owl-carousel-o 21.0.3, ngx-infinite-scroll 21.0.0, @zxing/ngx-scanner 21.0.0, angular-fontawesome 4.0.0, ngx-device-detector 11.0.0.
  • Peer ranges match. primeng wants @angular/core ^21.0.7, ngx-bootstrap ^21.2.0, zxing typescript ^5.9, @angular-eslint allows eslint ^8.57 || ^9 || ^10. Angular 21 itself peers zone.js ~0.15 || ~0.16 and typescript >=5.9 <6.1.
  • Angular 22 requirements. Node ^22.22.3 || ^24.15.0 || >=26 and TypeScript >=6.0 <6.1, exactly as the plan says.
  • Builders survive. @angular-devkit/build-angular@21.2.24 still ships karma and server, so the test and serverless targets keep working.
  • SSR advisory. GHSA-x288-3778-4hhx is patched in @angular/ssr 21.1.5 (and 20.3.17). allowedHosts is already passed to CommonEngine at server.ts:191.
  • AngularFire null-on-server behaviour. Both provideMessaging and provideRemoteConfig return null when isPlatformServer. The && this.remoteConfig check in getRemoteConfigValue is what keeps the isPrerendered path safe today. The plan's analysis of that is right.
  • @angular/fire is stuck. The next tag is 21.0.0-rc.0, latest is 20.0.1, and the rc depends on firebase ^12.4.0. Nothing newer exists.
  • Codebase counts. 0 spec files, 46 files touching NgZone, 0 hits for moduleId, interpolation, ngComponentOutletContent, ApplicationConfig, ignoreChangesOutsideZone, lastSuccessfulNavigation, NgModuleFactory, emitDeclarationOnly. @angular/elements appears only in a comment in main.server.ts. Only formArrayName is used in templates. FirebaseService is referenced in 15 files. The service worker loads compat 9.6.1 from gstatic.
  • SSR shape. AppServerModule imports AppModule, so the zone provider migration covers the server bundle too. CommonEngine still accepts an NgModule type as bootstrap.

Wrong or misleading

Ordered by how much they change the work.

  • .npmrc already sets legacy-peer-deps=true

    The repo has a committed .npmrc containing legacy-peer-deps=true. That single line invalidates three statements in the plan:

    • "All three already fail npm ls peer checks today." They do not. npm ls exits 0 with zero invalid entries because peer resolution is switched off.
    • "AngularFire still needs --legacy-peer-deps" as a reason to drop it. Every install in this repo already runs that way, so it is not a differentiator. The Firebase 12 coupling is the real reason, and it stands on its own.
    • Phase 2: "add an overrides block rather than --force so future npm install runs are clean without a flag." The flag is already permanent, and overrides does not touch the check that will actually fail.
    Why it matters: ng update runs its own peer check against installed peerDependencies (@angular/cli/src/commands/update/schematic/index.js:198) and throws Incompatible peer dependencies found. The only way past it is --force. ng-ivs (declares Angular 12), ngx-audio-player (17) and ngx-stripe (18) will all trigger it. Plan for --force up front instead of discovering it mid-phase.
  • Untracked files also block ng update

    The cleanliness check is git status --porcelain (update/cli.js:724), and ?? entries count as dirty. Phase 0 says to commit or stash the 8 modified files, but the 12 untracked *.html plans in the repo root (this plan included) will stop ng update just the same.

    Fix: commit them, add them to .gitignore, or pass --allow-dirty. Do this in Phase 0, not when the command refuses.
  • The @HostBinding inventory is incomplete

    The plan names three components plus video-player-v2. Two more files use @HostBinding and will be type-checked for the first time in 21:

    • src/app/components/skeleton/profile-skeleton/profile-skeleton.component.ts: class.main-content getter, typed boolean, probably fine.
    • src/app/modules/auth/components/signup-by-refer/signup-by-refer.component.ts: style.--profile-url on an untyped getter.

    The header.component.ts getters are also untyped. The host: {} scan is correct: 37 files declare host metadata but only video-player-v2 has actual [...] bindings, the rest are static attributes.

  • CDK usage is undercounted

    The plan says "~25" files for drag-drop and clipboard. The real number is 33 for those two entry points, and 35 overall once you add @angular/cdk/scrolling in promotional.module.ts and @angular/cdk/layout in shared/services/mobile-optimization.service.ts. Neither of the extra two appears in the Phase 6 smoke list.

  • @angular-devkit/architect version is misstated

    The matrix lists it moving to 21.2.24. The architect package uses its own scheme; the matching release is 0.2102.24. ng update handles it, so this is a documentation error, not a build risk.

  • ngx-stripe and stripe-js

    The plan says ngx-stripe 21 "forces @stripe/stripe-js from 3 to 5 or later." The latest 21 release (21.9.0) requires >=9 <10. Separately, the installed ngx-stripe 18.1.0 already declares @stripe/stripe-js >=4 <5 against installed 3.5.0, so stripe-js is out of range today, not only after an upgrade. It also sits in devDependencies, which is odd for a runtime SDK. All of this belongs in the deferred-task note.

  • Small counts

    Material bottom-sheet: 6 files import MatBottomSheet, 7 import from the entry point (one module file). Untracked files: 12, not 11, once the plan itself is written to the repo root.

Gaps

Things the plan does not mention that the repo makes relevant.

  • Custom UrlSerializer has a dedicated router change

    The 21.0.0 router notes include "Respect custom UrlSerializer handling of query parameters (#64449)". This repo provides TrailingSlashUrlSerializer as UrlSerializer at app.module.ts:98. That is a targeted behaviour change for exactly this class, separate from the general microtask-timing item the plan already lists.

    The serializer only overrides parse and delegates serialize, so it will most likely keep working, but it deserves a verify row in the checklist and a smoke case such as /username/?key=x and /creators?key=x.
  • requestFirebaseToken has no browser guard

    Its only caller is header.component.ts:825. Today it survives on the server because _messaging is null and the promise .catch swallows the failure. With the raw SDK, getMessaging() throws synchronously on the server before any .catch can run.

    The plan's contract list says "guard with isBrowser", but only calls out getRemoteConfigValue. Apply the same guard, plus isSupported(), to the token path.
  • Zone migration is mandatory, not a prompt

    Phase 2 says "say yes to the zone change detection migration when prompted." The provideZoneChangeDetection migration in ng update @angular/core@21 is a required migration and runs without asking. Harmless, but the checkpoint should read "confirm the provider landed in AppModule.providers" rather than expecting a prompt.

  • The extra CDK entry points need smoke coverage

    cdk/layout drives mobile-optimization.service.ts (breakpoint observation) and cdk/scrolling is imported by the promotional module. Add "mobile breakpoint switch" and "promotional list scroll" to Phase 6.

Corrected Phase 2

What the framework step should say once the findings above are folded in.

git add -A && git commit -m "chore: commit marketing data and plan files before ng update"

ng update @angular/core@21 @angular/cli@21 --force
ng update @angular/material@21 --force

--force is needed because ng update checks installed peer ranges itself and .npmrc cannot influence that. It does not skip migrations. The provideZoneChangeDetection() migration runs automatically; verify it appears in AppModule.providers before committing. Do not add an overrides block for the three out-of-range libraries; legacy-peer-deps already covers npm install, and overrides would not affect the ng update check.

How this was checked

Claim type Evidence
Installed versions and peer ranges node_modules/*/package.json read directly for every package named in the plan.
Target versions npm view <pkg>@21 version peerDependencies for each library; dist-tags for @angular/fire.
Builders in 21.2.24 builders.json from the published @angular-devkit/build-angular@21.2.24 tarball.
Advisory gh api /advisories/GHSA-x288-3778-4hhx.
Breaking changes list Angular 21.0.0 GitHub release notes, Breaking Changes section, cross-checked row by row against the plan's table.
Codebase occurrences rg over src/ and server.ts for each symbol, decorator, and import path.
ng update behaviour Read @angular/cli/src/commands/update/cli.js and update/schematic/index.js for the dirty-tree check and the peer compatibility check.
Install state .npmrc, npm ls exit code, git status --porcelain.