@silvestv/migration-planificator 6.0.0 → 6.0.2
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/dist/src/core/app-analyzer.js +10 -2
- package/dist/src/core/ast/matchers/html/html-template-ref-collector.js +95 -0
- package/dist/src/core/ast/matchers/html/html-text-matcher.js +85 -32
- package/dist/src/core/ast/matchers/html/index.js +16 -10
- package/dist/src/core/ast/matchers/index.js +44 -0
- package/dist/src/core/ast/matchers/ts/collection-matcher.js +4 -0
- package/dist/src/core/ast/matchers/ts/context-matcher.js +7 -4
- package/dist/src/core/ast/matchers/ts/decorator-matcher.js +145 -144
- package/dist/src/core/ast/matchers/ts/file-matcher.js +199 -8
- package/dist/src/core/ast/matchers/ts/host-binding-property-matcher.js +327 -0
- package/dist/src/core/ast/matchers/ts/mutation-matcher.js +248 -0
- package/dist/src/core/ast/matchers/ts/node-matcher.js +17 -1
- package/dist/src/core/ast/matchers/ts/symbol-matcher.js +98 -2
- package/dist/src/core/ast/matchers/ts/type-matcher.js +5 -2
- package/dist/src/core/ast/matchers/utils/matcher-helpers.js +60 -0
- package/dist/src/core/ast/matchers/utils/template-cache.js +50 -0
- package/dist/src/core/project-detector.js +22 -10
- package/dist/src/core/project-strategy/nx-strategy.js +25 -22
- package/dist/src/data/angular-migration-rules.json +32 -57
- package/dist/src/data/rules/rearchitecture/rearchitecture-rules.json +30 -0
- package/dist/src/data/rules/to18/rules-18-obligatoire.json +8 -14
- package/dist/src/data/rules/to18/rules-18-optionnelle.json +0 -56
- package/dist/src/data/rules/to18/rules-18-recommande.json +32 -139
- package/dist/src/data/rules/to19/rules-19-obligatoire.json +4 -1
- package/dist/src/data/rules/to19/rules-19-optionnelle.json +3 -0
- package/dist/src/data/rules/to19/rules-19-recommande.json +30 -2
- package/dist/src/data/rules/to20/rules-20-optionnelle.json +0 -35
- package/dist/src/data/rules/to20/rules-20-recommande.json +44 -36
- package/dist/src/data/rules/to21/rules-21-obligatoire.json +23 -10
- package/package.json +1 -1
|
@@ -139,7 +139,7 @@ function resolveSpecifierOneLevel(specifier) {
|
|
|
139
139
|
/**
|
|
140
140
|
* Vérifie si un nœud référence un symbole qui correspond au pattern referTo
|
|
141
141
|
* @param node Nœud à vérifier (généralement un Identifier dans un tableau providers)
|
|
142
|
-
* @param referToPattern Pattern de référence (decorator, propertyNotEqual, etc.)
|
|
142
|
+
* @param referToPattern Pattern de référence (decorator, propertyNotEqual, functionBody, etc.)
|
|
143
143
|
* @returns true si le symbole référencé matche le pattern
|
|
144
144
|
*/
|
|
145
145
|
function matchesReferTo(node, referToPattern) {
|
|
@@ -149,7 +149,20 @@ function matchesReferTo(node, referToPattern) {
|
|
|
149
149
|
const definitions = identifierNode.getDefinitionNodes();
|
|
150
150
|
if (definitions.length === 0)
|
|
151
151
|
return false;
|
|
152
|
-
return definitions.some((def) =>
|
|
152
|
+
return definitions.some((def) => {
|
|
153
|
+
// Mode existant : décorateur de classe
|
|
154
|
+
if (referToPattern.decorator) {
|
|
155
|
+
return matchesDefinition(def, referToPattern);
|
|
156
|
+
}
|
|
157
|
+
// NOUVEAU : analyse du body de fonction (guards fonctionnels)
|
|
158
|
+
if (referToPattern.functionBody) {
|
|
159
|
+
const body = resolveToFunctionBody(def);
|
|
160
|
+
if (!body)
|
|
161
|
+
return false;
|
|
162
|
+
return checkFunctionBodyMatch(body, referToPattern.functionBody);
|
|
163
|
+
}
|
|
164
|
+
return false;
|
|
165
|
+
});
|
|
153
166
|
}
|
|
154
167
|
/**
|
|
155
168
|
* Extrait le nœud Identifier du nœud donné selon le pattern
|
|
@@ -279,3 +292,86 @@ function checkPropertyNotEqual(decorator, propertyNotEqual) {
|
|
|
279
292
|
const actualValue = propValue.getText().replace(/['"]/g, '');
|
|
280
293
|
return actualValue !== value;
|
|
281
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Résout une définition vers le body d'une ArrowFunction ou FunctionExpression
|
|
297
|
+
* Pour les guards fonctionnels (CanActivateFn, etc.)
|
|
298
|
+
* @param def - Définition (peut être ImportSpecifier, ExportSpecifier ou VariableDeclaration)
|
|
299
|
+
* @returns Body de la fonction ou undefined
|
|
300
|
+
*/
|
|
301
|
+
function resolveToFunctionBody(def) {
|
|
302
|
+
let targetNode = def;
|
|
303
|
+
// Si c'est un Import/ExportSpecifier, résoudre récursivement
|
|
304
|
+
if ((0, ast_helpers_1.isImportOrExportSpecifier)(def)) {
|
|
305
|
+
targetNode = resolveSpecifierRecursively(def);
|
|
306
|
+
}
|
|
307
|
+
// Si c'est une VariableDeclaration, extraire l'initializer
|
|
308
|
+
if (ts_morph_1.Node.isVariableDeclaration(targetNode)) {
|
|
309
|
+
const initializer = targetNode.getInitializer();
|
|
310
|
+
if (initializer && (ts_morph_1.Node.isArrowFunction(initializer) || ts_morph_1.Node.isFunctionExpression(initializer))) {
|
|
311
|
+
return initializer.getBody();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Si c'est directement une ArrowFunction ou FunctionExpression
|
|
315
|
+
if (ts_morph_1.Node.isArrowFunction(targetNode) || ts_morph_1.Node.isFunctionExpression(targetNode)) {
|
|
316
|
+
return targetNode.getBody();
|
|
317
|
+
}
|
|
318
|
+
return undefined;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Vérifie si le body d'une fonction contient les patterns recherchés
|
|
322
|
+
* @param body - Body de la fonction à analyser
|
|
323
|
+
* @param pattern - Pattern avec contains et/ou hasConditional
|
|
324
|
+
* @returns true si le body matche le pattern
|
|
325
|
+
*/
|
|
326
|
+
function checkFunctionBodyMatch(body, pattern) {
|
|
327
|
+
let hasContains = true;
|
|
328
|
+
let hasConditional = true;
|
|
329
|
+
// Vérifier contains.functionName
|
|
330
|
+
if (pattern.contains?.functionName) {
|
|
331
|
+
const funcNames = Array.isArray(pattern.contains.functionName)
|
|
332
|
+
? pattern.contains.functionName
|
|
333
|
+
: [pattern.contains.functionName];
|
|
334
|
+
hasContains = false;
|
|
335
|
+
body.forEachDescendant((child) => {
|
|
336
|
+
if (hasContains)
|
|
337
|
+
return; // Early exit si déjà trouvé
|
|
338
|
+
if (ts_morph_1.Node.isCallExpression(child)) {
|
|
339
|
+
const expr = child.getExpression();
|
|
340
|
+
const name = expr.getText().split('.').pop() || '';
|
|
341
|
+
if (funcNames.includes(name)) {
|
|
342
|
+
hasContains = true;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
if (!hasContains)
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
// Vérifier hasConditional (?: || && if)
|
|
350
|
+
if (pattern.hasConditional) {
|
|
351
|
+
hasConditional = false;
|
|
352
|
+
body.forEachDescendant((child) => {
|
|
353
|
+
if (hasConditional)
|
|
354
|
+
return; // Early exit si déjà trouvé
|
|
355
|
+
// Ternaire ? :
|
|
356
|
+
if (ts_morph_1.Node.isConditionalExpression(child)) {
|
|
357
|
+
hasConditional = true;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
// if statement
|
|
361
|
+
if (ts_morph_1.Node.isIfStatement(child)) {
|
|
362
|
+
hasConditional = true;
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// Opérateurs logiques || &&
|
|
366
|
+
if (ts_morph_1.Node.isBinaryExpression(child)) {
|
|
367
|
+
const op = child.getOperatorToken().getText();
|
|
368
|
+
if (op === '||' || op === '&&') {
|
|
369
|
+
hasConditional = true;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
if (!hasConditional)
|
|
374
|
+
return false;
|
|
375
|
+
}
|
|
376
|
+
return hasContains && hasConditional;
|
|
377
|
+
}
|
|
@@ -139,11 +139,14 @@ function matchesInitializer(node, initializerPattern, matchesAstPatternFn) {
|
|
|
139
139
|
if (!initializer) {
|
|
140
140
|
return false;
|
|
141
141
|
}
|
|
142
|
-
// Vérifier notWrappedIn
|
|
142
|
+
// Vérifier notWrappedIn (support string ou array)
|
|
143
143
|
if (initializerPattern.notWrappedIn) {
|
|
144
144
|
if (ts_morph_1.Node.isCallExpression(initializer)) {
|
|
145
145
|
const funcName = initializer.getExpression().getText();
|
|
146
|
-
|
|
146
|
+
const wrappers = Array.isArray(initializerPattern.notWrappedIn)
|
|
147
|
+
? initializerPattern.notWrappedIn
|
|
148
|
+
: [initializerPattern.notWrappedIn];
|
|
149
|
+
if (wrappers.includes(funcName)) {
|
|
147
150
|
return false;
|
|
148
151
|
}
|
|
149
152
|
}
|
|
@@ -7,6 +7,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
7
7
|
exports.ensureArray = ensureArray;
|
|
8
8
|
exports.normalizeValue = normalizeValue;
|
|
9
9
|
exports.matchesTextValue = matchesTextValue;
|
|
10
|
+
exports.stripQuotes = stripQuotes;
|
|
11
|
+
exports.extractVariableNames = extractVariableNames;
|
|
12
|
+
exports.hasExplicitFunctionCall = hasExplicitFunctionCall;
|
|
10
13
|
/**
|
|
11
14
|
* Normalise une valeur en tableau
|
|
12
15
|
* @param value Valeur simple ou tableau
|
|
@@ -35,3 +38,60 @@ function matchesTextValue(nodeText, expectedValue) {
|
|
|
35
38
|
nodeText === `"${expectedValue}"` ||
|
|
36
39
|
nodeText === `${expectedValue}`;
|
|
37
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Enlève les quotes simples ou doubles autour d'une string
|
|
43
|
+
* Utile pour computed properties: "'[class.active]'" → "[class.active]"
|
|
44
|
+
* @param str String potentiellement quotée
|
|
45
|
+
* @returns String sans quotes
|
|
46
|
+
*/
|
|
47
|
+
function stripQuotes(str) {
|
|
48
|
+
if ((str.startsWith("'") && str.endsWith("'")) || (str.startsWith('"') && str.endsWith('"'))) {
|
|
49
|
+
return str.slice(1, -1);
|
|
50
|
+
}
|
|
51
|
+
return str;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extrait tous les noms de variables d'une expression de binding
|
|
55
|
+
* Exemples:
|
|
56
|
+
* 'isActive' → ['isActive']
|
|
57
|
+
* 'count > 0' → ['count']
|
|
58
|
+
* 'count1 + count2' → ['count1', 'count2']
|
|
59
|
+
* 'sig() ? "a" : "b"' → ['sig']
|
|
60
|
+
* 'obj.prop' → ['obj']
|
|
61
|
+
* @param expression Expression de binding
|
|
62
|
+
* @returns Array de noms de variables
|
|
63
|
+
*/
|
|
64
|
+
function extractVariableNames(expression) {
|
|
65
|
+
const cleaned = expression.trim();
|
|
66
|
+
const variables = new Set();
|
|
67
|
+
// Pattern pour extraire tous les identifiants valides
|
|
68
|
+
const identifierPattern = /\b([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g;
|
|
69
|
+
const matches = cleaned.matchAll(identifierPattern);
|
|
70
|
+
// Mots-clés JavaScript à exclure
|
|
71
|
+
const keywords = new Set([
|
|
72
|
+
'true', 'false', 'null', 'undefined', 'this',
|
|
73
|
+
'if', 'else', 'for', 'while', 'return', 'function',
|
|
74
|
+
'const', 'let', 'var', 'new', 'typeof', 'instanceof'
|
|
75
|
+
]);
|
|
76
|
+
for (const match of matches) {
|
|
77
|
+
const varName = match[1];
|
|
78
|
+
if (!keywords.has(varName)) {
|
|
79
|
+
variables.add(varName);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return Array.from(variables);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Détecte si une expression contient un appel de fonction explicite pour une variable
|
|
86
|
+
* Exemples:
|
|
87
|
+
* 'isActive' → false
|
|
88
|
+
* 'isActive()' → true
|
|
89
|
+
* 'sig() ? "a" : "b"' → true (pour 'sig')
|
|
90
|
+
* @param expression Expression à tester
|
|
91
|
+
* @param variableName Nom de la variable
|
|
92
|
+
* @returns true si appel de fonction détecté
|
|
93
|
+
*/
|
|
94
|
+
function hasExplicitFunctionCall(expression, variableName) {
|
|
95
|
+
const regex = new RegExp(`\\b${variableName}\\s*\\(`);
|
|
96
|
+
return regex.test(expression);
|
|
97
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clearTemplateCache = clearTemplateCache;
|
|
4
|
+
exports.getTemplateFromCache = getTemplateFromCache;
|
|
5
|
+
exports.setTemplateInCache = setTemplateInCache;
|
|
6
|
+
/**
|
|
7
|
+
* Cache LRU pour templates HTML externes (templateUrl)
|
|
8
|
+
* Réutilisé par decorator-matcher et host-binding-property-matcher
|
|
9
|
+
* Principe DRY: une seule implémentation
|
|
10
|
+
*/
|
|
11
|
+
class TemplateCache {
|
|
12
|
+
cache = new Map();
|
|
13
|
+
maxSize = 100;
|
|
14
|
+
get(filePath) {
|
|
15
|
+
return this.cache.get(filePath);
|
|
16
|
+
}
|
|
17
|
+
set(filePath, content) {
|
|
18
|
+
// LRU: si cache plein, supprimer le plus ancien
|
|
19
|
+
if (this.cache.size >= this.maxSize) {
|
|
20
|
+
const firstKey = this.cache.keys().next().value;
|
|
21
|
+
if (firstKey !== undefined) {
|
|
22
|
+
this.cache.delete(firstKey);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
this.cache.set(filePath, content);
|
|
26
|
+
}
|
|
27
|
+
clear() {
|
|
28
|
+
this.cache.clear();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Instance globale du cache (partagée entre tous les matchers)
|
|
32
|
+
const templateCache = new TemplateCache();
|
|
33
|
+
/**
|
|
34
|
+
* Efface le cache des templates (pour tests ou libération mémoire)
|
|
35
|
+
*/
|
|
36
|
+
function clearTemplateCache() {
|
|
37
|
+
templateCache.clear();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Récupère un template depuis le cache
|
|
41
|
+
*/
|
|
42
|
+
function getTemplateFromCache(filePath) {
|
|
43
|
+
return templateCache.get(filePath);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Ajoute un template au cache
|
|
47
|
+
*/
|
|
48
|
+
function setTemplateInCache(filePath, content) {
|
|
49
|
+
templateCache.set(filePath, content);
|
|
50
|
+
}
|
|
@@ -53,12 +53,14 @@ function detectProject(projectPath) {
|
|
|
53
53
|
const hasAngularJson = fs.existsSync(angularJsonPath);
|
|
54
54
|
const hasAppsDir = fs.existsSync(appsDir);
|
|
55
55
|
const hasLibsDir = fs.existsSync(libsDir);
|
|
56
|
+
const hasProjectJsonAtRoot = fs.existsSync(path.join(projectPath, 'project.json'));
|
|
56
57
|
let type = 'unknown';
|
|
57
58
|
let appNames = [];
|
|
58
59
|
let libNames = [];
|
|
59
60
|
// Lire angular.json UNE SEULE FOIS si présent
|
|
60
61
|
const angularJson = hasAngularJson ? readJsonFile(angularJsonPath) : null;
|
|
61
|
-
|
|
62
|
+
// Nx monorepo : avec apps/ OU single-app (project.json à la racine)
|
|
63
|
+
if (hasNxJson && (hasAppsDir || hasProjectJsonAtRoot)) {
|
|
62
64
|
type = 'nx-monorepo';
|
|
63
65
|
appNames = extractNxApps(projectPath);
|
|
64
66
|
libNames = hasLibsDir ? extractNxLibs(projectPath) : [];
|
|
@@ -116,17 +118,27 @@ function extractAngularProjects(angularJson) {
|
|
|
116
118
|
}
|
|
117
119
|
function extractNxApps(projectPath) {
|
|
118
120
|
const appsDir = path.join(projectPath, 'apps');
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
121
|
+
// Cas standard : dossier apps/ existe
|
|
122
|
+
if (fs.existsSync(appsDir)) {
|
|
123
|
+
try {
|
|
124
|
+
return fs.readdirSync(appsDir, { withFileTypes: true })
|
|
125
|
+
.filter(entry => entry.isDirectory())
|
|
126
|
+
.filter(dir => fs.existsSync(path.join(appsDir, dir.name, 'project.json')))
|
|
127
|
+
.map(dir => dir.name);
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
126
132
|
}
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
// Nx single-app : project.json à la racine + src/
|
|
134
|
+
const projectJsonPath = path.join(projectPath, 'project.json');
|
|
135
|
+
const srcDir = path.join(projectPath, 'src');
|
|
136
|
+
if (fs.existsSync(projectJsonPath) && fs.existsSync(srcDir)) {
|
|
137
|
+
const projectJson = readJsonFile(projectJsonPath);
|
|
138
|
+
const appName = projectJson?.name || path.basename(projectPath);
|
|
139
|
+
return [appName];
|
|
129
140
|
}
|
|
141
|
+
return [];
|
|
130
142
|
}
|
|
131
143
|
function extractNxLibs(projectPath) {
|
|
132
144
|
const libsDir = path.join(projectPath, 'libs');
|
|
@@ -38,7 +38,8 @@ const path = __importStar(require("path"));
|
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
/**
|
|
40
40
|
* Stratégie pour projets Nx Monorepo
|
|
41
|
-
* - Structure apps/ et libs/
|
|
41
|
+
* - Structure apps/ et libs/ (standard)
|
|
42
|
+
* - Structure src/ à la racine (single-app Nx)
|
|
42
43
|
* - Routines globales séparées des targets apps/libs
|
|
43
44
|
* - Target "monorepo" pour règles de routine
|
|
44
45
|
*/
|
|
@@ -50,46 +51,46 @@ class NxStrategy {
|
|
|
50
51
|
if (fs.existsSync(appPath)) {
|
|
51
52
|
return appPath;
|
|
52
53
|
}
|
|
53
|
-
|
|
54
|
+
if (fs.existsSync(libPath)) {
|
|
55
|
+
return libPath;
|
|
56
|
+
}
|
|
57
|
+
// Nx single-app : fallback vers src/
|
|
58
|
+
const srcPath = path.join(projectInfo.path, 'src');
|
|
59
|
+
if (fs.existsSync(srcPath)) {
|
|
60
|
+
return srcPath;
|
|
61
|
+
}
|
|
62
|
+
return appPath; // fallback original
|
|
54
63
|
}
|
|
55
64
|
getTargetType(projectInfo, targetName) {
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
if (hasMultipleTargets && targetName === projectInfo.name) {
|
|
65
|
+
// Target monorepo = nom du projet (pour routines globales)
|
|
66
|
+
if (targetName === projectInfo.name) {
|
|
59
67
|
return 'monorepo';
|
|
60
68
|
}
|
|
61
|
-
// Vérifier dans les apps
|
|
62
|
-
if (projectInfo.apps?.some(app => app.name === targetName)) {
|
|
63
|
-
return 'app';
|
|
64
|
-
}
|
|
65
69
|
// Vérifier dans les libs
|
|
66
70
|
if (projectInfo.libs?.some(lib => lib.name === targetName)) {
|
|
67
71
|
return 'lib';
|
|
68
72
|
}
|
|
69
|
-
// Par défaut app
|
|
73
|
+
// Par défaut app (inclut Nx single-app)
|
|
70
74
|
return 'app';
|
|
71
75
|
}
|
|
72
76
|
extractTargetFromFilePath(filePath, projectInfo) {
|
|
73
77
|
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
74
|
-
// Nx
|
|
78
|
+
// Nx standard : apps/XXX ou libs/XXX (ou libs/groupe/XXX pour structure groupée)
|
|
75
79
|
const appsMatch = normalizedPath.match(/^apps\/([^\/]+)/);
|
|
76
80
|
if (appsMatch)
|
|
77
81
|
return appsMatch[1];
|
|
78
82
|
// Support libs groupées : libs/apis/git-platform → "apis/git-platform"
|
|
79
83
|
const libsMatch = normalizedPath.match(/^libs\/(.*?)(?:\/|$)/);
|
|
80
84
|
if (libsMatch) {
|
|
81
|
-
// Extraire tout le chemin après libs/ jusqu'au premier fichier
|
|
82
85
|
const fullLibPath = normalizedPath.substring(5); // Enlever "libs/"
|
|
83
|
-
// Trouver la lib complète en cherchant dans projectInfo.libs
|
|
84
86
|
const matchingLib = projectInfo.libs?.find(lib => fullLibPath.startsWith(lib.name));
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return
|
|
87
|
+
return matchingLib?.name || libsMatch[1];
|
|
88
|
+
}
|
|
89
|
+
// Nx single-app : src/... → nom de l'app (premier de la liste)
|
|
90
|
+
if (normalizedPath.startsWith('src/')) {
|
|
91
|
+
return projectInfo.apps?.[0]?.name || projectInfo.name;
|
|
90
92
|
}
|
|
91
|
-
//
|
|
92
|
-
// Pour Nx monorepo, ces règles de routine vont dans le target "monorepo" (nom du projet)
|
|
93
|
+
// Fichiers racine (package.json, nx.json, etc.) → nom du projet (routines)
|
|
93
94
|
return projectInfo.name;
|
|
94
95
|
}
|
|
95
96
|
shouldFilterRoutinesFromTarget(targetName, projectInfo) {
|
|
@@ -123,8 +124,10 @@ class NxStrategy {
|
|
|
123
124
|
const libs = projectInfo.libs || [];
|
|
124
125
|
return [...apps, ...libs];
|
|
125
126
|
}
|
|
126
|
-
isMultiTarget(
|
|
127
|
-
|
|
127
|
+
isMultiTarget(_projectInfo) {
|
|
128
|
+
// Nx = toujours multi-target pour afficher l'UI complète
|
|
129
|
+
// même avec 1 seule app (affiche strate Monorepo)
|
|
130
|
+
return true;
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
exports.NxStrategy = NxStrategy;
|
|
@@ -356,32 +356,53 @@
|
|
|
356
356
|
],
|
|
357
357
|
"recommande": [
|
|
358
358
|
{
|
|
359
|
-
"key": "
|
|
360
|
-
"summary": "
|
|
361
|
-
"description": "
|
|
362
|
-
"estimated_time_per_occurrence":
|
|
363
|
-
"onFile": null,
|
|
359
|
+
"key": "guard_redirect_to_function",
|
|
360
|
+
"summary": "Guard de redirection conditionnelle → utiliser redirectTo fonction",
|
|
361
|
+
"description": "Angular 18 permet d'utiliser redirectTo comme fonction. Les guards qui redirigent conditionnellement via createUrlTree peuvent être simplifiés en redirectTo fonction, éliminant le besoin de guard et d'injection de Router.",
|
|
362
|
+
"estimated_time_per_occurrence": 8,
|
|
364
363
|
"fileTypes": [
|
|
365
364
|
"*.ts"
|
|
366
365
|
],
|
|
367
|
-
"regex": "
|
|
366
|
+
"regex": "canActivate\\s*:\\s*\\[[^\\]]*createUrlTree[^\\]]*\\?|canActivate\\s*:\\s*\\[[^\\]]*\\?[^\\]]*createUrlTree",
|
|
368
367
|
"category": "routing",
|
|
369
368
|
"isAutoFixable": false,
|
|
370
|
-
"migration_command": null,
|
|
371
369
|
"risk_level": "low",
|
|
372
|
-
"code_description": "// Avant:\
|
|
370
|
+
"code_description": "// Avant (guard de redirection conditionnelle):\n{\n path: '',\n canActivate: [() => {\n return inject(AuthService).isAdmin()\n ? inject(Router).createUrlTree(['/admin'])\n : inject(Router).createUrlTree(['/user']);\n }]\n}\n\n// Après (redirectTo fonction):\n{\n path: '',\n redirectTo: () => inject(AuthService).isAdmin() ? '/admin' : '/user'\n}",
|
|
373
371
|
"astPattern": {
|
|
374
372
|
"nodeType": "PropertyAssignment",
|
|
375
|
-
"name": "
|
|
373
|
+
"name": ["canActivate", "canActivateChild", "canMatch"],
|
|
376
374
|
"initializer": {
|
|
377
|
-
"nodeType": "
|
|
375
|
+
"nodeType": "ArrayLiteralExpression",
|
|
376
|
+
"elements": {
|
|
377
|
+
"containsAny": [
|
|
378
|
+
{
|
|
379
|
+
"nodeType": "ArrowFunction",
|
|
380
|
+
"body": {
|
|
381
|
+
"contains": {
|
|
382
|
+
"functionName": "createUrlTree"
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
"nodeType": "Identifier",
|
|
388
|
+
"referTo": {
|
|
389
|
+
"functionBody": {
|
|
390
|
+
"contains": {
|
|
391
|
+
"functionName": "createUrlTree"
|
|
392
|
+
},
|
|
393
|
+
"hasConditional": true
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
}
|
|
378
399
|
},
|
|
379
400
|
"excludeContext": [
|
|
380
401
|
"StringLiteral",
|
|
381
402
|
"Comment"
|
|
382
403
|
]
|
|
383
404
|
},
|
|
384
|
-
"doc_url": "https://angular.dev/
|
|
405
|
+
"doc_url": "https://angular.dev/guide/routing/redirecting-routes"
|
|
385
406
|
},
|
|
386
407
|
{
|
|
387
408
|
"key": "zoneless_ts_provider",
|
|
@@ -543,34 +564,6 @@
|
|
|
543
564
|
]
|
|
544
565
|
},
|
|
545
566
|
"doc_url": "https://angular.dev/guide/ssr#hydration"
|
|
546
|
-
},
|
|
547
|
-
{
|
|
548
|
-
"key": "forms_events_unified",
|
|
549
|
-
"summary": "Utiliser control.events unifié",
|
|
550
|
-
"description": "L'API control.events unifie valueChanges et statusChanges en un seul stream d'événements typés, simplifiant la gestion des changements de formulaires et offrant plus de types d'événements (PristineChangeEvent, TouchedChangeEvent, etc.).",
|
|
551
|
-
"estimated_time_per_occurrence": 5,
|
|
552
|
-
"onFile": null,
|
|
553
|
-
"fileTypes": [
|
|
554
|
-
"*.ts"
|
|
555
|
-
],
|
|
556
|
-
"regex": "\\.(valueChanges|statusChanges)[!?]?\\s*(?:\\.[\\s\\S]*?pipe\\([^)]*\\))?\\s*[\\n\\r\\s]*\\.?subscribe",
|
|
557
|
-
"category": "forms",
|
|
558
|
-
"isAutoFixable": false,
|
|
559
|
-
"migration_command": null,
|
|
560
|
-
"risk_level": "low",
|
|
561
|
-
"code_description": "// Avant:\ncontrol.valueChanges.subscribe(v => {});\ncontrol.statusChanges.subscribe(s => {});\n\n// Après:\ncontrol.events.subscribe(event => {\n if (event instanceof ValueChangeEvent) { }\n if (event instanceof StatusChangeEvent) { }\n});",
|
|
562
|
-
"astPattern": {
|
|
563
|
-
"nodeType": "PropertyAccessExpression",
|
|
564
|
-
"propertyName": [
|
|
565
|
-
"valueChanges",
|
|
566
|
-
"statusChanges"
|
|
567
|
-
],
|
|
568
|
-
"excludeContext": [
|
|
569
|
-
"StringLiteral",
|
|
570
|
-
"Comment"
|
|
571
|
-
]
|
|
572
|
-
},
|
|
573
|
-
"doc_url": "https://angular.dev/reference/migrations"
|
|
574
567
|
}
|
|
575
568
|
],
|
|
576
569
|
"optionnelle": [
|
|
@@ -651,24 +644,6 @@
|
|
|
651
644
|
},
|
|
652
645
|
"doc_url": "https://angular.dev/guide/router#redirectcommand"
|
|
653
646
|
},
|
|
654
|
-
{
|
|
655
|
-
"key": "ng_content_fallback",
|
|
656
|
-
"summary": "Utiliser fallback content dans ng-content",
|
|
657
|
-
"description": "Angular 18 permet de définir un contenu par défaut à l'intérieur de ng-content qui sera affiché si aucun contenu n'est projeté. Utile pour créer des composants avec des valeurs par défaut sans logique conditionnelle complexe.",
|
|
658
|
-
"estimated_time_per_occurrence": 3,
|
|
659
|
-
"onFile": null,
|
|
660
|
-
"fileTypes": [
|
|
661
|
-
"*.html",
|
|
662
|
-
"*.ts"
|
|
663
|
-
],
|
|
664
|
-
"regex": "<ng-content[^>]*></ng-content>",
|
|
665
|
-
"category": "template",
|
|
666
|
-
"isAutoFixable": false,
|
|
667
|
-
"migration_command": null,
|
|
668
|
-
"risk_level": "low",
|
|
669
|
-
"code_description": "// Avant:\n<ng-content></ng-content>\n\n// Après:\n<ng-content>\n <div class=\"default\">Contenu par défaut</div>\n</ng-content>",
|
|
670
|
-
"doc_url": "https://angular.dev/reference/migrations"
|
|
671
|
-
},
|
|
672
647
|
{
|
|
673
648
|
"key": "http_cache_auth",
|
|
674
649
|
"summary": "Cache HTTP avec headers auth",
|
|
@@ -62,5 +62,35 @@
|
|
|
62
62
|
"risk_level": "medium",
|
|
63
63
|
"code_description": "// Avant: fonction complexe (> 75 lignes)\nexport class UserService {\n processUserData(user: User): ProcessedUser {\n // 75+ lignes de logique\n // - Validation\n // - Transformation\n // - Calculs\n // - Formatage\n // - Gestion erreurs\n // ...\n }\n}\n\n// Après: décomposition en fonctions plus petites\nexport class UserService {\n processUserData(user: User): ProcessedUser {\n const validated = this.validateUser(user);\n const transformed = this.transformUserData(validated);\n const calculated = this.calculateMetrics(transformed);\n return this.formatOutput(calculated);\n }\n\n private validateUser(user: User): ValidatedUser {\n // Validation uniquement (~10 lignes)\n }\n\n private transformUserData(user: ValidatedUser): TransformedUser {\n // Transformation uniquement (~15 lignes)\n }\n\n private calculateMetrics(user: TransformedUser): CalculatedUser {\n // Calculs uniquement (~20 lignes)\n }\n\n private formatOutput(user: CalculatedUser): ProcessedUser {\n // Formatage uniquement (~10 lignes)\n }\n}\n\n// Bénéfices:\n// - Chaque fonction a une responsabilité unique\n// - Code plus lisible et maintenable\n// - Tests unitaires plus faciles\n// - Réutilisation possible des sous-fonctions",
|
|
64
64
|
"doc_url": "https://angular.dev/style-guide#small-functions"
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
"key": "decouple_legacy_providers",
|
|
68
|
+
"summary": "Découpler les services fournis dans des modules ou composants",
|
|
69
|
+
"description": "Détecte les services Angular encore déclarés dans des tableaux `providers` d’un `@NgModule` ou d’un `@Component`. Depuis Angular standalone, il est recommandé que les services soient autonomes et marqués `providedIn: 'root'` pour être des singletons globaux. Les providers locaux créent des instances dupliquées et complexifient la gestion d’état.",
|
|
70
|
+
"estimated_time_per_occurrence": 25,
|
|
71
|
+
"onFile": null,
|
|
72
|
+
"fileTypes": ["*.ts"],
|
|
73
|
+
"regex": "@(?:NgModule|Component)\\s*\\([^)]*\\bproviders\\s*:\\s*\\[[^\\]]*\\]",
|
|
74
|
+
"category": "architecture",
|
|
75
|
+
"isAutoFixable": false,
|
|
76
|
+
"migration_command": null,
|
|
77
|
+
"risk_level": "high",
|
|
78
|
+
"code_description": "// Avant (ancien modèle):\n@NgModule({\n providers: [UserService, AuthService]\n})\nexport class UserModule {}\n\n@Component({\n selector: 'app-profile',\n providers: [ProfileService]\n})\nexport class ProfileComponent {}\n\n// Après (standalone / moderne):\n@Injectable({ providedIn: 'root' })\nexport class UserService {}\n\n@Injectable({ providedIn: 'root' })\nexport class ProfileService {}\n\n// Puis retirer les providers des décorateurs.",
|
|
79
|
+
"astPattern": {
|
|
80
|
+
"nodeType": ["Identifier", "CallExpression"],
|
|
81
|
+
"context": "ProvidersArray",
|
|
82
|
+
"referTo": {
|
|
83
|
+
"decorator": "Injectable",
|
|
84
|
+
"elementNodeType": ["Identifier", "CallExpression"],
|
|
85
|
+
"propertyNotEqual": {
|
|
86
|
+
"key": "providedIn",
|
|
87
|
+
"value": "root"
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"excludeContext": [
|
|
91
|
+
"StringLiteral",
|
|
92
|
+
"Comment"
|
|
93
|
+
]
|
|
94
|
+
}
|
|
65
95
|
}
|
|
66
96
|
]
|
|
@@ -301,7 +301,7 @@
|
|
|
301
301
|
{
|
|
302
302
|
"key": "onpush_host_bindings",
|
|
303
303
|
"summary": "OnPush + host bindings nécessitent markForCheck()",
|
|
304
|
-
"description": "Avec OnPush change detection, les host bindings ne sont plus automatiquement mis à jour.
|
|
304
|
+
"description": "Avec OnPush change detection, les host bindings basés sur des propriétés simples ou getters ne sont plus automatiquement mis à jour. Les signals et computed() ont une détection de changement automatique. Appeler markForCheck() uniquement pour les propriétés simples et getters après leur modification.",
|
|
305
305
|
"estimated_time_per_occurrence": 8,
|
|
306
306
|
"onFile": null,
|
|
307
307
|
"fileTypes": [
|
|
@@ -312,23 +312,17 @@
|
|
|
312
312
|
"isAutoFixable": false,
|
|
313
313
|
"migration_command": null,
|
|
314
314
|
"risk_level": "high",
|
|
315
|
-
"code_description": "// Avant:\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { '[class.active]': 'isActive' }\n})\
|
|
315
|
+
"code_description": "// Avant (propriété simple - NÉCESSITE markForCheck):\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { '[class.active]': 'isActive' }\n})\nexport class MyComponent {\n isActive = false;\n toggle() { this.isActive = true; }\n}\n\n// Après:\ntoggle() {\n this.isActive = true;\n this.cdr.markForCheck();\n}\n\n// OK (signal - PAS BESOIN de markForCheck):\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { '[class.active]': 'isActive()' }\n})\nexport class MyComponent {\n isActive = signal(false);\n toggle() { this.isActive.set(true); }\n}\n\n// OK (@HostBinding avec signal):\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush\n})\nexport class MyComponent {\n @HostBinding('class.active')\n isActive = signal(false);\n}\n\n// NÉCESSITE markForCheck (multi-variables):\n@Component({\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { '[class.visible]': 'count1 + count2 > 0' }\n})\nexport class MyComponent {\n count1 = 0;\n count2 = 0;\n}",
|
|
316
316
|
"astPattern": {
|
|
317
317
|
"nodeType": "Decorator",
|
|
318
318
|
"name": "Component",
|
|
319
319
|
"properties": {
|
|
320
|
-
"changeDetection": "ChangeDetectionStrategy.OnPush"
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
"nodeType": "StringLiteral",
|
|
329
|
-
"valueMatches": "\\(.*\\)"
|
|
330
|
-
}
|
|
331
|
-
]
|
|
320
|
+
"changeDetection": "ChangeDetectionStrategy.OnPush"
|
|
321
|
+
},
|
|
322
|
+
"hostBindingProperty": {
|
|
323
|
+
"bindingType": "property",
|
|
324
|
+
"resolvedProperty": {
|
|
325
|
+
"notWrappedIn": ["signal", "computed", "inject"]
|
|
332
326
|
}
|
|
333
327
|
},
|
|
334
328
|
"excludeContext": [
|