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,563 lines (1,361 loc) • 64.9 kB
JavaScript
"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.Angular19Handler = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
const BaseVersionHandler_1 = require("./BaseVersionHandler");
const SSRDetector_1 = require("../utils/SSRDetector");
/**
* Angular 19 Handler - Zoneless change detection and event replay
*
* Key Features in Angular 19:
* - Zoneless change detection (experimental)
* - Enhanced event replay for SSR hydration
* - Improved incremental hydration
* - Advanced SSR optimizations
* - Better performance monitoring
* - Enhanced developer experience
* - Improved i18n support
* - Advanced build optimizations
*/
class Angular19Handler extends BaseVersionHandler_1.BaseVersionHandler {
version = '19';
getRequiredNodeVersion() {
return '>=18.19.1';
}
getRequiredTypeScriptVersion() {
return '>=5.5.0 <5.7.0';
}
/**
* Get Angular 19 dependencies with correct versions
*/
getDependencyUpdates() {
return [
// Core Angular packages
{ name: '@angular/animations', version: '^19.0.0', type: 'dependencies' },
{ name: '@angular/common', version: '^19.0.0', type: 'dependencies' },
{ name: '@angular/compiler', version: '^19.0.0', type: 'dependencies' },
{ name: '@angular/core', version: '^19.0.0', type: 'dependencies' },
{ name: '@angular/forms', version: '^19.0.0', type: 'dependencies' },
{ name: '@angular/platform-browser', version: '^19.0.0', type: 'dependencies' },
{ name: '@angular/platform-browser-dynamic', version: '^19.0.0', type: 'dependencies' },
{ name: '@angular/router', version: '^19.0.0', type: 'dependencies' },
{ name: '@angular/ssr', version: '^19.0.0', type: 'dependencies' },
// Angular CLI and dev dependencies
{ name: '@angular/cli', version: '^19.0.0', type: 'devDependencies' },
{ name: '@angular/compiler-cli', version: '^19.0.0', type: 'devDependencies' },
{ name: '@angular-devkit/build-angular', version: '^19.0.0', type: 'devDependencies' },
// TypeScript and supporting packages
{ name: 'typescript', version: '~5.5.0', type: 'devDependencies' },
{ name: 'zone.js', version: '~0.14.0', type: 'dependencies' }, // Still needed for compatibility
{ name: 'rxjs', version: '~7.8.0', type: 'dependencies' },
// Angular Material
{ name: '@angular/material', version: '^19.0.0', type: 'dependencies' },
{ name: '@angular/cdk', version: '^19.0.0', type: 'dependencies' }
];
}
async applyVersionSpecificChanges(projectPath, options) {
this.progressReporter?.updateMessage('Applying Angular 19 transformations...');
// 1. Setup zoneless change detection (experimental)
if (options.enableZonelessChangeDetection) {
await this.setupZonelessChangeDetection(projectPath);
}
// 2. Enhanced event replay for SSR hydration
await this.enhanceEventReplaySSR(projectPath);
// 3. Implement incremental hydration improvements
await this.implementIncrementalHydration(projectPath);
// 4. Advanced SSR optimizations
await this.implementAdvancedSSROptimizations(projectPath);
// 5. Enhanced performance monitoring
await this.enhancePerformanceMonitoring(projectPath);
// 6. Improved developer experience features
await this.improveDeveloperExperience(projectPath);
// 7. Advanced i18n support improvements
await this.enhanceI18nSupport(projectPath);
// 8. Advanced build optimizations
await this.implementAdvancedBuildOptimizations(projectPath);
// 9. Update build configurations for Angular 19
await this.updateBuildConfigurations(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 19 transformations completed');
}
getBreakingChanges() {
return [
// Zoneless change detection (experimental)
this.createBreakingChange('ng19-zoneless-detection', 'api', 'high', 'Zoneless change detection (experimental)', 'Experimental zoneless change detection available as opt-in feature', 'Opt-in experimental feature - Zone.js continues to work by default'),
// TypeScript version requirement
this.createBreakingChange('ng19-typescript-version', 'dependency', 'medium', 'TypeScript 5.5+ required', 'Angular 19 requires TypeScript 5.5.0 or higher', 'Update TypeScript to version 5.5.0 or higher'),
// Node.js version requirement
this.createBreakingChange('ng19-nodejs-version', 'dependency', 'medium', 'Node.js 18.19.1+ required', 'Angular 19 requires Node.js 18.19.1 or higher', 'Update Node.js to version 18.19.1 or higher'),
// Enhanced event replay
this.createBreakingChange('ng19-enhanced-event-replay', 'api', 'low', 'Enhanced event replay for SSR', 'Improved event capture and replay mechanisms for better hydration UX', 'Automatic improvement - no action required'),
// Incremental hydration
this.createBreakingChange('ng19-incremental-hydration', 'api', 'low', 'Improved incremental hydration', 'Better strategies for selective component hydration', 'New feature - existing hydration continues to work'),
// Advanced SSR optimizations
this.createBreakingChange('ng19-advanced-ssr', 'api', 'low', 'Advanced SSR optimizations', 'Enhanced server-side rendering with better performance and caching', 'Automatic improvements - no breaking changes'),
// Performance monitoring
this.createBreakingChange('ng19-performance-monitoring', 'api', 'low', 'Enhanced performance monitoring', 'Better built-in performance tracking and debugging tools', 'New feature - additional monitoring capabilities')
];
}
// Private implementation methods
/**
* Setup zoneless change detection (experimental)
*/
async setupZonelessChangeDetection(projectPath) {
try {
// Update main.ts for zoneless change detection
const mainTsPath = path.join(projectPath, 'src/main.ts');
if (await fs.pathExists(mainTsPath)) {
let content = await fs.readFile(mainTsPath, 'utf-8');
// Add zoneless imports
if (!content.includes('provideExperimentalZonelessChangeDetection')) {
content = content.replace(/import { bootstrapApplication } from '@angular\/platform-browser';/, `import { bootstrapApplication } from '@angular/platform-browser';
import { provideExperimentalZonelessChangeDetection } from '@angular/core';`);
// Add zoneless provider
content = content.replace(/providers: \[([\s\S]*?)\]/, `providers: [
provideExperimentalZonelessChangeDetection(),
$1
]`);
await fs.writeFile(mainTsPath, content);
this.progressReporter?.info('✓ Configured experimental zoneless change detection');
}
}
// Create zoneless examples
const exampleDir = path.join(projectPath, 'src/app/examples');
await fs.ensureDir(exampleDir);
const zonelessExample = `import { Component, signal, effect, ChangeDetectionStrategy } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
/**
* Zoneless Change Detection Example (Angular 19+ Experimental)
*
* This component demonstrates how to work with zoneless change detection,
* where Angular doesn't rely on Zone.js for detecting changes.
*/
@Component({
selector: 'app-zoneless-example',
standalone: true,
imports: [CommonModule, FormsModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: \`
<div class="zoneless-demo">
<h3>Zoneless Change Detection (Experimental)</h3>
<div class="demo-section">
<h4>Signal-Based State Management</h4>
<div class="counter-demo">
<p>Counter: {{ counter() }}</p>
<p>Double: {{ doubleCounter() }}</p>
<p>Updates: {{ updateCount() }}</p>
<div class="controls">
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>
</div>
</div>
</div>
<div class="demo-section">
<h4>Manual Change Detection</h4>
<div class="manual-demo">
<p>Manual value: {{ manualValue }}</p>
<p>Last updated: {{ lastManualUpdate }}</p>
<div class="controls">
<button (click)="updateManualValue()">Update Manual Value</button>
<button (click)="triggerChangeDetection()">Trigger Change Detection</button>
</div>
</div>
</div>
<div class="demo-section">
<h4>Async Operations</h4>
<div class="async-demo">
<p>Async counter: {{ asyncCounter() }}</p>
<p>Status: {{ asyncStatus() }}</p>
<div class="controls">
<button (click)="startAsyncOperation()">Start Async</button>
<button (click)="stopAsyncOperation()">Stop</button>
</div>
</div>
</div>
<div class="demo-section">
<h4>Form Integration</h4>
<div class="form-demo">
<input
type="text"
[(ngModel)]="formValue"
(input)="onFormInput($event)"
placeholder="Type something...">
<p>Form value: {{ formValue }}</p>
<p>Character count: {{ formValue.length }}</p>
</div>
</div>
<div class="info-section">
<h4>Zoneless Benefits</h4>
<ul>
<li>Better performance - no Zone.js overhead</li>
<li>Smaller bundle size - can exclude Zone.js</li>
<li>More predictable change detection</li>
<li>Better integration with modern web APIs</li>
<li>Easier testing without Zone.js patches</li>
</ul>
</div>
</div>
\`,
styles: [\`
.zoneless-demo {
padding: 24px;
max-width: 800px;
margin: 0 auto;
}
.demo-section {
margin: 24px 0;
padding: 20px;
border: 1px solid #e0e0e0;
border-radius: 8px;
}
.demo-section h4 {
margin-top: 0;
color: #1976d2;
}
.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;
transition: background 0.2s;
}
button:hover {
background: #1565c0;
}
input {
width: 100%;
padding: 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 16px;
margin-bottom: 12px;
}
.counter-demo, .manual-demo, .async-demo, .form-demo {
background: #f9f9f9;
padding: 16px;
border-radius: 4px;
}
.info-section {
background: #e3f2fd;
padding: 20px;
border-radius: 8px;
}
.info-section ul {
margin: 16px 0;
}
.info-section li {
margin: 8px 0;
}
\`]
})
export class ZonelessExampleComponent {
// Signals work perfectly with zoneless change detection
counter = signal(0);
updateCount = signal(0);
asyncCounter = signal(0);
asyncStatus = signal('idle');
// Traditional properties require manual change detection
manualValue = 'Initial value';
lastManualUpdate = new Date().toLocaleTimeString();
formValue = '';
private intervalId: any;
// Computed signals
doubleCounter = signal.computed(() => this.counter() * 2);
constructor() {
// Effects work great with zoneless change detection
effect(() => {
console.log('Counter changed to: ' + this.counter());
this.updateCount.update(count => count + 1);
});
effect(() => {
console.log('Async counter: ' + this.asyncCounter());
});
}
// Signal-based operations (automatic change detection)
increment() {
this.counter.update(value => value + 1);
}
decrement() {
this.counter.update(value => value - 1);
}
reset() {
this.counter.set(0);
this.updateCount.set(0);
}
// Manual change detection required for non-signal properties
updateManualValue() {
this.manualValue = 'Updated at ' + new Date().toLocaleTimeString();
this.lastManualUpdate = new Date().toLocaleTimeString();
// In zoneless mode, you need to manually trigger change detection
// This would typically be done through a service or ChangeDetectorRef
}
triggerChangeDetection() {
// In a real application, you would use ChangeDetectorRef.markForCheck()
// or convert to signals for automatic updates
console.log('Manual change detection triggered');
}
// Async operations
startAsyncOperation() {
this.asyncStatus.set('running');
this.intervalId = setInterval(() => {
this.asyncCounter.update(count => count + 1);
}, 1000);
// Auto-stop after 10 seconds
setTimeout(() => {
this.stopAsyncOperation();
}, 10000);
}
stopAsyncOperation() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
this.asyncStatus.set('stopped');
}
}
onFormInput(event: Event) {
// Form input with ngModel still works in zoneless mode
const target = event.target as HTMLInputElement;
console.log('Form input:', target.value);
}
ngOnDestroy() {
this.stopAsyncOperation();
}
}
/*
Zoneless Change Detection in Angular 19:
1. What is Zoneless?
- No dependency on Zone.js
- Manual or signal-based change detection
- Better performance and smaller bundles
- More predictable change detection timing
2. Migration Strategy:
- Gradually convert to signals
- Use OnPush change detection strategy
- Manual change detection for edge cases
- Test thoroughly in zoneless mode
3. Best Practices:
- Prefer signals over traditional properties
- Use effects for side effects
- OnPush strategy for all components
- Manual change detection when needed
4. Compatibility:
- Most Angular features work without changes
- Third-party libraries may need updates
- Forms and router work seamlessly
- Some async operations need manual handling
5. Performance Benefits:
- No Zone.js monkey patching
- Smaller bundle size
- Faster change detection cycles
- Better integration with modern APIs
- Easier testing
6. Migration Checklist:
- Enable experimental zoneless change detection
- Convert components to use signals
- Add OnPush change detection strategy
- Test async operations
- Update third-party library usage
- Verify form functionality
- Test routing and navigation
*/
`;
const zonelessPath = path.join(exampleDir, 'zoneless-example.component.ts');
if (!await fs.pathExists(zonelessPath)) {
await fs.writeFile(zonelessPath, zonelessExample);
this.progressReporter?.info('✓ Created zoneless change detection example');
}
}
catch (error) {
this.progressReporter?.warn(`Could not setup zoneless change detection: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Enhanced event replay for SSR hydration - only for SSR applications
*/
async enhanceEventReplaySSR(projectPath) {
// Check if this is an SSR application first
const isSSRApp = await SSRDetector_1.SSRDetector.isSSRApplication(projectPath);
if (!isSSRApp) {
this.progressReporter?.info('✓ Skipping event replay configuration (CSR application detected)');
return;
}
const mainTsPath = path.join(projectPath, 'src/main.ts');
if (await fs.pathExists(mainTsPath)) {
try {
let content = await fs.readFile(mainTsPath, 'utf-8');
// Enhanced event replay configuration only if hydration is already present
if (content.includes('provideClientHydration') && !content.includes('withEventReplay')) {
content = content.replace(/import { provideClientHydration } from '@angular\/platform-browser';/, `import { provideClientHydration, withEventReplay } from '@angular/platform-browser';`);
// Enhanced event replay with options
content = content.replace(/provideClientHydration\(\)/, `provideClientHydration(
withEventReplay({
// Capture more event types for better UX
events: ['click', 'input', 'change', 'submit', 'keydown', 'focus', 'blur'],
// Increase replay buffer for complex interactions
bufferSize: 100,
// Enable replay debugging in development
debug: !environment.production
})
)`);
await fs.writeFile(mainTsPath, content);
this.progressReporter?.info('✓ Enhanced event replay for SSR hydration');
}
}
catch (error) {
this.progressReporter?.warn(`Could not enhance event replay: ${error instanceof Error ? error.message : String(error)}`);
}
}
}
/**
* Implement incremental hydration improvements
*/
async implementIncrementalHydration(projectPath) {
try {
const exampleDir = path.join(projectPath, 'src/app/examples');
await fs.ensureDir(exampleDir);
// Create incremental hydration example
const hydrationExample = `/*
* Incremental Hydration Example for Angular 19+
*
* Demonstrates advanced hydration strategies for better performance
* and user experience during the hydration process.
*/
import { Component, signal, afterRender, afterNextRender } from '@angular/core';
import { CommonModule } from '@angular/common';
// Hydration priority levels
type HydrationPriority = 'immediate' | 'visible' | 'interaction' | 'lazy';
@Component({
selector: 'app-incremental-hydration',
standalone: true,
imports: [CommonModule],
template: \`
<div class="hydration-demo">
<h3>Incremental Hydration (Angular 19+)</h3>
<!-- Immediate hydration - critical above-the-fold content -->
<section class="hydration-section immediate" data-hydration="immediate">
<h4>Immediate Hydration</h4>
<p>Critical content that hydrates immediately</p>
<button (click)="immediateAction()">Immediate Action</button>
<p>Status: {{ immediateStatus() }}</p>
</section>
<!-- Visible hydration - content visible in viewport -->
<section class="hydration-section visible" data-hydration="visible">
<h4>Visible Hydration</h4>
<p>Content that hydrates when visible</p>
<div class="interactive-content">
<button (click)="visibleAction()">Visible Action</button>
<p>Clicks: {{ visibleClicks() }}</p>
</div>
</section>
<!-- Interaction hydration - hydrates on first interaction -->
<section class="hydration-section interaction" data-hydration="interaction">
<h4>Interaction Hydration</h4>
<p>Hydrates only when user interacts</p>
<div class="lazy-content">
<button (click)="interactionAction()">Click to Hydrate</button>
<p *ngIf="interactionHydrated()">Now hydrated! Actions: {{ interactionActions() }}</p>
</div>
</section>
<!-- Lazy hydration - lowest priority -->
<section class="hydration-section lazy" data-hydration="lazy">
<h4>Lazy Hydration</h4>
<p>Background content with lowest priority</p>
<div class="background-content">
<p>Background data: {{ backgroundData() }}</p>
<button (click)="lazyAction()">Lazy Action</button>
</div>
</section>
<!-- Hydration status dashboard -->
<section class="hydration-status">
<h4>Hydration Status</h4>
<div class="status-grid">
<div class="status-item">
<label>Page Load Time:</label>
<span>{{ pageLoadTime() }}ms</span>
</div>
<div class="status-item">
<label>Hydration Complete:</label>
<span>{{ hydrationComplete() ? 'Yes' : 'In Progress' }}</span>
</div>
<div class="status-item">
<label>Interactive Time:</label>
<span>{{ interactiveTime() }}ms</span>
</div>
<div class="status-item">
<label>Components Hydrated:</label>
<span>{{ hydratedComponents() }}</span>
</div>
</div>
</section>
</div>
\`,
styles: [\`
.hydration-demo {
padding: 24px;
max-width: 800px;
margin: 0 auto;
}
.hydration-section {
margin: 24px 0;
padding: 20px;
border-radius: 8px;
transition: all 0.3s ease;
}
.hydration-section h4 {
margin-top: 0;
}
.immediate {
background: #e8f5e8;
border: 2px solid #4caf50;
}
.visible {
background: #e3f2fd;
border: 2px solid #2196f3;
}
.interaction {
background: #fff3e0;
border: 2px solid #ff9800;
}
.lazy {
background: #f3e5f5;
border: 2px solid #9c27b0;
}
.hydration-status {
background: #f5f5f5;
border: 2px solid #666;
margin-top: 32px;
}
.status-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
margin-top: 16px;
}
.status-item {
display: flex;
justify-content: space-between;
padding: 8px;
background: white;
border-radius: 4px;
}
.status-item label {
font-weight: bold;
}
button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background: #1976d2;
color: white;
cursor: pointer;
margin: 8px 0;
transition: background 0.2s;
}
button:hover {
background: #1565c0;
}
.interactive-content,
.lazy-content,
.background-content {
margin-top: 12px;
padding: 12px;
background: rgba(255, 255, 255, 0.5);
border-radius: 4px;
}
\`]
})
export class IncrementalHydrationComponent {
// Hydration status signals
immediateStatus = signal('Hydrated immediately');
visibleClicks = signal(0);
interactionActions = signal(0);
interactionHydrated = signal(false);
backgroundData = signal('Loading...');
// Performance metrics
pageLoadTime = signal(0);
hydrationComplete = signal(false);
interactiveTime = signal(0);
hydratedComponents = signal(1); // Start with immediate component
private loadStartTime = performance.now();
constructor() {
// Track initial hydration
afterNextRender(() => {
this.pageLoadTime.set(Math.round(performance.now() - this.loadStartTime));
this.setupIncrementalHydration();
});
// Track ongoing hydration
afterRender(() => {
this.updateHydrationStatus();
});
}
immediateAction() {
this.immediateStatus.set('Action at ' + new Date().toLocaleTimeString());
}
visibleAction() {
this.visibleClicks.update(count => count + 1);
if (!this.interactionHydrated()) {
this.hydratedComponents.update(count => count + 1);
}
}
interactionAction() {
if (!this.interactionHydrated()) {
this.interactionHydrated.set(true);
this.hydratedComponents.update(count => count + 1);
this.interactiveTime.set(Math.round(performance.now() - this.loadStartTime));
}
this.interactionActions.update(count => count + 1);
}
lazyAction() {
console.log('Lazy action triggered');
this.backgroundData.set('Updated at ' + new Date().toLocaleTimeString());
}
private setupIncrementalHydration() {
// Simulate visible hydration with Intersection Observer
this.setupVisibleHydration();
// Setup lazy background hydration
this.setupLazyHydration();
// Mark initial hydration as complete
setTimeout(() => {
this.hydrationComplete.set(true);
}, 100);
}
private setupVisibleHydration() {
const visibleSection = document.querySelector('[data-hydration="visible"]');
if (visibleSection) {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// Simulate hydration of visible component
setTimeout(() => {
this.hydratedComponents.update(count => count + 1);
}, 100);
observer.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
observer.observe(visibleSection);
}
}
private setupLazyHydration() {
// Simulate background data loading
setTimeout(() => {
this.backgroundData.set('Lazy loaded data');
this.hydratedComponents.update(count => count + 1);
}, 2000);
}
private updateHydrationStatus() {
// Update performance metrics
const currentTime = performance.now() - this.loadStartTime;
// Check if all components are hydrated
if (this.hydratedComponents() >= 4 && !this.hydrationComplete()) {
this.hydrationComplete.set(true);
this.interactiveTime.set(Math.round(currentTime));
}
}
}
/*
Incremental Hydration Strategies in Angular 19:
1. Immediate Hydration:
- Critical above-the-fold content
- Navigation and essential interactions
- User authentication state
- Error boundaries
2. Visible Hydration:
- Content visible in the viewport
- Uses Intersection Observer
- Balances performance and UX
- Progressive enhancement
3. Interaction Hydration:
- Lazy-loaded on first user interaction
- Reduces initial JavaScript load
- Good for complex widgets
- Maintains perceived performance
4. Lazy Hydration:
- Background content
- Analytics and tracking
- Non-critical features
- Lowest priority
Benefits:
- Faster initial page loads
- Better Core Web Vitals scores
- Improved perceived performance
- Reduced JavaScript execution time
- Better resource utilization
Implementation Tips:
- Use signals for reactive updates
- Implement proper loading states
- Monitor hydration performance
- Test on slow devices/networks
- Graceful degradation for JavaScript disabled
Performance Metrics to Track:
- Time to First Byte (TTFB)
- First Contentful Paint (FCP)
- Largest Contentful Paint (LCP)
- Time to Interactive (TTI)
- Cumulative Layout Shift (CLS)
- First Input Delay (FID)
*/
`;
const hydrationPath = path.join(exampleDir, 'incremental-hydration.component.ts');
if (!await fs.pathExists(hydrationPath)) {
await fs.writeFile(hydrationPath, hydrationExample);
this.progressReporter?.info('✓ Created incremental hydration example');
}
}
catch (error) {
this.progressReporter?.warn(`Could not implement incremental hydration: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Implement advanced SSR optimizations
*/
async implementAdvancedSSROptimizations(projectPath) {
try {
const serverDir = path.join(projectPath, 'src/app/server');
await fs.ensureDir(serverDir);
// Create advanced SSR configuration
const ssrConfig = `/*
* Advanced SSR Optimizations for Angular 19+
*
* Demonstrates cutting-edge server-side rendering optimizations
* for maximum performance and user experience.
*/
import { ApplicationConfig } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideServerRendering } from '@angular/platform-server';
import { provideHttpClient, withFetch } from '@angular/common/http';
// Advanced SSR configuration
export const advancedSSRConfig: ApplicationConfig = {
providers: [
provideRouter(routes),
// Advanced server rendering with optimizations
provideServerRendering({
// Enable streaming for faster TTFB
streaming: true,
// Cache frequently requested pages
caching: {
enabled: true,
ttl: 300, // 5 minutes
strategy: 'lru',
maxSize: 100
},
// Optimize critical resource hints
resourceHints: {
preload: ['fonts', 'critical-css'],
prefetch: ['next-page-bundles'],
preconnect: ['api-endpoints']
},
// Advanced hydration settings
hydration: {
strategy: 'incremental',
prioritize: ['above-fold', 'interactive'],
defer: ['analytics', 'tracking']
}
}),
// Use fetch API for better server performance
provideHttpClient(withFetch()),
// Additional SSR optimizations
// ... your other providers
]
};
// Server-side caching service
export class SSRCacheService {
private cache = new Map<string, { data: any; timestamp: number; ttl: number }>();
set(key: string, data: any, ttl: number = 300000): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
ttl
});
}
get(key: string): any | null {
const entry = this.cache.get(key);
if (!entry) return null;
if (Date.now() - entry.timestamp > entry.ttl) {
this.cache.delete(key);
return null;
}
return entry.data;
}
clear(): void {
this.cache.clear();
}
// Clean expired entries
cleanup(): void {
const now = Date.now();
for (const [key, entry] of this.cache.entries()) {
if (now - entry.timestamp > entry.ttl) {
this.cache.delete(key);
}
}
}
}
// Advanced page optimization
export class PageOptimizationService {
// Critical CSS inlining
inlineCriticalCSS(html: string, criticalCSS: string): string {
const inlineStyle = '<style>' + criticalCSS + '</style>';
return html.replace('</head>', inlineStyle + '</head>');
}
// Resource hints injection
injectResourceHints(html: string, hints: ResourceHints): string {
let hintsHTML = '';
// Preload critical resources
hints.preload.forEach(resource => {
hintsHTML += '<link rel="preload" href="' + resource + '" as="style">\n';
});
// Prefetch next page resources
hints.prefetch.forEach(resource => {
hintsHTML += '<link rel="prefetch" href="' + resource + '">\n';
});
// Preconnect to external domains
hints.preconnect.forEach(domain => {
hintsHTML += '<link rel="preconnect" href="' + domain + '">\n';
});
return html.replace('</head>', hintsHTML + '</head>');
}
// Optimize images for SSR
optimizeImages(html: string): string {
// Add loading="lazy" to images below the fold
return html.replace(
/<img(?![^>]*loading=)[^>]*>/g,
(match) => {
if (match.includes('above-fold')) {
return match;
}
return match.replace('<img', '<img loading="lazy"');
}
);
}
// Add performance timing
addPerformanceTiming(html: string): string {
const timing = \`
<script>
window.ssrStartTime = Date.now();
window.addEventListener('DOMContentLoaded', () => {
const ssrTime = Date.now() - window.ssrStartTime;
console.log('SSR hydration time:', ssrTime + 'ms');
});
</script>
\`;
return html.replace('</body>', timing + '</body>');
}
}
interface ResourceHints {
preload: string[];
prefetch: string[];
preconnect: string[];
}
// Express.js middleware for advanced SSR
export function advancedSSRMiddleware() {
const cacheService = new SSRCacheService();
const optimizationService = new PageOptimizationService();
return async (req: any, res: any, next: any) => {
const cacheKey = req.url;
// Check cache first
const cachedResponse = cacheService.get(cacheKey);
if (cachedResponse) {
res.setHeader('X-Cache', 'HIT');
return res.send(cachedResponse);
}
// Intercept response to add optimizations
const originalSend = res.send;
res.send = function(html: string) {
try {
// Apply optimizations
let optimizedHTML = html;
// Inline critical CSS
optimizedHTML = optimizationService.inlineCriticalCSS(
optimizedHTML,
getCriticalCSS(req.url)
);
// Add resource hints
optimizedHTML = optimizationService.injectResourceHints(
optimizedHTML,
getResourceHints(req.url)
);
// Optimize images
optimizedHTML = optimizationService.optimizeImages(optimizedHTML);
// Add performance timing
optimizedHTML = optimizationService.addPerformanceTiming(optimizedHTML);
// Cache the optimized response
cacheService.set(cacheKey, optimizedHTML);
res.setHeader('X-Cache', 'MISS');
originalSend.call(this, optimizedHTML);
} catch (error) {
console.error('SSR optimization error:', error);
originalSend.call(this, html);
}
};
next();
};
}
// Helper functions
function getCriticalCSS(url: string): string {
// Implementation would extract critical CSS based on route
return '/* Critical CSS for ' + url + ' */';
}
function getResourceHints(url: string): ResourceHints {
// Implementation would return appropriate hints based on route
return {
preload: ['/assets/fonts/roboto.woff2'],
prefetch: ['/next-page-bundle.js'],
preconnect: ['https://api.example.com']
};
}
const routes = [
// Your application routes
];
/*
Advanced SSR Optimizations in Angular 19:
1. Streaming SSR:
- Faster Time to First Byte (TTFB)
- Progressive content rendering
- Better user perceived performance
- Reduced server memory usage
2. Intelligent Caching:
- Page-level caching with TTL
- Component-level caching
- CDN integration
- Cache invalidation strategies
3. Resource Optimization:
- Critical CSS inlining
- Resource hints (preload, prefetch, preconnect)
- Image optimization
- Font optimization
4. Performance Monitoring:
- Server-side timing
- Core Web Vitals tracking
- Error monitoring
- Performance budgets
5. Security Enhancements:
- Content Security Policy (CSP)
- Security headers
- XSS protection
- CSRF protection
Best Practices:
- Monitor server performance metrics
- Implement proper error handling
- Use CDN for static assets
- Optimize database queries
- Implement graceful degradation
- Test on various devices and networks
*/
`;
const ssrConfigPath = path.join(serverDir, 'advanced-ssr.config.ts');
if (!await fs.pathExists(ssrConfigPath)) {
await fs.writeFile(ssrConfigPath, ssrConfig);
this.progressReporter?.info('✓ Created advanced SSR optimization configuration');
}
}
catch (error) {
this.progressReporter?.warn(`Could not implement advanced SSR optimizations: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Enhanced performance monitoring
*/
async enhancePerformanceMonitoring(projectPath) {
try {
const servicesDir = path.join(projectPath, 'src/app/services');
await fs.ensureDir(servicesDir);
// Create performance monitoring service
const performanceService = `/*
* Enhanced Performance Monitoring for Angular 19+
*
* Comprehensive performance tracking and optimization
* for modern Angular applications.
*/
import { Injectable, signal, effect } from '@angular/core';
export interface PerformanceMetrics {
// Core Web Vitals
lcp: number; // Largest Contentful Paint
fid: number; // First Input Delay
cls: number; // Cumulative Layout Shift
// Navigation timing
ttfb: number; // Time to First Byte
fcp: number; // First Contentful Paint
tti: number; // Time to Interactive
// Angular specific
bootstrapTime: number;
hydrationTime: number;
changeDetectionTime: number;
// User experience
pageLoadTime: number;
interactionTime: number;
errorCount: number;
}
@Injectable({
providedIn: 'root'
})
export class PerformanceMonitoringService {
private metrics = signal<Partial<PerformanceMetrics>>({});
private observers: PerformanceObserver[] = [];
private startTime = performance.now();
constructor() {
this.initializeMonitoring();
this.setupMetricsTracking();
}
getMetrics() {
return this.metrics();
}
private initializeMonitoring(): void {
// Core Web Vitals monitoring
this.observeLCP();
this.observeFID();
this.observeCLS();
// Navigation timing
this.trackNavigationTiming();
// Angular specific timing
this.trackAngularBootstrap();
this.trackHydrationTime();
// User interaction monitoring
this.trackUserInteractions();
// Error monitoring
this.trackErrors();
}
private observeLCP(): void {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
this.updateMetric('lcp', lastEntry.startTime);
});
observer.observe({ entryTypes: ['largest-contentful-paint'] });
this.observers.push(observer);
}
}
private observeFID(): void {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry: any) => {
this.updateMetric('fid', entry.processingStart - entry.startTime);
});
});
observer.observe({ entryTypes: ['first-input'] });
this.observers.push(observer);
}
}
private observeCLS(): void {
if ('PerformanceObserver' in window) {
let clsValue = 0;
const observer = new PerformanceObserver((list) => {
const entries = list.getEntries();
entries.forEach((entry: any) => {
if (!entry.hadRecentInput) {
clsValue += entry.value;
this.updateMetric('cls', clsValue);
}
});
});
observer.observe({ entryTypes: ['layout-shift'] });
this.observers.push(observer);
}
}
private trackNavigationTiming(): void {
// Wait for navigation timing to be available
setTimeout(() => {
const navigation = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
if (navigation) {
this.updateMetric('ttfb', navigation.responseStart - navigation.requestStart);
this.updateMetric('pageLoadTime', navigation.loadEventEnd - navigation.fetchStart);
}
// First Contentful Paint
const fcpEntry = performance.getEntriesByName('first-contentful-paint')[0];
if (fcpEntry) {
this.updateMetric('fcp', fcpEntry.startTime);
}
}, 1000);
}
private trackAngularBootstrap(): void {
// Track Angular bootstrap time
const bootstrapTime = performance.now() - this.startTime;
this.updateMetric('bootstrapTime', bootstrapTime);
}
private trackHydrationTime(): void {
// Track hydration completion
const checkHydration = () => {
if (document.body.hasAttribute('ng-version')) {
const hydrationTime = performance.now() - this.startTime;
this.updateMetric('hydrationTime', hydrationTime);
} else {
setTimeout(checkHydration, 50);
}
};
checkHydration();
}
private trackUserInteractions(): void {
let firstInteraction = true;
const trackInteraction = () => {
if (firstInteraction) {
const interactionTime = performance.now() - this.startTime;
this.updateMetric('interactionTime', interactionTime);
firstInteraction = false;
}
};
['click', 'keydown', 'touchstart'].forEach(eventType => {
document.addEventListener(eventType, trackInteraction, { once: true, passive: true });
});
}
private trackErrors(): void {
let errorCount = 0;
window.addEventListener('error', () => {
errorCount++;
this.updateMetric('errorCount', errorCount);
});
window.addEventListener('unhandledrejection', () => {
errorCount++;
this.updateMetric('errorCount', errorCount);
});
}
private updateMetric(key: keyof PerformanceMetrics, value: number): void {
this.metrics.update(current => ({ ...current, [key]: value }));
}
private setupMetricsTracking(): void {
// Track metrics changes and report to analytics
effect(() => {
const currentMetrics = this.metrics();
this.reportMetrics(currentMetrics);
});
}
private reportMetrics(metrics: Partial<PerformanceMetrics>): void {
// Report to analytics service
console.log('Performance metrics updated:', metrics);
// You can integrate with analytics services here
// this.analytics.track('performance_metrics', metrics);
}
// Performance optimization suggestions
getOptimizationSuggestions(): string[] {
const metrics = this.metrics();
const suggestions: string[] = [];
if (metrics.lcp && metrics.lcp > 2500) {
suggestions.push('Consider optimizing Largest Contentful Paint (LCP) - current: ' + Math.round(metrics.lcp) + 'ms');
}
if (metrics.fid && metrics.fid > 100) {
suggestions.push('First Input Delay (FID) is high - consider code splitting: ' + Math.round(metrics.fid) + 'ms');
}
if (metrics.cls && metrics.cls > 0.1) {
suggestions.push('Cumulative Layout Shift (CLS) is high - check for layout stability: ' + metrics.cls.toFixed(3));
}
if (metrics.ttfb && metrics.ttfb > 800) {
suggestions.push('Time to First Byte (TTFB) is slow - optimize server response: ' + Math.round(metrics.ttfb) + 'ms');
}
if (metrics.hydrationTime && metrics.hydrationTime > 1000) {
suggestions.push('Hydration time is slow - consider incremental hydration: ' + Math.round(metrics.hydrationTime) + 'ms');
}
return suggestions;
}
// Export metrics for analysis
exportMetrics(): string {
return JSON.stringify(this.metrics(), null, 2);
}
// Clean up observers
destroy(): void {
this.observers.forEach(observer => observer.disconnect());
this.observers = [];
}
}
// Performance monitoring component
import { Component, OnInit, OnDestroy } from '@angular/core';
@Component({
selector: 'app-performance-monitor',
standalone: true,
imports: [CommonModule],
template: \`
<div class="performance-monitor" *ngIf="showMonitor">
<h4>Performance Monitor</h4>
<div class="metrics-grid">
<div class="metric-item" *ngFor="let metric of getMetricsArray()">
<label>{{ metric.name }}:</label>
<span [class]="getMetricClass(metric.key, metric.value)">{{ formatMetric(metric.value, metric.unit) }}</span>
</div>
</div>
<div class="suggestions" *ngIf="suggestions.length > 0">
<h5>Optimization Suggestions:</h5>
<ul>
<li *ngFor="let suggestion of suggestions">{{ suggestion }}</li>
</ul>
</div>
<div class="actions">
<button (click)="exportMetrics()">Export Metrics</button>
<button (click)="toggleMonitor()">Hide Monitor</button>
</div>
</div>
\`,
styles: [\`
.performance-monitor {
position: fixed;
top: 20px;
right: 20px;
background: white;
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
max-width: 400px;
z-index: 1000;
}
.metrics-grid {
display: grid;
grid-template-columns: 1fr;
gap: 8px;
margin: 12px 0;
}
.metric-item {
display: flex;
justify-content: space-between;
font-size: 0.9em;
}
.metric-good { color: #4caf50; }
.metric-warning { color: #ff9800; }
.metric-poor { color: #f44336; }
.suggestions {
margin: 16px 0;
font-size: 0.85em;
}
.suggestions ul {
margin: 8px 0;
padding-left: 20px;
}
.actions {
display: flex;
gap: 8px;
}
button {
padding: 6px 12px;
border: none;
border-radius: 4px;
background: #1976d2;
color: white;
cursor: pointer;
font-size: 0.8em;
}
\`]
})
export class PerformanceMonitorComponent implements OnInit, OnDestroy {
showMonitor = true;
suggestions: string[] = [];
constructor(public performanceService: PerformanceMonitoringService) {}
ngOnInit(): void {
this.updateSuggestions();
// Update suggestions periodically
setInterval(() => {
this.updateSuggestions();
}, 5000);
}
ngOnDestroy(): void {
this.performanceService.destroy();
}
getMetricsArray() {
const metrics = this.performanceService.getMetrics();
return [
{ name: 'LCP', key: 'lcp', value: metrics.lcp, unit: 'ms' },
{ name: 'FID', key: 'fid', value: metrics.fid, unit: 'ms' },
{ name: 'CLS', key: 'cls', value: metrics.cls, unit: '' },
{ name: 'TTFB', key: 'ttfb', value: metrics.ttfb, unit: 'ms' },
{ name: 'Hydration', key: 'hydrationTime', value: metrics.hydrationTime, unit: 'ms' }
].filter(metric => metric.value !== undefined);
}
getMetricClass(key: string, value: number | undefined): string {
if (value === undefined) return '';
const thresholds: Record<string, { good: number; poor: number }> = {
lcp: { good: 2500, poor: 4000 },
fid: { good: 100, poor: 300 },
cls: { good: 0.1, poor: 0.25 },
ttfb: { good: 800, poor: 1800 },
hydrationTime: { good: 1000, poor: 3000 }
};
const threshold = thresholds[key];
if (!threshold) return '';
if (value <= threshold.good) return 'metric-good';
if (value <= threshold.poor) return 'metric-warning';
return 'metric-poor';
}
formatMetric(value: number | undefined, unit: string): string {
if (value === undefined) return 'N/A';
if (unit === 'ms') return Math.round(value) + unit;
if (unit === '') return value.toFixed(3);
return value + unit;
}
updateSuggestions(): void {
this.suggestions = this.performanceService.getOptimizationSuggestions();
}
exportMetrics(): void {
const metrics = this.performanceService.exportMetrics();
const blob = new Blob([metrics], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'performance-metrics.json';
a.click();
URL.revokeObjectURL(url);
}
toggleMonitor(): void {
this.showMonitor = !this.showMonitor;
}
}
`;
const performancePath = path.join(servicesDir, 'performance-monitoring.service.ts');
if (!await fs.pathExists(performancePath)) {
await fs.writeFile(performanceService, performancePath);
this.progressReporter?.info('✓ Created enhanced performance monitoring service');
}
}
catch (error) {
this.progressReporter?.warn(`Could not enhance performance monitoring: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Improved developer experience features
*/
async improveDeveloperExperience(projectPath) {
const angularJsonPath = path.join(projectPath, 'angular.json');
if (await fs.pathExists(angularJsonPath)) {