@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.
- package/README.fr.md +10 -11
- package/README.md +10 -11
- package/dist/client.bundle.js +98 -98
- package/dist/src/config/migration.config.js +69 -0
- package/dist/src/core/app-analyzer.js +69 -79
- package/dist/src/core/ast/matchers/html/html-element-matcher.js +32 -7
- package/dist/src/core/ast/matchers/html/index.js +36 -4
- package/dist/src/core/ast/matchers/ts/collection-matcher.js +18 -0
- package/dist/src/core/ast/matchers/ts/decorator-matcher.js +33 -4
- package/dist/src/core/ast/matchers/ts/expression-matcher.js +1 -1
- package/dist/src/core/ast/matchers/ts/node-matcher.js +4 -0
- package/dist/src/core/project-detector.js +94 -106
- package/dist/src/core/project-strategy/nx-strategy.js +25 -22
- package/dist/src/core/project-strategy/standalone-strategy.js +43 -14
- package/dist/src/core/rules-loader.js +21 -13
- package/dist/src/core/scan-reporter.js +3 -1
- package/dist/src/core/scanner-orchestrator.js +1 -1
- package/dist/src/core/workload/constants.js +7 -4
- package/dist/src/data/markdown/angular-migration-20-21.md +1010 -0
- package/dist/src/data/rules/to19/rules-19-optionnelle.json +3 -0
- package/dist/src/data/rules/to21/rules-21-obligatoire.json +423 -0
- package/dist/src/data/rules/to21/rules-21-optionnelle.json +253 -0
- package/dist/src/data/rules/to21/rules-21-recommande.json +139 -0
- package/dist/src/index.js +1 -1
- package/dist/src/templates/landing/project-overview.template.js +2 -1
- package/dist/src/templates/page/migration-guide.template.js +1 -0
- package/dist/src/templates/workload/guide-rule-card.template.js +3 -1
- package/dist/src/templates/workload/hierarchy-shared.js +6 -5
- package/dist/src/templates/workload/unified-settings-panel.template.js +27 -33
- package/dist/src/utils/core/args-parser.js +19 -18
- package/dist/styles.css +1 -1
- 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)
|