AngularJS 1.8 → Angular 17
Plan Técnico de Migración

Propuesta de migración incremental para GestorPro SaaS — plataforma B2B de gestión de proyectos para agencias de marketing. Estrategia Hybrid Approach con ngUpgrade. 6 meses. Cero interrupciones en producción.

12.000 Líneas AngularJS
6 Módulos a migrar
6 meses Timeline
Angular 17 Target
3 Desarrolladores
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 recomendado

Incremental — 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 riesgo

Vertical 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
upgradeInjectable
——→
ngUpgrade Bridge
UpgradeModule
@angular/upgrade
downgradeComponent
——→
Angular 17
ClientesComponent
standalone
app.module.ts — Bootstrap híbrido (Fase 2)
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
import { downgradeComponent, downgradeInjectable } from '@angular/upgrade/static';
import { ClientesComponent } from './clientes/clientes.component';
import { ProyectoService } from './proyectos/proyecto.service';

@NgModule({
  imports: [BrowserModule, UpgradeModule, HttpClientModule, RouterModule.forRoot(routes)],
  declarations: [ClientesComponent],
})
export class AppModule {
  constructor(private upgrade: UpgradeModule) {}

  ngDoBootstrap() {
    // Exponer nuevo componente Angular a AngularJS
    angular.module('gestorpro')
      .directive('appClientes', downgradeComponent({ component: ClientesComponent }))
      // Exponer nuevo servicio Angular a AngularJS
      .factory('proyectoService', downgradeInjectable(ProyectoService));

    this.upgrade.bootstrap(document.body, ['gestorpro'], { strictDi: true });
  }
}
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)