@craft-ng/dev-tools 0.5.0-beta.4 → 0.5.1-beta.0
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.md +132 -23
- package/package.json +6 -2
- package/src/bin/craft-migrate-primitives.d.ts +2 -0
- package/src/bin/craft-migrate-primitives.js +71 -0
- package/src/bin/craft-migrate-primitives.js.map +1 -0
- package/src/bin/craft-migrate-routes.d.ts +2 -0
- package/src/bin/craft-migrate-routes.js +77 -0
- package/src/bin/craft-migrate-routes.js.map +1 -0
- package/src/bin/craft-migrate-services.d.ts +2 -0
- package/src/bin/craft-migrate-services.js +75 -0
- package/src/bin/craft-migrate-services.js.map +1 -0
- package/src/bin/craft-migrate.d.ts +2 -0
- package/src/bin/craft-migrate.js +90 -0
- package/src/bin/craft-migrate.js.map +1 -0
- package/src/eslint-rules/global-exception-registry-match.cjs +4 -4
- package/src/eslint-rules/index.cjs +6 -0
- package/src/eslint-rules/require-craft-exception-handler.cjs +142 -0
- package/src/eslint-rules/require-exception-component-di-check.cjs +314 -0
- package/src/eslint-rules/require-lazy-load-with-retry.cjs +148 -0
- package/src/eslint-rules/require-pending-component-di-check.cjs +3 -3
- package/src/index.d.ts +8 -0
- package/src/index.js +8 -0
- package/src/index.js.map +1 -1
- package/src/scripts/angular-brand-codemod.d.ts +17 -0
- package/src/scripts/angular-brand-codemod.js +44 -8
- package/src/scripts/angular-brand-codemod.js.map +1 -1
- package/src/scripts/angular-brand-codemod.spec.ts +3 -3
- package/src/scripts/angular-brand-codemod.ts +78 -7
- package/src/scripts/migrate.d.ts +32 -0
- package/src/scripts/migrate.js +67 -0
- package/src/scripts/migrate.js.map +1 -0
- package/src/scripts/migrate.ts +128 -0
- package/src/scripts/migration-workspace.d.ts +2 -0
- package/src/scripts/migration-workspace.js +72 -0
- package/src/scripts/migration-workspace.js.map +1 -0
- package/src/scripts/migration-workspace.ts +93 -0
- package/src/scripts/primitives/migrate-primitives.d.ts +27 -0
- package/src/scripts/primitives/migrate-primitives.js +354 -0
- package/src/scripts/primitives/migrate-primitives.js.map +1 -0
- package/src/scripts/primitives/migrate-primitives.spec.ts +279 -0
- package/src/scripts/primitives/migrate-primitives.ts +543 -0
- package/src/scripts/primitives/migration-diagnostic.d.ts +8 -0
- package/src/scripts/primitives/migration-diagnostic.js +2 -0
- package/src/scripts/primitives/migration-diagnostic.js.map +1 -0
- package/src/scripts/primitives/migration-diagnostic.ts +13 -0
- package/src/scripts/routes/migrate-routes.d.ts +34 -0
- package/src/scripts/routes/migrate-routes.js +669 -0
- package/src/scripts/routes/migrate-routes.js.map +1 -0
- package/src/scripts/routes/migrate-routes.spec.ts +264 -0
- package/src/scripts/routes/migrate-routes.ts +897 -0
- package/src/scripts/routes/migration-diagnostic.d.ts +16 -0
- package/src/scripts/routes/migration-diagnostic.js +2 -0
- package/src/scripts/routes/migration-diagnostic.js.map +1 -0
- package/src/scripts/routes/migration-diagnostic.ts +25 -0
- package/src/scripts/services/config.d.ts +2 -0
- package/src/scripts/services/config.js +39 -0
- package/src/scripts/services/config.js.map +1 -0
- package/src/scripts/services/config.ts +60 -0
- package/src/scripts/services/migrate-services.d.ts +29 -0
- package/src/scripts/services/migrate-services.js +895 -0
- package/src/scripts/services/migrate-services.js.map +1 -0
- package/src/scripts/services/migrate-services.spec.ts +719 -0
- package/src/scripts/services/migrate-services.ts +1282 -0
- package/src/scripts/services/migration-diagnostic.d.ts +8 -0
- package/src/scripts/services/migration-diagnostic.js +2 -0
- package/src/scripts/services/migration-diagnostic.js.map +1 -0
- package/src/scripts/services/migration-diagnostic.ts +27 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { __awaiter } from "tslib";
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
5
|
+
import { Node, Project, QuoteKind, SyntaxKind, ts, } from 'ts-morph';
|
|
6
|
+
import { findAngularDecoratedClass, transformSourceFile as generateAngularDependencies, } from '../angular-brand-codemod.js';
|
|
7
|
+
import { migrateEslintConfig } from '../migration-workspace.js';
|
|
8
|
+
export function runRoutesMigration() {
|
|
9
|
+
return __awaiter(this, arguments, void 0, function* (options = {}) {
|
|
10
|
+
var _a, _b, _c;
|
|
11
|
+
const rootDir = resolve((_a = options.rootDir) !== null && _a !== void 0 ? _a : process.cwd());
|
|
12
|
+
const tsConfigFilePath = options.tsConfigFilePath
|
|
13
|
+
? resolve(options.tsConfigFilePath)
|
|
14
|
+
: defaultTsConfig(rootDir);
|
|
15
|
+
const project = createProject(tsConfigFilePath);
|
|
16
|
+
project.addSourceFilesAtPaths([
|
|
17
|
+
join(rootDir, '**/*.ts'),
|
|
18
|
+
`!${join(rootDir, '**/node_modules/**')}`,
|
|
19
|
+
`!${join(rootDir, '**/dist/**')}`,
|
|
20
|
+
`!${join(rootDir, '**/.angular/**')}`,
|
|
21
|
+
]);
|
|
22
|
+
setQuoteKind(project);
|
|
23
|
+
const selectedFiles = ((_b = options.files) === null || _b === void 0 ? void 0 : _b.length)
|
|
24
|
+
? new Set(options.files.map((file) => resolve(file)))
|
|
25
|
+
: undefined;
|
|
26
|
+
const sourceFiles = project.getSourceFiles().filter((sourceFile) => {
|
|
27
|
+
const filePath = sourceFile.getFilePath();
|
|
28
|
+
return (isInside(filePath, rootDir) &&
|
|
29
|
+
!sourceFile.isDeclarationFile() &&
|
|
30
|
+
!/\.(?:spec|test)\.ts$/.test(filePath) &&
|
|
31
|
+
(!selectedFiles || selectedFiles.has(resolve(filePath))));
|
|
32
|
+
});
|
|
33
|
+
const diagnostics = [];
|
|
34
|
+
const generatedComponentFiles = new Set();
|
|
35
|
+
const files = [];
|
|
36
|
+
const legacyCollectionsBefore = sourceFiles.reduce((count, sourceFile) => count + findLegacyRouteDeclarations(sourceFile).length, 0);
|
|
37
|
+
for (const sourceFile of sourceFiles) {
|
|
38
|
+
const before = sourceFile.getFullText();
|
|
39
|
+
const collections = migrateRoutesSourceFile(sourceFile, Object.assign(Object.assign({}, options), { diagnostics,
|
|
40
|
+
generatedComponentFiles }));
|
|
41
|
+
files.push({
|
|
42
|
+
filePath: sourceFile.getFilePath(),
|
|
43
|
+
changed: before !== sourceFile.getFullText(),
|
|
44
|
+
collections,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
for (const sourceFile of sourceFiles) {
|
|
48
|
+
if (rewriteHybridLazyChildren(sourceFile)) {
|
|
49
|
+
const report = files.find((file) => file.filePath === sourceFile.getFilePath());
|
|
50
|
+
if (report)
|
|
51
|
+
report.changed = true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
for (const sourceFile of sourceFiles) {
|
|
55
|
+
const result = generateAngularDependencies(sourceFile);
|
|
56
|
+
if (result.changed)
|
|
57
|
+
generatedComponentFiles.add(sourceFile);
|
|
58
|
+
}
|
|
59
|
+
for (const sourceFile of sourceFiles) {
|
|
60
|
+
const before = sourceFile.getFullText();
|
|
61
|
+
const appConfigMigrated = migrateApplicationConfigSourceFile(sourceFile);
|
|
62
|
+
if (!appConfigMigrated &&
|
|
63
|
+
sourceFile.getVariableDeclaration('appConfig') &&
|
|
64
|
+
sourceFile.getFullText().includes('provideRouter(')) {
|
|
65
|
+
diagnostics.push({
|
|
66
|
+
code: 'APP_CONFIG_REQUIRES_CRAFT_ROUTES',
|
|
67
|
+
filePath: sourceFile.getFilePath(),
|
|
68
|
+
message: 'appConfig reste en ApplicationConfig tant que la collection racine et ses guards ne sont pas migrés vers craftRoutes.',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
migrateBootstrapSourceFile(sourceFile);
|
|
72
|
+
if (before === sourceFile.getFullText())
|
|
73
|
+
continue;
|
|
74
|
+
sourceFile.organizeImports();
|
|
75
|
+
const report = files.find((file) => file.filePath === sourceFile.getFilePath());
|
|
76
|
+
if (report)
|
|
77
|
+
report.changed = true;
|
|
78
|
+
}
|
|
79
|
+
const eslintConfig = migrateEslintConfig(project, tsConfigFilePath ? dirname(tsConfigFilePath) : rootDir);
|
|
80
|
+
const changedSourceFiles = new Set(files
|
|
81
|
+
.filter((file) => file.changed)
|
|
82
|
+
.map((file) => project.getSourceFileOrThrow(file.filePath)));
|
|
83
|
+
for (const sourceFile of generatedComponentFiles) {
|
|
84
|
+
if (sourceFile.wasForgotten())
|
|
85
|
+
continue;
|
|
86
|
+
changedSourceFiles.add(sourceFile);
|
|
87
|
+
}
|
|
88
|
+
if (eslintConfig)
|
|
89
|
+
changedSourceFiles.add(eslintConfig);
|
|
90
|
+
if (options.write) {
|
|
91
|
+
yield Promise.all([...changedSourceFiles].map((file) => file.save()));
|
|
92
|
+
}
|
|
93
|
+
const remainingLegacyCollections = options.write
|
|
94
|
+
? sourceFiles.reduce((count, sourceFile) => count + findLegacyRouteDeclarations(sourceFile).length, 0)
|
|
95
|
+
: legacyCollectionsBefore;
|
|
96
|
+
const changedFiles = [...changedSourceFiles].map((file) => file.getFilePath());
|
|
97
|
+
const exitCode = (options.check && remainingLegacyCollections > 0) ||
|
|
98
|
+
(options.failOnManual && diagnostics.length > 0)
|
|
99
|
+
? 1
|
|
100
|
+
: 0;
|
|
101
|
+
const result = {
|
|
102
|
+
changedFiles,
|
|
103
|
+
files,
|
|
104
|
+
diagnostics,
|
|
105
|
+
remainingLegacyCollections,
|
|
106
|
+
exitCode,
|
|
107
|
+
};
|
|
108
|
+
if (options.jsonFilePath) {
|
|
109
|
+
const jsonFilePath = resolve(options.jsonFilePath);
|
|
110
|
+
yield mkdir(dirname(jsonFilePath), { recursive: true });
|
|
111
|
+
yield writeFile(jsonFilePath, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
112
|
+
}
|
|
113
|
+
logResult(result, (_c = options.log) !== null && _c !== void 0 ? _c : console.log, options.write === true);
|
|
114
|
+
return result;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export function migrateApplicationConfigSourceFile(sourceFile) {
|
|
118
|
+
const declaration = sourceFile.getVariableDeclaration('appConfig');
|
|
119
|
+
const initializer = declaration === null || declaration === void 0 ? void 0 : declaration.getInitializerIfKind(SyntaxKind.ObjectLiteralExpression);
|
|
120
|
+
if (!declaration || !initializer)
|
|
121
|
+
return false;
|
|
122
|
+
const routerCall = initializer
|
|
123
|
+
.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
124
|
+
.find((call) => call.getExpression().getText() === 'provideRouter');
|
|
125
|
+
const routesArgument = routerCall === null || routerCall === void 0 ? void 0 : routerCall.getArguments()[0];
|
|
126
|
+
if (!routerCall || !Node.isIdentifier(routesArgument))
|
|
127
|
+
return false;
|
|
128
|
+
const routesName = routesArgument.getText();
|
|
129
|
+
if (!isCraftRoutesImport(sourceFile, routesName))
|
|
130
|
+
return false;
|
|
131
|
+
routerCall.getExpression().replaceWithText('provideCraftRouter');
|
|
132
|
+
routesArgument.replaceWithText(`${routesName}.toRoutes()`);
|
|
133
|
+
const configText = initializer.getText();
|
|
134
|
+
declaration.setInitializer(`craftAppConfig({ routingDeps: ${routesName}.META_DATA, ${configText.slice(1, -1)} })`);
|
|
135
|
+
declaration.removeType();
|
|
136
|
+
ensureValueImport(sourceFile, 'craftAppConfig');
|
|
137
|
+
ensureValueImport(sourceFile, 'provideCraftRouter');
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
function isCraftRoutesImport(sourceFile, name) {
|
|
141
|
+
var _a;
|
|
142
|
+
const declaration = sourceFile
|
|
143
|
+
.getImportDeclarations()
|
|
144
|
+
.find((item) => item.getNamedImports().some((namedImport) => namedImport.getName() === name));
|
|
145
|
+
return (((_a = declaration === null || declaration === void 0 ? void 0 : declaration.getModuleSpecifierSourceFile()) === null || _a === void 0 ? void 0 : _a.getFullText().includes('craftRoutes(')) === true);
|
|
146
|
+
}
|
|
147
|
+
export function migrateBootstrapSourceFile(sourceFile) {
|
|
148
|
+
var _a;
|
|
149
|
+
const appConfigImport = sourceFile
|
|
150
|
+
.getImportDeclarations()
|
|
151
|
+
.find((item) => item
|
|
152
|
+
.getNamedImports()
|
|
153
|
+
.some((namedImport) => namedImport.getName() === 'appConfig'));
|
|
154
|
+
if (appConfigImport &&
|
|
155
|
+
!((_a = appConfigImport
|
|
156
|
+
.getModuleSpecifierSourceFile()) === null || _a === void 0 ? void 0 : _a.getFullText().includes('craftAppConfig('))) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
let changed = false;
|
|
160
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
161
|
+
if (call.getExpression().getText() !== 'bootstrapApplication')
|
|
162
|
+
continue;
|
|
163
|
+
const config = call.getArguments()[1];
|
|
164
|
+
if (!Node.isIdentifier(config) || config.getText() !== 'appConfig')
|
|
165
|
+
continue;
|
|
166
|
+
config.replaceWithText('toApplicationConfig(appConfig)');
|
|
167
|
+
ensureValueImport(sourceFile, 'toApplicationConfig');
|
|
168
|
+
changed = true;
|
|
169
|
+
}
|
|
170
|
+
return changed;
|
|
171
|
+
}
|
|
172
|
+
function rewriteHybridLazyChildren(sourceFile) {
|
|
173
|
+
var _a;
|
|
174
|
+
let changed = false;
|
|
175
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
176
|
+
const thenAccess = call.getExpression();
|
|
177
|
+
if (!Node.isPropertyAccessExpression(thenAccess) || thenAccess.getName() !== 'then') {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
const importCall = thenAccess.getExpression();
|
|
181
|
+
if (!Node.isCallExpression(importCall) ||
|
|
182
|
+
importCall.getExpression().getKind() !== SyntaxKind.ImportKeyword) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const moduleArgument = importCall.getArguments()[0];
|
|
186
|
+
const callback = call.getArguments()[0];
|
|
187
|
+
if (!Node.isStringLiteral(moduleArgument) ||
|
|
188
|
+
!Node.isArrowFunction(callback) ||
|
|
189
|
+
!Node.isPropertyAccessExpression(callback.getBody()) ||
|
|
190
|
+
callback.getBody().getText().endsWith('.toRoutes()')) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const base = resolve(dirname(sourceFile.getFilePath()), moduleArgument.getLiteralValue());
|
|
194
|
+
const imported = (_a = sourceFile.getProject().getSourceFile(`${base}.ts`)) !== null && _a !== void 0 ? _a : sourceFile.getProject().getSourceFile(join(base, 'index.ts'));
|
|
195
|
+
if (!(imported === null || imported === void 0 ? void 0 : imported.getFullText().includes('craftRoutes(')))
|
|
196
|
+
continue;
|
|
197
|
+
callback.getBody().replaceWithText(`${callback.getBody().getText()}.toRoutes()`);
|
|
198
|
+
changed = true;
|
|
199
|
+
}
|
|
200
|
+
return changed;
|
|
201
|
+
}
|
|
202
|
+
export function migrateRoutesSourceFile(sourceFile, options = {}) {
|
|
203
|
+
var _a, _b, _c;
|
|
204
|
+
const declarations = findLegacyRouteDeclarations(sourceFile);
|
|
205
|
+
if (declarations.length === 0)
|
|
206
|
+
return [];
|
|
207
|
+
const diagnostics = (_a = options.diagnostics) !== null && _a !== void 0 ? _a : [];
|
|
208
|
+
const generatedComponentFiles = (_b = options.generatedComponentFiles) !== null && _b !== void 0 ? _b : new Set();
|
|
209
|
+
const migratedCollections = [];
|
|
210
|
+
for (const declaration of declarations) {
|
|
211
|
+
const array = getRoutesArray(declaration);
|
|
212
|
+
if (!array)
|
|
213
|
+
continue;
|
|
214
|
+
const collectionName = (_c = options.collectionName) !== null && _c !== void 0 ? _c : inferCollectionName(sourceFile, declaration);
|
|
215
|
+
const generatedRoutesName = `${collectionName}Routes`;
|
|
216
|
+
const exportedName = declaration.getName();
|
|
217
|
+
const context = {
|
|
218
|
+
sourceFile,
|
|
219
|
+
diagnostics,
|
|
220
|
+
usedCraftRoute: false,
|
|
221
|
+
generatedComponentFiles,
|
|
222
|
+
};
|
|
223
|
+
if (hasBlockingAngularRoute(array, context, ''))
|
|
224
|
+
continue;
|
|
225
|
+
if (options.parentMount && options.parentNames === undefined) {
|
|
226
|
+
diagnostics.push({
|
|
227
|
+
code: 'PARENT_CONTEXT_UNKNOWN',
|
|
228
|
+
filePath: sourceFile.getFilePath(),
|
|
229
|
+
routePath: options.parentMount,
|
|
230
|
+
message: 'Le contexte DI du parent n’est pas déclaré. Passez --parent-names (une valeur vide confirme explicitement qu’aucun provider nommé n’est hérité).',
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
for (const element of [...array.getElements()]) {
|
|
234
|
+
if (Node.isObjectLiteralExpression(element)) {
|
|
235
|
+
transformRoute(element, context, '');
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const arrayText = array.getText();
|
|
239
|
+
const binding = exportedName === generatedRoutesName
|
|
240
|
+
? generatedRoutesName
|
|
241
|
+
: `${generatedRoutesName}: ${exportedName}`;
|
|
242
|
+
const parentSuffix = options.parentMount
|
|
243
|
+
? `.withParent<ParentRoutes<${JSON.stringify(options.parentMount)}>>()`
|
|
244
|
+
: '';
|
|
245
|
+
declaration
|
|
246
|
+
.getVariableStatementOrThrow()
|
|
247
|
+
.replaceWithText(`export const { ${binding} } = craftRoutes(${JSON.stringify(collectionName)}, ${arrayText})${parentSuffix};`);
|
|
248
|
+
ensureValueImport(sourceFile, 'craftRoutes');
|
|
249
|
+
if (context.usedCraftRoute)
|
|
250
|
+
ensureValueImport(sourceFile, 'craftRoute');
|
|
251
|
+
if (options.parentMount)
|
|
252
|
+
ensureTypeImport(sourceFile, 'ParentRoutes');
|
|
253
|
+
addDiCheck(sourceFile, exportedName, collectionName, options.parentNames);
|
|
254
|
+
removeUnusedAngularRoutesImport(sourceFile);
|
|
255
|
+
migratedCollections.push(exportedName);
|
|
256
|
+
}
|
|
257
|
+
if (migratedCollections.length > 0)
|
|
258
|
+
sourceFile.formatText();
|
|
259
|
+
return migratedCollections;
|
|
260
|
+
}
|
|
261
|
+
function findLegacyRouteDeclarations(sourceFile) {
|
|
262
|
+
return sourceFile.getVariableDeclarations().filter((declaration) => {
|
|
263
|
+
var _a, _b;
|
|
264
|
+
const statement = declaration.getVariableStatement();
|
|
265
|
+
if (!(statement === null || statement === void 0 ? void 0 : statement.isExported()) || !getRoutesArray(declaration))
|
|
266
|
+
return false;
|
|
267
|
+
const typeText = (_a = declaration.getTypeNode()) === null || _a === void 0 ? void 0 : _a.getText();
|
|
268
|
+
const initializer = declaration.getInitializer();
|
|
269
|
+
if (typeText === 'Routes')
|
|
270
|
+
return true;
|
|
271
|
+
if (!initializer || !Node.isSatisfiesExpression(initializer))
|
|
272
|
+
return false;
|
|
273
|
+
return ((_b = initializer.getTypeNode()) === null || _b === void 0 ? void 0 : _b.getText()) === 'Routes';
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
function getRoutesArray(declaration) {
|
|
277
|
+
let initializer = declaration.getInitializer();
|
|
278
|
+
if (Node.isSatisfiesExpression(initializer) || Node.isAsExpression(initializer)) {
|
|
279
|
+
initializer = initializer.getExpression();
|
|
280
|
+
}
|
|
281
|
+
return Node.isArrayLiteralExpression(initializer) ? initializer : undefined;
|
|
282
|
+
}
|
|
283
|
+
function transformRoute(route, context, parentPath) {
|
|
284
|
+
var _a;
|
|
285
|
+
const pathProperty = getProperty(route, 'path');
|
|
286
|
+
const pathInitializer = pathProperty === null || pathProperty === void 0 ? void 0 : pathProperty.getInitializer();
|
|
287
|
+
const path = getStaticString(pathInitializer);
|
|
288
|
+
const routeLabel = path === undefined ? parentPath || '<unknown>' : joinRoute(parentPath, path);
|
|
289
|
+
if (path === undefined) {
|
|
290
|
+
diagnose(context, 'DYNAMIC_PATH', routeLabel, 'Le path de la route n’est pas une chaîne statique.');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (getProperty(route, 'children')) {
|
|
294
|
+
diagnose(context, 'ROUTE_SPLIT_RECOMMENDED', routeLabel, 'craftRoute ne prend pas en charge children dans l’API actuelle ; extrayez ces routes dans une collection montée par loadChildren.');
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const redirect = getProperty(route, 'redirectTo');
|
|
298
|
+
if (redirect && getStaticString(redirect.getInitializer()) === undefined) {
|
|
299
|
+
diagnose(context, 'DYNAMIC_REDIRECT', routeLabel, 'Le redirectTo est dynamique et doit être vérifié manuellement.');
|
|
300
|
+
}
|
|
301
|
+
const hasComponent = Boolean((_a = getProperty(route, 'component')) !== null && _a !== void 0 ? _a : getProperty(route, 'loadComponent'));
|
|
302
|
+
if (!hasComponent)
|
|
303
|
+
return;
|
|
304
|
+
if (getProperty(route, 'loadChildren')) {
|
|
305
|
+
diagnose(context, 'ROUTE_SPLIT_RECOMMENDED', routeLabel, 'La route combine un composant et loadChildren ; séparez-la en collections explicites.');
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
if (hasAngularGuard(route, context, routeLabel))
|
|
309
|
+
return;
|
|
310
|
+
const component = resolveComponent(route, context.sourceFile);
|
|
311
|
+
if (!component) {
|
|
312
|
+
diagnose(context, 'COMPONENT_NOT_RESOLVABLE', routeLabel, 'Le composant routé ou son module ne peut pas être résolu sans ambiguïté.');
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (!ensureGeneratedDeps(component, context)) {
|
|
316
|
+
diagnose(context, 'COMPONENT_NOT_RESOLVABLE', routeLabel, `GenDeps_${component.componentName} ne peut pas être généré de façon sûre.`);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
if (!getProperty(route, 'componentDeps')) {
|
|
320
|
+
route.addPropertyAssignment({
|
|
321
|
+
name: 'componentDeps',
|
|
322
|
+
initializer: `{} as import(${JSON.stringify(component.moduleSpecifier)}).GenDeps_${component.componentName}`,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
pathProperty === null || pathProperty === void 0 ? void 0 : pathProperty.remove();
|
|
326
|
+
const definition = route.getText();
|
|
327
|
+
route.replaceWithText(`craftRoute(${JSON.stringify(path)}, ${definition})`);
|
|
328
|
+
context.usedCraftRoute = true;
|
|
329
|
+
}
|
|
330
|
+
function hasBlockingAngularRoute(routes, context, parentPath) {
|
|
331
|
+
var _a;
|
|
332
|
+
let blocked = false;
|
|
333
|
+
for (const element of routes.getElements()) {
|
|
334
|
+
if (!Node.isObjectLiteralExpression(element))
|
|
335
|
+
continue;
|
|
336
|
+
const path = getStaticString((_a = getProperty(element, 'path')) === null || _a === void 0 ? void 0 : _a.getInitializer());
|
|
337
|
+
const routePath = path === undefined ? parentPath || '<unknown>' : joinRoute(parentPath, path);
|
|
338
|
+
if (getProperty(element, 'children')) {
|
|
339
|
+
diagnose(context, 'ROUTE_SPLIT_RECOMMENDED', routePath, 'craftRoute ne prend pas en charge children dans l’API actuelle ; extrayez ces routes dans une collection montée par loadChildren.');
|
|
340
|
+
blocked = true;
|
|
341
|
+
}
|
|
342
|
+
if (hasAngularGuard(element, context, routePath))
|
|
343
|
+
blocked = true;
|
|
344
|
+
}
|
|
345
|
+
return blocked;
|
|
346
|
+
}
|
|
347
|
+
function hasAngularGuard(route, context, routePath) {
|
|
348
|
+
var _a;
|
|
349
|
+
let found = false;
|
|
350
|
+
for (const guardName of ['canActivate', 'canMatch']) {
|
|
351
|
+
const guard = (_a = getProperty(route, guardName)) === null || _a === void 0 ? void 0 : _a.getInitializer();
|
|
352
|
+
if (!guard)
|
|
353
|
+
continue;
|
|
354
|
+
if (Node.isCallExpression(guard) &&
|
|
355
|
+
guard.getExpression().getText() ===
|
|
356
|
+
(guardName === 'canActivate' ? 'craftCanActivate' : 'craftCanMatch')) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
found = true;
|
|
360
|
+
if (Node.isArrayLiteralExpression(guard) && guard.getElements().length > 1) {
|
|
361
|
+
diagnose(context, 'MULTIPLE_GUARDS_REQUIRE_COMPOSITION', routePath, `${guardName} contient plusieurs guards ; leur composition craft-ng est une décision métier.`);
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
diagnose(context, 'ANGULAR_GUARD_REQUIRES_REWRITE', routePath, `${guardName} doit être réécrit avec craftCanActivate/craftCanMatch.`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return found;
|
|
368
|
+
}
|
|
369
|
+
function resolveComponent(route, routeSourceFile) {
|
|
370
|
+
var _a, _b, _c, _d;
|
|
371
|
+
const direct = (_a = getProperty(route, 'component')) === null || _a === void 0 ? void 0 : _a.getInitializer();
|
|
372
|
+
if (Node.isIdentifier(direct)) {
|
|
373
|
+
const defaultImport = routeSourceFile
|
|
374
|
+
.getImportDeclarations()
|
|
375
|
+
.find((candidate) => { var _a; return ((_a = candidate.getDefaultImport()) === null || _a === void 0 ? void 0 : _a.getText()) === direct.getText(); });
|
|
376
|
+
if (defaultImport) {
|
|
377
|
+
const componentSourceFile = defaultImport.getModuleSpecifierSourceFile();
|
|
378
|
+
return {
|
|
379
|
+
moduleSpecifier: defaultImport.getModuleSpecifierValue(),
|
|
380
|
+
componentName: (_b = inferSingleComponentName(componentSourceFile)) !== null && _b !== void 0 ? _b : direct.getText(),
|
|
381
|
+
sourceFile: componentSourceFile,
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const importDeclaration = routeSourceFile
|
|
385
|
+
.getImportDeclarations()
|
|
386
|
+
.find((candidate) => candidate.getNamedImports().some((specifier) => {
|
|
387
|
+
var _a, _b;
|
|
388
|
+
return ((_b = (_a = specifier.getAliasNode()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : specifier.getName()) ===
|
|
389
|
+
direct.getText();
|
|
390
|
+
}));
|
|
391
|
+
const namedImport = importDeclaration === null || importDeclaration === void 0 ? void 0 : importDeclaration.getNamedImports().find((specifier) => {
|
|
392
|
+
var _a, _b;
|
|
393
|
+
return ((_b = (_a = specifier.getAliasNode()) === null || _a === void 0 ? void 0 : _a.getText()) !== null && _b !== void 0 ? _b : specifier.getName()) ===
|
|
394
|
+
direct.getText();
|
|
395
|
+
});
|
|
396
|
+
if (importDeclaration && namedImport) {
|
|
397
|
+
return {
|
|
398
|
+
moduleSpecifier: importDeclaration.getModuleSpecifierValue(),
|
|
399
|
+
componentName: namedImport.getName(),
|
|
400
|
+
sourceFile: importDeclaration.getModuleSpecifierSourceFile(),
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
return {
|
|
404
|
+
moduleSpecifier: `./${basename(routeSourceFile.getFilePath(), extname(routeSourceFile.getFilePath()))}`,
|
|
405
|
+
componentName: direct.getText(),
|
|
406
|
+
sourceFile: routeSourceFile,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const lazy = (_c = getProperty(route, 'loadComponent')) === null || _c === void 0 ? void 0 : _c.getInitializer();
|
|
410
|
+
if (!Node.isArrowFunction(lazy) && !Node.isFunctionExpression(lazy))
|
|
411
|
+
return undefined;
|
|
412
|
+
const body = lazy.getBody();
|
|
413
|
+
const expression = (Node.isBlock(body)
|
|
414
|
+
? (_d = body.getDescendantsOfKind(SyntaxKind.ReturnStatement)[0]) === null || _d === void 0 ? void 0 : _d.getExpression()
|
|
415
|
+
: body);
|
|
416
|
+
if (!expression)
|
|
417
|
+
return undefined;
|
|
418
|
+
return resolveLazyComponent(expression, routeSourceFile);
|
|
419
|
+
}
|
|
420
|
+
function resolveLazyComponent(expression, routeSourceFile) {
|
|
421
|
+
var _a;
|
|
422
|
+
if (Node.isAwaitExpression(expression))
|
|
423
|
+
expression = expression.getExpression();
|
|
424
|
+
if (Node.isCallExpression(expression)) {
|
|
425
|
+
const calledExpression = expression.getExpression();
|
|
426
|
+
if (!Node.isPropertyAccessExpression(calledExpression)) {
|
|
427
|
+
return resolveBareImport(expression, routeSourceFile);
|
|
428
|
+
}
|
|
429
|
+
const propertyAccess = calledExpression;
|
|
430
|
+
if (propertyAccess.getName() === 'then') {
|
|
431
|
+
const importCall = propertyAccess.getExpression();
|
|
432
|
+
const callback = expression.getArguments()[0];
|
|
433
|
+
if (!Node.isCallExpression(importCall) || importCall.getExpression().getText() !== 'import' || !Node.isArrowFunction(callback))
|
|
434
|
+
return undefined;
|
|
435
|
+
const moduleSpecifier = getStaticString(importCall.getArguments()[0]);
|
|
436
|
+
if (!moduleSpecifier)
|
|
437
|
+
return undefined;
|
|
438
|
+
const sourceFile = resolveModuleSourceFile(routeSourceFile, moduleSpecifier);
|
|
439
|
+
const componentName = resolveThenComponentName(callback);
|
|
440
|
+
if (!componentName)
|
|
441
|
+
return undefined;
|
|
442
|
+
return {
|
|
443
|
+
moduleSpecifier,
|
|
444
|
+
componentName: componentName === 'default'
|
|
445
|
+
? ((_a = inferSingleComponentName(sourceFile)) !== null && _a !== void 0 ? _a : componentName)
|
|
446
|
+
: componentName,
|
|
447
|
+
sourceFile,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
function resolveThenComponentName(callback) {
|
|
454
|
+
var _a, _b, _c, _d;
|
|
455
|
+
if (!Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback)) {
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
const body = callback.getBody();
|
|
459
|
+
const returnedExpression = Node.isBlock(body)
|
|
460
|
+
? (_a = body.getDescendantsOfKind(SyntaxKind.ReturnStatement)[0]) === null || _a === void 0 ? void 0 : _a.getExpression()
|
|
461
|
+
: body;
|
|
462
|
+
if (Node.isPropertyAccessExpression(returnedExpression)) {
|
|
463
|
+
return returnedExpression.getName();
|
|
464
|
+
}
|
|
465
|
+
if (!Node.isIdentifier(returnedExpression))
|
|
466
|
+
return undefined;
|
|
467
|
+
const parameterName = (_b = callback.getParameters()[0]) === null || _b === void 0 ? void 0 : _b.getNameNode();
|
|
468
|
+
if (!Node.isObjectBindingPattern(parameterName))
|
|
469
|
+
return undefined;
|
|
470
|
+
const binding = parameterName
|
|
471
|
+
.getElements()
|
|
472
|
+
.find((element) => element.getName() === returnedExpression.getText());
|
|
473
|
+
return (_d = (_c = binding === null || binding === void 0 ? void 0 : binding.getPropertyNameNode()) === null || _c === void 0 ? void 0 : _c.getText()) !== null && _d !== void 0 ? _d : binding === null || binding === void 0 ? void 0 : binding.getName();
|
|
474
|
+
}
|
|
475
|
+
function inferSingleComponentName(sourceFile) {
|
|
476
|
+
if (!sourceFile)
|
|
477
|
+
return undefined;
|
|
478
|
+
const genDeps = sourceFile
|
|
479
|
+
.getTypeAliases()
|
|
480
|
+
.filter((alias) => alias.isExported() && alias.getName().startsWith('GenDeps_'));
|
|
481
|
+
if (genDeps.length === 1) {
|
|
482
|
+
return genDeps[0].getName().slice('GenDeps_'.length);
|
|
483
|
+
}
|
|
484
|
+
const angularClass = findAngularDecoratedClass(sourceFile);
|
|
485
|
+
return angularClass.skipped ? undefined : angularClass.className;
|
|
486
|
+
}
|
|
487
|
+
function resolveBareImport(expression, routeSourceFile) {
|
|
488
|
+
if (expression.getExpression().getText() === 'import') {
|
|
489
|
+
const moduleSpecifier = getStaticString(expression.getArguments()[0]);
|
|
490
|
+
if (!moduleSpecifier)
|
|
491
|
+
return undefined;
|
|
492
|
+
const sourceFile = resolveModuleSourceFile(routeSourceFile, moduleSpecifier);
|
|
493
|
+
if (!sourceFile)
|
|
494
|
+
return undefined;
|
|
495
|
+
const componentName = inferSingleComponentName(sourceFile);
|
|
496
|
+
if (componentName) {
|
|
497
|
+
return {
|
|
498
|
+
moduleSpecifier,
|
|
499
|
+
componentName,
|
|
500
|
+
sourceFile,
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return undefined;
|
|
505
|
+
}
|
|
506
|
+
function ensureGeneratedDeps(component, context) {
|
|
507
|
+
const sourceFile = component.sourceFile;
|
|
508
|
+
if (!sourceFile)
|
|
509
|
+
return false;
|
|
510
|
+
if (sourceFile.getTypeAlias(`GenDeps_${component.componentName}`))
|
|
511
|
+
return true;
|
|
512
|
+
const angularClass = findAngularDecoratedClass(sourceFile);
|
|
513
|
+
if (angularClass.skipped ||
|
|
514
|
+
angularClass.className !== component.componentName) {
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
const result = generateAngularDependencies(sourceFile);
|
|
518
|
+
if (result.changed)
|
|
519
|
+
context.generatedComponentFiles.add(sourceFile);
|
|
520
|
+
return Boolean(sourceFile.getTypeAlias(`GenDeps_${component.componentName}`));
|
|
521
|
+
}
|
|
522
|
+
function resolveModuleSourceFile(sourceFile, moduleSpecifier) {
|
|
523
|
+
if (!moduleSpecifier.startsWith('.'))
|
|
524
|
+
return undefined;
|
|
525
|
+
const base = resolve(dirname(sourceFile.getFilePath()), moduleSpecifier);
|
|
526
|
+
const project = sourceFile.getProject();
|
|
527
|
+
for (const candidate of [
|
|
528
|
+
`${base}.ts`,
|
|
529
|
+
`${base}.tsx`,
|
|
530
|
+
join(base, 'index.ts'),
|
|
531
|
+
]) {
|
|
532
|
+
const resolved = project.getSourceFile(candidate);
|
|
533
|
+
if (resolved)
|
|
534
|
+
return resolved;
|
|
535
|
+
}
|
|
536
|
+
return undefined;
|
|
537
|
+
}
|
|
538
|
+
function addDiCheck(sourceFile, routesName, collectionName, parentNames) {
|
|
539
|
+
ensureTypeImport(sourceFile, 'CanRun');
|
|
540
|
+
ensureTypeImport(sourceFile, 'ValidateCascadeRoutesFile');
|
|
541
|
+
ensureTypeImport(sourceFile, 'Router', '@angular/router');
|
|
542
|
+
const suffix = toPascalCase(collectionName);
|
|
543
|
+
const names = (parentNames === null || parentNames === void 0 ? void 0 : parentNames.length)
|
|
544
|
+
? parentNames.map((name) => JSON.stringify(name)).join(' | ')
|
|
545
|
+
: 'never';
|
|
546
|
+
sourceFile.addStatements(`\ntype _Check${suffix}DI = ValidateCascadeRoutesFile<${names}, Router, typeof ${routesName}>;\nexport type _CanRun${suffix} = CanRun<_Check${suffix}DI>;`);
|
|
547
|
+
}
|
|
548
|
+
function ensureValueImport(sourceFile, name, moduleSpecifier = '@craft-ng/core') {
|
|
549
|
+
let declaration = sourceFile
|
|
550
|
+
.getImportDeclarations()
|
|
551
|
+
.find((item) => item.getModuleSpecifierValue() === moduleSpecifier && !item.isTypeOnly());
|
|
552
|
+
if (!declaration) {
|
|
553
|
+
declaration = sourceFile.addImportDeclaration({ moduleSpecifier, namedImports: [name] });
|
|
554
|
+
}
|
|
555
|
+
else if (!declaration.getNamedImports().some((item) => item.getName() === name)) {
|
|
556
|
+
declaration.addNamedImport(name);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
function ensureTypeImport(sourceFile, name, moduleSpecifier = '@craft-ng/core') {
|
|
560
|
+
const alreadyImported = sourceFile.getImportDeclarations().some((item) => item.getModuleSpecifierValue() === moduleSpecifier &&
|
|
561
|
+
item.getNamedImports().some((specifier) => specifier.getName() === name));
|
|
562
|
+
if (alreadyImported)
|
|
563
|
+
return;
|
|
564
|
+
const declaration = sourceFile
|
|
565
|
+
.getImportDeclarations()
|
|
566
|
+
.find((item) => item.getModuleSpecifierValue() === moduleSpecifier && item.isTypeOnly());
|
|
567
|
+
if (!declaration) {
|
|
568
|
+
sourceFile.addImportDeclaration({
|
|
569
|
+
moduleSpecifier,
|
|
570
|
+
isTypeOnly: true,
|
|
571
|
+
namedImports: [name],
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
declaration.addNamedImport(name);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function removeUnusedAngularRoutesImport(sourceFile) {
|
|
579
|
+
const importDeclaration = sourceFile
|
|
580
|
+
.getImportDeclarations()
|
|
581
|
+
.find((item) => item.getModuleSpecifierValue() === '@angular/router');
|
|
582
|
+
if (!importDeclaration)
|
|
583
|
+
return;
|
|
584
|
+
const routesImport = importDeclaration
|
|
585
|
+
.getNamedImports()
|
|
586
|
+
.find((item) => item.getName() === 'Routes');
|
|
587
|
+
if (!routesImport)
|
|
588
|
+
return;
|
|
589
|
+
const withoutImports = sourceFile
|
|
590
|
+
.getFullText()
|
|
591
|
+
.replace(importDeclaration.getFullText(), '');
|
|
592
|
+
if (/\bRoutes\b/.test(withoutImports))
|
|
593
|
+
return;
|
|
594
|
+
routesImport.remove();
|
|
595
|
+
if (importDeclaration.getNamedImports().length === 0 &&
|
|
596
|
+
!importDeclaration.getDefaultImport() &&
|
|
597
|
+
!importDeclaration.getNamespaceImport()) {
|
|
598
|
+
importDeclaration.remove();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function getProperty(object, name) {
|
|
602
|
+
const property = object.getProperty(name);
|
|
603
|
+
return Node.isPropertyAssignment(property) ? property : undefined;
|
|
604
|
+
}
|
|
605
|
+
function getStaticString(node) {
|
|
606
|
+
if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
607
|
+
return node.getLiteralValue();
|
|
608
|
+
}
|
|
609
|
+
return undefined;
|
|
610
|
+
}
|
|
611
|
+
function diagnose(context, code, routePath, message) {
|
|
612
|
+
context.diagnostics.push({
|
|
613
|
+
code,
|
|
614
|
+
filePath: context.sourceFile.getFilePath(),
|
|
615
|
+
routePath,
|
|
616
|
+
message,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
function inferCollectionName(sourceFile, declaration) {
|
|
620
|
+
const variableName = declaration.getName();
|
|
621
|
+
if (variableName !== 'routes' && variableName.endsWith('Routes')) {
|
|
622
|
+
return variableName.slice(0, -'Routes'.length);
|
|
623
|
+
}
|
|
624
|
+
const fileName = basename(sourceFile.getFilePath()).replace(/\.routes?\.ts$/, '');
|
|
625
|
+
return toCamelCase(fileName === basename(sourceFile.getFilePath()) ? 'app' : fileName);
|
|
626
|
+
}
|
|
627
|
+
function toCamelCase(value) {
|
|
628
|
+
return value.replace(/[-_.]+([a-zA-Z0-9])/g, (_, character) => character.toUpperCase());
|
|
629
|
+
}
|
|
630
|
+
function toPascalCase(value) {
|
|
631
|
+
const camel = toCamelCase(value);
|
|
632
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
633
|
+
}
|
|
634
|
+
function joinRoute(parent, child) {
|
|
635
|
+
return [parent, child].filter(Boolean).join('/') || '<root>';
|
|
636
|
+
}
|
|
637
|
+
function createProject(tsConfigFilePath) {
|
|
638
|
+
return tsConfigFilePath
|
|
639
|
+
? new Project({ tsConfigFilePath, skipAddingFilesFromTsConfig: false })
|
|
640
|
+
: new Project({
|
|
641
|
+
compilerOptions: {
|
|
642
|
+
experimentalDecorators: true,
|
|
643
|
+
module: ts.ModuleKind.Preserve,
|
|
644
|
+
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
645
|
+
target: ts.ScriptTarget.ES2022,
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
function defaultTsConfig(rootDir) {
|
|
650
|
+
const path = join(rootDir, 'tsconfig.json');
|
|
651
|
+
return existsSync(path) ? path : undefined;
|
|
652
|
+
}
|
|
653
|
+
function isInside(filePath, rootDir) {
|
|
654
|
+
const absolute = isAbsolute(filePath) ? filePath : resolve(filePath);
|
|
655
|
+
return absolute === rootDir || absolute.startsWith(`${rootDir}/`);
|
|
656
|
+
}
|
|
657
|
+
function setQuoteKind(project) {
|
|
658
|
+
project.manipulationSettings.set({ quoteKind: QuoteKind.Single });
|
|
659
|
+
}
|
|
660
|
+
function logResult(result, log, wrote) {
|
|
661
|
+
for (const file of result.files.filter((item) => item.changed)) {
|
|
662
|
+
log(`✓ ${file.filePath}: ${wrote ? 'migré' : 'migration proposée'}`);
|
|
663
|
+
}
|
|
664
|
+
for (const diagnostic of result.diagnostics) {
|
|
665
|
+
log(`! ${diagnostic.filePath}${diagnostic.routePath ? ` / ${diagnostic.routePath}` : ''}: ${diagnostic.code}\n ${diagnostic.message}`);
|
|
666
|
+
}
|
|
667
|
+
log(`summary changed=${result.changedFiles.length} manual=${result.diagnostics.length} remaining=${result.remainingLegacyCollections}`);
|
|
668
|
+
}
|
|
669
|
+
//# sourceMappingURL=migrate-routes.js.map
|