@craft-ng/dev-tools 0.5.0-beta.5 → 0.6.0-beta.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.md +198 -23
- package/generators.json +26 -0
- package/package.json +23 -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/bin/craft.d.ts +2 -0
- package/src/bin/craft.js +166 -0
- package/src/bin/craft.js.map +1 -0
- package/src/eslint-rules/craft-computed-name-match.cjs +8 -125
- package/src/eslint-rules/craft-method-name-match.cjs +8 -166
- package/src/eslint-rules/craft-name-match-utils.cjs +179 -0
- package/src/eslint-rules/craft-signal-source-name-match.cjs +8 -0
- package/src/eslint-rules/craft-source-name-match.cjs +8 -0
- package/src/eslint-rules/global-exception-registry-match.cjs +78 -13
- package/src/eslint-rules/index.cjs +10 -2
- package/src/eslint-rules/require-cascade-route-di-check.cjs +223 -0
- package/src/eslint-rules/require-exception-component-di-check.cjs +66 -10
- package/src/eslint-rules/require-lazy-load-with-retry.cjs +148 -0
- package/src/eslint-rules/require-primitive-generator-unwrap.cjs +267 -0
- package/src/generators/route/compat.d.ts +3 -0
- package/src/generators/route/compat.js +6 -0
- package/src/generators/route/compat.js.map +1 -0
- package/src/generators/route/generator.d.ts +31 -0
- package/src/generators/route/generator.js +434 -0
- package/src/generators/route/generator.js.map +1 -0
- package/src/generators/route/schema.json +50 -0
- package/src/generators/route/split-schema.json +19 -0
- package/src/index.d.ts +9 -0
- package/src/index.js +9 -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-primitive-generators.d.ts +27 -0
- package/src/scripts/primitives/migrate-primitive-generators.js +315 -0
- package/src/scripts/primitives/migrate-primitive-generators.js.map +1 -0
- package/src/scripts/primitives/migrate-primitive-generators.ts +402 -0
- package/src/scripts/primitives/migrate-primitives.d.ts +27 -0
- package/src/scripts/primitives/migrate-primitives.js +386 -0
- package/src/scripts/primitives/migrate-primitives.js.map +1 -0
- package/src/scripts/primitives/migrate-primitives.spec.ts +322 -0
- package/src/scripts/primitives/migrate-primitives.ts +601 -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 +14 -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 +895 -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/routes/route-command.d.ts +75 -0
- package/src/scripts/routes/route-command.js +1187 -0
- package/src/scripts/routes/route-command.js.map +1 -0
- package/src/scripts/routes/route-command.spec.ts +553 -0
- package/src/scripts/routes/route-command.ts +1790 -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 +929 -0
- package/src/scripts/services/migrate-services.js.map +1 -0
- package/src/scripts/services/migrate-services.spec.ts +761 -0
- package/src/scripts/services/migrate-services.ts +1333 -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 +28 -0
- package/src/eslint-rules/require-track-on-dependent-primitives.cjs +0 -219
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { basename, dirname, extname, isAbsolute, join, resolve } from 'node:path';
|
|
4
|
+
import {
|
|
5
|
+
ArrayLiteralExpression,
|
|
6
|
+
CallExpression,
|
|
7
|
+
Expression,
|
|
8
|
+
Node,
|
|
9
|
+
ObjectLiteralExpression,
|
|
10
|
+
Project,
|
|
11
|
+
PropertyAssignment,
|
|
12
|
+
QuoteKind,
|
|
13
|
+
SourceFile,
|
|
14
|
+
SyntaxKind,
|
|
15
|
+
VariableDeclaration,
|
|
16
|
+
ts,
|
|
17
|
+
} from 'ts-morph';
|
|
18
|
+
import {
|
|
19
|
+
findAngularDecoratedClass,
|
|
20
|
+
transformSourceFile as generateAngularDependencies,
|
|
21
|
+
} from '../angular-brand-codemod.js';
|
|
22
|
+
import {
|
|
23
|
+
RouteMigrationDiagnostic,
|
|
24
|
+
RouteMigrationDiagnosticCode,
|
|
25
|
+
} from './migration-diagnostic.js';
|
|
26
|
+
import { migrateEslintConfig } from '../migration-workspace.js';
|
|
27
|
+
|
|
28
|
+
export type MigrateRoutesOptions = {
|
|
29
|
+
rootDir?: string;
|
|
30
|
+
tsConfigFilePath?: string;
|
|
31
|
+
files?: readonly string[];
|
|
32
|
+
write?: boolean;
|
|
33
|
+
check?: boolean;
|
|
34
|
+
collectionName?: string;
|
|
35
|
+
parentMount?: string;
|
|
36
|
+
parentNames?: readonly string[];
|
|
37
|
+
jsonFilePath?: string;
|
|
38
|
+
failOnManual?: boolean;
|
|
39
|
+
log?: (message: string) => void;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type MigratedRoutesFile = {
|
|
43
|
+
filePath: string;
|
|
44
|
+
changed: boolean;
|
|
45
|
+
collections: string[];
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type MigrateRoutesResult = {
|
|
49
|
+
changedFiles: string[];
|
|
50
|
+
files: MigratedRoutesFile[];
|
|
51
|
+
diagnostics: RouteMigrationDiagnostic[];
|
|
52
|
+
remainingLegacyCollections: number;
|
|
53
|
+
exitCode: number;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
type ComponentResolution = {
|
|
57
|
+
moduleSpecifier: string;
|
|
58
|
+
componentName: string;
|
|
59
|
+
sourceFile?: SourceFile;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
type TransformContext = {
|
|
63
|
+
sourceFile: SourceFile;
|
|
64
|
+
diagnostics: RouteMigrationDiagnostic[];
|
|
65
|
+
usedCraftRoute: boolean;
|
|
66
|
+
generatedComponentFiles: Set<SourceFile>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export async function runRoutesMigration(
|
|
70
|
+
options: MigrateRoutesOptions = {},
|
|
71
|
+
): Promise<MigrateRoutesResult> {
|
|
72
|
+
const rootDir = resolve(options.rootDir ?? process.cwd());
|
|
73
|
+
const tsConfigFilePath = options.tsConfigFilePath
|
|
74
|
+
? resolve(options.tsConfigFilePath)
|
|
75
|
+
: defaultTsConfig(rootDir);
|
|
76
|
+
const project = createProject(tsConfigFilePath);
|
|
77
|
+
project.addSourceFilesAtPaths([
|
|
78
|
+
join(rootDir, '**/*.ts'),
|
|
79
|
+
`!${join(rootDir, '**/node_modules/**')}`,
|
|
80
|
+
`!${join(rootDir, '**/dist/**')}`,
|
|
81
|
+
`!${join(rootDir, '**/.angular/**')}`,
|
|
82
|
+
]);
|
|
83
|
+
setQuoteKind(project);
|
|
84
|
+
|
|
85
|
+
const selectedFiles = options.files?.length
|
|
86
|
+
? new Set(options.files.map((file) => resolve(file)))
|
|
87
|
+
: undefined;
|
|
88
|
+
const sourceFiles = project.getSourceFiles().filter((sourceFile) => {
|
|
89
|
+
const filePath = sourceFile.getFilePath();
|
|
90
|
+
return (
|
|
91
|
+
isInside(filePath, rootDir) &&
|
|
92
|
+
!sourceFile.isDeclarationFile() &&
|
|
93
|
+
!/\.(?:spec|test)\.ts$/.test(filePath) &&
|
|
94
|
+
(!selectedFiles || selectedFiles.has(resolve(filePath)))
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const diagnostics: RouteMigrationDiagnostic[] = [];
|
|
99
|
+
const generatedComponentFiles = new Set<SourceFile>();
|
|
100
|
+
const files: MigratedRoutesFile[] = [];
|
|
101
|
+
const legacyCollectionsBefore = sourceFiles.reduce(
|
|
102
|
+
(count, sourceFile) => count + findLegacyRouteDeclarations(sourceFile).length,
|
|
103
|
+
0,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
for (const sourceFile of sourceFiles) {
|
|
107
|
+
const before = sourceFile.getFullText();
|
|
108
|
+
const collections = migrateRoutesSourceFile(sourceFile, {
|
|
109
|
+
...options,
|
|
110
|
+
diagnostics,
|
|
111
|
+
generatedComponentFiles,
|
|
112
|
+
});
|
|
113
|
+
files.push({
|
|
114
|
+
filePath: sourceFile.getFilePath(),
|
|
115
|
+
changed: before !== sourceFile.getFullText(),
|
|
116
|
+
collections,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
for (const sourceFile of sourceFiles) {
|
|
121
|
+
if (rewriteHybridLazyChildren(sourceFile)) {
|
|
122
|
+
const report = files.find(
|
|
123
|
+
(file) => file.filePath === sourceFile.getFilePath(),
|
|
124
|
+
);
|
|
125
|
+
if (report) report.changed = true;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const sourceFile of sourceFiles) {
|
|
130
|
+
const result = generateAngularDependencies(sourceFile);
|
|
131
|
+
if (result.changed) generatedComponentFiles.add(sourceFile);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
for (const sourceFile of sourceFiles) {
|
|
135
|
+
const before = sourceFile.getFullText();
|
|
136
|
+
const appConfigMigrated = migrateApplicationConfigSourceFile(sourceFile);
|
|
137
|
+
if (
|
|
138
|
+
!appConfigMigrated &&
|
|
139
|
+
sourceFile.getVariableDeclaration('appConfig') &&
|
|
140
|
+
sourceFile.getFullText().includes('provideRouter(')
|
|
141
|
+
) {
|
|
142
|
+
diagnostics.push({
|
|
143
|
+
code: 'APP_CONFIG_REQUIRES_CRAFT_ROUTES',
|
|
144
|
+
filePath: sourceFile.getFilePath(),
|
|
145
|
+
message:
|
|
146
|
+
'appConfig reste en ApplicationConfig tant que la collection racine et ses guards ne sont pas migrés vers craftRoutes.',
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
migrateBootstrapSourceFile(sourceFile);
|
|
150
|
+
if (before === sourceFile.getFullText()) continue;
|
|
151
|
+
sourceFile.organizeImports();
|
|
152
|
+
const report = files.find((file) => file.filePath === sourceFile.getFilePath());
|
|
153
|
+
if (report) report.changed = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const eslintConfig = migrateEslintConfig(
|
|
157
|
+
project,
|
|
158
|
+
tsConfigFilePath ? dirname(tsConfigFilePath) : rootDir,
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const changedSourceFiles = new Set(
|
|
162
|
+
files
|
|
163
|
+
.filter((file) => file.changed)
|
|
164
|
+
.map((file) => project.getSourceFileOrThrow(file.filePath)),
|
|
165
|
+
);
|
|
166
|
+
for (const sourceFile of generatedComponentFiles) {
|
|
167
|
+
if (sourceFile.wasForgotten()) continue;
|
|
168
|
+
changedSourceFiles.add(sourceFile);
|
|
169
|
+
}
|
|
170
|
+
if (eslintConfig) changedSourceFiles.add(eslintConfig);
|
|
171
|
+
|
|
172
|
+
if (options.write) {
|
|
173
|
+
await Promise.all([...changedSourceFiles].map((file) => file.save()));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const remainingLegacyCollections = options.write
|
|
177
|
+
? sourceFiles.reduce(
|
|
178
|
+
(count, sourceFile) =>
|
|
179
|
+
count + findLegacyRouteDeclarations(sourceFile).length,
|
|
180
|
+
0,
|
|
181
|
+
)
|
|
182
|
+
: legacyCollectionsBefore;
|
|
183
|
+
const changedFiles = [...changedSourceFiles].map((file) => file.getFilePath());
|
|
184
|
+
const exitCode =
|
|
185
|
+
(options.check && remainingLegacyCollections > 0) ||
|
|
186
|
+
(options.failOnManual && diagnostics.length > 0)
|
|
187
|
+
? 1
|
|
188
|
+
: 0;
|
|
189
|
+
const result: MigrateRoutesResult = {
|
|
190
|
+
changedFiles,
|
|
191
|
+
files,
|
|
192
|
+
diagnostics,
|
|
193
|
+
remainingLegacyCollections,
|
|
194
|
+
exitCode,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
if (options.jsonFilePath) {
|
|
198
|
+
const jsonFilePath = resolve(options.jsonFilePath);
|
|
199
|
+
await mkdir(dirname(jsonFilePath), { recursive: true });
|
|
200
|
+
await writeFile(jsonFilePath, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
logResult(result, options.log ?? console.log, options.write === true);
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function migrateApplicationConfigSourceFile(
|
|
208
|
+
sourceFile: SourceFile,
|
|
209
|
+
): boolean {
|
|
210
|
+
const declaration = sourceFile.getVariableDeclaration('appConfig');
|
|
211
|
+
const initializer = declaration?.getInitializerIfKind(
|
|
212
|
+
SyntaxKind.ObjectLiteralExpression,
|
|
213
|
+
);
|
|
214
|
+
if (!declaration || !initializer) return false;
|
|
215
|
+
const routerCall = initializer
|
|
216
|
+
.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
217
|
+
.find((call) => call.getExpression().getText() === 'provideRouter');
|
|
218
|
+
const routesArgument = routerCall?.getArguments()[0];
|
|
219
|
+
if (!routerCall || !Node.isIdentifier(routesArgument)) return false;
|
|
220
|
+
const routesName = routesArgument.getText();
|
|
221
|
+
if (!isCraftRoutesImport(sourceFile, routesName)) return false;
|
|
222
|
+
routerCall.getExpression().replaceWithText('provideCraftRouter');
|
|
223
|
+
routesArgument.replaceWithText(`${routesName}.toRoutes()`);
|
|
224
|
+
const configText = initializer.getText();
|
|
225
|
+
declaration.setInitializer(
|
|
226
|
+
`craftAppConfig({ routingDeps: ${routesName}.META_DATA, ${configText.slice(1, -1)} })`,
|
|
227
|
+
);
|
|
228
|
+
declaration.removeType();
|
|
229
|
+
ensureValueImport(sourceFile, 'craftAppConfig');
|
|
230
|
+
ensureValueImport(sourceFile, 'provideCraftRouter');
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isCraftRoutesImport(sourceFile: SourceFile, name: string): boolean {
|
|
235
|
+
const declaration = sourceFile
|
|
236
|
+
.getImportDeclarations()
|
|
237
|
+
.find((item) =>
|
|
238
|
+
item.getNamedImports().some((namedImport) => namedImport.getName() === name),
|
|
239
|
+
);
|
|
240
|
+
return (
|
|
241
|
+
declaration
|
|
242
|
+
?.getModuleSpecifierSourceFile()
|
|
243
|
+
?.getFullText()
|
|
244
|
+
.includes('craftRoutes(') === true
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function migrateBootstrapSourceFile(sourceFile: SourceFile): boolean {
|
|
249
|
+
const appConfigImport = sourceFile
|
|
250
|
+
.getImportDeclarations()
|
|
251
|
+
.find((item) =>
|
|
252
|
+
item
|
|
253
|
+
.getNamedImports()
|
|
254
|
+
.some((namedImport) => namedImport.getName() === 'appConfig'),
|
|
255
|
+
);
|
|
256
|
+
if (
|
|
257
|
+
appConfigImport &&
|
|
258
|
+
!appConfigImport
|
|
259
|
+
.getModuleSpecifierSourceFile()
|
|
260
|
+
?.getFullText()
|
|
261
|
+
.includes('craftAppConfig(')
|
|
262
|
+
) {
|
|
263
|
+
return false;
|
|
264
|
+
}
|
|
265
|
+
let changed = false;
|
|
266
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
267
|
+
if (call.getExpression().getText() !== 'bootstrapApplication') continue;
|
|
268
|
+
const config = call.getArguments()[1];
|
|
269
|
+
if (!Node.isIdentifier(config) || config.getText() !== 'appConfig') continue;
|
|
270
|
+
config.replaceWithText('toApplicationConfig(appConfig)');
|
|
271
|
+
ensureValueImport(sourceFile, 'toApplicationConfig');
|
|
272
|
+
changed = true;
|
|
273
|
+
}
|
|
274
|
+
return changed;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function rewriteHybridLazyChildren(sourceFile: SourceFile): boolean {
|
|
278
|
+
let changed = false;
|
|
279
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
280
|
+
const thenAccess = call.getExpression();
|
|
281
|
+
if (!Node.isPropertyAccessExpression(thenAccess) || thenAccess.getName() !== 'then') {
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
const importCall = thenAccess.getExpression();
|
|
285
|
+
if (
|
|
286
|
+
!Node.isCallExpression(importCall) ||
|
|
287
|
+
importCall.getExpression().getKind() !== SyntaxKind.ImportKeyword
|
|
288
|
+
) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const moduleArgument = importCall.getArguments()[0];
|
|
292
|
+
const callback = call.getArguments()[0];
|
|
293
|
+
if (
|
|
294
|
+
!Node.isStringLiteral(moduleArgument) ||
|
|
295
|
+
!Node.isArrowFunction(callback) ||
|
|
296
|
+
!Node.isPropertyAccessExpression(callback.getBody()) ||
|
|
297
|
+
callback.getBody().getText().endsWith('.toRoutes()')
|
|
298
|
+
) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
const base = resolve(
|
|
302
|
+
dirname(sourceFile.getFilePath()),
|
|
303
|
+
moduleArgument.getLiteralValue(),
|
|
304
|
+
);
|
|
305
|
+
const imported =
|
|
306
|
+
sourceFile.getProject().getSourceFile(`${base}.ts`) ??
|
|
307
|
+
sourceFile.getProject().getSourceFile(join(base, 'index.ts'));
|
|
308
|
+
if (!imported?.getFullText().includes('craftRoutes(')) continue;
|
|
309
|
+
callback.getBody().replaceWithText(`${callback.getBody().getText()}.toRoutes()`);
|
|
310
|
+
changed = true;
|
|
311
|
+
}
|
|
312
|
+
return changed;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function migrateRoutesSourceFile(
|
|
316
|
+
sourceFile: SourceFile,
|
|
317
|
+
options: Pick<
|
|
318
|
+
MigrateRoutesOptions,
|
|
319
|
+
'collectionName' | 'parentMount' | 'parentNames'
|
|
320
|
+
> & {
|
|
321
|
+
diagnostics?: RouteMigrationDiagnostic[];
|
|
322
|
+
generatedComponentFiles?: Set<SourceFile>;
|
|
323
|
+
} = {},
|
|
324
|
+
): string[] {
|
|
325
|
+
const declarations = findLegacyRouteDeclarations(sourceFile);
|
|
326
|
+
if (declarations.length === 0) return [];
|
|
327
|
+
|
|
328
|
+
const diagnostics = options.diagnostics ?? [];
|
|
329
|
+
const generatedComponentFiles = options.generatedComponentFiles ?? new Set();
|
|
330
|
+
const migratedCollections: string[] = [];
|
|
331
|
+
|
|
332
|
+
for (const declaration of declarations) {
|
|
333
|
+
const array = getRoutesArray(declaration);
|
|
334
|
+
if (!array) continue;
|
|
335
|
+
|
|
336
|
+
const collectionName =
|
|
337
|
+
options.collectionName ?? inferCollectionName(sourceFile, declaration);
|
|
338
|
+
const generatedRoutesName = `${collectionName}Routes`;
|
|
339
|
+
const exportedName = declaration.getName();
|
|
340
|
+
const context: TransformContext = {
|
|
341
|
+
sourceFile,
|
|
342
|
+
diagnostics,
|
|
343
|
+
usedCraftRoute: false,
|
|
344
|
+
generatedComponentFiles,
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
if (hasBlockingAngularRoute(array, context, '')) continue;
|
|
348
|
+
|
|
349
|
+
if (options.parentMount && options.parentNames === undefined) {
|
|
350
|
+
diagnostics.push({
|
|
351
|
+
code: 'PARENT_CONTEXT_UNKNOWN',
|
|
352
|
+
filePath: sourceFile.getFilePath(),
|
|
353
|
+
routePath: options.parentMount,
|
|
354
|
+
message:
|
|
355
|
+
'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é).',
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
for (const element of [...array.getElements()]) {
|
|
360
|
+
if (Node.isObjectLiteralExpression(element)) {
|
|
361
|
+
transformRoute(element, context, '');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const arrayText = array.getText();
|
|
366
|
+
const binding =
|
|
367
|
+
exportedName === generatedRoutesName
|
|
368
|
+
? generatedRoutesName
|
|
369
|
+
: `${generatedRoutesName}: ${exportedName}`;
|
|
370
|
+
const parentSuffix = options.parentMount
|
|
371
|
+
? `.withParent<ParentRoutes<${JSON.stringify(options.parentMount)}>>()`
|
|
372
|
+
: '';
|
|
373
|
+
declaration
|
|
374
|
+
.getVariableStatementOrThrow()
|
|
375
|
+
.replaceWithText(
|
|
376
|
+
`export const { ${binding} } = craftRoutes(${JSON.stringify(collectionName)}, ${arrayText})${parentSuffix};`,
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
ensureValueImport(sourceFile, 'craftRoutes');
|
|
380
|
+
if (context.usedCraftRoute) ensureValueImport(sourceFile, 'craftRoute');
|
|
381
|
+
if (options.parentMount) ensureTypeImport(sourceFile, 'ParentRoutes');
|
|
382
|
+
addDiCheck(sourceFile, exportedName, collectionName, options.parentNames);
|
|
383
|
+
removeUnusedAngularRoutesImport(sourceFile);
|
|
384
|
+
migratedCollections.push(exportedName);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (migratedCollections.length > 0) sourceFile.formatText();
|
|
388
|
+
return migratedCollections;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function findLegacyRouteDeclarations(
|
|
392
|
+
sourceFile: SourceFile,
|
|
393
|
+
): VariableDeclaration[] {
|
|
394
|
+
return sourceFile.getVariableDeclarations().filter((declaration) => {
|
|
395
|
+
const statement = declaration.getVariableStatement();
|
|
396
|
+
if (!statement?.isExported() || !getRoutesArray(declaration)) return false;
|
|
397
|
+
const typeText = declaration.getTypeNode()?.getText();
|
|
398
|
+
const initializer = declaration.getInitializer();
|
|
399
|
+
if (typeText === 'Routes') return true;
|
|
400
|
+
if (!initializer || !Node.isSatisfiesExpression(initializer)) return false;
|
|
401
|
+
return initializer.getTypeNode()?.getText() === 'Routes';
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function getRoutesArray(
|
|
406
|
+
declaration: VariableDeclaration,
|
|
407
|
+
): ArrayLiteralExpression | undefined {
|
|
408
|
+
let initializer = declaration.getInitializer();
|
|
409
|
+
if (Node.isSatisfiesExpression(initializer) || Node.isAsExpression(initializer)) {
|
|
410
|
+
initializer = initializer.getExpression();
|
|
411
|
+
}
|
|
412
|
+
return Node.isArrayLiteralExpression(initializer) ? initializer : undefined;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function transformRoute(
|
|
416
|
+
route: ObjectLiteralExpression,
|
|
417
|
+
context: TransformContext,
|
|
418
|
+
parentPath: string,
|
|
419
|
+
): void {
|
|
420
|
+
const pathProperty = getProperty(route, 'path');
|
|
421
|
+
const pathInitializer = pathProperty?.getInitializer();
|
|
422
|
+
const path = getStaticString(pathInitializer);
|
|
423
|
+
const routeLabel = path === undefined ? parentPath || '<unknown>' : joinRoute(parentPath, path);
|
|
424
|
+
|
|
425
|
+
if (path === undefined) {
|
|
426
|
+
diagnose(context, 'DYNAMIC_PATH', routeLabel, 'Le path de la route n’est pas une chaîne statique.');
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (getProperty(route, 'children')) {
|
|
431
|
+
diagnose(
|
|
432
|
+
context,
|
|
433
|
+
'ROUTE_SPLIT_RECOMMENDED',
|
|
434
|
+
routeLabel,
|
|
435
|
+
'craftRoute ne prend pas en charge children dans l’API actuelle ; extrayez ces routes dans une collection montée par loadChildren.',
|
|
436
|
+
);
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const redirect = getProperty(route, 'redirectTo');
|
|
441
|
+
if (redirect && getStaticString(redirect.getInitializer()) === undefined) {
|
|
442
|
+
diagnose(context, 'DYNAMIC_REDIRECT', routeLabel, 'Le redirectTo est dynamique et doit être vérifié manuellement.');
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const hasComponent = Boolean(
|
|
446
|
+
getProperty(route, 'component') ?? getProperty(route, 'loadComponent'),
|
|
447
|
+
);
|
|
448
|
+
if (!hasComponent) return;
|
|
449
|
+
|
|
450
|
+
if (getProperty(route, 'loadChildren')) {
|
|
451
|
+
diagnose(context, 'ROUTE_SPLIT_RECOMMENDED', routeLabel, 'La route combine un composant et loadChildren ; séparez-la en collections explicites.');
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (hasAngularGuard(route, context, routeLabel)) return;
|
|
456
|
+
|
|
457
|
+
const component = resolveComponent(route, context.sourceFile);
|
|
458
|
+
if (!component) {
|
|
459
|
+
diagnose(context, 'COMPONENT_NOT_RESOLVABLE', routeLabel, 'Le composant routé ou son module ne peut pas être résolu sans ambiguïté.');
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
if (!ensureGeneratedDeps(component, context)) {
|
|
464
|
+
diagnose(context, 'COMPONENT_NOT_RESOLVABLE', routeLabel, `GenDeps_${component.componentName} ne peut pas être généré de façon sûre.`);
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (!getProperty(route, 'componentDeps')) {
|
|
469
|
+
route.addPropertyAssignment({
|
|
470
|
+
name: 'componentDeps',
|
|
471
|
+
initializer: `{} as import(${JSON.stringify(component.moduleSpecifier)}).GenDeps_${component.componentName}`,
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
pathProperty?.remove();
|
|
475
|
+
const definition = route.getText();
|
|
476
|
+
route.replaceWithText(`craftRoute(${JSON.stringify(path)}, ${definition})`);
|
|
477
|
+
context.usedCraftRoute = true;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function hasBlockingAngularRoute(
|
|
481
|
+
routes: ArrayLiteralExpression,
|
|
482
|
+
context: TransformContext,
|
|
483
|
+
parentPath: string,
|
|
484
|
+
): boolean {
|
|
485
|
+
let blocked = false;
|
|
486
|
+
for (const element of routes.getElements()) {
|
|
487
|
+
if (!Node.isObjectLiteralExpression(element)) continue;
|
|
488
|
+
const path = getStaticString(
|
|
489
|
+
getProperty(element, 'path')?.getInitializer(),
|
|
490
|
+
);
|
|
491
|
+
const routePath =
|
|
492
|
+
path === undefined ? parentPath || '<unknown>' : joinRoute(parentPath, path);
|
|
493
|
+
if (getProperty(element, 'children')) {
|
|
494
|
+
diagnose(
|
|
495
|
+
context,
|
|
496
|
+
'ROUTE_SPLIT_RECOMMENDED',
|
|
497
|
+
routePath,
|
|
498
|
+
'craftRoute ne prend pas en charge children dans l’API actuelle ; extrayez ces routes dans une collection montée par loadChildren.',
|
|
499
|
+
);
|
|
500
|
+
blocked = true;
|
|
501
|
+
}
|
|
502
|
+
if (hasAngularGuard(element, context, routePath)) blocked = true;
|
|
503
|
+
}
|
|
504
|
+
return blocked;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function hasAngularGuard(
|
|
508
|
+
route: ObjectLiteralExpression,
|
|
509
|
+
context: TransformContext,
|
|
510
|
+
routePath: string,
|
|
511
|
+
): boolean {
|
|
512
|
+
let found = false;
|
|
513
|
+
for (const guardName of ['canActivate', 'canMatch'] as const) {
|
|
514
|
+
const guard = getProperty(route, guardName)?.getInitializer();
|
|
515
|
+
if (!guard) continue;
|
|
516
|
+
// A craft guard is authored as a bare generator function (`function* () {}`);
|
|
517
|
+
// the outlet drives it after commit. Such a guard is already migrated.
|
|
518
|
+
if (Node.isFunctionExpression(guard) && guard.isGenerator()) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
found = true;
|
|
522
|
+
if (Node.isArrayLiteralExpression(guard) && guard.getElements().length > 1) {
|
|
523
|
+
diagnose(context, 'MULTIPLE_GUARDS_REQUIRE_COMPOSITION', routePath, `${guardName} contient plusieurs guards ; leur composition craft-ng est une décision métier.`);
|
|
524
|
+
} else {
|
|
525
|
+
diagnose(context, 'ANGULAR_GUARD_REQUIRES_REWRITE', routePath, `${guardName} doit être réécrit en générateur craft (function* () {}).`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return found;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function resolveComponent(
|
|
532
|
+
route: ObjectLiteralExpression,
|
|
533
|
+
routeSourceFile: SourceFile,
|
|
534
|
+
): ComponentResolution | undefined {
|
|
535
|
+
const direct = getProperty(route, 'component')?.getInitializer();
|
|
536
|
+
if (Node.isIdentifier(direct)) {
|
|
537
|
+
const defaultImport = routeSourceFile
|
|
538
|
+
.getImportDeclarations()
|
|
539
|
+
.find(
|
|
540
|
+
(candidate) =>
|
|
541
|
+
candidate.getDefaultImport()?.getText() === direct.getText(),
|
|
542
|
+
);
|
|
543
|
+
if (defaultImport) {
|
|
544
|
+
const componentSourceFile = defaultImport.getModuleSpecifierSourceFile();
|
|
545
|
+
return {
|
|
546
|
+
moduleSpecifier: defaultImport.getModuleSpecifierValue(),
|
|
547
|
+
componentName:
|
|
548
|
+
inferSingleComponentName(componentSourceFile) ?? direct.getText(),
|
|
549
|
+
sourceFile: componentSourceFile,
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
const importDeclaration = routeSourceFile
|
|
553
|
+
.getImportDeclarations()
|
|
554
|
+
.find((candidate) =>
|
|
555
|
+
candidate.getNamedImports().some(
|
|
556
|
+
(specifier) =>
|
|
557
|
+
(specifier.getAliasNode()?.getText() ?? specifier.getName()) ===
|
|
558
|
+
direct.getText(),
|
|
559
|
+
),
|
|
560
|
+
);
|
|
561
|
+
const namedImport = importDeclaration?.getNamedImports().find(
|
|
562
|
+
(specifier) =>
|
|
563
|
+
(specifier.getAliasNode()?.getText() ?? specifier.getName()) ===
|
|
564
|
+
direct.getText(),
|
|
565
|
+
);
|
|
566
|
+
if (importDeclaration && namedImport) {
|
|
567
|
+
return {
|
|
568
|
+
moduleSpecifier: importDeclaration.getModuleSpecifierValue(),
|
|
569
|
+
componentName: namedImport.getName(),
|
|
570
|
+
sourceFile: importDeclaration.getModuleSpecifierSourceFile(),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
return {
|
|
574
|
+
moduleSpecifier: `./${basename(routeSourceFile.getFilePath(), extname(routeSourceFile.getFilePath()))}`,
|
|
575
|
+
componentName: direct.getText(),
|
|
576
|
+
sourceFile: routeSourceFile,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const lazy = getProperty(route, 'loadComponent')?.getInitializer();
|
|
581
|
+
if (!Node.isArrowFunction(lazy) && !Node.isFunctionExpression(lazy)) return undefined;
|
|
582
|
+
const body = lazy.getBody();
|
|
583
|
+
const expression = (Node.isBlock(body)
|
|
584
|
+
? body.getDescendantsOfKind(SyntaxKind.ReturnStatement)[0]?.getExpression()
|
|
585
|
+
: body) as Expression | undefined;
|
|
586
|
+
if (!expression) return undefined;
|
|
587
|
+
return resolveLazyComponent(expression, routeSourceFile);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function resolveLazyComponent(
|
|
591
|
+
expression: Expression,
|
|
592
|
+
routeSourceFile: SourceFile,
|
|
593
|
+
): ComponentResolution | undefined {
|
|
594
|
+
if (Node.isAwaitExpression(expression)) expression = expression.getExpression();
|
|
595
|
+
if (Node.isCallExpression(expression)) {
|
|
596
|
+
const calledExpression = expression.getExpression();
|
|
597
|
+
if (!Node.isPropertyAccessExpression(calledExpression)) {
|
|
598
|
+
return resolveBareImport(expression, routeSourceFile);
|
|
599
|
+
}
|
|
600
|
+
const propertyAccess = calledExpression;
|
|
601
|
+
if (propertyAccess.getName() === 'then') {
|
|
602
|
+
const importCall = propertyAccess.getExpression();
|
|
603
|
+
const callback = expression.getArguments()[0];
|
|
604
|
+
if (!Node.isCallExpression(importCall) || importCall.getExpression().getText() !== 'import' || !Node.isArrowFunction(callback)) return undefined;
|
|
605
|
+
const moduleSpecifier = getStaticString(importCall.getArguments()[0]);
|
|
606
|
+
if (!moduleSpecifier) return undefined;
|
|
607
|
+
const sourceFile = resolveModuleSourceFile(
|
|
608
|
+
routeSourceFile,
|
|
609
|
+
moduleSpecifier,
|
|
610
|
+
);
|
|
611
|
+
const componentName = resolveThenComponentName(callback);
|
|
612
|
+
if (!componentName) return undefined;
|
|
613
|
+
return {
|
|
614
|
+
moduleSpecifier,
|
|
615
|
+
componentName:
|
|
616
|
+
componentName === 'default'
|
|
617
|
+
? (inferSingleComponentName(sourceFile) ?? componentName)
|
|
618
|
+
: componentName,
|
|
619
|
+
sourceFile,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return undefined;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function resolveThenComponentName(callback: Node): string | undefined {
|
|
628
|
+
if (!Node.isArrowFunction(callback) && !Node.isFunctionExpression(callback)) {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
const body = callback.getBody();
|
|
632
|
+
const returnedExpression = Node.isBlock(body)
|
|
633
|
+
? body.getDescendantsOfKind(SyntaxKind.ReturnStatement)[0]?.getExpression()
|
|
634
|
+
: body;
|
|
635
|
+
if (Node.isPropertyAccessExpression(returnedExpression)) {
|
|
636
|
+
return returnedExpression.getName();
|
|
637
|
+
}
|
|
638
|
+
if (!Node.isIdentifier(returnedExpression)) return undefined;
|
|
639
|
+
const parameterName = callback.getParameters()[0]?.getNameNode();
|
|
640
|
+
if (!Node.isObjectBindingPattern(parameterName)) return undefined;
|
|
641
|
+
const binding = parameterName
|
|
642
|
+
.getElements()
|
|
643
|
+
.find((element) => element.getName() === returnedExpression.getText());
|
|
644
|
+
return binding?.getPropertyNameNode()?.getText() ?? binding?.getName();
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function inferSingleComponentName(
|
|
648
|
+
sourceFile: SourceFile | undefined,
|
|
649
|
+
): string | undefined {
|
|
650
|
+
if (!sourceFile) return undefined;
|
|
651
|
+
const genDeps = sourceFile
|
|
652
|
+
.getTypeAliases()
|
|
653
|
+
.filter(
|
|
654
|
+
(alias) => alias.isExported() && alias.getName().startsWith('GenDeps_'),
|
|
655
|
+
);
|
|
656
|
+
if (genDeps.length === 1) {
|
|
657
|
+
return genDeps[0].getName().slice('GenDeps_'.length);
|
|
658
|
+
}
|
|
659
|
+
const angularClass = findAngularDecoratedClass(sourceFile);
|
|
660
|
+
return angularClass.skipped ? undefined : angularClass.className;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function resolveBareImport(
|
|
664
|
+
expression: CallExpression,
|
|
665
|
+
routeSourceFile: SourceFile,
|
|
666
|
+
): ComponentResolution | undefined {
|
|
667
|
+
if (expression.getExpression().getText() === 'import') {
|
|
668
|
+
const moduleSpecifier = getStaticString(expression.getArguments()[0]);
|
|
669
|
+
if (!moduleSpecifier) return undefined;
|
|
670
|
+
const sourceFile = resolveModuleSourceFile(routeSourceFile, moduleSpecifier);
|
|
671
|
+
if (!sourceFile) return undefined;
|
|
672
|
+
const componentName = inferSingleComponentName(sourceFile);
|
|
673
|
+
if (componentName) {
|
|
674
|
+
return {
|
|
675
|
+
moduleSpecifier,
|
|
676
|
+
componentName,
|
|
677
|
+
sourceFile,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return undefined;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function ensureGeneratedDeps(
|
|
685
|
+
component: ComponentResolution,
|
|
686
|
+
context: TransformContext,
|
|
687
|
+
): boolean {
|
|
688
|
+
const sourceFile = component.sourceFile;
|
|
689
|
+
if (!sourceFile) return false;
|
|
690
|
+
if (sourceFile.getTypeAlias(`GenDeps_${component.componentName}`)) return true;
|
|
691
|
+
const angularClass = findAngularDecoratedClass(sourceFile);
|
|
692
|
+
if (
|
|
693
|
+
angularClass.skipped ||
|
|
694
|
+
angularClass.className !== component.componentName
|
|
695
|
+
) {
|
|
696
|
+
return false;
|
|
697
|
+
}
|
|
698
|
+
const result = generateAngularDependencies(sourceFile);
|
|
699
|
+
if (result.changed) context.generatedComponentFiles.add(sourceFile);
|
|
700
|
+
return Boolean(sourceFile.getTypeAlias(`GenDeps_${component.componentName}`));
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function resolveModuleSourceFile(
|
|
704
|
+
sourceFile: SourceFile,
|
|
705
|
+
moduleSpecifier: string,
|
|
706
|
+
): SourceFile | undefined {
|
|
707
|
+
if (!moduleSpecifier.startsWith('.')) return undefined;
|
|
708
|
+
const base = resolve(dirname(sourceFile.getFilePath()), moduleSpecifier);
|
|
709
|
+
const project = sourceFile.getProject();
|
|
710
|
+
for (const candidate of [
|
|
711
|
+
`${base}.ts`,
|
|
712
|
+
`${base}.tsx`,
|
|
713
|
+
join(base, 'index.ts'),
|
|
714
|
+
]) {
|
|
715
|
+
const resolved = project.getSourceFile(candidate);
|
|
716
|
+
if (resolved) return resolved;
|
|
717
|
+
}
|
|
718
|
+
return undefined;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function addDiCheck(
|
|
722
|
+
sourceFile: SourceFile,
|
|
723
|
+
routesName: string,
|
|
724
|
+
collectionName: string,
|
|
725
|
+
parentNames: readonly string[] | undefined,
|
|
726
|
+
): void {
|
|
727
|
+
ensureTypeImport(sourceFile, 'CanRun');
|
|
728
|
+
ensureTypeImport(sourceFile, 'ValidateCascadeRoutesFile');
|
|
729
|
+
ensureTypeImport(sourceFile, 'Router', '@angular/router');
|
|
730
|
+
const suffix = toPascalCase(collectionName);
|
|
731
|
+
const names = parentNames?.length
|
|
732
|
+
? parentNames.map((name) => JSON.stringify(name)).join(' | ')
|
|
733
|
+
: 'never';
|
|
734
|
+
sourceFile.addStatements(
|
|
735
|
+
`\ntype _Check${suffix}DI = ValidateCascadeRoutesFile<${names}, Router, typeof ${routesName}>;\nexport type _CanRun${suffix} = CanRun<_Check${suffix}DI>;`,
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function ensureValueImport(
|
|
740
|
+
sourceFile: SourceFile,
|
|
741
|
+
name: string,
|
|
742
|
+
moduleSpecifier = '@craft-ng/core',
|
|
743
|
+
): void {
|
|
744
|
+
let declaration = sourceFile
|
|
745
|
+
.getImportDeclarations()
|
|
746
|
+
.find((item) => item.getModuleSpecifierValue() === moduleSpecifier && !item.isTypeOnly());
|
|
747
|
+
if (!declaration) {
|
|
748
|
+
declaration = sourceFile.addImportDeclaration({ moduleSpecifier, namedImports: [name] });
|
|
749
|
+
} else if (!declaration.getNamedImports().some((item) => item.getName() === name)) {
|
|
750
|
+
declaration.addNamedImport(name);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function ensureTypeImport(
|
|
755
|
+
sourceFile: SourceFile,
|
|
756
|
+
name: string,
|
|
757
|
+
moduleSpecifier = '@craft-ng/core',
|
|
758
|
+
): void {
|
|
759
|
+
const alreadyImported = sourceFile.getImportDeclarations().some(
|
|
760
|
+
(item) =>
|
|
761
|
+
item.getModuleSpecifierValue() === moduleSpecifier &&
|
|
762
|
+
item.getNamedImports().some((specifier) => specifier.getName() === name),
|
|
763
|
+
);
|
|
764
|
+
if (alreadyImported) return;
|
|
765
|
+
const declaration = sourceFile
|
|
766
|
+
.getImportDeclarations()
|
|
767
|
+
.find((item) => item.getModuleSpecifierValue() === moduleSpecifier && item.isTypeOnly());
|
|
768
|
+
if (!declaration) {
|
|
769
|
+
sourceFile.addImportDeclaration({
|
|
770
|
+
moduleSpecifier,
|
|
771
|
+
isTypeOnly: true,
|
|
772
|
+
namedImports: [name],
|
|
773
|
+
});
|
|
774
|
+
} else {
|
|
775
|
+
declaration.addNamedImport(name);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
function removeUnusedAngularRoutesImport(sourceFile: SourceFile): void {
|
|
780
|
+
const importDeclaration = sourceFile
|
|
781
|
+
.getImportDeclarations()
|
|
782
|
+
.find((item) => item.getModuleSpecifierValue() === '@angular/router');
|
|
783
|
+
if (!importDeclaration) return;
|
|
784
|
+
const routesImport = importDeclaration
|
|
785
|
+
.getNamedImports()
|
|
786
|
+
.find((item) => item.getName() === 'Routes');
|
|
787
|
+
if (!routesImport) return;
|
|
788
|
+
const withoutImports = sourceFile
|
|
789
|
+
.getFullText()
|
|
790
|
+
.replace(importDeclaration.getFullText(), '');
|
|
791
|
+
if (/\bRoutes\b/.test(withoutImports)) return;
|
|
792
|
+
routesImport.remove();
|
|
793
|
+
if (
|
|
794
|
+
importDeclaration.getNamedImports().length === 0 &&
|
|
795
|
+
!importDeclaration.getDefaultImport() &&
|
|
796
|
+
!importDeclaration.getNamespaceImport()
|
|
797
|
+
) {
|
|
798
|
+
importDeclaration.remove();
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function getProperty(
|
|
803
|
+
object: ObjectLiteralExpression,
|
|
804
|
+
name: string,
|
|
805
|
+
): PropertyAssignment | undefined {
|
|
806
|
+
const property = object.getProperty(name);
|
|
807
|
+
return Node.isPropertyAssignment(property) ? property : undefined;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
function getStaticString(node: Node | undefined): string | undefined {
|
|
811
|
+
if (Node.isStringLiteral(node) || Node.isNoSubstitutionTemplateLiteral(node)) {
|
|
812
|
+
return node.getLiteralValue();
|
|
813
|
+
}
|
|
814
|
+
return undefined;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
function diagnose(
|
|
818
|
+
context: TransformContext,
|
|
819
|
+
code: RouteMigrationDiagnosticCode,
|
|
820
|
+
routePath: string,
|
|
821
|
+
message: string,
|
|
822
|
+
): void {
|
|
823
|
+
context.diagnostics.push({
|
|
824
|
+
code,
|
|
825
|
+
filePath: context.sourceFile.getFilePath(),
|
|
826
|
+
routePath,
|
|
827
|
+
message,
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function inferCollectionName(
|
|
832
|
+
sourceFile: SourceFile,
|
|
833
|
+
declaration: VariableDeclaration,
|
|
834
|
+
): string {
|
|
835
|
+
const variableName = declaration.getName();
|
|
836
|
+
if (variableName !== 'routes' && variableName.endsWith('Routes')) {
|
|
837
|
+
return variableName.slice(0, -'Routes'.length);
|
|
838
|
+
}
|
|
839
|
+
const fileName = basename(sourceFile.getFilePath()).replace(/\.routes?\.ts$/, '');
|
|
840
|
+
return toCamelCase(fileName === basename(sourceFile.getFilePath()) ? 'app' : fileName);
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function toCamelCase(value: string): string {
|
|
844
|
+
return value.replace(/[-_.]+([a-zA-Z0-9])/g, (_, character: string) => character.toUpperCase());
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function toPascalCase(value: string): string {
|
|
848
|
+
const camel = toCamelCase(value);
|
|
849
|
+
return camel.charAt(0).toUpperCase() + camel.slice(1);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function joinRoute(parent: string, child: string): string {
|
|
853
|
+
return [parent, child].filter(Boolean).join('/') || '<root>';
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function createProject(tsConfigFilePath: string | undefined): Project {
|
|
857
|
+
return tsConfigFilePath
|
|
858
|
+
? new Project({ tsConfigFilePath, skipAddingFilesFromTsConfig: false })
|
|
859
|
+
: new Project({
|
|
860
|
+
compilerOptions: {
|
|
861
|
+
experimentalDecorators: true,
|
|
862
|
+
module: ts.ModuleKind.Preserve,
|
|
863
|
+
moduleResolution: ts.ModuleResolutionKind.Node10,
|
|
864
|
+
target: ts.ScriptTarget.ES2022,
|
|
865
|
+
},
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
function defaultTsConfig(rootDir: string): string | undefined {
|
|
870
|
+
const path = join(rootDir, 'tsconfig.json');
|
|
871
|
+
return existsSync(path) ? path : undefined;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function isInside(filePath: string, rootDir: string): boolean {
|
|
875
|
+
const absolute = isAbsolute(filePath) ? filePath : resolve(filePath);
|
|
876
|
+
return absolute === rootDir || absolute.startsWith(`${rootDir}/`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function setQuoteKind(project: Project): void {
|
|
880
|
+
project.manipulationSettings.set({ quoteKind: QuoteKind.Single });
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
function logResult(
|
|
884
|
+
result: MigrateRoutesResult,
|
|
885
|
+
log: (message: string) => void,
|
|
886
|
+
wrote: boolean,
|
|
887
|
+
): void {
|
|
888
|
+
for (const file of result.files.filter((item) => item.changed)) {
|
|
889
|
+
log(`✓ ${file.filePath}: ${wrote ? 'migré' : 'migration proposée'}`);
|
|
890
|
+
}
|
|
891
|
+
for (const diagnostic of result.diagnostics) {
|
|
892
|
+
log(`! ${diagnostic.filePath}${diagnostic.routePath ? ` / ${diagnostic.routePath}` : ''}: ${diagnostic.code}\n ${diagnostic.message}`);
|
|
893
|
+
}
|
|
894
|
+
log(`summary changed=${result.changedFiles.length} manual=${result.diagnostics.length} remaining=${result.remainingLegacyCollections}`);
|
|
895
|
+
}
|