Org Skills

A worked upgrade: Angular 17 NgModule app → Angular 22

Supporting material for angular-migration. Agents load it on demand; it ships inside the skill folder.

RawSource

A realistic sequence for a medium app (≈120 components, NgModules, Karma, @angular/animations, Material). Adapt the version hops; the shape holds for any starting point. Verified 2026-09 against https://angular.dev/reference/releases and https://angular.dev/update-guide.

Phase 0 — before touching anything (half a day)

  1. Record the baseline: npm test, ng build, and the number of failing/skipped specs. An upgrade onto a red suite cannot be evaluated.
  2. Inventory dependencies: npm ls --depth=0. For every package with @angular as a peer dependency, find the version that supports v22. Anything with no such release is a decision — replace, fork, or stop one major short — and it is cheaper now than in week three.
  3. Read https://angular.dev/update-guide for 17 → 18, 18 → 19, 19 → 20, 20 → 21 and 21 → 22 with "Advanced" turned on. Note the manual steps; there are usually two or three per hop.
  4. Agree the rule with the team: trunk stays deployable after every step. No long-lived branch.

Phase 1 — the version hops (one PR each)

ng update @angular/core@18 @angular/cli@18 && npm test && ng build
ng update @angular/material@18                                  # same hop, not later
# commit, merge, deploy
ng update @angular/core@19 @angular/cli@19 && npm test && ng build
ng update @angular/core@20 @angular/cli@20 && npm test && ng build
ng update @angular/core@21 @angular/cli@21 && npm test && ng build
ng update @angular/core@22 @angular/cli@22 && npm test && ng build

Checkpoints worth expecting on the way:

Hop What usually bites
→ 18 Material 3 theming API; @angular/material theme files need rework
→ 19 standalone: true becomes the default — remove it from new code, and expect lint noise
→ 20 afterRender renamed to afterEveryRender; the AfterRenderPhase enum removed; CLI starts generating suffix-less file names
→ 21 Zoneless becomes the default for new apps and Vitest the default runner — existing config is untouched, but the defaults you read about change
→ 22 @angular/animations deprecation surfaces; withIncrementalHydration()/withEventReplay() become redundant; ChangeDetectionStrategy.Default deprecated in favour of Eager

Do not start modernising between hops. Mixing an ng update migration with a hand rewrite in one PR makes both unreviewable.

Phase 2 — standalone (three PRs)

ng generate @angular/core:standalone     # choose mode 1: convert to standalone
npm test && ng build                     # commit
ng generate @angular/core:standalone     # mode 2: remove unnecessary NgModules
npm test && ng build                     # commit
ng generate @angular/core:standalone     # mode 3: bootstrap with standalone APIs
npm test && ng build                     # commit

Run the modes in order and verify between each. Mode 2 will leave NgModules that genuinely still provide something — that is correct, not a failure; convert those by hand later or leave them.

The one hand step after mode 3: main.ts now calls bootstrapApplication(App, appConfig), and every forRoot() provider from the old AppModule has to be a provide*() function in app.config.ts. importProvidersFrom(SomeModule) is the escape hatch for a library that still only ships an NgModule.

Phase 3 — template and DI modernisation (one PR each)

ng generate @angular/core:control-flow
ng generate @angular/core:inject
ng generate @angular/core:signal-input-migration
ng generate @angular/core:output-migration
ng generate @angular/core:signal-queries-migration
ng generate @angular/core:common-to-standalone
ng generate @angular/core:ngclass-to-class
ng generate @angular/core:ngstyle-to-style
ng generate @angular/core:cleanup-unused-imports
ng generate @angular/core:self-closing-tag
ng generate @angular/core:route-lazy-loading

For a repo where one of these touches more than ~300 files, split by feature:

ng generate @angular/core:control-flow --path src/app/features/orders

Review each diff. The two things schematics cannot decide for you: whether a converted @for should have gained an @empty branch, and whether a component's remaining imports are the minimal set.

Phase 4 — test suite (one PR, or two)

Karma → Vitest as described in manual-migrations.md. Do this before phase 5: removing zone.js breaks every fakeAsync test, and you want that breakage on a suite you have already converted.

Phase 5 — zoneless (one PR, plus a click-through)

Remove zone.js from polyfills, confirm no provideZoneChangeDetection() remains, then click through every feature by hand. This is the one step a green unit suite does not validate, because the specs never exercised zone patching in the first place.

Budget a day for the fallout in a medium app, concentrated in: third-party widget callbacks, addEventListener outside Angular, and anything that used to update "somehow" after a promise.

Phase 6 — the optional rest

  • @angular/animationsanimate.enter/animate.leave (one PR per animated feature; there is no rush, but the package will not be there forever).
  • .component.ts → suffix-less names, if the team wants it: one mechanical PR, no behaviour change, landed on a quiet day.
  • New forms in Signal Forms. Existing Reactive Forms stay.

Estimating

For a medium app with a green suite, on a team that keeps trunk deployable:

Phase Typical
0 inventory and reading 0.5 day
1 five version hops 1–2 days, mostly waiting on CI and fixing Material theming
2 standalone 1 day
3 template/DI schematics 1–2 days, almost all of it reviewing diffs
4 Karma → Vitest 1–3 days, depending on how much fakeAsync is in the suite
5 zoneless 1 day plus the click-through
6 optional ongoing

The number that blows up an estimate is never the schematics. It is a dependency with no v22 release, or a suite that was already red. Both are visible in phase 0 — which is why phase 0 is not optional.

Rollback

Every phase is one merged PR, so rollback is git revert of that PR plus a redeploy. That property is the entire reason for the one-PR-per-step rule; it is worth more than the time it appears to cost.