01 — Estrategia
Elección de Estrategia de Migración
Big Bang
Reescritura completa en paralelo. Arranque único al finalizar. Equipo dividido, riesgo máximo.
Alto riesgo No recomendadoIncremental — Hybrid App
AngularJS y Angular coexisten vía ngUpgrade. Migración módulo a módulo. Features nuevas en Angular desde el día 1.
Seleccionada Bajo riesgoVertical Slice
Un feature completo migrado, el resto en AngularJS. Bueno para apps medianas con features muy separadas.
Medio riesgo Alternativa
Justificación: GestorPro tiene 12.000 líneas y clientes activos —
el Big Bang crearía una parada de 4+ meses. El Hybrid Approach con
@angular/upgrade
permite desplegar mejoras incrementales en producción cada 2 semanas, mantener el equipo de 2 frontends productivo,
y aprovechar al consultor externo de CULTIVA IA sin bloquear al equipo.
02 — Roadmap
Fases de Migración (24 semanas)
1
Setup
Semanas 1-3
- Angular CLI + TypeScript
- Configurar ngUpgrade
- Webpack 5 / esbuild
- CI/CD GitHub Actions
- Tests base (Jest + Karma)
2
Infraestructura
Semanas 4-8
- Auth module (guards + JWT)
- Servicios HTTP → HttpClient
- Router Angular principal
- NgRx Signals Store
- Shared components lib
3
Migración Features
Semanas 9-20
- Dashboard (semanas 9-11)
- Módulo Clientes (12-14)
- Módulo Proyectos (15-18)
- Facturación + Stripe (19-20)
4
Cleanup
Semanas 21-24
- Eliminar AngularJS 1.x
- Eliminar ngUpgrade
- Optimizar bundle
- Migrar Reportes
- Auditoría final
03 — Módulos
Inventario y Priorización de Módulos
| Prioridad | Módulo | Líneas | Fase | Técnica | Complejidad |
|---|---|---|---|---|---|
1 |
Autenticación Login, sesión, JWT guards |
~600 | Fase 2 | downgradeComponent |
|
2 |
Dashboard 3 widgets + nav lateral |
~1.800 | Fase 3a | upgradeComponent |
|
3 |
Módulo Clientes CRUD + tabla paginada |
~2.400 | Fase 3b | Standalone comp. |
|
4 |
Módulo Proyectos Kanban + Gantt |
~3.200 | Fase 3c | Signals + NgRx |
|
5 |
Facturación PDF + Stripe integration |
~2.000 | Fase 3d | Standalone + RxJS |
|
6 |
Reportes Chart.js + exports |
~2.000 | Fase 4 | Standalone + Signals |
04 — Patrones de Código
Antes / Después — Casos Críticos de GestorPro
CASO 1
ClientesController → ClientesComponent (standalone)
AngularJS 1.8 — controllers/clientes.js
// GestorPro legacy — ClientesController angular .module('gestorpro') .controller('ClientesController', function( $scope, $http, $routeParams, ClienteService, ToastService ) { $scope.clientes = []; $scope.loading = false; $scope.pagina = 1; $scope.cargarClientes = function() { $scope.loading = true; ClienteService.listar($scope.pagina) .then(function(res) { $scope.clientes = res.data; $scope.loading = false; }); }; $scope.eliminarCliente = function(id) { if(confirm('¿Eliminar?')) { ClienteService.eliminar(id).then(() => { ToastService.ok('Eliminado'); $scope.cargarClientes(); }); } }; // Init $scope.cargarClientes(); });
Angular 17 — clientes/clientes.component.ts
// GestorPro modernizado — Angular 17 Standalone @Component({ selector: 'app-clientes', standalone: true, imports: [CommonModule, RouterModule], template: ` <div class="clientes-page"> <app-clientes-tabla [clientes]="clientes()" [cargando]="cargando()" (eliminar)="eliminarCliente($event)"> </app-clientes-tabla> </div> ` }) export class ClientesComponent implements OnInit { // Signals en lugar de $scope private clienteService = inject(ClienteService); private toast = inject(ToastService); clientes = signal<Cliente[]>([]); cargando = signal(false); pagina = signal(1); ngOnInit() { this.cargarClientes(); } cargarClientes() { this.cargando.set(true); this.clienteService.listar(this.pagina()) .pipe(finalize(() => this.cargando.set(false))) .subscribe(data => this.clientes.set(data)); } eliminarCliente(id: number) { this.clienteService.eliminar(id).subscribe(() => { this.toast.ok('Cliente eliminado'); this.cargarClientes(); }); } }
CASO 2
ProyectoService factory → Injectable con HttpClient
AngularJS — services/proyecto.service.js
// GestorPro legacy — ProyectoService angular .module('gestorpro') .factory('ProyectoService', function($http) { var API = '/api/proyectos'; return { listar: function(page) { return $http.get(API, { params: { page: page, limit: 20 } }); }, crear: function(data) { return $http.post(API, data); }, actualizar: function(id, data) { return $http.put(API + '/' + id, data); }, cambiarEstado: function(id, estado) { return $http.patch(API + '/' + id, { estado: estado }); } }; });
Angular 17 — proyectos/proyecto.service.ts
// GestorPro moderno — ProyectoService import { Injectable, inject } from '@angular/core'; import { HttpClient, HttpParams } from '@angular/common/http'; import { Observable } from 'rxjs'; import type { Proyecto, EstadoProyecto } from './proyecto.model'; @Injectable({ providedIn: 'root' }) export class ProyectoService { private http = inject(HttpClient); private readonly API = '/api/proyectos'; listar(page: number): Observable<Proyecto[]> { const params = new HttpParams() .set('page', page) .set('limit', 20); return this.http.get<Proyecto[]>(this.API, { params }); } crear(data: Partial<Proyecto>): Observable<Proyecto> { return this.http.post<Proyecto>(this.API, data); } actualizar(id: number, data: Partial<Proyecto>): Observable<Proyecto> { return this.http.put<Proyecto>(`${this.API}/${id}`, data); } cambiarEstado(id: number, estado: EstadoProyecto): Observable<Proyecto> { return this.http.patch<Proyecto>(`${this.API}/${id}`, { estado }); } }
CASO 3
$routeProvider → RouterModule (lazy-loading por feature)
AngularJS — app.config.js
// GestorPro legacy — routing angular .module('gestorpro') .config(function($routeProvider) { $routeProvider .when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardController' }) .when('/clientes', { templateUrl: 'views/clientes.html', controller: 'ClientesController' }) .when('/clientes/:id', { templateUrl: 'views/cliente-detalle.html', controller: 'ClienteDetalleController' }) .when('/proyectos', { templateUrl: 'views/proyectos.html', controller: 'ProyectosController' }) .otherwise({ redirectTo: '/dashboard' }); });
Angular 17 — app.routes.ts
// GestorPro moderno — lazy routes import { Routes } from '@angular/router'; import { authGuard } from './auth/auth.guard'; export const routes: Routes = [ { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, { path: 'dashboard', loadComponent: () => import( './dashboard/dashboard.component' ).then(m => m.DashboardComponent), canActivate: [authGuard] }, { path: 'clientes', loadChildren: () => import( './clientes/clientes.routes' ).then(m => m.CLIENTES_ROUTES), canActivate: [authGuard] }, { path: 'proyectos', loadChildren: () => import( './proyectos/proyectos.routes' ).then(m => m.PROYECTOS_ROUTES), canActivate: [authGuard] } ];
05 — Interoperabilidad
Puente ngUpgrade — Modo Híbrido
AngularJS 1.8
AuthService
legacy factory
legacy factory
upgradeInjectable
——→
ngUpgrade Bridge
UpgradeModule
@angular/upgrade
@angular/upgrade
downgradeComponent
——→
Angular 17
ClientesComponent
standalone
standalone
06 — Riesgos
Análisis de Riesgos y Mitigaciones
Alto
Bundle size en modo híbrido
Durante la Fase 3 coexisten AngularJS y Angular, lo que puede duplicar el JS enviado al cliente y degradar LCP.
Mitigación: Activar code-splitting agresivo, lazy-load de módulos migrados, medir con Lighthouse cada sprint.
Alto
Pérdida de contexto de sesión
El AuthService de AngularJS y el guard de Angular 17 pueden desincronizarse en el estado de sesión durante el híbrido.
Mitigación: Migrar Auth primero (Fase 2, semana 4). Un único store de sesión (NgRx Signals) como fuente de verdad.
Medio
Tests rotos al cambiar $scope
Tests Jasmine/Karma que usan $scope directamente fallarán al migrar los controladores a componentes.
Mitigación: Migrar a Jest + Testing Library en paralelo. Mínimo 80% coverage antes de cada módulo.
Medio
Kanban/Gantt con librerías legacy
El módulo Proyectos usa Dhtmlx Gantt (jQuery-based). Puede no ser compatible con el renderizador de Angular.
Mitigación: Evaluar migración a @angular/cdk o bryntum-angular. Envolverlo en ViewChild si se mantiene temporalmente.
Bajo
Stripe SDK v2 deprecado
El módulo de Facturación usa Stripe.js v2 incrustado con $http. Al migrar habrá que actualizar a Stripe Elements v3.
Mitigación: Migrar a ngx-stripe + Stripe Elements. Plazo: semanas 19-20 (ya incluido en roadmap).
Bajo
Webpack 4 → esbuild/Vite
La configuración actual de Webpack 4 puede tener loaders personalizados incompatibles con Angular CLI 17.
Mitigación: Auditar webpack.config.js en Fase 1. Angular CLI 17 usa esbuild por defecto; migrar loader a plugin si es necesario.
07 — Checklist Fase 1
Setup Inmediato (Semanas 1-3)
- Instalar Node 20 LTS y Angular CLI 17
- Crear repositorio feature/
angular-migration - Añadir
@angular/upgradeytsconfig.json - Configurar modo híbrido en
main.ts - Activar
strictDi: trueen AngularJS - Migrar build a Angular CLI (esbuild)
- Configurar GitHub Actions (build + test)
- Instalar Jest + Angular Testing Library
- Definir linting ESLint + Prettier + Angular schematic
- Dashboard Lighthouse baseline (antes de migrar)