@silvestv/migration-planificator 5.0.2 → 6.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.fr.md +10 -11
  2. package/README.md +10 -11
  3. package/dist/client.bundle.js +98 -98
  4. package/dist/src/config/migration.config.js +69 -0
  5. package/dist/src/core/app-analyzer.js +69 -79
  6. package/dist/src/core/ast/matchers/html/html-element-matcher.js +32 -7
  7. package/dist/src/core/ast/matchers/html/index.js +36 -4
  8. package/dist/src/core/ast/matchers/ts/collection-matcher.js +18 -0
  9. package/dist/src/core/ast/matchers/ts/decorator-matcher.js +33 -4
  10. package/dist/src/core/ast/matchers/ts/expression-matcher.js +1 -1
  11. package/dist/src/core/ast/matchers/ts/node-matcher.js +4 -0
  12. package/dist/src/core/project-detector.js +94 -106
  13. package/dist/src/core/project-strategy/nx-strategy.js +25 -22
  14. package/dist/src/core/project-strategy/standalone-strategy.js +43 -14
  15. package/dist/src/core/rules-loader.js +21 -13
  16. package/dist/src/core/scan-reporter.js +3 -1
  17. package/dist/src/core/scanner-orchestrator.js +1 -1
  18. package/dist/src/core/workload/constants.js +7 -4
  19. package/dist/src/data/markdown/angular-migration-20-21.md +1010 -0
  20. package/dist/src/data/rules/to19/rules-19-optionnelle.json +3 -0
  21. package/dist/src/data/rules/to21/rules-21-obligatoire.json +423 -0
  22. package/dist/src/data/rules/to21/rules-21-optionnelle.json +253 -0
  23. package/dist/src/data/rules/to21/rules-21-recommande.json +139 -0
  24. package/dist/src/index.js +1 -1
  25. package/dist/src/templates/landing/project-overview.template.js +2 -1
  26. package/dist/src/templates/page/migration-guide.template.js +1 -0
  27. package/dist/src/templates/workload/guide-rule-card.template.js +3 -1
  28. package/dist/src/templates/workload/hierarchy-shared.js +6 -5
  29. package/dist/src/templates/workload/unified-settings-panel.template.js +27 -33
  30. package/dist/src/utils/core/args-parser.js +19 -18
  31. package/dist/styles.css +1 -1
  32. package/package.json +3 -2
