AngularJS still in your app? The readers your compiler cannot see
Migrating AngularJS to Angular with an AI agent: in a hybrid app a green build proves less than you think. How to find the hidden readers first.

Part 4 ended on a safe delete: the route flips, the legacy lib goes, and if you delete too early the compiler stops you with one error. Part 5 pinned the old screen with tests that open its URL. Both nets have a hole.
If your app still runs AngularJS next to Angular, the compiler sees only half of it.
I have done this migration more than once, as lead, as architect and as engineer. This part is what that code taught me, rebuilt in the demo repo so you can run it.
Two frameworks, one screen
The demo repo has a hybrid corner. The beneficiaries screen is 1.8, mounted inside the 22 app with @angular/upgrade. Turn on the "Framework map" button in the live demo and you see it: an orange box of AngularJS inside the teal Angular shell, and inside the orange box, small teal boxes. Those are Angular components living in an AngularJS template.
That is how a real hybrid grows. New Angular components get used by old screens before the old screens are gone:
// libs/angularjs/beneficiaries/src/lib/beneficiaries.module.ts
.directive('atlasAmount', downgradeComponent({ component: Amount }))
.factory('accountsStore', downgradeInjectable(AccountsStore))And somewhere in an AngularJS template string: <atlas-amount [value]="b.sentMad">.
The green build that lies
Ask an agent to clean up and it greps for Amount. It finds the import above, maybe a few Angular templates, and concludes it knows every reader. It does not. The AngularJS template uses a kebab-case tag inside a string, and the service is injected by a name in quotes. No import points at either.
I tried it in the demo repo. Rename the registration from 'atlasAmount' to 'atlasMoney', the kind of tidy-up an agent does without asking. typecheck passes. lint:arch passes. The production build passes. On the screen, every amount in the table is empty. AngularJS treats <atlas-amount> as an unknown element and renders nothing, without an error.
In pure Angular the same mistake is a red build. In a hybrid it is a quiet hole in a banking screen.
Step zero: list the readers before the plan
So before any migration plan, one task the agent is good at: list every bridge and everyone who walks over it.
Read AGENTS.md. List every downgradeComponent and downgradeInjectable registration in libs and apps, with its AngularJS name. For each component name, find its kebab-case tag in every AngularJS template string. For each injectable name, find the quoted string in every AngularJS factory, service and controller. Reply as a table: Angular class, AngularJS name, every file that uses it. Do not edit any file.
That table goes into the pull request before any code. Every row is a reader the compiler cannot see, and every rename or input change on it needs a test that renders the AngularJS screen. In the demo repo one hybrid spec catches the rename above: expected [ '', '' ] to deeply equal [ '-3 000,00 MAD', '-250,00 MAD' ].
Split before you port
The second thing old AngularJS code teaches you: the screens are big. The beneficiaries service in the demo mixes four jobs in one file: reading the store, the rules (is this RIB valid, is this beneficiary new), formatting, and DOM work like focusing a field. The controller holds five concerns. That is ten years of features landing where there was room, and it is the normal case.
Port that as it is and you get the same knot in Angular syntax. So the task file asks for the split first: rules become pure functions with their own spec, formatting comes from the domain lib that already has it (the old service had its own copy of the RIB formatter, and its header total drifted from formatMad), DOM work moves into the template, and the store is read directly. Then build beside, flip the route, delete last, exactly as in part 4.
One line for Angular 22
If your app is , as new Angular apps are by default, UpgradeModule refuses to start: NG0908: In this configuration Angular requires Zone.js. You do not need zone.js back. Provide zoneless change detection in the module that imports UpgradeModule, and AngularJS gets a $digest after every Angular tick:
@NgModule({ imports: [UpgradeModule], providers: [provideZonelessChangeDetection()] })
export class BeneficiariesUpgradeModule {}On Monday
- Hybrid app? Make the agent list every
downgradeComponentanddowngradeInjectableand their AngularJS users before it plans anything. - Keep at least one test that renders each AngularJS screen. It is the only check that reads the template strings.
- Split the oversized controller and service before porting them.
Exercise: module 2b in the demo repo, with the hybrid corner, the framework map and task 006. Start: git switch -c m2b/you main. Reference: origin/track1/m2b-angularjs-solution.
Every step in this track so far ends with tests the agent wrote. The last part is how to read them, and what the whole migration added up to.
Track 1, part 6 of the free series behind the Fast and safe: AI in your Angular codebase workshop.
Fast and safe: AI in your Angular codebase
Let agents write Angular under rules, modernise legacy code without breaking production, and keep it secure, in three days, on an enterprise-shaped codebase.
See the agenda
