UNPKG

ng-upgrade-orchestrator

Version:

Enterprise-grade Angular Multi-Version Upgrade Orchestrator with automatic npm installation, comprehensive dependency management, and seamless integration of all 9 official Angular migrations. Safely migrate Angular applications across multiple major vers

1,512 lines (1,346 loc) 71.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.Angular18Handler = void 0; const fs = __importStar(require("fs-extra")); const path = __importStar(require("path")); const BaseVersionHandler_1 = require("./BaseVersionHandler"); const SSRDetector_1 = require("../utils/SSRDetector"); const DependencyCompatibilityMatrix_1 = require("../utils/DependencyCompatibilityMatrix"); /** * Angular 18 Handler - Material 3 and built-in control flow stabilization * * Key Features in Angular 18: * - Material Design 3 (M3) support in Angular Material * - Built-in control flow syntax stabilization (@if, @for, @switch) * - New lifecycle hooks (afterRender, afterNextRender) * - Event replay for SSR hydration * - Hybrid rendering capabilities * - Angular DevKit improvements * - Improved i18n extraction and tooling * - Enhanced change detection optimizations */ class Angular18Handler extends BaseVersionHandler_1.BaseVersionHandler { version = '18'; getRequiredNodeVersion() { return '>=18.19.1'; } getRequiredTypeScriptVersion() { return '>=5.4.0 <5.6.0'; } /** * Get Angular 18 dependencies with correct versions */ getDependencyUpdates() { return [ // Core Angular packages { name: '@angular/animations', version: '^18.0.0', type: 'dependencies' }, { name: '@angular/common', version: '^18.0.0', type: 'dependencies' }, { name: '@angular/compiler', version: '^18.0.0', type: 'dependencies' }, { name: '@angular/core', version: '^18.0.0', type: 'dependencies' }, { name: '@angular/forms', version: '^18.0.0', type: 'dependencies' }, { name: '@angular/platform-browser', version: '^18.0.0', type: 'dependencies' }, { name: '@angular/platform-browser-dynamic', version: '^18.0.0', type: 'dependencies' }, { name: '@angular/router', version: '^18.0.0', type: 'dependencies' }, { name: '@angular/ssr', version: '^18.0.0', type: 'dependencies' }, // Angular CLI and dev dependencies { name: '@angular/cli', version: '^18.0.0', type: 'devDependencies' }, { name: '@angular/compiler-cli', version: '^18.0.0', type: 'devDependencies' }, { name: '@angular-devkit/build-angular', version: '^18.0.0', type: 'devDependencies' }, // TypeScript and supporting packages { name: 'typescript', version: '~5.4.0', type: 'devDependencies' }, { name: 'zone.js', version: '~0.14.0', type: 'dependencies' }, { name: 'rxjs', version: '~7.8.0', type: 'dependencies' }, // Angular Material with Material 3 support { name: '@angular/material', version: '^18.0.0', type: 'dependencies' }, { name: '@angular/cdk', version: '^18.0.0', type: 'dependencies' }, // Third-party Angular ecosystem packages ...DependencyCompatibilityMatrix_1.DependencyCompatibilityMatrix.getCompatibleDependencies('18').map(dep => ({ name: dep.name, version: dep.version, type: dep.type })) ]; } async applyVersionSpecificChanges(projectPath, options) { this.progressReporter?.updateMessage('Applying Angular 18 transformations...'); // Check if this is an SSR application const isSSRApp = await SSRDetector_1.SSRDetector.isSSRApplication(projectPath); this.progressReporter?.info(`Application type: ${isSSRApp ? 'SSR (Server-Side Rendering)' : 'CSR (Client-Side Rendering)'}`); // 1. Implement Material Design 3 support await this.implementMaterial3Support(projectPath); // 2. Stabilize built-in control flow syntax await this.stabilizeBuiltInControlFlow(projectPath); // 3. Implement new lifecycle hooks await this.implementNewLifecycleHooks(projectPath); // 4. Setup event replay for SSR hydration await this.setupEventReplaySSR(projectPath); // 5. Configure hybrid rendering capabilities await this.configureHybridRendering(projectPath); // 6. Enhanced i18n extraction and tooling await this.enhanceI18nTooling(projectPath); // 7. Optimize change detection improvements await this.optimizeChangeDetection(projectPath); // 8. Update build configurations for Angular 18 await this.updateBuildConfigurations(projectPath); // 9. Complete public folder migration (Angular 18+ default structure) await this.completePublicFolderMigration(projectPath); // 10. Migrate from webpack-dev-server to esbuild dev server (Angular 18+) await this.migrateToEsbuildDevServer(projectPath); // 11. Validate third-party compatibility await this.validateThirdPartyCompatibility(projectPath); this.progressReporter?.success('✓ Angular 18 transformations completed'); } getBreakingChanges() { return [ // Material Design 3 support this.createBreakingChange('ng18-material3-support', 'dependency', 'medium', 'Material Design 3 support', 'Angular Material now supports Material Design 3 (M3) theming and components', 'Update Material themes and component usage for M3 compatibility'), // Built-in control flow stabilization this.createBreakingChange('ng18-control-flow-stable', 'template', 'low', 'Built-in control flow syntax stable', '@if, @for, @switch syntax is now stable and recommended over structural directives', 'Migration is optional - both syntaxes are supported'), // Public folder structure (Angular 18+ default) this.createBreakingChange('ng18-public-folder-structure', 'config', 'low', 'Public folder as default asset structure', 'Angular 18+ uses public folder as default for static assets. Existing src/assets continues to work with proper configuration.', 'Assets are copied to public folder while maintaining src/assets for backward compatibility. Both paths work during transition.'), // TypeScript version requirement this.createBreakingChange('ng18-typescript-version', 'dependency', 'medium', 'TypeScript 5.4+ required', 'Angular 18 requires TypeScript 5.4.0 or higher', 'Update TypeScript to version 5.4.0 or higher'), // Node.js version requirement this.createBreakingChange('ng18-nodejs-version', 'dependency', 'medium', 'Node.js 18.19.1+ required', 'Angular 18 requires Node.js 18.19.1 or higher', 'Update Node.js to version 18.19.1 or higher'), // New lifecycle hooks this.createBreakingChange('ng18-lifecycle-hooks', 'api', 'low', 'New lifecycle hooks available', 'afterRender and afterNextRender lifecycle hooks for DOM manipulation', 'New feature - existing lifecycle hooks continue to work'), // Event replay for SSR this.createBreakingChange('ng18-event-replay', 'api', 'low', 'Event replay for SSR hydration', 'Automatic event replay during hydration for better user experience', 'New feature - automatically captures and replays user events'), // Hybrid rendering this.createBreakingChange('ng18-hybrid-rendering', 'api', 'low', 'Hybrid rendering capabilities', 'Support for combining SSR and client-side rendering strategies', 'New feature - existing rendering approaches continue to work') ]; } // Private implementation methods /** * Implement Material Design 3 support */ async implementMaterial3Support(projectPath) { try { const exampleDir = path.join(projectPath, 'src/app/examples'); await fs.ensureDir(exampleDir); // Create Material 3 theming and component examples const material3Example = `import { Component } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatIconModule } from '@angular/material/icon'; import { MatChipsModule } from '@angular/material/chips'; import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatRadioModule } from '@angular/material/radio'; @Component({ selector: 'app-material3-example', standalone: true, imports: [ MatButtonModule, MatCardModule, MatFormFieldModule, MatInputModule, MatIconModule, MatChipsModule, MatSlideToggleModule, MatCheckboxModule, MatRadioModule ], template: \` <div class="material3-showcase"> <h2>Material Design 3 Showcase (Angular 18+)</h2> <!-- M3 Button Variations --> <mat-card class="demo-card"> <mat-card-header> <mat-card-title>M3 Button Variations</mat-card-title> </mat-card-header> <mat-card-content> <div class="button-row"> <button mat-raised-button color="primary">Filled Button</button> <button mat-button color="primary">Text Button</button> <button mat-stroked-button color="primary">Outlined Button</button> <button mat-fab color="accent"> <mat-icon>add</mat-icon> </button> <button mat-mini-fab color="warn"> <mat-icon>edit</mat-icon> </button> </div> </mat-card-content> </mat-card> <!-- M3 Form Controls --> <mat-card class="demo-card"> <mat-card-header> <mat-card-title>M3 Form Controls</mat-card-title> </mat-card-header> <mat-card-content> <div class="form-controls"> <mat-form-field appearance="outline"> <mat-label>Outlined Input</mat-label> <input matInput placeholder="Enter text"> <mat-icon matSuffix>search</mat-icon> </mat-form-field> <mat-form-field appearance="fill"> <mat-label>Filled Input</mat-label> <input matInput placeholder="Enter text"> </mat-form-field> <div class="controls-row"> <mat-checkbox color="primary">M3 Checkbox</mat-checkbox> <mat-slide-toggle color="accent">M3 Toggle</mat-slide-toggle> </div> <mat-radio-group> <mat-radio-button value="option1">M3 Radio Option 1</mat-radio-button> <mat-radio-button value="option2">M3 Radio Option 2</mat-radio-button> </mat-radio-group> </div> </mat-card-content> </mat-card> <!-- M3 Chips --> <mat-card class="demo-card"> <mat-card-header> <mat-card-title>M3 Chips</mat-card-title> </mat-card-header> <mat-card-content> <mat-chip-set> <mat-chip>Angular</mat-chip> <mat-chip>Material</mat-chip> <mat-chip>Design 3</mat-chip> <mat-chip removable (removed)="removeChip('removable')">Removable</mat-chip> </mat-chip-set> </mat-card-content> </mat-card> <!-- M3 Color Tokens Info --> <mat-card class="demo-card"> <mat-card-header> <mat-card-title>Material 3 Color System</mat-card-title> </mat-card-header> <mat-card-content> <div class="color-info"> <p>Material 3 introduces a new color system with:</p> <ul> <li><strong>Dynamic Color:</strong> Colors that adapt to user preferences</li> <li><strong>Expanded Palette:</strong> More color roles and variants</li> <li><strong>Improved Contrast:</strong> Better accessibility and readability</li> <li><strong>Color Tokens:</strong> Semantic color naming system</li> </ul> <div class="color-demo"> <div class="color-box primary">Primary</div> <div class="color-box secondary">Secondary</div> <div class="color-box tertiary">Tertiary</div> <div class="color-box error">Error</div> </div> </div> </mat-card-content> </mat-card> </div> \`, styles: [\` .material3-showcase { padding: 24px; max-width: 800px; margin: 0 auto; } .demo-card { margin: 16px 0; } .button-row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; } .form-controls { display: flex; flex-direction: column; gap: 16px; } .controls-row { display: flex; gap: 24px; align-items: center; } .color-demo { display: flex; gap: 12px; margin-top: 16px; flex-wrap: wrap; } .color-box { padding: 12px 16px; border-radius: 8px; color: white; font-weight: 500; min-width: 80px; text-align: center; } .color-box.primary { background-color: var(--mdc-theme-primary, #6750a4); } .color-box.secondary { background-color: var(--mdc-theme-secondary, #625b71); } .color-box.tertiary { background-color: var(--mdc-theme-tertiary, #7d5260); } .color-box.error { background-color: var(--mdc-theme-error, #ba1a1a); } .color-info ul { margin: 16px 0; } mat-chip-set { margin: 8px 0; } \`] }) export class Material3ExampleComponent { removeChip(chipName: string) { console.log('Removed chip: ' + chipName); } } /* Material Design 3 in Angular 18 Key Features: 1. Dynamic Color System: - Colors adapt to user preferences and system theme - Improved color contrast and accessibility - New color tokens and semantic naming 2. Enhanced Components: - Updated visual design following M3 guidelines - Improved touch targets and interaction states - Better alignment with native platform conventions 3. Theming Improvements: - New M3 theme generator - Better support for dark mode - Customizable color schemes 4. Typography Updates: - M3 typography scale - Improved readability - Better hierarchy and emphasis 5. Motion and Animation: - Updated motion guidelines - Smoother transitions - Better performance Migration Notes: - Existing M2 themes continue to work - Gradual migration to M3 is supported - Some visual changes may require design review - Custom component styling may need updates */ `; const material3Path = path.join(exampleDir, 'material3-example.component.ts'); if (!await fs.pathExists(material3Path)) { await fs.writeFile(material3Path, material3Example); this.progressReporter?.info('✓ Created Material Design 3 example'); } // Create M3 theme configuration guide const themeGuide = `/* * Material Design 3 Theme Configuration for Angular 18+ * * This file demonstrates how to configure Material 3 theming * in your Angular application. */ @use '@angular/material' as mat; // Define your M3 color palette $m3-primary: mat.m3-define-palette(mat.$m3-blue-palette); $m3-secondary: mat.m3-define-palette(mat.$m3-green-palette); $m3-tertiary: mat.m3-define-palette(mat.$m3-yellow-palette); $m3-error: mat.m3-define-palette(mat.$m3-red-palette); // Create M3 theme $m3-theme: mat.m3-define-theme(( color: ( theme-type: light, primary: $m3-primary, secondary: $m3-secondary, tertiary: $m3-tertiary, error: $m3-error, ), typography: ( brand-family: 'Roboto, sans-serif', plain-family: 'Roboto, sans-serif', ), density: ( scale: 0 ) )); // Apply the M3 theme @include mat.all-component-themes($m3-theme); // Dark theme variant $m3-dark-theme: mat.m3-define-theme(( color: ( theme-type: dark, primary: $m3-primary, secondary: $m3-secondary, tertiary: $m3-tertiary, error: $m3-error, ) )); // Apply dark theme with class selector .dark-theme { @include mat.all-component-colors($m3-dark-theme); } /* Usage in your component: 1. Import the theme in your global styles 2. Use Material components as normal 3. Components automatically use M3 styling 4. Toggle dark theme by adding/removing 'dark-theme' class Example TypeScript for theme switching: export class AppComponent { isDarkTheme = false; toggleTheme() { this.isDarkTheme = !this.isDarkTheme; document.body.classList.toggle('dark-theme', this.isDarkTheme); } } */ `; const themePath = path.join(projectPath, 'src/styles/material3-theme.scss'); const stylesDir = path.dirname(themePath); await fs.ensureDir(stylesDir); if (!await fs.pathExists(themePath)) { await fs.writeFile(themePath, themeGuide); this.progressReporter?.info('✓ Created Material 3 theme configuration guide'); } } catch (error) { this.progressReporter?.warn(`Could not implement Material 3 support: ${error instanceof Error ? error.message : String(error)}`); } } /** * Stabilize built-in control flow syntax */ async stabilizeBuiltInControlFlow(projectPath) { try { const exampleDir = path.join(projectPath, 'src/app/examples'); await fs.ensureDir(exampleDir); // Create stable control flow examples const controlFlowExample = `import { Component, signal } from '@angular/core'; import { CommonModule } from '@angular/common'; interface Task { id: number; title: string; completed: boolean; priority: 'low' | 'medium' | 'high'; } @Component({ selector: 'app-stable-control-flow', standalone: true, imports: [CommonModule], template: \` <div class="control-flow-demo"> <h3>Stable Built-in Control Flow (Angular 18+)</h3> <!-- Stable @if syntax --> <section class="demo-section"> <h4>@if Control Flow</h4> <div class="controls"> <button (click)="toggleUser()">Toggle User</button> <button (click)="toggleAdmin()">Toggle Admin</button> </div> @if (currentUser()) { <div class="user-info"> <p>Welcome, {{ currentUser()?.name }}!</p> @if (currentUser()?.isAdmin) { <div class="admin-panel"> <p>Admin Panel Access Granted</p> <button>Manage Users</button> <button>System Settings</button> </div> } @else { <div class="user-panel"> <p>Standard User Access</p> <button>Profile Settings</button> </div> } </div> } @else { <div class="login-prompt"> <p>Please log in to continue</p> <button (click)="login()">Login</button> </div> } </section> <!-- Stable @for syntax --> <section class="demo-section"> <h4>@for Control Flow</h4> <div class="controls"> <button (click)="addTask()">Add Task</button> <button (click)="clearTasks()">Clear All</button> <select (change)="setFilter($event)"> <option value="all">All Tasks</option> <option value="completed">Completed</option> <option value="pending">Pending</option> </select> </div> <div class="task-list"> @for (task of filteredTasks(); track task.id) { <div class="task-item" [class.completed]="task.completed"> <input type="checkbox" [checked]="task.completed" (change)="toggleTask(task.id)"> <span class="task-title">{{ task.title }}</span> <span class="task-priority priority-{{ task.priority }}">{{ task.priority }}</span> <button (click)="removeTask(task.id)">Remove</button> </div> } @empty { <div class="empty-state"> <p>No tasks found</p> <p class="hint">Add a task to get started!</p> </div> } </div> </section> <!-- Stable @switch syntax --> <section class="demo-section"> <h4>@switch Control Flow</h4> <div class="controls"> <label>Select Theme:</label> <select (change)="setTheme($event)"> <option value="light">Light</option> <option value="dark">Dark</option> <option value="auto">Auto</option> <option value="custom">Custom</option> </select> </div> @switch (selectedTheme()) { @case ('light') { <div class="theme-preview light-theme"> <h5>Light Theme</h5> <p>Clean and bright interface for daytime use</p> <div class="color-samples"> <div class="color-sample" style="background: #ffffff"></div> <div class="color-sample" style="background: #f5f5f5"></div> <div class="color-sample" style="background: #1976d2"></div> </div> </div> } @case ('dark') { <div class="theme-preview dark-theme"> <h5>Dark Theme</h5> <p>Comfortable viewing experience for low-light environments</p> <div class="color-samples"> <div class="color-sample" style="background: #121212"></div> <div class="color-sample" style="background: #1e1e1e"></div> <div class="color-sample" style="background: #bb86fc"></div> </div> </div> } @case ('auto') { <div class="theme-preview auto-theme"> <h5>Auto Theme</h5> <p>Automatically adapts to system preference</p> <div class="color-samples"> <div class="color-sample" style="background: linear-gradient(45deg, #ffffff, #121212)"></div> <div class="color-sample" style="background: linear-gradient(45deg, #f5f5f5, #1e1e1e)"></div> <div class="color-sample" style="background: linear-gradient(45deg, #1976d2, #bb86fc)"></div> </div> </div> } @case ('custom') { <div class="theme-preview custom-theme"> <h5>Custom Theme</h5> <p>Personalized color scheme and preferences</p> <div class="custom-controls"> <input type="color" (change)="setCustomColor($event)" value="#ff5722"> <label>Primary Color</label> </div> </div> } @default { <div class="theme-preview default-theme"> <h5>Default Theme</h5> <p>Standard application theme</p> </div> } } </section> </div> \`, styles: [\` .control-flow-demo { padding: 24px; max-width: 800px; margin: 0 auto; } .demo-section { margin: 32px 0; padding: 24px; border: 1px solid #e0e0e0; border-radius: 8px; } .demo-section h4 { margin-top: 0; color: #1976d2; } .controls { margin: 16px 0; display: flex; gap: 12px; align-items: center; flex-wrap: wrap; } button { padding: 8px 16px; border: none; border-radius: 4px; background: #1976d2; color: white; cursor: pointer; transition: background 0.3s; } button:hover { background: #1565c0; } .user-info, .login-prompt { padding: 16px; border-radius: 8px; margin: 16px 0; } .user-info { background: #e8f5e8; border: 1px solid #4caf50; } .login-prompt { background: #fff3e0; border: 1px solid #ff9800; } .admin-panel { background: #e3f2fd; padding: 12px; border-radius: 4px; margin-top: 8px; } .user-panel { background: #f3e5f5; padding: 12px; border-radius: 4px; margin-top: 8px; } .task-list { margin: 16px 0; } .task-item { display: flex; align-items: center; gap: 12px; padding: 12px; border: 1px solid #e0e0e0; border-radius: 4px; margin: 8px 0; } .task-item.completed { opacity: 0.7; background: #f5f5f5; } .task-item.completed .task-title { text-decoration: line-through; } .task-priority { padding: 4px 8px; border-radius: 12px; font-size: 0.8em; font-weight: 500; } .priority-low { background: #e8f5e8; color: #2e7d32; } .priority-medium { background: #fff3e0; color: #f57c00; } .priority-high { background: #ffebee; color: #d32f2f; } .empty-state { text-align: center; padding: 40px; color: #666; } .empty-state .hint { font-size: 0.9em; margin-top: 8px; } .theme-preview { padding: 20px; border-radius: 8px; margin: 16px 0; transition: all 0.3s ease; } .light-theme { background: #ffffff; border: 1px solid #e0e0e0; color: #333; } .dark-theme { background: #121212; border: 1px solid #333; color: #ffffff; } .auto-theme { background: linear-gradient(135deg, #ffffff 50%, #121212 50%); border: 1px solid #666; color: #333; } .custom-theme { background: linear-gradient(135deg, #ff5722, #ffccbc); border: 1px solid #ff5722; color: white; } .color-samples { display: flex; gap: 8px; margin-top: 12px; } .color-sample { width: 40px; height: 40px; border-radius: 4px; border: 1px solid #ccc; } .custom-controls { display: flex; gap: 8px; align-items: center; margin-top: 12px; } select, input[type="color"] { padding: 6px 12px; border: 1px solid #ccc; border-radius: 4px; } \`] }) export class StableControlFlowComponent { // Signals for reactive state management currentUser = signal<{ name: string; isAdmin: boolean } | null>(null); tasks = signal<Task[]>([ { id: 1, title: 'Review Angular 18 features', completed: false, priority: 'high' }, { id: 2, title: 'Update control flow syntax', completed: true, priority: 'medium' }, { id: 3, title: 'Test Material 3 components', completed: false, priority: 'low' } ]); filter = signal<'all' | 'completed' | 'pending'>('all'); selectedTheme = signal<string>('light'); // Computed values filteredTasks = signal.computed(() => { const allTasks = this.tasks(); const currentFilter = this.filter(); switch (currentFilter) { case 'completed': return allTasks.filter(task => task.completed); case 'pending': return allTasks.filter(task => !task.completed); default: return allTasks; } }); toggleUser() { const current = this.currentUser(); if (current) { this.currentUser.set(null); } else { this.currentUser.set({ name: 'John Doe', isAdmin: false }); } } toggleAdmin() { const current = this.currentUser(); if (current) { this.currentUser.set({ ...current, isAdmin: !current.isAdmin }); } } login() { this.currentUser.set({ name: 'Jane Smith', isAdmin: true }); } addTask() { const newTask: Task = { id: Date.now(), title: 'New task ' + (this.tasks().length + 1), completed: false, priority: 'medium' }; this.tasks.update(tasks => [...tasks, newTask]); } removeTask(id: number) { this.tasks.update(tasks => tasks.filter(task => task.id !== id)); } toggleTask(id: number) { this.tasks.update(tasks => tasks.map(task => task.id === id ? { ...task, completed: !task.completed } : task ) ); } clearTasks() { this.tasks.set([]); } setFilter(event: Event) { const value = (event.target as HTMLSelectElement).value as 'all' | 'completed' | 'pending'; this.filter.set(value); } setTheme(event: Event) { const value = (event.target as HTMLSelectElement).value; this.selectedTheme.set(value); } setCustomColor(event: Event) { const color = (event.target as HTMLInputElement).value; console.log('Custom color selected:', color); // In a real app, you would apply this color to your theme } } /* Built-in Control Flow Benefits (Angular 18+): 1. Performance: - Better tree-shaking and bundle size optimization - Faster runtime performance - More efficient change detection 2. Developer Experience: - Cleaner, more readable templates - Better TypeScript integration - Improved IDE support and intellisense 3. Maintainability: - Less verbose than structural directives - More intuitive syntax - Better error messages 4. Features: - @if/@else blocks with better nesting - @for with built-in @empty state - @switch with @case and @default - Automatic track optimization in @for 5. Migration: - Existing *ngIf, *ngFor, *ngSwitch continue to work - Gradual migration is supported - Angular schematics can assist with migration */ `; const controlFlowPath = path.join(exampleDir, 'stable-control-flow.component.ts'); if (!await fs.pathExists(controlFlowPath)) { await fs.writeFile(controlFlowPath, controlFlowExample); this.progressReporter?.info('✓ Created stable built-in control flow example'); } } catch (error) { this.progressReporter?.warn(`Could not stabilize control flow: ${error instanceof Error ? error.message : String(error)}`); } } /** * Implement new lifecycle hooks */ async implementNewLifecycleHooks(projectPath) { try { const exampleDir = path.join(projectPath, 'src/app/examples'); await fs.ensureDir(exampleDir); // Create new lifecycle hooks example const lifecycleExample = `import { Component, afterRender, afterNextRender, ElementRef, ViewChild, signal } from '@angular/core'; @Component({ selector: 'app-lifecycle-hooks-example', standalone: true, template: \` <div class="lifecycle-demo"> <h3>New Lifecycle Hooks (Angular 18+)</h3> <div class="demo-section"> <h4>afterRender & afterNextRender Examples</h4> <div class="controls"> <button (click)="addItem()">Add Item</button> <button (click)="scrollToBottom()">Scroll to Bottom</button> <button (click)="focusInput()">Focus Input</button> <button (click)="measurePerformance()">Measure Performance</button> </div> <div class="input-section"> <input #userInput type="text" placeholder="Type something..." (input)="updateInputValue($event)"> <p>Characters: {{ inputLength() }}</p> </div> <div class="scrollable-list" #scrollContainer> @for (item of items(); track item.id) { <div class="list-item" [class.highlighted]="item.highlighted"> <span>{{ item.text }}</span> <small>Added at: {{ item.timestamp | date:'short' }}</small> </div> } </div> <div class="performance-info"> <h5>Performance Metrics</h5> <p>Render time: {{ renderTime() }}ms</p> <p>DOM updates: {{ domUpdates() }}</p> <p>Last measurement: {{ lastMeasurement() }}</p> </div> </div> </div> \`, styles: [\` .lifecycle-demo { padding: 24px; max-width: 600px; margin: 0 auto; } .demo-section { margin: 20px 0; padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; } .controls { display: flex; gap: 12px; margin: 16px 0; flex-wrap: wrap; } button { padding: 8px 16px; border: none; border-radius: 4px; background: #1976d2; color: white; cursor: pointer; } .input-section { margin: 16px 0; } input { width: 100%; padding: 12px; border: 1px solid #ccc; border-radius: 4px; font-size: 16px; } .scrollable-list { height: 200px; overflow-y: auto; border: 1px solid #e0e0e0; border-radius: 4px; margin: 16px 0; } .list-item { padding: 12px; border-bottom: 1px solid #f0f0f0; transition: background-color 0.3s; } .list-item.highlighted { background-color: #fff3e0; animation: highlight 2s ease-out; } @keyframes highlight { 0% { background-color: #ffeb3b; } 100% { background-color: #fff3e0; } } .list-item small { display: block; color: #666; margin-top: 4px; } .performance-info { background: #f5f5f5; padding: 16px; border-radius: 4px; margin-top: 16px; } .performance-info h5 { margin-top: 0; } \`] }) export class LifecycleHooksExampleComponent { @ViewChild('userInput') userInput!: ElementRef<HTMLInputElement>; @ViewChild('scrollContainer') scrollContainer!: ElementRef<HTMLDivElement>; // Signals for reactive state items = signal<Array<{id: number, text: string, timestamp: Date, highlighted: boolean}>>([ { id: 1, text: 'Initial item 1', timestamp: new Date(), highlighted: false }, { id: 2, text: 'Initial item 2', timestamp: new Date(), highlighted: false } ]); inputLength = signal(0); renderTime = signal(0); domUpdates = signal(0); lastMeasurement = signal(''); constructor() { // afterRender: Runs after every render cycle // Perfect for DOM measurements, analytics, or cleanup afterRender(() => { const startTime = performance.now(); // Measure DOM elements const listItems = document.querySelectorAll('.list-item'); // Update performance metrics const endTime = performance.now(); this.renderTime.set(Number((endTime - startTime).toFixed(2))); this.domUpdates.update(count => count + 1); // Log render information (useful for debugging) console.log('Render completed: ' + listItems.length + ' items rendered in ' + (endTime - startTime) + 'ms'); }); // afterNextRender: Runs only after the next render cycle // Perfect for one-time DOM setup, focus management, or initial measurements afterNextRender(() => { console.log('Component has completed initial render'); // Initial DOM setup this.setupInitialDOM(); // Focus management this.setupKeyboardShortcuts(); // Initial measurements this.measureInitialLayout(); }); } addItem() { const newItem = { id: Date.now(), text: 'Item ' + (this.items().length + 1), timestamp: new Date(), highlighted: true }; this.items.update(items => [...items, newItem]); // Remove highlight after animation setTimeout(() => { this.items.update(items => items.map(item => item.id === newItem.id ? { ...item, highlighted: false } : item ) ); }, 2000); // afterNextRender for immediate DOM interaction afterNextRender(() => { this.scrollToBottom(); }); } scrollToBottom() { if (this.scrollContainer) { const element = this.scrollContainer.nativeElement; element.scrollTop = element.scrollHeight; } } focusInput() { // afterNextRender ensures the input is rendered before focusing afterNextRender(() => { if (this.userInput) { this.userInput.nativeElement.focus(); this.userInput.nativeElement.select(); } }); } updateInputValue(event: Event) { const target = event.target as HTMLInputElement; this.inputLength.set(target.value.length); } measurePerformance() { const startTime = performance.now(); // Simulate some work for (let i = 0; i < 1000; i++) { document.createElement('div'); } afterNextRender(() => { const endTime = performance.now(); const measurementTime = (endTime - startTime).toFixed(2); this.lastMeasurement.set(measurementTime + 'ms at ' + new Date().toLocaleTimeString()); }); } private setupInitialDOM() { // This runs only once after the first render console.log('Setting up initial DOM structure'); // Add initial classes or setup document.body.classList.add('lifecycle-demo-active'); // Setup intersection observers, resize observers, etc. this.setupObservers(); } private setupKeyboardShortcuts() { // Setup global keyboard shortcuts document.addEventListener('keydown', (event) => { if (event.ctrlKey && event.key === 'k') { event.preventDefault(); this.focusInput(); } }); } private measureInitialLayout() { // Measure initial layout for performance baseline const listElement = this.scrollContainer?.nativeElement; if (listElement) { const rect = listElement.getBoundingClientRect(); console.log('Initial list dimensions:', rect); } } private setupObservers() { // Setup intersection observer for lazy loading or animations const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { // Handle intersection console.log('Element is visible:', entry.target); } }); }); // Observe elements document.querySelectorAll('.list-item').forEach(item => { observer.observe(item); }); } } /* New Lifecycle Hooks in Angular 18: 1. afterRender(): - Runs after every render cycle - Perfect for: * DOM measurements * Analytics tracking * Performance monitoring * Cleanup operations - Use with caution: runs frequently 2. afterNextRender(): - Runs only after the next render cycle - Perfect for: * One-time DOM setup * Focus management * Initial measurements * Setting up observers - More efficient for one-time operations Best Practices: 1. Use afterNextRender() for: - Initial DOM setup - Focus management - One-time measurements - Observer setup 2. Use afterRender() for: - Continuous monitoring - Analytics - Performance tracking - Cleanup that needs to run after each render 3. Performance Considerations: - afterRender() runs frequently, keep operations lightweight - afterNextRender() is better for expensive one-time operations - Both run outside Angular's change detection cycle - Can access DOM safely without triggering additional cycles 4. Common Use Cases: - Scroll position management - Focus management - DOM measurements - Third-party library integration - Performance monitoring - Analytics and tracking */ `; const lifecyclePath = path.join(exampleDir, 'lifecycle-hooks-example.component.ts'); if (!await fs.pathExists(lifecyclePath)) { await fs.writeFile(lifecyclePath, lifecycleExample); this.progressReporter?.info('✓ Created new lifecycle hooks example'); } } catch (error) { this.progressReporter?.warn(`Could not implement lifecycle hooks: ${error instanceof Error ? error.message : String(error)}`); } } /** * Setup event replay for SSR hydration */ async setupEventReplaySSR(projectPath) { const mainTsPath = path.join(projectPath, 'src/main.ts'); if (await fs.pathExists(mainTsPath)) { try { let content = await fs.readFile(mainTsPath, 'utf-8'); // Add event replay import and configuration if (content.includes('bootstrapApplication') && !content.includes('withEventReplay')) { content = content.replace(/import { bootstrapApplication } from '@angular\/platform-browser';/, `import { bootstrapApplication } from '@angular/platform-browser'; import { provideClientHydration, withEventReplay } from '@angular/platform-browser';`); // Add event replay provider if (content.includes('provideClientHydration()')) { content = content.replace(/provideClientHydration\(\)/, 'provideClientHydration(withEventReplay())'); } else { content = content.replace(/providers: \[([\s\S]*?)\]/, `providers: [ provideClientHydration(withEventReplay()), $1 ]`); } await fs.writeFile(mainTsPath, content); this.progressReporter?.info('✓ Configured event replay for SSR hydration'); } } catch (error) { this.progressReporter?.warn(`Could not setup event replay: ${error instanceof Error ? error.message : String(error)}`); } } } /** * Configure hybrid rendering capabilities */ async configureHybridRendering(projectPath) { try { const configDir = path.join(projectPath, 'src/app/config'); await fs.ensureDir(configDir); // Create hybrid rendering configuration example const hybridConfig = `/* * Hybrid Rendering Configuration for Angular 18+ * * Demonstrates how to configure hybrid rendering strategies * combining SSR and client-side rendering for optimal performance. */ import { ApplicationConfig } from '@angular/core'; import { provideRouter, withPreloading, PreloadAllModules } from '@angular/router'; import { provideClientHydration, withEventReplay, withNoopReplay } from '@angular/platform-browser'; import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http'; // Example hybrid rendering configuration export const hybridRenderingConfig: ApplicationConfig = { providers: [ // Router with preloading strategy provideRouter(routes, withPreloading(PreloadAllModules)), // Hydration with event replay for better UX provideClientHydration( withEventReplay() // Replays user events during hydration ), // HTTP client with fetch API and interceptors provideHttpClient( withFetch(), // Use fetch API for better performance withInterceptors([/* your interceptors */]) ), // Additional providers for hybrid rendering // ... your other providers ] }; // Alternative configuration for development export const developmentHybridConfig: ApplicationConfig = { providers: [ provideRouter(routes), // No event replay in development for easier debugging provideClientHydration(withNoopReplay()), provideHttpClient(withFetch()), // Development-specific providers // ... your dev providers ] }; /* Hybrid Rendering Strategies: 1. Full SSR with Event Replay: - Server renders initial content - Client hydrates with event replay - Best for: Content-heavy applications, SEO-critical pages 2. Selective Hydration: - Some components hydrate immediately - Others hydrate on interaction - Best for: Large applications with varied interactivity 3. Progressive Enhancement: - Basic functionality works without JavaScript - Enhanced features activate after hydration - Best for: Accessibility-first applications 4. Client-Side Rendering with Prerendering: - Static content prerendered at build time - Dynamic content rendered on client - Best for: Applications with mostly static content Configuration Examples: // For content-heavy sites provideClientHydration( withEventReplay(), // Capture and replay user events withDomHydration() // Full DOM hydration ) // For interactive applications provideClientHydration( withEventReplay(), withSelectiveHydration() // Hydrate components on demand ) // For development provideClientHydration( withNoopReplay() // Skip event replay for easier debugging ) Benefits: - Improved perceived performance - Better SEO and initial page load - Smooth user experience during hydration - Flexible rendering strategies - Automatic event preservation */ const routes = [ // Your application routes ]; `; const configPath = path.join(configDir, 'hybrid-rendering.config.ts'); if (!await fs.pathExists(configPath)) { await fs.writeFile(configPath, hybridConfig); this.progressReporter?.info('✓ Created hybrid rendering configuration'); } } catch (error) { this.progressReporter?.warn(`Could not configure hybrid rendering: ${error instanceof Error ? error.message : String(error)}`); } } /** * Enhanced i18n extraction and tooling */ async enhanceI18nTooling(projectPath) { const angularJsonPath = path.join(projectPath, 'angular.json'); if (await fs.pathExists(angularJsonPath)) { try { const angularJson = await fs.readJson(angularJsonPath); // Update i18n configurations for Angular 18 for (const projectName in angularJson.projects) { const project = angularJson.projects[projectName]; if (project.architect) { // Add enhanced i18n extraction project.architect['extract-i18n'] = { builder: '@angular-devkit/build-angular:extract-i18n', options: { buildTarget: projectName + ':build', format: 'xlf2', outputPath: 'src/locale', progress: true } }; // Enhanced build configurations for i18n if (project.architect.build) { if (!project.architect.build.configurations) { project.architect.build.configurations = {}; } // Add i18n build configurations project.architect.build.configurations.fr = { aot: true, outputPath: 'dist/fr/', i18nFile: 'src/locale/messages.fr.xlf', i18nFormat: 'xlf2', i18nLocale: 'fr' }; project.architect.build.configurations.es = { aot: true, outputPath: 'dist/es/', i18nFile: 'src/locale/messages.es.xlf', i18nFormat: 'xlf2', i18nLocale: 'es' }; } } } await fs.writeJson(angularJsonPath, angularJson, { spaces: 2 }); this.progressReporter?.info('✓ Enhanced i18n extraction and tooling configuration'); } catch (error) { this.progressReporter?.warn(`Could not enhance i18n tooling: ${error instanceof Error ? error.message : String(error)}`); } } } /** * Optimize change detection improvements */ async optimizeChangeDetection(projectPath) { try { const exampleDir = path.join(projectPath, 'src/app/examples'); await fs.ensureDir(exampleDir); // Create change detection optimization example const optimizationExample = `/* * Change Detectio