@@ -0,0 +1,1010 @@
1
+ # Guide Migration Angular 20→21 avec Exemples
2
+
3
+ ## 🔴 RÈGLES OBLIGATOIRES (Breaking Changes)
4
+
5
+ ### 1. Environnement & Mise à jour
6
+ ```bash
7
+ # ✅ Mise à jour obligatoire
8
+ npm install typescript@~5.9.0 # Minimum 5.9 requis
9
+ ng update @angular/core@21 @angular/cli@21
10
+ ng update @angular/material@21 # Si Material est utilisé
11
+ ```
12
+
13
+ ### 2. Zoneless par défaut - Migration Applications Zone.js
14
+ ```typescript
15
+ // ⚠️ BREAKING CHANGE: Les nouvelles apps sont zoneless par défaut
16
+ // Les applications existantes utilisant Zone.js doivent ajouter explicitement provideZoneChangeDetection()
17
+
18
+ // ❌ Avant (implicite)
19
+ bootstrapApplication(AppComponent, {
20
+ providers: [/* Zone.js géré automatiquement */]
21
+ });
22
+
23
+ // ✅ Après - Standalone apps
24
+ import { provideZoneChangeDetection } from '@angular/core';
25
+
26
+ bootstrapApplication(AppComponent, {
27
+ providers: [
28
+ provideZoneChangeDetection(), // Obligatoire si vous utilisez Zone.js
29
+ // ... autres providers
30
+ ]
31
+ });
32
+
33
+ // ✅ Après - NgModule apps
34
+ @NgModule({
35
+ providers: [
36
+ provideZoneChangeDetection() // À ajouter dans le AppModule
37
+ ]
38
+ })
39
+ export class AppModule {}
40
+
41
+ // Note: Une migration automatique devrait gérer cela lors du ng update
42
+ ```
43
+
44
+ ### 3. Signal Inputs sur Custom Elements
45
+ ```typescript
46
+ // ⚠️ BREAKING CHANGE: Accès aux signal inputs sur les Angular Custom Elements
47
+
48
+ // ❌ Avant - Appel de fonction
49
+ const value = elementRef.newInput(); // Function call
50
+
51
+ // ✅ Après - Accès direct (aligne avec les decorator-based inputs)
52
+ const value = elementRef.newInput; // Direct property access
53
+ ```
54
+
55
+ ### 4. Scheduler interne provideZoneChangeDetection
56
+ ```typescript
57
+ // ⚠️ BREAKING CHANGE: Le scheduler interne est maintenant toujours activé
58
+ // quand provideZoneChangeDetection() est utilisé sans le polyfill ZoneJS
59
+
60
+ // Vérifier le comportement de votre app - le timing peut avoir changé
61
+ // si vous dépendiez du scheduler désactivé auparavant
62
+ ```
63
+
64
+ ### 5. Suppression de la propriété 'interpolation'
65
+ ```typescript
66
+ // ❌ Avant
67
+ @Component({
68
+ selector: 'app-custom',
69
+ template: `<% name %>`,
70
+ interpolation: ['<%', '%>'] // Propriété supprimée
71
+ })
72
+
73
+ // ✅ Après - Seuls {{ }} sont supportés
74
+ @Component({
75
+ selector: 'app-custom',
76
+ template: `{{ name }}` // Utiliser les marqueurs par défaut uniquement
77
+ })
78
+ ```
79
+
80
+ ### 6. Suppression de la propriété 'moduleId'
81
+ ```typescript
82
+ // ❌ Avant
83
+ @Component({
84
+ selector: 'app-legacy',
85
+ moduleId: module.id, // Propriété supprimée
86
+ templateUrl: './legacy.component.html',
87
+ styleUrls: ['./legacy.component.css']
88
+ })
89
+
90
+ // ✅ Après - Supprimer simplement moduleId (géré par les build tools modernes)
91
+ @Component({
92
+ selector: 'app-legacy',
93
+ templateUrl: './legacy.component.html',
94
+ styleUrls: ['./legacy.component.css']
95
+ })
96
+ ```
97
+
98
+ ### 7. ngComponentOutletContent strictement typé
99
+ ```typescript
100
+ // ⚠️ BREAKING CHANGE: Type changé de any[][] vers Node[][]
101
+
102
+ // ❌ Avant
103
+ @Component({
104
+ template: `<ng-container *ngComponentOutlet="comp; content: myContent"></ng-container>`
105
+ })
106
+ export class MyComponent {
107
+ myContent: any[][] = [['texte']]; // any[][] accepté
108
+ }
109
+
110
+ // ✅ Après
111
+ @Component({
112
+ template: `<ng-container *ngComponentOutlet="comp; content: myContent"></ng-container>`
113
+ })
114
+ export class MyComponent {
115
+ myContent: Node[][] | undefined = [
116
+ [document.createTextNode('texte')]
117
+ ]; // Node[][] | undefined requis
118
+ }
119
+ ```
120
+
121
+ ### 8. Host Binding Type Checking activé par défaut
122
+ ```typescript
123
+ // ⚠️ BREAKING CHANGE: Peut faire apparaître de nouvelles erreurs de build
124
+
125
+ // ❌ Avant - Erreurs masquées
126
+ @Component({
127
+ selector: 'app-example',
128
+ host: {
129
+ '[class.active]': 'isActive', // Type non vérifié
130
+ '[attr.aria-label]': 'label'
131
+ }
132
+ })
133
+ export class ExampleComponent {
134
+ isActive: string = 'true'; // Type incorrect mais accepté
135
+ label: number = 123;
136
+ }
137
+
138
+ // ✅ Après - Option 1: Corriger les types
139
+ @Component({
140
+ selector: 'app-example',
141
+ host: {
142
+ '[class.active]': 'isActive',
143
+ '[attr.aria-label]': 'label'
144
+ }
145
+ })
146
+ export class ExampleComponent {
147
+ isActive: boolean = true; // Type correct
148
+ label: string = 'Description'; // Type correct
149
+ }
150
+
151
+ // ✅ Après - Option 2: Désactiver temporairement (non recommandé)
152
+ // tsconfig.json
153
+ {
154
+ "angularCompilerOptions": {
155
+ "typeCheckHostBindings": false
156
+ }
157
+ }
158
+ ```
159
+
160
+ ### 9. ApplicationConfig depuis @angular/core
161
+ ```typescript
162
+ // ❌ Avant
163
+ import { ApplicationConfig } from '@angular/platform-browser'; // Supprimé
164
+
165
+ // ✅ Après
166
+ import { ApplicationConfig } from '@angular/core';
167
+
168
+ export const appConfig: ApplicationConfig = {
169
+ providers: [
170
+ // ...
171
+ ]
172
+ };
173
+ ```
174
+
175
+ ### 10. Suppression de ignoreChangesOutsideZone
176
+ ```typescript
177
+ // ❌ Avant - Dans polyfills.ts ou zone-flags.ts
178
+ (window as any).__zone_symbol__ignoreChangesOutsideZone = true;
179
+
180
+ // ✅ Après - Supprimer cette option, elle n'est plus disponible
181
+ // Si vous avez besoin de ce comportement, migrez vers zoneless
182
+ ```
183
+
184
+ ### 11. TestBed rethrows errors avec provideZoneChangeDetection
185
+ ```typescript
186
+ // ⚠️ BREAKING CHANGE: TestBed relance maintenant les erreurs
187
+
188
+ // ❌ Avant - Erreurs potentiellement avalées
189
+ beforeEach(() => {
190
+ TestBed.configureTestingModule({
191
+ providers: [provideZoneChangeDetection()]
192
+ });
193
+ });
194
+
195
+ // ✅ Après - Option 1: Corriger les erreurs sous-jacentes dans vos tests
196
+
197
+ // ✅ Après - Option 2: Désactiver (dernier recours)
198
+ beforeEach(() => {
199
+ TestBed.configureTestingModule({
200
+ providers: [provideZoneChangeDetection()],
201
+ rethrowApplicationErrors: false // Désactive le rethrow
202
+ });
203
+ });
204
+ ```
205
+
206
+ ### 12. Tests Navigation Router - Timing
207
+ ```typescript
208
+ // ⚠️ BREAKING CHANGE: Les navigations peuvent nécessiter des microtasks supplémentaires
209
+
210
+ // ❌ Avant
211
+ it('should navigate', () => {
212
+ router.navigate(['/home']);
213
+ expect(location.path()).toBe('/home'); // Peut échouer
214
+ });
215
+
216
+ // ✅ Après - Option 1: fakeAsync avec flush
217
+ it('should navigate', fakeAsync(() => {
218
+ router.navigate(['/home']);
219
+ flush(); // Attendre toutes les microtasks
220
+ expect(location.path()).toBe('/home');
221
+ }));
222
+
223
+ // ✅ Après - Option 2: async/await
224
+ it('should navigate', async () => {
225
+ await router.navigate(['/home']);
226
+ expect(location.path()).toBe('/home');
227
+ });
228
+
229
+ // ✅ Après - Option 3: Observable
230
+ it('should navigate', (done) => {
231
+ router.navigate(['/home']).then(() => {
232
+ expect(location.path()).toBe('/home');
233
+ done();
234
+ });
235
+ });
236
+ ```
237
+
238
+ ### 13. TestBed FakePlatformLocation
239
+ ```typescript
240
+ // ⚠️ BREAKING CHANGE: Nouvelle FakePlatformLocation par défaut
241
+
242
+ // ❌ Avant - MockPlatformLocation utilisé implicitement
243
+
244
+ // ✅ Après - Si vos tests échouent, restaurer l'ancien comportement
245
+ import { MockPlatformLocation } from '@angular/common/testing';
246
+ import { PlatformLocation } from '@angular/common';
247
+
248
+ beforeEach(() => {
249
+ TestBed.configureTestingModule({
250
+ providers: [
251
+ { provide: PlatformLocation, useClass: MockPlatformLocation }
252
+ ]
253
+ });
254
+ });
255
+ ```
256
+
257
+ ### 14. UpgradeAdapter supprimé
258
+ ```typescript
259
+ // ❌ Avant
260
+ import { UpgradeAdapter } from '@angular/upgrade';
261
+
262
+ const upgradeAdapter = new UpgradeAdapter(AppModule);
263
+
264
+ // ✅ Après - Utiliser les APIs statiques
265
+ import {
266
+ downgradeComponent,
267
+ downgradeInjectable,
268
+ UpgradeModule
269
+ } from '@angular/upgrade/static';
270
+
271
+ @NgModule({
272
+ imports: [UpgradeModule],
273
+ // ...
274
+ })
275
+ export class AppModule {
276
+ constructor(private upgrade: UpgradeModule) {}
277
+
278
+ ngDoBootstrap() {
279
+ this.upgrade.bootstrap(document.body, ['myAngularJSApp']);
280
+ }
281
+ }
282
+ ```
283
+
284
+ ### 15. FormArray Directive - Conflit potentiel
285
+ ```typescript
286
+ // ⚠️ BREAKING CHANGE: Nouvelle directive standalone formArray peut créer des conflits
287
+
288
+ // ❌ Avant - Directive custom nommée FormArray
289
+ @Directive({
290
+ selector: '[formArray]', // Conflit potentiel
291
+ standalone: true
292
+ })
293
+ export class FormArrayDirective { }
294
+
295
+ // ✅ Après - Renommer votre directive custom
296
+ @Directive({
297
+ selector: '[customFormArray]', // Nom différent
298
+ standalone: true
299
+ })
300
+ export class CustomFormArrayDirective { }
301
+
302
+ // Ou renommer l'input si sur un élément avec reactive forms
303
+ @Component({
304
+ template: `<div [customArray]="data" [formGroup]="form"></div>`
305
+ })
306
+ ```
307
+
308
+ ### 16. NgModuleFactory supprimé
309
+ ```typescript
310
+ // ❌ Avant
311
+ import { NgModuleFactory, Compiler } from '@angular/core';
312
+
313
+ @Injectable()
314
+ export class DynamicLoader {
315
+ constructor(private compiler: Compiler) {}
316
+
317
+ async loadModule(): Promise<NgModuleFactory<any>> {
318
+ const module = await import('./lazy.module');
319
+ return this.compiler.compileModuleAsync(module.LazyModule);
320
+ }
321
+ }
322
+
323
+ // ✅ Après - Utiliser NgModule directement
324
+ import { createNgModule, Injector, Type } from '@angular/core';
325
+
326
+ @Injectable()
327
+ export class DynamicLoader {
328
+ constructor(private injector: Injector) {}
329
+
330
+ async loadModule<T>(moduleType: Type<T>) {
331
+ const moduleRef = createNgModule(moduleType, this.injector);
332
+ return moduleRef;
333
+ }
334
+ }
335
+
336
+ // Ou pour les composants standalone (recommandé)
337
+ const component = await import('./lazy.component').then(m => m.LazyComponent);
338
+ ```
339
+
340
+ ### 17. emitDeclarationOnly non supporté
341
+ ```json
342
+ // ❌ Avant - tsconfig.json
343
+ {
344
+ "compilerOptions": {
345
+ "emitDeclarationOnly": true // Non supporté
346
+ }
347
+ }
348
+
349
+ // ✅ Après - Désactiver cette option
350
+ {
351
+ "compilerOptions": {
352
+ "emitDeclarationOnly": false // Ou supprimer la ligne
353
+ }
354
+ }
355
+ ```
356
+
357
+ ### 18. lastSuccessfulNavigation est un Signal
358
+ ```typescript
359
+ // ⚠️ BREAKING CHANGE: Propriété convertie en signal
360
+
361
+ // ❌ Avant
362
+ @Component({/*...*/})
363
+ export class NavComponent {
364
+ constructor(private router: Router) {
365
+ const lastNav = this.router.lastSuccessfulNavigation; // Accès direct
366
+ }
367
+ }
368
+
369
+ // ✅ Après - Invoquer comme une fonction
370
+ @Component({/*...*/})
371
+ export class NavComponent {
372
+ constructor(private router: Router) {
373
+ const lastNav = this.router.lastSuccessfulNavigation(); // Appel de fonction
374
+ }
375
+
376
+ // Dans le template
377
+ // {{ router.lastSuccessfulNavigation()?.extractedUrl }}
378
+ }
379
+ ```
380
+
381
+ ---
382
+
383
+ ## 🟡 RÈGLES RECOMMANDÉES
384
+
385
+ ### 1. Migration vers Vitest (Test Runner par défaut)
386
+ ```bash
387
+ # Vitest est maintenant le test runner par défaut et stable
388
+ # Jest et Web Test Runner expérimentaux seront supprimés en v22
389
+
390
+ # Migration automatique Jasmine → Vitest
391
+ ng generate @schematics/angular:refactor-jasmine-vitest
392
+
393
+ # Options utiles:
394
+ # --add-imports : Ajoute les imports Vitest explicites
395
+ # --file-suffix : Si vous utilisez un suffixe différent de .spec.ts
396
+ # --include : Cibler des fichiers/dossiers spécifiques
397
+ ```
398
+
399
+ ```json
400
+ // ✅ Configuration minimale angular.json
401
+ {
402
+ "test": {
403
+ "builder": "@angular/build:unit-test"
404
+ }
405
+ }
406
+ ```
407
+
408
+ ```typescript
409
+ // ⚠️ Note: Les tests fakeAsync doivent être réécrits
410
+ // fakeAsync repose sur Zone.js patchée, pas de version Vitest
411
+
412
+ // ❌ Avant - Jasmine avec fakeAsync
413
+ it('should work', fakeAsync(() => {
414
+ tick(1000);
415
+ expect(value).toBe(true);
416
+ }));
417
+
418
+ // ✅ Après - Vitest avec vi.useFakeTimers ou async/await
419
+ import { vi } from 'vitest';
420
+
421
+ it('should work', () => {
422
+ vi.useFakeTimers();
423
+ vi.advanceTimersByTime(1000);
424
+ expect(value).toBe(true);
425
+ vi.useRealTimers();
426
+ });
427
+ ```
428
+
429
+ ### 2. HttpClient fourni par défaut
430
+ ```typescript
431
+ // ✅ HttpClient est maintenant fourni dans le root injector par défaut
432
+ // Plus besoin de provideHttpClient() sauf pour personnalisation
433
+
434
+ // ❌ Avant
435
+ export const appConfig: ApplicationConfig = {
436
+ providers: [
437
+ provideHttpClient(), // N'est plus nécessaire
438
+ provideHttpClientTesting() // Pour les tests
439
+ ]
440
+ };
441
+
442
+ // ✅ Après - Configuration basique
443
+ export const appConfig: ApplicationConfig = {
444
+ providers: [
445
+ // HttpClient disponible automatiquement
446
+ ]
447
+ };
448
+
449
+ // ✅ Après - Avec interceptors (provideHttpClient toujours requis)
450
+ export const appConfig: ApplicationConfig = {
451
+ providers: [
452
+ provideHttpClient(
453
+ withInterceptors([authInterceptor])
454
+ )
455
+ ]
456
+ };
457
+
458
+ // ✅ Tests simplifiés
459
+ beforeEach(() => {
460
+ TestBed.configureTestingModule({
461
+ providers: [
462
+ provideHttpClientTesting() // Suffit maintenant
463
+ ]
464
+ });
465
+ });
466
+ ```
467
+
468
+ ### 3. SimpleChanges générique (Type-Safe)
469
+ ```typescript
470
+ // ✅ SimpleChanges supporte maintenant les génériques
471
+
472
+ interface UserInputs {
473
+ name: string;
474
+ age: number;
475
+ }
476
+
477
+ @Component({/*...*/})
478
+ export class UserComponent implements OnChanges {
479
+ @Input({ required: true }) name!: string;
480
+ @Input({ required: true }) age!: number;
481
+
482
+ // ❌ Avant - any implicite
483
+ ngOnChanges(changes: SimpleChanges): void {
484
+ const nameChange = changes['name']; // SimpleChange avec any
485
+ }
486
+
487
+ // ✅ Après - Type-safe
488
+ ngOnChanges(changes: SimpleChanges<UserComponent>): void {
489
+ if (changes.age) {
490
+ const newAge: number = changes.age.currentValue; // Typé!
491
+ const oldAge: number | undefined = changes.age.previousValue;
492
+ console.log(`Age changed from ${oldAge} to ${newAge}`);
493
+ }
494
+ }
495
+ }
496
+ ```
497
+
498
+ ### 4. Migration NgClass vers Class Binding
499
+ ```bash
500
+ # Schematic de migration automatique (officiel Angular)
501
+ ng generate @angular/core:ngclass-to-class
502
+
503
+ # Options disponibles:
504
+ # --path : Sous-répertoire à migrer
505
+ # --migrate-space-separated-key : Migre les clés avec espaces
506
+ ```
507
+
508
+ ```typescript
509
+ // ❌ Avant
510
+ @Component({
511
+ imports: [NgClass],
512
+ template: `
513
+ <button [ngClass]="{ 'active': isActive(), 'disabled': isDisabled }">
514
+ Click
515
+ </button>
516
+ `
517
+ })
518
+
519
+ // ✅ Après
520
+ @Component({
521
+ // imports: [NgClass] - Plus nécessaire
522
+ template: `
523
+ <button [class]="{ 'active': isActive(), 'disabled': isDisabled }">
524
+ Click
525
+ </button>
526
+ `
527
+ })
528
+ ```
529
+
530
+ ### 5. Migration NgStyle vers Style Binding
531
+ ```bash
532
+ # Schematic de migration automatique (officiel Angular)
533
+ ng generate @angular/core:ngstyle-to-style
534
+ ```
535
+
536
+ ```typescript
537
+ // ❌ Avant
538
+ @Component({
539
+ imports: [NgStyle],
540
+ template: `
541
+ <div [ngStyle]="{ 'background-color': bgColor(), 'font-size': fontSize }">
542
+ Content
543
+ </div>
544
+ `
545
+ })
546
+
547
+ // ✅ Après
548
+ @Component({
549
+ // imports: [NgStyle] - Plus nécessaire
550
+ template: `
551
+ <div [style]="{ 'background-color': bgColor(), 'font-size': fontSize }">
552
+ Content
553
+ </div>
554
+ `
555
+ })
556
+ ```
557
+
558
+ ### 6. Migration CommonModule vers Imports Standalone
559
+ ```bash
560
+ # Schematic de migration automatique
561
+ ng generate @angular/core:common-to-standalone
562
+ ```
563
+
564
+ ```typescript
565
+ // ❌ Avant
566
+ @Component({
567
+ imports: [CommonModule], // Import global
568
+ template: `
569
+ @for (item of items; track item.id) {
570
+ <div>{{ item.name | uppercase }}</div>
571
+ }
572
+ `
573
+ })
574
+
575
+ // ✅ Après
576
+ import { UpperCasePipe } from '@angular/common';
577
+
578
+ @Component({
579
+ imports: [UpperCasePipe], // Imports individuels
580
+ template: `
581
+ @for (item of items; track item.id) {
582
+ <div>{{ item.name | uppercase }}</div>
583
+ }
584
+ `
585
+ })
586
+ ```
587
+
588
+ ---
589
+
590
+ ## 🟢 RÈGLES OPTIONNELLES
591
+
592
+ ### 1. Signal Forms (Expérimental)
593
+ ```typescript
594
+ // ✅ Nouvelle API expérimentale de formulaires basée sur les Signals
595
+ import { form, Field, required, minLength, maxLength } from '@angular/forms/signals';
596
+
597
+ @Component({
598
+ imports: [Field],
599
+ template: `
600
+ <form (ngSubmit)="onSubmit()">
601
+ <input [field]="loginForm.email" type="email" />
602
+ @for (err of loginForm.email().errors(); track $index) {
603
+ @if (err.kind === 'required') {
604
+ <p>Email requis</p>
605
+ }
606
+ }
607
+
608
+ <input [field]="loginForm.password" type="password" />
609
+ <button [disabled]="loginForm().invalid()">Submit</button>
610
+ </form>
611
+
612
+ <pre>{{ loginForm().value() | json }}</pre>
613
+ `
614
+ })
615
+ export class LoginComponent {
616
+ // Signal comme modèle du formulaire
617
+ private login = signal({
618
+ email: '',
619
+ password: ''
620
+ });
621
+
622
+ // Formulaire avec validation
623
+ loginForm = form(this.login, (path) => {
624
+ required(path.email, { message: 'Email is required' });
625
+ required(path.password);
626
+ minLength(path.password, 6, {
627
+ message: (pwd) => `Min 6 chars, got ${pwd.value().length}`
628
+ });
629
+ });
630
+
631
+ onSubmit() {
632
+ if (!this.loginForm().invalid()) {
633
+ console.log('Form value:', this.login());
634
+ }
635
+ }
636
+ }
637
+
638
+ // Note: Plus besoin de ControlValueAccessor pour les composants custom
639
+ // La liaison est entièrement signal-based
640
+ ```
641
+
642
+ ### 2. Angular Aria (Developer Preview)
643
+ ```bash
644
+ # Bibliothèque de composants headless accessibles
645
+ npm install @angular/aria
646
+ ```
647
+
648
+ ```typescript
649
+ // ✅ Composants headless accessibles et personnalisables
650
+ // 8 patterns, 13 composants: Accordion, Combobox, Grid, Listbox,
651
+ // Menu, Tabs, Toolbar, Tree
652
+
653
+ import { CdkAccordion, CdkAccordionItem } from '@angular/aria';
654
+
655
+ @Component({
656
+ imports: [CdkAccordion, CdkAccordionItem],
657
+ template: `
658
+ <div cdkAccordion>
659
+ <div cdkAccordionItem #item1="cdkAccordionItem">
660
+ <button (click)="item1.toggle()">Section 1</button>
661
+ @if (item1.expanded) {
662
+ <div>Contenu section 1</div>
663
+ }
664
+ </div>
665
+ </div>
666
+ `
667
+ })
668
+ export class AccessibleAccordionComponent {}
669
+ ```
670
+
671
+ ### 3. Regex dans les Templates
672
+ ```typescript
673
+ // ✅ Les expressions régulières sont maintenant supportées dans les templates
674
+
675
+ @Component({
676
+ template: `
677
+ @let isValidNumber = /\\d+/.test(inputValue());
678
+
679
+ @if (!isValidNumber) {
680
+ <p>{{ inputValue() }} n'est pas un nombre valide!</p>
681
+ }
682
+
683
+ @let isEmail = /^[^@]+@[^@]+\\.[^@]+$/.test(email());
684
+ @if (!isEmail) {
685
+ <p>Email invalide</p>
686
+ }
687
+ `
688
+ })
689
+ export class ValidationComponent {
690
+ inputValue = signal('abc');
691
+ email = signal('test@');
692
+ }
693
+ ```
694
+
695
+ ### 4. IntersectionObserver Options pour @defer
696
+ ```typescript
697
+ // ✅ Nouvelles options pour le trigger viewport de @defer
698
+
699
+ @Component({
700
+ template: `
701
+ <!-- Déclencher 100px avant d'entrer dans le viewport -->
702
+ @defer (on viewport({ trigger: triggerRef, rootMargin: '100px' })) {
703
+ <heavy-component />
704
+ } @placeholder {
705
+ <div #triggerRef>Loading...</div>
706
+ }
707
+
708
+ <!-- Avec threshold personnalisé -->
709
+ @defer (on viewport({ trigger: el, threshold: 0.5 })) {
710
+ <analytics-widget />
711
+ }
712
+ `
713
+ })
714
+ export class LazyLoadComponent {}
715
+ ```
716
+
717
+ ### 5. HttpResponse.responseType
718
+ ```typescript
719
+ // ✅ Nouvelle propriété pour diagnostiquer les problèmes CORS
720
+
721
+ @Injectable({ providedIn: 'root' })
722
+ export class DataService {
723
+ private http = inject(HttpClient);
724
+
725
+ getData(): Observable<any> {
726
+ return this.http.get('/api/data', { observe: 'response' }).pipe(
727
+ tap(response => {
728
+ console.log('Response type:', response.responseType);
729
+ // 'basic' | 'cors' | 'opaque' | 'opaqueredirect'
730
+
731
+ if (response.responseType === 'opaque') {
732
+ console.warn('CORS issue detected - response is opaque');
733
+ }
734
+ }),
735
+ map(response => response.body)
736
+ );
737
+ }
738
+ }
739
+ ```
740
+
741
+ ### 6. Router Scroll Option
742
+ ```typescript
743
+ // ✅ Nouvelle option scroll pour contrôler le comportement par navigation
744
+
745
+ @Component({/*...*/})
746
+ export class NavigationComponent {
747
+ private router = inject(Router);
748
+
749
+ navigateWithoutScroll() {
750
+ // Désactive le scroll même si activé globalement
751
+ this.router.navigateByUrl('/target', { scroll: 'manual' });
752
+ }
753
+
754
+ navigateWithScroll() {
755
+ // Suit le comportement global
756
+ this.router.navigateByUrl('/target', { scroll: 'after-transition' });
757
+ }
758
+ }
759
+
760
+ // Configuration globale du scroll
761
+ bootstrapApplication(AppComponent, {
762
+ providers: [
763
+ provideRouter(routes,
764
+ withInMemoryScrolling({ scrollPositionRestoration: 'enabled' })
765
+ )
766
+ ]
767
+ });
768
+ ```
769
+
770
+ ### 7. Tailwind CSS Schematic
771
+ ```bash
772
+ # Nouveau schematic pour configurer Tailwind facilement
773
+
774
+ # Nouvelle application avec Tailwind
775
+ ng new my-app --style tailwind
776
+
777
+ # Ajouter à un projet existant
778
+ ng add tailwindcss
779
+ ```
780
+
781
+ ### 8. Variables --define pour ng serve
782
+ ```bash
783
+ # ✅ Définir des variables remplacées au build (comme ng build depuis v17.2)
784
+
785
+ ng serve --define VERSION="'1.0.0'" --define API_URL="'http://localhost:3000'"
786
+ ```
787
+
788
+ ```typescript
789
+ // Dans le code
790
+ declare const VERSION: string;
791
+ declare const API_URL: string;
792
+
793
+ // @ts-expect-error defined with --define flag
794
+ console.log(`Version: ${VERSION}, API: ${API_URL}`);
795
+ ```
796
+
797
+ ### 9. ExperimentalIsolatedShadowDom (Expérimental)
798
+ ```typescript
799
+ // ✅ Nouvelle stratégie d'encapsulation des styles
800
+ // Isole complètement le composant, y compris des styles globaux
801
+
802
+ import { ViewEncapsulation } from '@angular/core';
803
+
804
+ @Component({
805
+ selector: 'app-isolated',
806
+ template: `<div class="content">Isolated content</div>`,
807
+ styles: [`
808
+ .content { color: blue; }
809
+ `],
810
+ encapsulation: ViewEncapsulation.ExperimentalIsolatedShadowDom
811
+ // ⚠️ Encore expérimental, pas recommandé pour production
812
+ })
813
+ export class IsolatedComponent {}
814
+ ```
815
+
816
+ ### 10. KeyValuePipe avec clés optionnelles
817
+ ```typescript
818
+ // ✅ keyvalue pipe supporte maintenant les objets avec clés optionnelles
819
+
820
+ interface User {
821
+ name: string;
822
+ surname?: string; // Optionnel
823
+ age?: number; // Optionnel
824
+ }
825
+
826
+ @Component({
827
+ imports: [KeyValuePipe],
828
+ template: `
829
+ @for (prop of user | keyvalue; track $index) {
830
+ <p>{{ prop.key }}: {{ prop.value }}</p>
831
+ }
832
+ `
833
+ })
834
+ export class UserComponent {
835
+ user: User = { name: 'John' }; // Plus d'erreur TypeScript
836
+ }
837
+ ```
838
+
839
+ ### 11. CLDR mis à jour (v41 → v47)
840
+ ```typescript
841
+ // ✅ Mise à jour automatique des données de localisation
842
+ // Peut affecter le formatage des dates, nombres, devises
843
+
844
+ // Vérifier vos tests de formatage localisé
845
+ // Les formats peuvent avoir légèrement changé
846
+ ```
847
+
848
+ ### 12. ng version amélioré
849
+ ```bash
850
+ # ✅ Informations plus détaillées sur les packages
851
+
852
+ ng version
853
+ # Affiche versions demandées vs installées
854
+
855
+ ng version --json
856
+ # Output JSON pour scripts d'automatisation
857
+ ```
858
+
859
+ ---
860
+
861
+ ## 📊 Checklist Migration Complète
862
+
863
+ ```typescript
864
+ interface MigrationStatus21 {
865
+ obligatoire: {
866
+ typescript59: boolean; // v5.9+
867
+ zonelessDefault: boolean; // provideZoneChangeDetection() ajouté
868
+ signalInputsCustomElements: boolean; // Accès direct vs function call
869
+ removeInterpolation: boolean; // Supprimer propriété interpolation
870
+ removeModuleId: boolean; // Supprimer propriété moduleId
871
+ ngComponentOutletContent: boolean; // Type Node[][]
872
+ hostBindingTypeCheck: boolean; // Corriger ou désactiver
873
+ applicationConfigImport: boolean; // Import depuis @angular/core
874
+ removeIgnoreChangesOutsideZone: boolean; // Supprimer option
875
+ testBedRethrowErrors: boolean; // Gérer ou désactiver
876
+ routerNavigationTiming: boolean; // flush() ou await
877
+ testBedFakePlatformLocation: boolean; // MockPlatformLocation si besoin
878
+ upgradeAdapter: boolean; // Migrer vers static APIs
879
+ formArrayConflict: boolean; // Renommer directives custom
880
+ ngModuleFactory: boolean; // Utiliser NgModule directement
881
+ emitDeclarationOnly: boolean; // Désactiver option
882
+ lastSuccessfulNavigation: boolean; // Appeler comme fonction
883
+ };
884
+ recommandee: {
885
+ vitestMigration?: boolean; // Migrer de Karma/Jest
886
+ httpClientDefault?: boolean; // Retirer provideHttpClient()
887
+ simpleChangesGeneric?: boolean; // Typer SimpleChanges
888
+ ngClassMigration?: boolean; // Vers class binding
889
+ ngStyleMigration?: boolean; // Vers style binding
890
+ commonModuleMigration?: boolean; // Vers imports individuels
891
+ };
892
+ optionnelle: {
893
+ signalForms?: boolean; // Nouvelle API formulaires
894
+ angularAria?: boolean; // Composants accessibles
895
+ regexTemplates?: boolean; // Regex dans templates
896
+ deferViewportOptions?: boolean; // IntersectionObserver options
897
+ httpResponseType?: boolean; // Diagnostic CORS
898
+ routerScrollOption?: boolean; // Contrôle scroll navigation
899
+ tailwindSchematic?: boolean; // Setup Tailwind
900
+ defineServe?: boolean; // Variables ng serve
901
+ isolatedShadowDom?: boolean; // Encapsulation expérimentale
902
+ };
903
+ }
904
+ ```
905
+
906
+ ## 🚀 Script Migration Automatisé
907
+
908
+ ```bash
909
+ #!/bin/bash
910
+ # migration-v21.sh
911
+
912
+ echo "🔄 Angular 20 -> 21 Migration Starting..."
913
+
914
+ # 1. Backup
915
+ git checkout -b migration-angular-21
916
+ git add . && git commit -m "Pre-migration v21 backup"
917
+
918
+ # 2. Core updates
919
+ echo "📦 Updating to Angular 21..."
920
+ npm install typescript@~5.9.0
921
+ ng update @angular/core@21 @angular/cli@21 --verbose
922
+ ng update @angular/material@21 # Si utilisé
923
+
924
+ # 3. Migrations automatiques obligatoires
925
+ echo "🔧 Automatic migrations applied by ng update..."
926
+ # - provideZoneChangeDetection() ajouté automatiquement
927
+ # - Control flow migration appliquée si pas encore fait
928
+
929
+ # 4. Fix manuels obligatoires
930
+ echo "🔧 Applying manual fixes..."
931
+
932
+ # Rechercher les propriétés à supprimer
933
+ echo "Checking for deprecated properties..."
934
+ grep -rn "interpolation:" src/ --include="*.ts" && echo "⚠️ Remove 'interpolation' from @Component"
935
+ grep -rn "moduleId:" src/ --include="*.ts" && echo "⚠️ Remove 'moduleId' from @Component"
936
+
937
+ # Vérifier ApplicationConfig import
938
+ grep -rn "from '@angular/platform-browser'" src/ --include="*.ts" | grep "ApplicationConfig" && \
939
+ echo "⚠️ Change ApplicationConfig import to @angular/core"
940
+
941
+ # Vérifier NgModuleFactory usage
942
+ grep -rn "NgModuleFactory" src/ --include="*.ts" && echo "⚠️ Replace NgModuleFactory with NgModule"
943
+
944
+ # Vérifier UpgradeAdapter
945
+ grep -rn "UpgradeAdapter" src/ --include="*.ts" && echo "⚠️ Migrate to @angular/upgrade/static"
946
+
947
+ # 5. Migrations recommandées
948
+ echo "🎯 Running recommended migrations..."
949
+
950
+ read -p "Migrate to Vitest? (y/n) " -n 1 -r
951
+ echo
952
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
953
+ ng generate @schematics/angular:refactor-jasmine-vitest
954
+ fi
955
+
956
+ read -p "Migrate NgClass to class binding? (y/n) " -n 1 -r
957
+ echo
958
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
959
+ ng generate @angular/core:ngclass-to-class
960
+ fi
961
+
962
+ read -p "Migrate NgStyle to style binding? (y/n) " -n 1 -r
963
+ echo
964
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
965
+ ng generate @angular/core:ngstyle-to-style
966
+ fi
967
+
968
+ read -p "Migrate CommonModule to standalone imports? (y/n) " -n 1 -r
969
+ echo
970
+ if [[ $REPLY =~ ^[Yy]$ ]]; then
971
+ ng generate @angular/core:common-to-standalone
972
+ fi
973
+
974
+ # 6. Tests
975
+ echo "🧪 Running validation..."
976
+ ng test --no-watch
977
+ ng build --configuration production
978
+
979
+ echo "✅ Migration complete! Review changes with: git diff"
980
+ echo "📝 Check for manual fixes needed above"
981
+ ```
982
+
983
+ ## ⚠️ Points d'Attention & Validation
984
+
985
+ | Catégorie | Risque | Solution | Vérification |
986
+ |-----------|--------|----------|--------------|
987
+ | Zone.js | Apps cassées | Ajouter `provideZoneChangeDetection()` | App démarre |
988
+ | Host Bindings | Erreurs build | Corriger types ou désactiver | Build sans erreurs |
989
+ | Tests Navigation | Tests échouent | Utiliser `flush()` ou `await` | Tests passent |
990
+ | TestBed Errors | Erreurs relancées | Corriger ou `rethrowApplicationErrors: false` | Tests stables |
991
+ | NgModuleFactory | Code legacy cassé | Migrer vers NgModule | Dynamic loading OK |
992
+ | Vitest | Tests Jasmine incompatibles | Migration + réécriture fakeAsync | Tests passent |
993
+ | Signal Inputs | Custom Elements cassés | Accès direct vs `()` | Elements fonctionnent |
994
+ | FormArray | Conflits directives | Renommer directives custom | Forms fonctionnent |
995
+
996
+ ## 📈 Métriques Cibles
997
+
998
+ - **Bundle**: -5% grâce à zoneless par défaut (pas de zone.js)
999
+ - **Tests**: +30% vitesse avec Vitest vs Karma
1000
+ - **Type Safety**: +20% erreurs détectées au build (host bindings)
1001
+ - **DX**: Meilleur debugging sans zone.js
1002
+ - **Accessibilité**: Angular Aria pour composants WCAG-compliant
1003
+
1004
+ ## 🔗 Ressources
1005
+
1006
+ - [Angular Update Guide](https://angular.dev/update-guide)
1007
+ - [Migration Vitest](https://angular.dev/guide/testing/migrating-to-vitest)
1008
+ - [Signal Forms Guide](https://angular.dev/guide/forms/signal-forms)
1009
+ - [Angular Aria](https://angular.dev/guide/aria)
1010
+ - [Zoneless Guide](https://angular.dev/guide/experimental/zoneless)