How to Migrate from AngularJS 1.x to Angular
How do I migrate from AngularJS 1.x to Angular 2+?
TL;DR
- Bottom line: Use Angular's
@angular/upgrade(ngUpgrade) module to run AngularJS and Angular side-by-side in a hybrid app, then migrate components incrementally bottom-up until no AngularJS code remains. [src1, src2] - Key tool/command:
import { UpgradeModule } from '@angular/upgrade/static' - Watch out for: Running both frameworks doubles change detection cycles -- performance degrades significantly in large hybrid apps if you don't minimize the hybrid phase. [src3, src4]
- Works with: AngularJS 1.5+ (component API required) to Angular 19-22. TypeScript 5.4+, Node.js 18+, Angular CLI 17+. Target Angular 20+ for standalone-by-default, stable signals, and zoneless change detection. Angular 19 reaches EOL 2026-05-19 -- prefer Angular 20-22 for new migrations. [src9]
Constraints
- AngularJS must be >= 1.5.x -- the
.component()API introduced in 1.5 is required for ngUpgrade. Apps on 1.2-1.4 must first upgrade to 1.8.x. [src1] - AngularJS reached EOL December 2021 -- no security patches from Google. Running AngularJS in production without HeroDevs NES (commercial LTS) is a security risk. [src6]
- Hybrid mode roughly doubles bundle size and change detection cost. Enterprise migrations should target 4-12 months maximum in hybrid state. [src3, src8]
- Third-party AngularJS libraries (angular-ui-bootstrap, angular-material, angular-translate) have no automatic ngUpgrade path -- budget time to replace each with Angular equivalents. [src3, src6]
- Never manually call
angular.bootstrap()alongsideplatformBrowserDynamic().bootstrapModule()-- this creates two separate apps that cannot share DI. Always useUpgradeModule. [src1, src7] - Angular 19+ defaults to
standalone: truefor all components. New components written during migration should use standalone APIs unless your hybrid setup requires NgModule. [src1]
Quick Reference
| AngularJS Pattern | Angular Equivalent | Example |
|---|---|---|
angular.module('app', []) | @NgModule({ ... }) or standalone components | @Component({ standalone: true, imports: [...] }) |
$scope / $rootScope | Component class properties + @Input/@Output | @Input() name: string; |
.controller('Ctrl', fn) | @Component({ ... }) class Ctrl | @Component({ selector: 'app-ctrl', template: '...' }) |
.directive('myDir', fn) | @Directive({ selector: '[myDir]' }) or @Component | @Directive({ selector: '[highlight]' }) |
.service('svc', fn) / .factory(...) | @Injectable({ providedIn: 'root' }) | @Injectable({ providedIn: 'root' }) class SvcService |
.filter('fmt', fn) | @Pipe({ name: 'fmt' }) | @Pipe({ name: 'fmt' }) class FmtPipe implements PipeTransform |
$http.get(url) | HttpClient.get(url) | this.http.get<Data[]>('/api/data') |
$q.defer() / promises | RxJS Observable (preferred) or native Promise | this.http.get(url).pipe(map(res => res.data)) |
$routeProvider / ui-router | RouterModule.forRoot(routes) | { path: 'home', component: HomeComponent } |
ng-repeat="item in items" | *ngFor="let item of items" (or @for in Angular 17+) | @for (item of items; track item.id) { ... } |
ng-if="condition" | *ngIf="condition" (or @if in Angular 17+) | @if (isLoaded) { <app-data /> } |
ng-model="value" | [(ngModel)]="value" (FormsModule) or reactive forms | <input [(ngModel)]="name"> / formControlName="name" |
$broadcast / $emit | EventEmitter, RxJS Subject, or Angular Signals | @Output() changed = new EventEmitter<string>(); |
$watch('expr', fn) | RxJS streams, OnChanges, or Angular Signals + effect() | effect(() => console.log(this.count())) |
$compile(html)($scope) | Dynamic components via ViewContainerRef | viewRef.createComponent(MyComponent) |
Decision Tree
START
|-- Is your AngularJS app < 5,000 LOC?
| |-- YES -> Consider a full rewrite (faster than hybrid setup overhead)
| +-- NO v
|-- Is the app on AngularJS 1.5+ with .component() API?
| |-- YES -> Proceed with ngUpgrade incremental migration [Step 1]
| +-- NO -> First upgrade to AngularJS 1.8.x and refactor to component architecture [Step 1]
|-- Do you use ui-router extensively?
| |-- YES -> Install @uirouter/angular-hybrid for parallel routing
| +-- NO v
|-- Do you need both frameworks' routes simultaneously?
| |-- YES -> Use setUpLocationSync() from @angular/upgrade/static [Step 5]
| +-- NO v
|-- Is the team experienced with TypeScript?
| |-- YES -> Migrate services first (bottom-up), then components
| +-- NO -> Invest 2-4 weeks in TypeScript training before starting migration
|-- Target Angular version?
| |-- Angular 19+ -> Use standalone components by default, plan for zoneless (Angular 21+)
| +-- Angular 17-18 -> Use NgModule or standalone (both supported)
+-- DEFAULT -> Use ngUpgrade hybrid approach, migrate leaf components first, work upward
Step-by-Step Guide
1. Prepare the AngularJS codebase
Refactor the AngularJS app to follow the component-based architecture introduced in AngularJS 1.5. Convert controllers + templates into .component() definitions. Follow the "Rule of One" -- one component per file. If on AngularJS 1.2-1.4, first upgrade to 1.8.x. [src1, src2]
// BEFORE: controller-based (AngularJS 1.x style)
angular.module('app').controller('HeroListCtrl', function($scope, HeroService) {
$scope.heroes = [];
HeroService.getAll().then(function(heroes) {
$scope.heroes = heroes;
});
});
// AFTER: component-based (AngularJS 1.5+ style -- required for ngUpgrade)
angular.module('app').component('heroList', {
template: '<div ng-repeat="hero in $ctrl.heroes">{{hero.name}}</div>',
controller: function(HeroService) {
this.$onInit = function() {
HeroService.getAll().then(heroes => this.heroes = heroes);
};
}
});
Verify: grep -r "$scope" src/ --include="*.js" | wc -l → expected: 0
2. Set up the Angular project alongside AngularJS
Create a new Angular project using Angular CLI. Install the upgrade package. Configure the build system to compile both AngularJS and Angular code. [src1, src7]
# Create Angular project (if not already existing)
ng new my-app --skip-install
cd my-app
# Install ngUpgrade
npm install @angular/upgrade
# If using ui-router, install the hybrid adapter
npm install @uirouter/angular-hybrid
// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
@NgModule({
imports: [
BrowserModule,
UpgradeModule
]
})
export class AppModule {
constructor(private upgrade: UpgradeModule) {}
ngDoBootstrap() {
// Bootstrap AngularJS inside Angular
this.upgrade.bootstrap(document.body, ['myAngularJSApp'], { strictDi: true });
}
}
Verify: ng build compiles without errors. The app loads and AngularJS components still render.
3. Downgrade an Angular component for use in AngularJS
Create your first Angular component. Use downgradeComponent to register it as an AngularJS directive so it can be used in AngularJS templates. [src1, src2]
// src/app/hero-detail/hero-detail.component.ts
import { Component, Input } from '@angular/core';
@Component({
selector: 'app-hero-detail',
template: `<h3>{{ hero.name }}</h3><p>ID: {{ hero.id }}</p>`
})
export class HeroDetailComponent {
@Input() hero: { id: number; name: string };
}
// src/app/hero-detail/hero-detail.downgrade.ts
import { downgradeComponent } from '@angular/upgrade/static';
import { HeroDetailComponent } from './hero-detail.component';
// Register as AngularJS directive
angular.module('myAngularJSApp')
.directive('appHeroDetail', downgradeComponent({
component: HeroDetailComponent
}));
<!-- Now usable in any AngularJS template -->
<app-hero-detail [hero]="$ctrl.selectedHero"></app-hero-detail>
Verify: The Angular component renders inside AngularJS templates. Inspect the DOM -- the element should have an ng-version attribute.
4. Migrate services from AngularJS to Angular
Rewrite AngularJS services as Angular @Injectable classes. Downgrade them for backward compatibility with remaining AngularJS components. [src2, src5]
// src/app/services/hero.service.ts -- new Angular service
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
interface Hero {
id: number;
name: string;
}
@Injectable({ providedIn: 'root' })
export class HeroService {
constructor(private http: HttpClient) {}
getAll(): Observable<Hero[]> {
return this.http.get<Hero[]>('/api/heroes');
}
getById(id: number): Observable<Hero> {
return this.http.get<Hero>(`/api/heroes/${id}`);
}
}
// Downgrade for AngularJS consumption
import { downgradeInjectable } from '@angular/upgrade/static';
import { HeroService } from './services/hero.service';
angular.module('myAngularJSApp')
.factory('heroService', downgradeInjectable(HeroService));
Verify: AngularJS components using the downgraded service still function. Check network tab -- HTTP calls go through Angular's HttpClient.
5. Set up routing coexistence
Configure Angular Router alongside AngularJS routing. Use setUpLocationSync() to keep both routers synchronized. [src1, src2]
// src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
// New Angular routes
{ path: 'dashboard', component: DashboardComponent },
{ path: 'heroes/:id', component: HeroDetailComponent },
// Catch-all: let AngularJS handle unknown routes
{ path: '**', component: AngularJSRouteComponent }
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule]
})
export class AppRoutingModule {}
// In AppModule -- sync location between frameworks
import { setUpLocationSync } from '@angular/upgrade/static';
import { Router } from '@angular/router';
export class AppModule {
constructor(private upgrade: UpgradeModule, private router: Router) {}
ngDoBootstrap() {
this.upgrade.bootstrap(document.body, ['myAngularJSApp'], { strictDi: true });
setUpLocationSync(this.upgrade);
}
}
Verify: Navigate between AngularJS and Angular routes. URL changes reflect in both frameworks. Browser back/forward buttons work correctly.
6. Migrate components bottom-up
Work from leaf components (no child dependencies) upward. For each component: rewrite in Angular, replace the AngularJS directive with a downgraded version, remove the old AngularJS code. Track progress by counting remaining AngularJS controllers. [src2, src3]
# Track migration progress
echo "AngularJS controllers remaining:"
grep -r "\.controller(" src/ --include="*.js" | wc -l
echo "AngularJS components remaining:"
grep -r "\.component(" src/ --include="*.js" | wc -l
echo "Angular components created:"
find src/app -name "*.component.ts" | wc -l
Verify: After each component migration, run the full test suite: ng test && npm run e2e
7. Remove AngularJS and finalize
Once all components and services are migrated, remove ngUpgrade, AngularJS dependencies, and the hybrid bootstrap. Switch to pure Angular bootstrap. For Angular 19+, migrate to standalone bootstrap using bootstrapApplication. [src1, src3]
# Remove AngularJS dependencies
npm uninstall angular @angular/upgrade
# If using ui-router hybrid
npm uninstall @uirouter/angular-hybrid @uirouter/angularjs
# Remove from angular.json -- delete any AngularJS script includes
# Update main.ts to standard Angular bootstrap
// src/main.ts -- clean Angular bootstrap (standalone, Angular 19+)
import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, appConfig)
.catch(err => console.error(err));
Verify: ng build --configuration production succeeds. No references to angular.module, $scope, or @angular/upgrade in codebase.
Code Examples
TypeScript/Angular: Upgrading an AngularJS Service for Use in Angular
// Input: An existing AngularJS service registered as angular.module('app').service('userService', ...)
// Output: An Angular provider that wraps the AngularJS service for use in Angular components
// src/app/ajs-upgraded-providers.ts
import { InjectionToken } from '@angular/core';
// Define a token for the AngularJS service
export const USER_SERVICE = new InjectionToken<any>('UserService');
// Factory that extracts the AngularJS service via $injector
export function userServiceFactory(i: any) {
return i.get('userService');
}
// Provider configuration for Angular module
export const userServiceProvider = {
provide: USER_SERVICE,
useFactory: userServiceFactory,
deps: ['$injector']
};
// src/app/profile/profile.component.ts -- using the upgraded service
import { Component, Inject, OnInit } from '@angular/core';
import { USER_SERVICE } from '../ajs-upgraded-providers';
@Component({
selector: 'app-profile',
template: `
<div *ngIf="user">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
</div>
`
})
export class ProfileComponent implements OnInit {
user: any;
constructor(@Inject(USER_SERVICE) private userService: any) {}
ngOnInit() {
this.userService.getCurrentUser().then((user: any) => {
this.user = user;
});
}
}
TypeScript/Angular: Downgrading an Angular Component with Inputs/Outputs
// Input: An Angular component with @Input and @Output bindings
// Output: A downgraded AngularJS directive that preserves all bindings
// src/app/search-bar/search-bar.component.ts
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'app-search-bar',
template: `
<input [value]="query" (input)="onInput($event)" placeholder="Search...">
<button (click)="onSearch()">Search</button>
`
})
export class SearchBarComponent {
@Input() query: string = '';
@Output() queryChange = new EventEmitter<string>();
@Output() search = new EventEmitter<string>();
onInput(event: Event) {
this.query = (event.target as HTMLInputElement).value;
this.queryChange.emit(this.query);
}
onSearch() {
this.search.emit(this.query);
}
}
// src/app/search-bar/search-bar.downgrade.ts
import { downgradeComponent } from '@angular/upgrade/static';
import { SearchBarComponent } from './search-bar.component';
angular.module('myAngularJSApp')
.directive('appSearchBar', downgradeComponent({
component: SearchBarComponent
}));
<!-- Usage in AngularJS template -- note: kebab-case attributes for Angular bindings -->
<app-search-bar
[query]="$ctrl.searchQuery"
(query-change)="$ctrl.searchQuery = $event"
(search)="$ctrl.doSearch($event)">
</app-search-bar>
JavaScript: Automated Migration Progress Tracker Script
Full script: javascript-automated-migration-progress-tracker-sc.js (56 lines)
// Input: Project source directory path
// Output: Migration progress report (AngularJS vs Angular component counts)
// scripts/migration-progress.js
const { execSync } = require('child_process');
function countPattern(dir, pattern, ext) {
try {
const result = execSync(
`grep -r "${pattern}" "${dir}" --include="*.${ext}" -l 2>/dev/null | wc -l`,
{ encoding: 'utf-8' }
);
return parseInt(result.trim(), 10);
} catch { return 0; }
}
const srcDir = process.argv[2] || './src';
const report = {
angularjs: {
controllers: countPattern(srcDir, '\\.controller(', 'js'),
directives: countPattern(srcDir, '\\.directive(', 'js'),
services: countPattern(srcDir, '\\.service(\\|.factory(', 'js'),
filters: countPattern(srcDir, '\\.filter(', 'js'),
},
angular: {
components: countPattern(srcDir, '@Component', 'ts'),
directives: countPattern(srcDir, '@Directive', 'ts'),
services: countPattern(srcDir, '@Injectable', 'ts'),
pipes: countPattern(srcDir, '@Pipe', 'ts'),
}
};
const totalAJS = Object.values(report.angularjs).reduce((a, b) => a + b, 0);
const totalNG = Object.values(report.angular).reduce((a, b) => a + b, 0);
const pct = totalNG + totalAJS > 0
? Math.round((totalNG / (totalNG + totalAJS)) * 100) : 0;
console.log(`Progress: ${pct}% migrated (${totalNG}/${totalNG + totalAJS})`);
Anti-Patterns
Wrong: Bootstrapping AngularJS manually alongside Angular
// BAD -- manually calling angular.bootstrap creates two separate apps
// that cannot share services or components
platformBrowserDynamic().bootstrapModule(AppModule);
angular.bootstrap(document.body, ['myApp']); // Separate bootstrap!
Correct: Use UpgradeModule to bootstrap AngularJS within Angular
// GOOD -- UpgradeModule creates a single hybrid app with shared DI
@NgModule({
imports: [BrowserModule, UpgradeModule]
})
export class AppModule {
constructor(private upgrade: UpgradeModule) {}
ngDoBootstrap() {
this.upgrade.bootstrap(document.body, ['myApp'], { strictDi: true });
}
}
platformBrowserDynamic().bootstrapModule(AppModule);
Wrong: Using $scope in migrated components
// BAD -- $scope creates tight coupling to AngularJS digest cycle
// and cannot be migrated to Angular
angular.module('app').component('userCard', {
controller: function($scope, UserService) {
$scope.user = null;
$scope.$watch('userId', function(id) {
UserService.get(id).then(function(u) { $scope.user = u; });
});
}
});
Correct: Use component bindings and lifecycle hooks
// GOOD -- component bindings map directly to Angular @Input/@Output
angular.module('app').component('userCard', {
bindings: { userId: '<' },
controller: function(UserService) {
this.$onChanges = function(changes) {
if (changes.userId && changes.userId.currentValue) {
UserService.get(this.userId).then(u => this.user = u);
}
};
}
});
Wrong: Migrating everything at once (big bang)
# BAD -- rewriting the entire app in one branch
git checkout -b full-angular-rewrite
# ... 6 months of parallel development ...
# Result: massive merge conflicts, feature drift, missed deadlines
Correct: Incremental migration with continuous deployment
# GOOD -- migrate one component at a time, merge to main frequently
git checkout -b migrate/hero-detail-component
# Migrate one component, downgrade for AngularJS compatibility
# Run tests, deploy, merge
git checkout -b migrate/hero-service
# Next component...
Wrong: Duplicating state between frameworks
// BAD -- maintaining separate state in both Angular and AngularJS
// leads to synchronization bugs
// In Angular:
this.cartItems = [...]; // Angular state
// In AngularJS:
$scope.cartItems = [...]; // AngularJS state -- gets out of sync!
Correct: Single source of truth via downgraded/upgraded services
// GOOD -- one service owns the state, shared via downgrade
@Injectable({ providedIn: 'root' })
export class CartService {
private items = new BehaviorSubject<CartItem[]>([]);
items$ = this.items.asObservable();
addItem(item: CartItem) {
this.items.next([...this.items.value, item]);
}
}
// Downgrade for AngularJS
angular.module('app').factory('cartService', downgradeInjectable(CartService));
Wrong: Ignoring Zone.js performance in hybrid mode
// BAD -- every AngularJS digest triggers Angular change detection
// and vice versa, creating cascading cycles
// No optimization = 2x-5x slower than either framework alone
Correct: Use OnPush change detection in migrated components
// GOOD -- OnPush prevents unnecessary change detection in hybrid mode
@Component({
selector: 'app-hero-list',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<div *ngFor="let hero of heroes$ | async">{{ hero.name }}</div>`
})
export class HeroListComponent {
heroes$ = this.heroService.getAll();
constructor(private heroService: HeroService) {}
}
Wrong: Using deprecated .toPromise() for Observable-to-Promise conversion
// BAD -- .toPromise() is deprecated since RxJS 7
const heroes = await this.heroService.getAll().toPromise();
Correct: Use firstValueFrom() or lastValueFrom()
// GOOD -- firstValueFrom() is the modern replacement (RxJS 7+)
import { firstValueFrom } from 'rxjs';
const heroes = await firstValueFrom(this.heroService.getAll());
Common Pitfalls
- Dual change detection performance hit: Running both AngularJS digest cycle and Angular Zone.js change detection simultaneously can degrade performance 2-5x. Fix: Use
ChangeDetectionStrategy.OnPushon all migrated Angular components and minimize cross-framework component boundaries. [src2, src4] - $scope.$apply errors in hybrid mode: Calling
$scope.$apply()when Angular Zone.js already triggers a digest causes "$apply already in progress" errors. Fix: Remove explicit$scope.$apply()calls; Zone.js handles triggering the AngularJS digest automatically in hybrid mode. [src1, src4] - Routing conflicts between frameworks: Both AngularJS and Angular routers try to handle URL changes, causing infinite loops or blank pages. Fix: Call
setUpLocationSync(this.upgrade)after bootstrap, and ensure routes are partitioned -- each URL is handled by exactly one framework. [src1, src2] - Missing entryComponents declaration: Downgraded Angular components must be listed in both
declarationsandentryComponentsarrays in NgModule (for Angular < 13; Ivy makes this optional). Fix: Add the component toentryComponents: [MyComponent]in the NgModule, or upgrade to Angular 13+ where Ivy handles this automatically. [src1, src7] - Observable vs Promise confusion: AngularJS uses
$qpromises everywhere; Angular uses RxJS Observables. Mixing them without conversion causes silent failures. Fix: UsefirstValueFrom()(RxJS 7+) when passing Observable results to AngularJS code..toPromise()is deprecated. [src5] - Dependency injection token mismatch: AngularJS uses string tokens (
'myService'); Angular uses class references orInjectionToken. Downgraded/upgraded services must use the correct token type for each framework. Fix: UsedowngradeInjectable(ServiceClass)with a matching.factory('serviceName', ...)registration. [src5, src7] - Third-party AngularJS library incompatibility: Libraries like
angular-ui-bootstrap,angular-translate, orangular-materialhave no ngUpgrade path. Fix: Replace with Angular equivalents (Angular Material, ngx-translate, ng-bootstrap) before or during migration, or wrap with a thin AngularJS component boundary. [src3, src6] - Forgetting to plan for zoneless and OnPush defaults: Angular 21+ defaults to zoneless change detection; Angular 22 (May 2026) makes OnPush the default change-detection strategy for new components. Migration code written against pre-22 defaults must be re-validated. Fix: Prefer
signal(),computed(), andeffect()in new components; assume OnPush semantics from day one. [src4, src9]
Decision Logic
If app is AngularJS 1.2-1.4
First upgrade to AngularJS 1.8.x and refactor controllers into .component() definitions. The .component() API (added in 1.5) is required for ngUpgrade -- there is no path to skip this step. [src1, src2]
If app is < 5,000 LOC and tolerates a feature freeze
Do a full rewrite in Angular 20-22 (standalone components, signals). Hybrid setup overhead exceeds rewrite cost at this size. [src3, src8]
If app is 5,000-50,000 LOC with active feature development
Use ngUpgrade incremental migration with UpgradeModule. Migrate leaf components first (bottom-up), then services, then routing. Budget 4-9 months in hybrid mode. [src1, src2, src8]
If app is > 50,000 LOC (enterprise)
Use ngUpgrade with route-based partitioning. Migrate one feature module at a time, deploy continuously. Budget 9-18 months. Hire or train Angular/TypeScript expertise before starting. [src2, src3, src8]
If routing uses ui-router
Install @uirouter/angular-hybrid and use it as the bridge instead of setUpLocationSync(). ui-router state-based routing maps cleanly to Angular Router via the hybrid adapter. [src1, src2]
If target is Angular 22 (May 2026 release)
Adopt OnPush as the default change-detection strategy in all new components (Angular 22 default) and prefer Signal Forms over reactive forms for new screens. Skip NgModule -- go standalone-only. [src1, src7, src9]
If target was Angular 19
Plan an immediate follow-on upgrade to Angular 20 or 21. Angular 19 reaches EOL on 2026-05-19 -- running a freshly migrated app on an EOL framework defeats the purpose of the migration. [src9]
If AngularJS app cannot be taken off production during migration
Stay on commercial AngularJS LTS (HeroDevs NES) for security patches while migrating, then cut over once hybrid mode is gone. Do not run unsupported AngularJS in production without commercial support. [src6, src9]
Diagnostic Commands
# Check AngularJS version
node -e "console.log(require('./node_modules/angular/package.json').version)"
# Check Angular CLI and framework version
ng version
# Count remaining AngularJS artifacts
grep -r "angular\.module\|\.controller(\|\.directive(\|\.factory(\|\.service(" src/ --include="*.js" | wc -l
# Find all $scope references (should be 0 before migration)
grep -rn "\$scope" src/ --include="*.js" --include="*.ts"
# Check for circular dependencies
npx madge --circular src/app
# Verify no AngularJS code remains after migration
grep -r "angular\." src/ --include="*.ts" --include="*.js" | grep -v node_modules | grep -v ".spec."
# Run Angular production build to catch any issues
ng build --configuration production
# Analyze bundle size (check for leftover AngularJS)
npx webpack-bundle-analyzer dist/my-app/stats.json
# Check for deprecated APIs before removing Zone.js (Angular 19+)
grep -rn "NgZone\|onMicrotaskEmpty\|onUnstable\|isStable\|onStable" src/ --include="*.ts"
Version History & Compatibility
| Version | Status | ngUpgrade Support | Migration Notes |
|---|---|---|---|
| AngularJS 1.8.x | EOL (Dec 2021) | Required source | Last AngularJS release. Must be on 1.5+ for component API. [src6] |
| Angular 2-5 | EOL | Full | Original ngUpgrade. Requires SystemJS or custom webpack config. |
| Angular 6-8 | EOL | Full | Angular CLI builder. @angular/upgrade/static is the stable API. |
| Angular 9-12 | EOL | Full (Ivy) | Ivy renderer. entryComponents still required until Angular 13. |
| Angular 13-15 | EOL | Full (Ivy) | entryComponents removed. ViewEngine dropped in 13. |
| Angular 16-17 | EOL | Full | Standalone components, signals, new control flow syntax (@if, @for). [src1] |
| Angular 18 | EOL | Full | Experimental zoneless change detection. Signal-based components. |
| Angular 19 | LTS (EOL 2026-05-19) | Full | standalone: true by default. Stable signals API. Upgrade to 20+ before EOL. [src7, src9] |
| Angular 20 | LTS (until Nov 2026) | Full | Zoneless stable. Signal-based components mature. |
| Angular 21 | Active support | Full | Zoneless by default. Angular MCP Server + AI-assisted dev tooling. [src9] |
| Angular 22 | Active (released May 2026) | Full | Selectorless components, stable Signal Forms, OnPush default, TypeScript 5.9. [src9] |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Existing AngularJS app > 5,000 LOC with active development | App is < 5,000 LOC or in maintenance-only mode | Full rewrite in Angular, React, or Vue |
| Team has Angular/TypeScript skills or budget to train | Team has no TypeScript experience and no training budget | Consider React (gentler JS-first learning curve) |
| Business requires continuous feature delivery during migration | App can go into feature freeze for 3-6 months | Big-bang rewrite during freeze period |
| Shared services and state make incremental approach necessary | Frontend and backend are being rewritten simultaneously | Greenfield Angular project from scratch |
| AngularJS security patches are needed (EOL means no patches) | Using HeroDevs NES (Never-Ending Support) for AngularJS | Stay on AngularJS with commercial LTS support |
| You want to stay in the Angular ecosystem with familiar patterns | Team prefers a completely different paradigm | Migrate to React, Vue, or Svelte instead |
Important Caveats
- AngularJS reached End of Life in December 2021. No security patches, bug fixes, or browser compatibility updates are provided by Google. Running AngularJS in production is a security risk. HeroDevs offers commercial "Never-Ending Support" as an alternative. [src6]
- The hybrid phase (running both frameworks) should be as short as possible. Every week in hybrid mode incurs double the bundle size, double the change detection cost, and increased complexity. Enterprise migrations typically take 4-12 months. [src3, src8]
- Angular's Ivy compiler (v9+) changed how downgraded components are compiled. If migrating from a pre-Ivy hybrid setup, re-test all downgraded components after enabling Ivy. [src1]
- Zone.js is deprecated in Angular 21+ (zoneless by default) and Angular 22 (May 2026) makes OnPush the default change-detection strategy. New Angular components written during migration should use signals and OnPush change detection for future compatibility. Plan ahead -- migrating to zoneless after removing AngularJS avoids doing two migrations. [src4, src9]
- Third-party AngularJS libraries (angular-ui-bootstrap, angular-material, etc.) must be replaced with Angular equivalents -- there is no automatic migration path for these. Budget extra time for replacing UI component libraries. [src3]
- TypeScript strict mode (
strict: truein tsconfig.json) is strongly recommended but may require significant type annotation effort when migrating untyped AngularJS JavaScript code. [src2] - Angular 19 defaults
standalone: truefor components. If your hybrid setup relies on NgModule, you may need to explicitly setstandalone: falseon some components during the migration, then switch to standalone after removing AngularJS. [src1]