@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,1282 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
4
|
+
import {
|
|
5
|
+
basename,
|
|
6
|
+
dirname,
|
|
7
|
+
extname,
|
|
8
|
+
isAbsolute,
|
|
9
|
+
join,
|
|
10
|
+
relative,
|
|
11
|
+
resolve,
|
|
12
|
+
} from 'node:path';
|
|
13
|
+
import { promisify } from 'node:util';
|
|
14
|
+
import {
|
|
15
|
+
CallExpression,
|
|
16
|
+
ClassDeclaration,
|
|
17
|
+
ConstructorDeclaration,
|
|
18
|
+
MethodDeclaration,
|
|
19
|
+
Node,
|
|
20
|
+
Project,
|
|
21
|
+
PropertyAssignment,
|
|
22
|
+
QuoteKind,
|
|
23
|
+
SourceFile,
|
|
24
|
+
SyntaxKind,
|
|
25
|
+
} from 'ts-morph';
|
|
26
|
+
import type {
|
|
27
|
+
ServiceMigrationOverride,
|
|
28
|
+
ServiceMigrationScope,
|
|
29
|
+
ServiceMigrationStrategy,
|
|
30
|
+
} from '../angular-brand-codemod.js';
|
|
31
|
+
import { loadCraftDevToolsConfig } from './config.js';
|
|
32
|
+
import type {
|
|
33
|
+
ServiceMigrationDiagnostic,
|
|
34
|
+
ServiceMigrationDiagnosticCode,
|
|
35
|
+
} from './migration-diagnostic.js';
|
|
36
|
+
import { migrateEslintConfig } from '../migration-workspace.js';
|
|
37
|
+
|
|
38
|
+
const execFileAsync = promisify(execFile);
|
|
39
|
+
const MANUAL_SCOPE = 'CRAFT_IMPLEMENTATION_REQUIRED';
|
|
40
|
+
|
|
41
|
+
export type MigrateServicesOptions = {
|
|
42
|
+
rootDir?: string;
|
|
43
|
+
tsConfigFilePath?: string;
|
|
44
|
+
configFilePath?: string;
|
|
45
|
+
files?: readonly string[];
|
|
46
|
+
write?: boolean;
|
|
47
|
+
check?: boolean;
|
|
48
|
+
json?: boolean;
|
|
49
|
+
jsonFilePath?: string;
|
|
50
|
+
failOnManual?: boolean;
|
|
51
|
+
eslint?: boolean;
|
|
52
|
+
log?: (message: string) => void;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type MigratedServiceFile = {
|
|
56
|
+
filePath: string;
|
|
57
|
+
changed: boolean;
|
|
58
|
+
symbols: string[];
|
|
59
|
+
companions: string[];
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type MigrateServicesResult = {
|
|
63
|
+
changedFiles: string[];
|
|
64
|
+
files: MigratedServiceFile[];
|
|
65
|
+
diagnostics: ServiceMigrationDiagnostic[];
|
|
66
|
+
remainingLegacyServices: number;
|
|
67
|
+
eslintRan: boolean;
|
|
68
|
+
exitCode: number;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type ServiceDescriptor = {
|
|
72
|
+
classDeclaration: ClassDeclaration;
|
|
73
|
+
sourceFile: SourceFile;
|
|
74
|
+
symbol: string;
|
|
75
|
+
name: string;
|
|
76
|
+
scope: ServiceMigrationScope;
|
|
77
|
+
strategy: ServiceMigrationStrategy;
|
|
78
|
+
override?: ServiceMigrationOverride;
|
|
79
|
+
helperSourceFile?: SourceFile;
|
|
80
|
+
httpOperations?: HttpOperationDescriptor[];
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
type HttpOperationDescriptor = {
|
|
84
|
+
methodName: string;
|
|
85
|
+
httpMethod: 'post' | 'put' | 'patch' | 'delete';
|
|
86
|
+
responseType: string;
|
|
87
|
+
urlText: string;
|
|
88
|
+
payloadText?: string;
|
|
89
|
+
parameterCount: number;
|
|
90
|
+
typeParametersText: string;
|
|
91
|
+
parametersText: string;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export async function runServicesMigration(
|
|
95
|
+
options: MigrateServicesOptions = {},
|
|
96
|
+
): Promise<MigrateServicesResult> {
|
|
97
|
+
const rootDir = resolve(options.rootDir ?? process.cwd());
|
|
98
|
+
const tsConfigFilePath = options.tsConfigFilePath
|
|
99
|
+
? resolve(options.tsConfigFilePath)
|
|
100
|
+
: defaultTsConfig(rootDir);
|
|
101
|
+
const project = new Project({
|
|
102
|
+
...(existsSync(tsConfigFilePath) ? { tsConfigFilePath } : {}),
|
|
103
|
+
manipulationSettings: { quoteKind: QuoteKind.Single },
|
|
104
|
+
skipAddingFilesFromTsConfig: false,
|
|
105
|
+
});
|
|
106
|
+
project.addSourceFilesAtPaths([
|
|
107
|
+
join(rootDir, '**/*.ts'),
|
|
108
|
+
`!${join(rootDir, '**/node_modules/**')}`,
|
|
109
|
+
`!${join(rootDir, '**/dist/**')}`,
|
|
110
|
+
`!${join(rootDir, '**/.angular/**')}`,
|
|
111
|
+
`!${join(rootDir, '**/*.d.ts')}`,
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
const config = loadCraftDevToolsConfig(rootDir, options.configFilePath);
|
|
115
|
+
const overrides = config.serviceMigration?.overrides ?? [];
|
|
116
|
+
const selected = options.files?.length
|
|
117
|
+
? new Set(options.files.map((file) => resolve(rootDir, file)))
|
|
118
|
+
: undefined;
|
|
119
|
+
const projectSourceFiles = project.getSourceFiles().filter((file) => {
|
|
120
|
+
const path = resolve(file.getFilePath());
|
|
121
|
+
return isInside(path, rootDir);
|
|
122
|
+
});
|
|
123
|
+
const sourceFiles = projectSourceFiles.filter(
|
|
124
|
+
(file) => !selected || selected.has(resolve(file.getFilePath())),
|
|
125
|
+
);
|
|
126
|
+
const diagnostics: ServiceMigrationDiagnostic[] = [];
|
|
127
|
+
const descriptors = sourceFiles.flatMap((sourceFile) =>
|
|
128
|
+
sourceFile
|
|
129
|
+
.getClasses()
|
|
130
|
+
.filter(isLegacyServiceClass)
|
|
131
|
+
.map((classDeclaration) =>
|
|
132
|
+
describeService(classDeclaration, overrides, rootDir, diagnostics),
|
|
133
|
+
),
|
|
134
|
+
);
|
|
135
|
+
inferFunctionScopes(descriptors, projectSourceFiles);
|
|
136
|
+
diagnoseCycles(descriptors, diagnostics);
|
|
137
|
+
|
|
138
|
+
const files = new Map<string, MigratedServiceFile>();
|
|
139
|
+
const touched = new Set<SourceFile>();
|
|
140
|
+
for (const descriptor of descriptors) {
|
|
141
|
+
const file = descriptor.sourceFile;
|
|
142
|
+
const report = getFileReport(files, file.getFilePath());
|
|
143
|
+
report.symbols.push(descriptor.symbol);
|
|
144
|
+
if (descriptor.strategy === 'ignore') continue;
|
|
145
|
+
const unsafe = getUnsafeReason(descriptor);
|
|
146
|
+
if (unsafe) {
|
|
147
|
+
diagnose(diagnostics, unsafe.code, descriptor, unsafe.message);
|
|
148
|
+
const companion = createCompanion(project, descriptor);
|
|
149
|
+
descriptor.helperSourceFile = companion.file;
|
|
150
|
+
report.companions.push(companion.file.getFilePath());
|
|
151
|
+
if (companion.changed) touched.add(companion.file);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
migrateSimpleClass(descriptor, descriptors, diagnostics);
|
|
155
|
+
touched.add(file);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
rewriteConsumers(sourceFiles, descriptors, diagnostics, touched);
|
|
159
|
+
rewriteHttpOperationSubscribers(sourceFiles, descriptors, diagnostics, touched);
|
|
160
|
+
rewriteProviders(sourceFiles, descriptors, diagnostics, touched);
|
|
161
|
+
const eslintConfig = migrateEslintConfig(project, dirname(tsConfigFilePath));
|
|
162
|
+
if (eslintConfig) touched.add(eslintConfig);
|
|
163
|
+
for (const file of touched) {
|
|
164
|
+
if (file === eslintConfig) file.formatText();
|
|
165
|
+
else file.organizeImports();
|
|
166
|
+
getFileReport(files, file.getFilePath()).changed = true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (options.write) {
|
|
170
|
+
await Promise.all([...touched].map((file) => file.save()));
|
|
171
|
+
}
|
|
172
|
+
let eslintRan = false;
|
|
173
|
+
if (options.write && options.eslint !== false && touched.size > 0) {
|
|
174
|
+
await runEslint(
|
|
175
|
+
[...touched].map((file) => file.getFilePath()),
|
|
176
|
+
rootDir,
|
|
177
|
+
);
|
|
178
|
+
eslintRan = true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const remainingLegacyServices = options.write
|
|
182
|
+
? project
|
|
183
|
+
.getSourceFiles()
|
|
184
|
+
.reduce(
|
|
185
|
+
(count, file) =>
|
|
186
|
+
count + file.getClasses().filter(isLegacyServiceClass).length,
|
|
187
|
+
0,
|
|
188
|
+
)
|
|
189
|
+
: descriptors.length;
|
|
190
|
+
const result: MigrateServicesResult = {
|
|
191
|
+
changedFiles: [...touched].map((file) => file.getFilePath()),
|
|
192
|
+
files: [...files.values()],
|
|
193
|
+
diagnostics,
|
|
194
|
+
remainingLegacyServices,
|
|
195
|
+
eslintRan,
|
|
196
|
+
exitCode:
|
|
197
|
+
(options.check && remainingLegacyServices > 0) ||
|
|
198
|
+
(options.failOnManual && diagnostics.length > 0)
|
|
199
|
+
? 1
|
|
200
|
+
: 0,
|
|
201
|
+
};
|
|
202
|
+
if (options.jsonFilePath) {
|
|
203
|
+
const path = resolve(options.jsonFilePath);
|
|
204
|
+
await mkdir(dirname(path), { recursive: true });
|
|
205
|
+
await writeFile(path, `${JSON.stringify(result, null, 2)}\n`, 'utf8');
|
|
206
|
+
}
|
|
207
|
+
const log = options.log ?? console.log;
|
|
208
|
+
if (options.json) log(JSON.stringify(result, null, 2));
|
|
209
|
+
else logSummary(result, log, options.write === true);
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function describeService(
|
|
214
|
+
classDeclaration: ClassDeclaration,
|
|
215
|
+
overrides: readonly ServiceMigrationOverride[],
|
|
216
|
+
rootDir: string,
|
|
217
|
+
diagnostics: ServiceMigrationDiagnostic[],
|
|
218
|
+
): ServiceDescriptor {
|
|
219
|
+
const sourceFile = classDeclaration.getSourceFile();
|
|
220
|
+
const symbol = classDeclaration.getNameOrThrow();
|
|
221
|
+
const module = relative(rootDir, sourceFile.getFilePath()).replace(
|
|
222
|
+
/\\/g,
|
|
223
|
+
'/',
|
|
224
|
+
);
|
|
225
|
+
const override = [...overrides]
|
|
226
|
+
.reverse()
|
|
227
|
+
.find(
|
|
228
|
+
(candidate) =>
|
|
229
|
+
(!candidate.file || matches(candidate.file, module)) &&
|
|
230
|
+
(!candidate.module || matches(candidate.module, module)) &&
|
|
231
|
+
(!candidate.symbol || candidate.symbol === symbol),
|
|
232
|
+
);
|
|
233
|
+
const decorator = classDeclaration
|
|
234
|
+
.getDecorators()
|
|
235
|
+
.find((item) => ['Injectable', 'Service'].includes(item.getName()))!;
|
|
236
|
+
const argument = decorator.getArguments()[0];
|
|
237
|
+
const text = argument?.getText() ?? '';
|
|
238
|
+
let scope: ServiceMigrationScope;
|
|
239
|
+
if (classDeclaration.isAbstract()) scope = 'abstract';
|
|
240
|
+
else if (
|
|
241
|
+
decorator.getName() === 'Service' &&
|
|
242
|
+
!/autoProvided\s*:\s*false/.test(text)
|
|
243
|
+
)
|
|
244
|
+
scope = 'global';
|
|
245
|
+
else if (/providedIn\s*:\s*['"]root['"]/.test(text)) scope = 'global';
|
|
246
|
+
else if (/providedIn\s*:/.test(text)) {
|
|
247
|
+
scope = 'toProvide';
|
|
248
|
+
diagnose(
|
|
249
|
+
diagnostics,
|
|
250
|
+
'UNSUPPORTED_PROVIDED_IN',
|
|
251
|
+
{ sourceFile, symbol } as ServiceDescriptor,
|
|
252
|
+
`providedIn de ${symbol} n'est pas transposable automatiquement.`,
|
|
253
|
+
);
|
|
254
|
+
} else scope = 'toProvide';
|
|
255
|
+
return {
|
|
256
|
+
classDeclaration,
|
|
257
|
+
sourceFile,
|
|
258
|
+
symbol,
|
|
259
|
+
name: override?.name ?? (symbol.replace(/Service$/, '') || symbol),
|
|
260
|
+
scope: override?.scope ?? scope,
|
|
261
|
+
strategy: override?.strategy ?? 'craftService',
|
|
262
|
+
override,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function inferFunctionScopes(
|
|
267
|
+
descriptors: readonly ServiceDescriptor[],
|
|
268
|
+
sourceFiles: readonly SourceFile[],
|
|
269
|
+
): void {
|
|
270
|
+
const runtimeFiles = sourceFiles.filter((file) => !isTestFile(file));
|
|
271
|
+
for (const descriptor of descriptors) {
|
|
272
|
+
if (
|
|
273
|
+
descriptor.scope !== 'toProvide' ||
|
|
274
|
+
descriptor.override?.scope ||
|
|
275
|
+
descriptor.strategy !== 'craftService' ||
|
|
276
|
+
getUnsafeReason(descriptor)
|
|
277
|
+
)
|
|
278
|
+
continue;
|
|
279
|
+
const injectionSites = runtimeFiles.flatMap((file) =>
|
|
280
|
+
collectInjectionSites(file, descriptor.symbol),
|
|
281
|
+
);
|
|
282
|
+
if (injectionSites.length === 1 && injectionSites[0]?.componentOwned)
|
|
283
|
+
descriptor.scope = 'function';
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function collectInjectionSites(
|
|
288
|
+
file: SourceFile,
|
|
289
|
+
symbol: string,
|
|
290
|
+
): { node: Node; componentOwned: boolean }[] {
|
|
291
|
+
const sites: { node: Node; componentOwned: boolean }[] = [];
|
|
292
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
293
|
+
if (
|
|
294
|
+
call.getExpression().getText() !== 'inject' ||
|
|
295
|
+
call.getArguments()[0]?.getText() !== symbol
|
|
296
|
+
)
|
|
297
|
+
continue;
|
|
298
|
+
const owner = call.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
|
|
299
|
+
sites.push({
|
|
300
|
+
node: call,
|
|
301
|
+
componentOwned: Boolean(owner && isComponentOwnedClass(owner)),
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
for (const parameter of file.getDescendantsOfKind(
|
|
305
|
+
SyntaxKind.Parameter,
|
|
306
|
+
)) {
|
|
307
|
+
if (parameter.getTypeNode()?.getText() !== symbol) continue;
|
|
308
|
+
const constructorDeclaration = parameter.getFirstAncestorByKind(
|
|
309
|
+
SyntaxKind.Constructor,
|
|
310
|
+
);
|
|
311
|
+
const owner = parameter.getFirstAncestorByKind(
|
|
312
|
+
SyntaxKind.ClassDeclaration,
|
|
313
|
+
);
|
|
314
|
+
if (constructorDeclaration)
|
|
315
|
+
sites.push({
|
|
316
|
+
node: parameter,
|
|
317
|
+
componentOwned: Boolean(owner && isComponentOwnedClass(owner)),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
return sites;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isComponentOwnedClass(value: ClassDeclaration): boolean {
|
|
324
|
+
return value
|
|
325
|
+
.getDecorators()
|
|
326
|
+
.some((decorator) =>
|
|
327
|
+
['Component', 'Directive'].includes(decorator.getName()),
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function isTestFile(file: SourceFile): boolean {
|
|
332
|
+
return /\.(?:spec|test)\.ts$/.test(file.getFilePath());
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function getUnsafeReason(
|
|
336
|
+
descriptor: ServiceDescriptor,
|
|
337
|
+
): { code: ServiceMigrationDiagnosticCode; message: string } | undefined {
|
|
338
|
+
const value = descriptor.classDeclaration;
|
|
339
|
+
if (descriptor.strategy === 'companion')
|
|
340
|
+
return {
|
|
341
|
+
code: 'NON_CONVERTIBLE_CLASS',
|
|
342
|
+
message: 'Stratégie companion imposée par la configuration.',
|
|
343
|
+
};
|
|
344
|
+
if (value.getExtends())
|
|
345
|
+
return {
|
|
346
|
+
code: 'NON_CONVERTIBLE_CLASS',
|
|
347
|
+
message: `${descriptor.symbol} utilise l'héritage.`,
|
|
348
|
+
};
|
|
349
|
+
if (value.getImplements().length)
|
|
350
|
+
return {
|
|
351
|
+
code: 'FRAMEWORK_CONTRACT',
|
|
352
|
+
message: `${descriptor.symbol} implémente un contrat de framework.`,
|
|
353
|
+
};
|
|
354
|
+
if (value.getGetAccessors().length || value.getSetAccessors().length)
|
|
355
|
+
return {
|
|
356
|
+
code: 'NON_CONVERTIBLE_CLASS',
|
|
357
|
+
message: `${descriptor.symbol} contient des accesseurs.`,
|
|
358
|
+
};
|
|
359
|
+
if (value.getConstructors().some(hasUnsafeConstructorLogic))
|
|
360
|
+
return {
|
|
361
|
+
code: 'NON_CONVERTIBLE_CLASS',
|
|
362
|
+
message: `${descriptor.symbol} contient de la logique constructeur.`,
|
|
363
|
+
};
|
|
364
|
+
if (value.getMethods().some((method) => /^ng[A-Z]/.test(method.getName())))
|
|
365
|
+
return {
|
|
366
|
+
code: 'LIFECYCLE_HOOK',
|
|
367
|
+
message: `${descriptor.symbol} contient un lifecycle Angular.`,
|
|
368
|
+
};
|
|
369
|
+
if (
|
|
370
|
+
value
|
|
371
|
+
.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
372
|
+
.some((call) => call.getExpression().getText() === 'injectAsync')
|
|
373
|
+
)
|
|
374
|
+
return {
|
|
375
|
+
code: 'INJECT_ASYNC',
|
|
376
|
+
message: `${descriptor.symbol} utilise injectAsync().`,
|
|
377
|
+
};
|
|
378
|
+
if (
|
|
379
|
+
value
|
|
380
|
+
.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
381
|
+
.some(hasInjectionFlags)
|
|
382
|
+
)
|
|
383
|
+
return {
|
|
384
|
+
code: 'INJECTION_FLAGS',
|
|
385
|
+
message: `${descriptor.symbol} utilise des flags d'injection.`,
|
|
386
|
+
};
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function hasUnsafeConstructorLogic(
|
|
391
|
+
constructorDeclaration: ConstructorDeclaration,
|
|
392
|
+
): boolean {
|
|
393
|
+
return constructorDeclaration
|
|
394
|
+
.getStatements()
|
|
395
|
+
.some((statement) => !isMigratableConstructorStatement(statement));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function isMigratableConstructorStatement(statement: Node): boolean {
|
|
399
|
+
if (!Node.isExpressionStatement(statement)) return false;
|
|
400
|
+
const expression = statement.getExpression();
|
|
401
|
+
if (!Node.isCallExpression(expression)) return false;
|
|
402
|
+
return ['effect'].includes(expression.getExpression().getText());
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function migrateSimpleClass(
|
|
406
|
+
descriptor: ServiceDescriptor,
|
|
407
|
+
all: readonly ServiceDescriptor[],
|
|
408
|
+
diagnostics: ServiceMigrationDiagnostic[],
|
|
409
|
+
): void {
|
|
410
|
+
const value = descriptor.classDeclaration;
|
|
411
|
+
const file = descriptor.sourceFile;
|
|
412
|
+
const yields: string[] = [];
|
|
413
|
+
const declarations: string[] = [];
|
|
414
|
+
const exposed: string[] = [];
|
|
415
|
+
let hasTrackedPrimitive = false;
|
|
416
|
+
migrateHttpResources(descriptor, diagnostics);
|
|
417
|
+
const httpOperations = collectHttpOperations(descriptor);
|
|
418
|
+
descriptor.httpOperations = httpOperations;
|
|
419
|
+
const propertyNames = new Set(value.getProperties().map((property) => property.getName()));
|
|
420
|
+
for (const parameter of value
|
|
421
|
+
.getConstructors()
|
|
422
|
+
.flatMap((ctor) => ctor.getParameters())) {
|
|
423
|
+
const token = parameter.getTypeNode()?.getText();
|
|
424
|
+
if (!token) continue;
|
|
425
|
+
if (
|
|
426
|
+
token === 'HttpClient' &&
|
|
427
|
+
!hasHttpClientUsageOutsideMigratedMethods(
|
|
428
|
+
value,
|
|
429
|
+
parameter.getName(),
|
|
430
|
+
httpOperations,
|
|
431
|
+
)
|
|
432
|
+
)
|
|
433
|
+
continue;
|
|
434
|
+
yields.push(
|
|
435
|
+
makeYield(toInternalName(parameter.getName()), token, descriptor, all, diagnostics),
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
for (const property of value.getProperties()) {
|
|
439
|
+
const name = property.getName();
|
|
440
|
+
const internalName = toInternalName(name);
|
|
441
|
+
const initializer = property.getInitializer();
|
|
442
|
+
if (!initializer) {
|
|
443
|
+
diagnose(
|
|
444
|
+
diagnostics,
|
|
445
|
+
'NON_CONVERTIBLE_CLASS',
|
|
446
|
+
descriptor,
|
|
447
|
+
`La propriété ${name} n'a pas d'initialiseur.`,
|
|
448
|
+
);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
if (
|
|
452
|
+
Node.isCallExpression(initializer) &&
|
|
453
|
+
initializer.getExpression().getText() === 'inject'
|
|
454
|
+
) {
|
|
455
|
+
const token = initializer.getArguments()[0]?.getText();
|
|
456
|
+
if (
|
|
457
|
+
token === 'HttpClient' &&
|
|
458
|
+
!hasHttpClientUsageOutsideMigratedMethods(value, name, httpOperations)
|
|
459
|
+
)
|
|
460
|
+
continue;
|
|
461
|
+
if (token)
|
|
462
|
+
yields.push(makeYield(internalName, token, descriptor, all, diagnostics));
|
|
463
|
+
} else {
|
|
464
|
+
const shouldTrack = isDependentPrimitive(initializer);
|
|
465
|
+
if (shouldTrack) {
|
|
466
|
+
hasTrackedPrimitive = true;
|
|
467
|
+
ensureCoreImports(file, ['track']);
|
|
468
|
+
}
|
|
469
|
+
const imperativeStateComment = property
|
|
470
|
+
.getFullText()
|
|
471
|
+
.includes('CRAFT_IMPERATIVE_CODE_DETECTED')
|
|
472
|
+
? '// CRAFT_IMPERATIVE_CODE_DETECTED: imperative code detected, prefer a declarative approach.\n'
|
|
473
|
+
: '';
|
|
474
|
+
declarations.push(
|
|
475
|
+
`${imperativeStateComment}const ${internalName} = ${shouldTrack ? 'yield* track(' : ''}${rewriteThis(initializer.getText(), propertyNames)}${shouldTrack ? ')' : ''};`,
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
if (
|
|
479
|
+
property.getScope() !== 'private' &&
|
|
480
|
+
property.getScope() !== 'protected'
|
|
481
|
+
)
|
|
482
|
+
exposed.push(`${name}: ${internalName}`);
|
|
483
|
+
}
|
|
484
|
+
for (const constructorStatement of value
|
|
485
|
+
.getConstructors()
|
|
486
|
+
.flatMap((ctor) => ctor.getStatements())) {
|
|
487
|
+
declarations.push(`${rewriteThis(constructorStatement.getText(), propertyNames)}`);
|
|
488
|
+
}
|
|
489
|
+
for (const method of value.getMethods()) {
|
|
490
|
+
const name = method.getName();
|
|
491
|
+
const httpOperation = httpOperations.find(
|
|
492
|
+
(operation) => operation.methodName === name,
|
|
493
|
+
);
|
|
494
|
+
if (httpOperation) {
|
|
495
|
+
declarations.push(printHttpOperationGenerator(httpOperation));
|
|
496
|
+
if (method.getScope() !== 'private' && method.getScope() !== 'protected')
|
|
497
|
+
exposed.push(name);
|
|
498
|
+
ensureCoreImports(file, ['CraftHttpClient']);
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
const asyncKeyword = method.isAsync() ? 'async ' : '';
|
|
502
|
+
const star = method.isGenerator() ? '*' : '';
|
|
503
|
+
const typeParameters = method.getTypeParameters().map((parameter) => parameter.getText()).join(', ');
|
|
504
|
+
const params = method
|
|
505
|
+
.getParameters()
|
|
506
|
+
.map((parameter) => parameter.getText())
|
|
507
|
+
.join(', ');
|
|
508
|
+
const containsMigratedQuery = method.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
509
|
+
.some((call) => call.getExpression().getText() === 'query');
|
|
510
|
+
const returnType = containsMigratedQuery ? undefined : method.getReturnTypeNode()?.getText();
|
|
511
|
+
const body = method.getBodyText() ?? '';
|
|
512
|
+
const imperativeWorkflowComment = getImperativeWorkflowComment(method);
|
|
513
|
+
if (imperativeWorkflowComment) {
|
|
514
|
+
diagnose(
|
|
515
|
+
diagnostics,
|
|
516
|
+
'IMPERATIVE_WORKFLOW_REQUIRES_REVIEW',
|
|
517
|
+
descriptor,
|
|
518
|
+
`${descriptor.name}.${name} orchestre impérativement un submit, plusieurs mises à jour d'état et/ou une navigation. Préférer insertFormSubmit avec une réaction au statut du formulaire, ou source$ avec on$/effect.`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
declarations.push(
|
|
522
|
+
`${imperativeWorkflowComment ? `${imperativeWorkflowComment}\n` : ''}${asyncKeyword}function${star} ${name}${typeParameters ? `<${typeParameters}>` : ''}(${params})${returnType ? `: ${returnType}` : ''} {${rewriteThis(body, propertyNames)}}`,
|
|
523
|
+
);
|
|
524
|
+
if (method.getScope() !== 'private' && method.getScope() !== 'protected')
|
|
525
|
+
exposed.push(name);
|
|
526
|
+
}
|
|
527
|
+
const contract = `{ ${value
|
|
528
|
+
.getMethods()
|
|
529
|
+
.filter(
|
|
530
|
+
(method) =>
|
|
531
|
+
method.getScope() !== 'private' && method.getScope() !== 'protected',
|
|
532
|
+
)
|
|
533
|
+
.map(
|
|
534
|
+
(method) =>
|
|
535
|
+
`${method.getName()}(${method
|
|
536
|
+
.getParameters()
|
|
537
|
+
.map((parameter) => parameter.getText())
|
|
538
|
+
.join(
|
|
539
|
+
', ',
|
|
540
|
+
)}): ${method.getReturnTypeNode()?.getText() ?? 'unknown'};`,
|
|
541
|
+
)
|
|
542
|
+
.join(' ')} }`;
|
|
543
|
+
const factory =
|
|
544
|
+
descriptor.scope === 'abstract'
|
|
545
|
+
? `abstract<${contract}>()`
|
|
546
|
+
: `${yields.length > 0 || hasTrackedPrimitive ? 'function*' : 'function'} () {\n${[...yields, ...declarations].map((line) => ` ${line}`).join('\n')}\n return { ${exposed.join(', ')} };\n}`;
|
|
547
|
+
const helpers =
|
|
548
|
+
descriptor.scope === 'abstract'
|
|
549
|
+
? `${descriptor.name}Requirement, inject${descriptor.name}, provide${descriptor.name}`
|
|
550
|
+
: `inject${descriptor.name}, ${descriptor.name}ToYield${isProviderCapableScope(descriptor.scope) ? `, provide${descriptor.name}` : ''}`;
|
|
551
|
+
const typeAlias = `\nexport type ${descriptor.symbol} = ReturnType<typeof inject${descriptor.name}>;`;
|
|
552
|
+
value.replaceWithText(
|
|
553
|
+
`export const { ${helpers} } = craftService({ name: '${descriptor.name}', scope: '${descriptor.scope}' }, ${factory});${typeAlias}`,
|
|
554
|
+
);
|
|
555
|
+
ensureCoreImports(file, [
|
|
556
|
+
'craftService',
|
|
557
|
+
...(descriptor.scope === 'abstract' ? ['abstract'] : []),
|
|
558
|
+
]);
|
|
559
|
+
removeDecoratorImport(file, 'Injectable');
|
|
560
|
+
removeDecoratorImport(file, 'Service');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function getImperativeWorkflowComment(
|
|
564
|
+
method: MethodDeclaration,
|
|
565
|
+
): string | undefined {
|
|
566
|
+
const calls = method.getDescendantsOfKind(SyntaxKind.CallExpression);
|
|
567
|
+
const hasSubmit = calls.some((call) => {
|
|
568
|
+
const expression = call.getExpression();
|
|
569
|
+
return (
|
|
570
|
+
expression.getText() === 'submit' ||
|
|
571
|
+
(Node.isPropertyAccessExpression(expression) &&
|
|
572
|
+
expression.getName() === 'submit')
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
if (!hasSubmit) return undefined;
|
|
576
|
+
|
|
577
|
+
const stateWrites = calls.filter((call) => {
|
|
578
|
+
const expression = call.getExpression();
|
|
579
|
+
return (
|
|
580
|
+
Node.isPropertyAccessExpression(expression) &&
|
|
581
|
+
['set', 'update'].includes(expression.getName())
|
|
582
|
+
);
|
|
583
|
+
}).length;
|
|
584
|
+
const hasNavigation = calls.some((call) => {
|
|
585
|
+
const expression = call.getExpression();
|
|
586
|
+
return (
|
|
587
|
+
Node.isPropertyAccessExpression(expression) &&
|
|
588
|
+
['navigate', 'navigateByUrl'].includes(expression.getName())
|
|
589
|
+
);
|
|
590
|
+
});
|
|
591
|
+
if (stateWrites < 2 && !(stateWrites >= 1 && hasNavigation))
|
|
592
|
+
return undefined;
|
|
593
|
+
|
|
594
|
+
return '// CRAFT_REACTIVE_WORKFLOW_RECOMMENDED: workflow impératif détecté...';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function isDependentPrimitive(initializer: Node): boolean {
|
|
598
|
+
if (
|
|
599
|
+
!Node.isCallExpression(initializer) ||
|
|
600
|
+
!['query', 'mutation', 'asyncProcess', 'state', 'craftMethod'].includes(
|
|
601
|
+
initializer.getExpression().getText(),
|
|
602
|
+
)
|
|
603
|
+
)
|
|
604
|
+
return false;
|
|
605
|
+
return initializer
|
|
606
|
+
.getDescendantsOfKind(SyntaxKind.YieldExpression)
|
|
607
|
+
.some((yieldExpression) => yieldExpression.getAsteriskToken() !== undefined);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function migrateHttpResources(
|
|
611
|
+
descriptor: ServiceDescriptor,
|
|
612
|
+
diagnostics: ServiceMigrationDiagnostic[],
|
|
613
|
+
): void {
|
|
614
|
+
const calls = descriptor.classDeclaration
|
|
615
|
+
.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
616
|
+
.filter((call) => call.getExpression().getText() === 'httpResource');
|
|
617
|
+
for (const call of calls) {
|
|
618
|
+
const params = call.getArguments()[0];
|
|
619
|
+
if (!params) continue;
|
|
620
|
+
let paramsText = params.getText();
|
|
621
|
+
const chainedNames = [
|
|
622
|
+
...paramsText.matchAll(/\bchain\(\s*([A-Za-z_$][\w$]*)\s*\)/g),
|
|
623
|
+
].map((match) => match[1]);
|
|
624
|
+
if (/\bchain\s*\(/.test(paramsText)) {
|
|
625
|
+
const rewritten = paramsText
|
|
626
|
+
.replace(/\(\s*\{\s*chain\s*\}\s*\)\s*=>/, '() =>')
|
|
627
|
+
.replace(/\bchain\(\s*([A-Za-z_$][\w$]*)\s*\)\s*;/g, '$1.value();');
|
|
628
|
+
if (/\bchain\s*\(/.test(rewritten)) {
|
|
629
|
+
diagnose(
|
|
630
|
+
diagnostics,
|
|
631
|
+
'RESOURCE_CHAIN_REQUIRES_REWRITE',
|
|
632
|
+
descriptor,
|
|
633
|
+
'httpResource utilise une forme complexe de chain(...); la dépendance entre queries doit être migrée manuellement.',
|
|
634
|
+
);
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
paramsText = rewritten;
|
|
638
|
+
const methodDeclaration = call.getFirstAncestorByKind(
|
|
639
|
+
SyntaxKind.MethodDeclaration,
|
|
640
|
+
);
|
|
641
|
+
for (const name of chainedNames) {
|
|
642
|
+
methodDeclaration
|
|
643
|
+
?.getParameter(name)
|
|
644
|
+
?.setType('{ readonly value: () => unknown }');
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
const stateType = call.getTypeArguments()[0]?.getText() ?? 'unknown';
|
|
648
|
+
const optionsNode = call.getArguments()[1];
|
|
649
|
+
const options = Node.isObjectLiteralExpression(optionsNode)
|
|
650
|
+
? optionsNode
|
|
651
|
+
.getProperties()
|
|
652
|
+
.map((property) => property.getText())
|
|
653
|
+
.join(',\n ')
|
|
654
|
+
: optionsNode
|
|
655
|
+
? `...${optionsNode.getText()}`
|
|
656
|
+
: '';
|
|
657
|
+
const method =
|
|
658
|
+
/\bmethod\s*:\s*['"]([A-Za-z]+)['"]/.exec(paramsText)?.[1]?.toUpperCase() ??
|
|
659
|
+
'GET';
|
|
660
|
+
call.replaceWithText(`query({
|
|
661
|
+
params: ${paramsText},
|
|
662
|
+
${options ? `${options},` : ''}
|
|
663
|
+
loader: function* ({ params: request }) {
|
|
664
|
+
return yield* CraftHttpClient.request(({ response }) => ({
|
|
665
|
+
...(typeof request === 'string' ? { url: request } : request),
|
|
666
|
+
method: '${method}',
|
|
667
|
+
success: response<${stateType}>(),
|
|
668
|
+
}));
|
|
669
|
+
},
|
|
670
|
+
}, ({ set, update }) => ({ set, update }))`);
|
|
671
|
+
ensureCoreImports(descriptor.sourceFile, ['CraftHttpClient', 'query']);
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function collectHttpOperations(
|
|
676
|
+
descriptor: ServiceDescriptor,
|
|
677
|
+
): HttpOperationDescriptor[] {
|
|
678
|
+
return descriptor.classDeclaration
|
|
679
|
+
.getMethods()
|
|
680
|
+
.map((method) => collectHttpOperation(method))
|
|
681
|
+
.filter((operation): operation is HttpOperationDescriptor =>
|
|
682
|
+
Boolean(operation),
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function collectHttpOperation(
|
|
687
|
+
method: MethodDeclaration,
|
|
688
|
+
): HttpOperationDescriptor | undefined {
|
|
689
|
+
const body = method.getBody();
|
|
690
|
+
const statements = body && Node.isBlock(body) ? body.getStatements() : [];
|
|
691
|
+
if (statements.length !== 1) return undefined;
|
|
692
|
+
const statement = statements[0];
|
|
693
|
+
if (!Node.isReturnStatement(statement)) return undefined;
|
|
694
|
+
const expression = statement.getExpression();
|
|
695
|
+
if (!Node.isCallExpression(expression)) return undefined;
|
|
696
|
+
const access = expression.getExpression();
|
|
697
|
+
if (!Node.isPropertyAccessExpression(access)) return undefined;
|
|
698
|
+
const httpMethod = access.getName();
|
|
699
|
+
if (!isWritableHttpMethod(httpMethod)) return undefined;
|
|
700
|
+
if (!looksLikeHttpClientReceiver(access.getExpression().getText()))
|
|
701
|
+
return undefined;
|
|
702
|
+
const args = expression.getArguments();
|
|
703
|
+
const urlText = args[0]?.getText();
|
|
704
|
+
if (!urlText) return undefined;
|
|
705
|
+
const payloadText =
|
|
706
|
+
httpMethod === 'delete' ? undefined : (args[1]?.getText() ?? '{}');
|
|
707
|
+
const responseType = expression.getTypeArguments()[0]?.getText() ?? 'unknown';
|
|
708
|
+
const typeParametersText = method
|
|
709
|
+
.getTypeParameters()
|
|
710
|
+
.map((parameter) => parameter.getText())
|
|
711
|
+
.join(', ');
|
|
712
|
+
const parametersText = method
|
|
713
|
+
.getParameters()
|
|
714
|
+
.map((parameter) => parameter.getText())
|
|
715
|
+
.join(', ');
|
|
716
|
+
return {
|
|
717
|
+
methodName: method.getName(),
|
|
718
|
+
httpMethod,
|
|
719
|
+
responseType,
|
|
720
|
+
urlText,
|
|
721
|
+
payloadText,
|
|
722
|
+
parameterCount: method.getParameters().length,
|
|
723
|
+
typeParametersText,
|
|
724
|
+
parametersText,
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
function isWritableHttpMethod(
|
|
729
|
+
value: string,
|
|
730
|
+
): value is HttpOperationDescriptor['httpMethod'] {
|
|
731
|
+
return ['post', 'put', 'patch', 'delete'].includes(value);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
function looksLikeHttpClientReceiver(value: string): boolean {
|
|
735
|
+
return /(^|\.|_)http(Client)?$/i.test(value);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function printHttpOperationGenerator(operation: HttpOperationDescriptor): string {
|
|
739
|
+
const payload =
|
|
740
|
+
operation.payloadText === undefined
|
|
741
|
+
? ''
|
|
742
|
+
: `\n payload: ${operation.payloadText},`;
|
|
743
|
+
return `function* ${operation.methodName}${operation.typeParametersText ? `<${operation.typeParametersText}>` : ''}(${operation.parametersText}) {
|
|
744
|
+
return yield* CraftHttpClient.${operation.httpMethod}(({ response }) => ({
|
|
745
|
+
url: ${operation.urlText},${payload}
|
|
746
|
+
success: response<${operation.responseType}>(),
|
|
747
|
+
}));
|
|
748
|
+
}`;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function hasHttpClientUsageOutsideMigratedMethods(
|
|
752
|
+
serviceClass: ClassDeclaration,
|
|
753
|
+
localName: string,
|
|
754
|
+
operations: readonly HttpOperationDescriptor[],
|
|
755
|
+
): boolean {
|
|
756
|
+
const migratedMethods = new Set(
|
|
757
|
+
operations.map((operation) => operation.methodName),
|
|
758
|
+
);
|
|
759
|
+
return serviceClass.getMethods().some((method) => {
|
|
760
|
+
if (migratedMethods.has(method.getName())) return false;
|
|
761
|
+
const text = method.getBodyText() ?? '';
|
|
762
|
+
return (
|
|
763
|
+
new RegExp(`\\bthis\\.${escapeRegex(localName)}\\b`).test(text) ||
|
|
764
|
+
new RegExp(`\\b${escapeRegex(localName)}\\s*\\.`).test(text)
|
|
765
|
+
);
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function makeYield(
|
|
770
|
+
localName: string,
|
|
771
|
+
token: string,
|
|
772
|
+
owner: ServiceDescriptor,
|
|
773
|
+
all: readonly ServiceDescriptor[],
|
|
774
|
+
diagnostics: ServiceMigrationDiagnostic[],
|
|
775
|
+
): string {
|
|
776
|
+
const dependency = all.find((item) => item.symbol === token);
|
|
777
|
+
if (token === 'Router') {
|
|
778
|
+
ensureCoreImports(owner.sourceFile, ['CraftRouterToYield']);
|
|
779
|
+
return `const ${localName} = yield* CraftRouterToYield();`;
|
|
780
|
+
}
|
|
781
|
+
if (!dependency) {
|
|
782
|
+
if (token === 'HttpClient') {
|
|
783
|
+
diagnose(
|
|
784
|
+
diagnostics,
|
|
785
|
+
'HTTP_CLIENT_REWRITE_REQUIRED',
|
|
786
|
+
owner,
|
|
787
|
+
'Les appels HttpClient impératifs restants doivent être migrés vers mutation/CraftHttpClient.',
|
|
788
|
+
);
|
|
789
|
+
ensureExternalAdapter(owner, token);
|
|
790
|
+
return `const ${localName} = yield* ${token}ToYield(); // HTTP_CLIENT_REWRITE_REQUIRED`;
|
|
791
|
+
}
|
|
792
|
+
diagnose(
|
|
793
|
+
diagnostics,
|
|
794
|
+
'THIRD_PARTY_SCOPE_REQUIRED',
|
|
795
|
+
owner,
|
|
796
|
+
`${token} requiert un adapter toCraftService avec scope CRAFT_SCOPE_REQUIRED.`,
|
|
797
|
+
);
|
|
798
|
+
ensureExternalAdapter(owner, token);
|
|
799
|
+
return `const ${localName} = yield* ${token}ToYield(); // CRAFT_SCOPE_REQUIRED`;
|
|
800
|
+
}
|
|
801
|
+
ensureRelativeImport(
|
|
802
|
+
owner.sourceFile,
|
|
803
|
+
dependency,
|
|
804
|
+
`${dependency.name}ToYield`,
|
|
805
|
+
);
|
|
806
|
+
return `const ${localName} = yield* ${dependency.name}ToYield();`;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
function ensureExternalAdapter(owner: ServiceDescriptor, token: string): void {
|
|
810
|
+
const file = owner.sourceFile;
|
|
811
|
+
if (
|
|
812
|
+
file.getFullText().includes(`const { ${token}ToYield } = toCraftService(`)
|
|
813
|
+
)
|
|
814
|
+
return;
|
|
815
|
+
const statementIndex = file.getStatements().indexOf(owner.classDeclaration);
|
|
816
|
+
file.insertStatements(
|
|
817
|
+
Math.max(0, statementIndex),
|
|
818
|
+
`const { ${token}ToYield } = toCraftService({ name: '${token}', scope: 'global', token: ${token} }); // HTTP_CLIENT_REWRITE_REQUIRED`,
|
|
819
|
+
);
|
|
820
|
+
ensureCoreImports(file, ['toCraftService']);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function createCompanion(
|
|
824
|
+
project: Project,
|
|
825
|
+
descriptor: ServiceDescriptor,
|
|
826
|
+
): { file: SourceFile; changed: boolean } {
|
|
827
|
+
const originalPath = descriptor.sourceFile.getFilePath();
|
|
828
|
+
const companionPath =
|
|
829
|
+
originalPath.slice(0, -extname(originalPath).length) + '.craft.ts';
|
|
830
|
+
const module = `./${basename(originalPath, extname(originalPath))}`;
|
|
831
|
+
const companionScope =
|
|
832
|
+
descriptor.scope === 'abstract' ? 'toProvide' : descriptor.scope;
|
|
833
|
+
const helpers = `inject${descriptor.name}, ${descriptor.name}ToYield${isProviderCapableScope(companionScope) ? `, provide${descriptor.name}` : ''}`;
|
|
834
|
+
const provide =
|
|
835
|
+
isProviderCapableScope(companionScope)
|
|
836
|
+
? ` provide: () => [${descriptor.symbol}],\n`
|
|
837
|
+
: '';
|
|
838
|
+
const text = `// Generated by craft-migrate-services. ${MANUAL_SCOPE}: keep the legacy class until it can be rewritten.\nimport { toCraftService } from '@craft-ng/core';\nimport { ${descriptor.symbol} } from '${module}';\n\nexport const { ${helpers} } = toCraftService({\n name: '${descriptor.name}',\n scope: '${companionScope}',\n token: ${descriptor.symbol},\n${provide}});\n`;
|
|
839
|
+
const existing = project.getSourceFile(companionPath);
|
|
840
|
+
if (existing) {
|
|
841
|
+
if (
|
|
842
|
+
existing.getFullText().includes(MANUAL_SCOPE) &&
|
|
843
|
+
existing.getFullText().includes(`token: ${descriptor.symbol}`)
|
|
844
|
+
) {
|
|
845
|
+
return { file: existing, changed: false };
|
|
846
|
+
}
|
|
847
|
+
existing.replaceWithText(text);
|
|
848
|
+
return { file: existing, changed: true };
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
file: project.createSourceFile(companionPath, text, { overwrite: false }),
|
|
852
|
+
changed: true,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function rewriteConsumers(
|
|
857
|
+
files: readonly SourceFile[],
|
|
858
|
+
descriptors: readonly ServiceDescriptor[],
|
|
859
|
+
diagnostics: ServiceMigrationDiagnostic[],
|
|
860
|
+
touched: Set<SourceFile>,
|
|
861
|
+
): void {
|
|
862
|
+
for (const file of files) {
|
|
863
|
+
for (const call of file.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
864
|
+
if (call.wasForgotten() || call.getExpression().getText() !== 'inject')
|
|
865
|
+
continue;
|
|
866
|
+
const token = call.getArguments()[0]?.getText();
|
|
867
|
+
const service = descriptors.find((item) => item.symbol === token);
|
|
868
|
+
if (!service) continue;
|
|
869
|
+
call.replaceWithText(`inject${service.name}()`);
|
|
870
|
+
ensureRelativeImport(file, service, `inject${service.name}`);
|
|
871
|
+
touched.add(file);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
function rewriteHttpOperationSubscribers(
|
|
877
|
+
files: readonly SourceFile[],
|
|
878
|
+
descriptors: readonly ServiceDescriptor[],
|
|
879
|
+
diagnostics: ServiceMigrationDiagnostic[],
|
|
880
|
+
touched: Set<SourceFile>,
|
|
881
|
+
): void {
|
|
882
|
+
for (const file of files) {
|
|
883
|
+
for (const classDeclaration of file.getClasses()) {
|
|
884
|
+
const injectedServices = collectInjectedCraftServices(
|
|
885
|
+
classDeclaration,
|
|
886
|
+
descriptors,
|
|
887
|
+
);
|
|
888
|
+
if (injectedServices.size === 0) continue;
|
|
889
|
+
for (const subscribeCall of [
|
|
890
|
+
...classDeclaration.getDescendantsOfKind(SyntaxKind.CallExpression),
|
|
891
|
+
]) {
|
|
892
|
+
if (subscribeCall.wasForgotten()) continue;
|
|
893
|
+
const subscribeAccess = subscribeCall.getExpression();
|
|
894
|
+
if (
|
|
895
|
+
!Node.isPropertyAccessExpression(subscribeAccess) ||
|
|
896
|
+
subscribeAccess.getName() !== 'subscribe'
|
|
897
|
+
)
|
|
898
|
+
continue;
|
|
899
|
+
const operationCall = subscribeAccess.getExpression();
|
|
900
|
+
if (!Node.isCallExpression(operationCall)) continue;
|
|
901
|
+
const operationAccess = operationCall.getExpression();
|
|
902
|
+
if (!Node.isPropertyAccessExpression(operationAccess)) continue;
|
|
903
|
+
const receiverName = readReceiverPropertyName(
|
|
904
|
+
operationAccess.getExpression().getText(),
|
|
905
|
+
);
|
|
906
|
+
if (!receiverName) continue;
|
|
907
|
+
const service = injectedServices.get(receiverName);
|
|
908
|
+
if (!service) continue;
|
|
909
|
+
const operation = service.httpOperations?.find(
|
|
910
|
+
(item) => item.methodName === operationAccess.getName(),
|
|
911
|
+
);
|
|
912
|
+
if (!operation) continue;
|
|
913
|
+
if (
|
|
914
|
+
subscribeCall.getArguments().length > 0 ||
|
|
915
|
+
operation.parameterCount === 0
|
|
916
|
+
) {
|
|
917
|
+
diagnose(
|
|
918
|
+
diagnostics,
|
|
919
|
+
'MUTATION_SUBSCRIBE_REQUIRES_REWRITE',
|
|
920
|
+
{ ...service, sourceFile: file },
|
|
921
|
+
`${service.name}.${operation.methodName} est un appel HTTP mutable avec subscribe complexe; créer une mutation locale et migrer les callbacks manuellement.`,
|
|
922
|
+
);
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
const mutationName = ensureLocalMutationProperty(
|
|
926
|
+
classDeclaration,
|
|
927
|
+
service,
|
|
928
|
+
operation,
|
|
929
|
+
);
|
|
930
|
+
subscribeCall.replaceWithText(
|
|
931
|
+
printMutationCall(mutationName, operationCall),
|
|
932
|
+
);
|
|
933
|
+
ensureCoreImports(file, ['mutation']);
|
|
934
|
+
ensureRelativeImport(file, service, `${service.name}ToYield`);
|
|
935
|
+
ensureRelativeImport(file, service, `inject${service.name}`);
|
|
936
|
+
touched.add(file);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
function collectInjectedCraftServices(
|
|
943
|
+
classDeclaration: ClassDeclaration,
|
|
944
|
+
descriptors: readonly ServiceDescriptor[],
|
|
945
|
+
): Map<string, ServiceDescriptor> {
|
|
946
|
+
const byInjectHelper: Map<string, ServiceDescriptor> = new Map(
|
|
947
|
+
descriptors.map((descriptor) => [
|
|
948
|
+
`inject${descriptor.name}`,
|
|
949
|
+
descriptor,
|
|
950
|
+
] as const),
|
|
951
|
+
);
|
|
952
|
+
const result = new Map<string, ServiceDescriptor>();
|
|
953
|
+
for (const property of classDeclaration.getProperties()) {
|
|
954
|
+
const initializer = property.getInitializer();
|
|
955
|
+
if (
|
|
956
|
+
!initializer ||
|
|
957
|
+
!Node.isCallExpression(initializer) ||
|
|
958
|
+
!Node.isIdentifier(initializer.getExpression())
|
|
959
|
+
)
|
|
960
|
+
continue;
|
|
961
|
+
const service = byInjectHelper.get(initializer.getExpression().getText());
|
|
962
|
+
if (service) result.set(property.getName(), service);
|
|
963
|
+
}
|
|
964
|
+
return result;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function readReceiverPropertyName(text: string): string | undefined {
|
|
968
|
+
const match = /^(?:this\.)?([A-Za-z_$][\w$]*)$/.exec(text.trim());
|
|
969
|
+
return match?.[1];
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function ensureLocalMutationProperty(
|
|
973
|
+
classDeclaration: ClassDeclaration,
|
|
974
|
+
service: ServiceDescriptor,
|
|
975
|
+
operation: HttpOperationDescriptor,
|
|
976
|
+
): string {
|
|
977
|
+
const baseName = `${operation.methodName}Mutation`;
|
|
978
|
+
const mutationName = uniqueClassPropertyName(classDeclaration, baseName);
|
|
979
|
+
if (classDeclaration.getProperty(mutationName)) return mutationName;
|
|
980
|
+
const paramsType = `Parameters<ReturnType<typeof inject${service.name}>['${operation.methodName}']>`;
|
|
981
|
+
const method =
|
|
982
|
+
operation.parameterCount === 1
|
|
983
|
+
? `(params: ${paramsType}[0]) => params`
|
|
984
|
+
: `(params: ${paramsType}) => params`;
|
|
985
|
+
const call =
|
|
986
|
+
operation.parameterCount === 1
|
|
987
|
+
? `${toLocalServiceName(service.name)}.${operation.methodName}(params)`
|
|
988
|
+
: `${toLocalServiceName(service.name)}.${operation.methodName}(...params)`;
|
|
989
|
+
classDeclaration.insertProperty(0, {
|
|
990
|
+
name: mutationName,
|
|
991
|
+
isReadonly: true,
|
|
992
|
+
initializer: `mutation({
|
|
993
|
+
method: ${method},
|
|
994
|
+
loader: function* ({ params }) {
|
|
995
|
+
const ${toLocalServiceName(service.name)} = yield* ${service.name}ToYield();
|
|
996
|
+
return yield* ${call};
|
|
997
|
+
},
|
|
998
|
+
})`,
|
|
999
|
+
});
|
|
1000
|
+
return mutationName;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function toLocalServiceName(name: string): string {
|
|
1004
|
+
return `${name.charAt(0).toLowerCase()}${name.slice(1)}`;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function uniqueClassPropertyName(
|
|
1008
|
+
classDeclaration: ClassDeclaration,
|
|
1009
|
+
baseName: string,
|
|
1010
|
+
): string {
|
|
1011
|
+
if (!classDeclaration.getProperty(baseName)) return baseName;
|
|
1012
|
+
let index = 2;
|
|
1013
|
+
while (classDeclaration.getProperty(`${baseName}${index}`)) index += 1;
|
|
1014
|
+
return `${baseName}${index}`;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function printMutationCall(
|
|
1018
|
+
mutationName: string,
|
|
1019
|
+
operationCall: CallExpression,
|
|
1020
|
+
): string {
|
|
1021
|
+
const args = operationCall.getArguments().map((arg) => arg.getText());
|
|
1022
|
+
const params = args.length === 1 ? args[0] : `[${args.join(', ')}]`;
|
|
1023
|
+
return `this.${mutationName}.mutate(${params})`;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function rewriteProviders(
|
|
1027
|
+
files: readonly SourceFile[],
|
|
1028
|
+
descriptors: readonly ServiceDescriptor[],
|
|
1029
|
+
diagnostics: ServiceMigrationDiagnostic[],
|
|
1030
|
+
touched: Set<SourceFile>,
|
|
1031
|
+
): void {
|
|
1032
|
+
for (const file of files) {
|
|
1033
|
+
const assignments = file
|
|
1034
|
+
.getDescendantsOfKind(SyntaxKind.PropertyAssignment)
|
|
1035
|
+
.filter((property) => property.getName() === 'providers');
|
|
1036
|
+
for (const assignment of assignments) {
|
|
1037
|
+
const array = assignment.getInitializerIfKind(
|
|
1038
|
+
SyntaxKind.ArrayLiteralExpression,
|
|
1039
|
+
);
|
|
1040
|
+
if (!array) {
|
|
1041
|
+
const owner = descriptors[0];
|
|
1042
|
+
if (owner)
|
|
1043
|
+
diagnose(
|
|
1044
|
+
diagnostics,
|
|
1045
|
+
'COMPLEX_PROVIDER',
|
|
1046
|
+
{ ...owner, sourceFile: file },
|
|
1047
|
+
'Provider non statique.',
|
|
1048
|
+
);
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
for (const element of [...array.getElements()]) {
|
|
1052
|
+
if (!Node.isIdentifier(element)) continue;
|
|
1053
|
+
const service = descriptors.find(
|
|
1054
|
+
(item) => item.symbol === element.getText(),
|
|
1055
|
+
);
|
|
1056
|
+
if (!service || service.scope === 'global') continue;
|
|
1057
|
+
if (service.scope === 'function') {
|
|
1058
|
+
array.removeElement(element);
|
|
1059
|
+
touched.add(file);
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
element.replaceWithText(`provide${service.name}()`);
|
|
1063
|
+
ensureRelativeImport(file, service, `provide${service.name}`);
|
|
1064
|
+
touched.add(file);
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function isProviderCapableScope(
|
|
1071
|
+
scope: ServiceMigrationScope,
|
|
1072
|
+
): scope is 'toProvide' | 'manuallyProvidedAtRoot' | 'abstract' {
|
|
1073
|
+
return [
|
|
1074
|
+
'toProvide',
|
|
1075
|
+
'manuallyProvidedAtRoot',
|
|
1076
|
+
'abstract',
|
|
1077
|
+
].includes(scope);
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
function diagnoseCycles(
|
|
1081
|
+
descriptors: readonly ServiceDescriptor[],
|
|
1082
|
+
diagnostics: ServiceMigrationDiagnostic[],
|
|
1083
|
+
): void {
|
|
1084
|
+
const edges = new Map<string, string[]>();
|
|
1085
|
+
for (const descriptor of descriptors) {
|
|
1086
|
+
const deps = descriptor.classDeclaration
|
|
1087
|
+
.getDescendantsOfKind(SyntaxKind.CallExpression)
|
|
1088
|
+
.filter((call) => call.getExpression().getText() === 'inject')
|
|
1089
|
+
.map((call) => call.getArguments()[0]?.getText())
|
|
1090
|
+
.filter((value): value is string => Boolean(value));
|
|
1091
|
+
edges.set(descriptor.symbol, deps);
|
|
1092
|
+
}
|
|
1093
|
+
for (const descriptor of descriptors) {
|
|
1094
|
+
if (hasPath(edges, descriptor.symbol, descriptor.symbol, new Set(), true)) {
|
|
1095
|
+
diagnose(
|
|
1096
|
+
diagnostics,
|
|
1097
|
+
'DI_CYCLE',
|
|
1098
|
+
descriptor,
|
|
1099
|
+
`Cycle DI impliquant ${descriptor.symbol}.`,
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function hasPath(
|
|
1106
|
+
edges: Map<string, string[]>,
|
|
1107
|
+
current: string,
|
|
1108
|
+
target: string,
|
|
1109
|
+
seen: Set<string>,
|
|
1110
|
+
first: boolean,
|
|
1111
|
+
): boolean {
|
|
1112
|
+
if (!first && current === target) return true;
|
|
1113
|
+
if (seen.has(current)) return false;
|
|
1114
|
+
seen.add(current);
|
|
1115
|
+
return (edges.get(current) ?? []).some((next) =>
|
|
1116
|
+
hasPath(edges, next, target, seen, false),
|
|
1117
|
+
);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function isLegacyServiceClass(value: ClassDeclaration): boolean {
|
|
1121
|
+
return value
|
|
1122
|
+
.getDecorators()
|
|
1123
|
+
.some((decorator) =>
|
|
1124
|
+
['Injectable', 'Service'].includes(decorator.getName()),
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function hasInjectionFlags(call: CallExpression): boolean {
|
|
1129
|
+
if (
|
|
1130
|
+
call.getExpression().getText() !== 'inject' ||
|
|
1131
|
+
call.getArguments().length < 2
|
|
1132
|
+
)
|
|
1133
|
+
return false;
|
|
1134
|
+
return /optional|self|host|skipSelf/.test(call.getArguments()[1].getText());
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function ensureCoreImports(file: SourceFile, names: string[]): void {
|
|
1138
|
+
let declaration = file.getImportDeclaration('@craft-ng/core');
|
|
1139
|
+
if (!declaration)
|
|
1140
|
+
declaration = file.addImportDeclaration({
|
|
1141
|
+
moduleSpecifier: '@craft-ng/core',
|
|
1142
|
+
});
|
|
1143
|
+
const existing = new Set(
|
|
1144
|
+
declaration.getNamedImports().map((item) => item.getName()),
|
|
1145
|
+
);
|
|
1146
|
+
declaration.addNamedImports(names.filter((name) => !existing.has(name)));
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function ensureRelativeImport(
|
|
1150
|
+
file: SourceFile,
|
|
1151
|
+
service: ServiceDescriptor,
|
|
1152
|
+
name: string,
|
|
1153
|
+
): void {
|
|
1154
|
+
if (file === service.sourceFile) return;
|
|
1155
|
+
const helperFile = service.helperSourceFile ?? service.sourceFile;
|
|
1156
|
+
let specifier = relative(
|
|
1157
|
+
dirname(file.getFilePath()),
|
|
1158
|
+
helperFile.getFilePath(),
|
|
1159
|
+
)
|
|
1160
|
+
.replace(/\\/g, '/')
|
|
1161
|
+
.replace(/\.ts$/, '');
|
|
1162
|
+
if (!specifier.startsWith('.')) specifier = `./${specifier}`;
|
|
1163
|
+
let declaration = file.getImportDeclaration(specifier);
|
|
1164
|
+
if (!declaration)
|
|
1165
|
+
declaration = file.addImportDeclaration({ moduleSpecifier: specifier });
|
|
1166
|
+
if (!declaration.getNamedImports().some((item) => item.getName() === name))
|
|
1167
|
+
declaration.addNamedImport(name);
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function removeDecoratorImport(file: SourceFile, name: string): void {
|
|
1171
|
+
for (const declaration of file.getImportDeclarations()) {
|
|
1172
|
+
const imported = declaration
|
|
1173
|
+
.getNamedImports()
|
|
1174
|
+
.find((item) => item.getName() === name);
|
|
1175
|
+
if (!imported) continue;
|
|
1176
|
+
imported.remove();
|
|
1177
|
+
if (
|
|
1178
|
+
!declaration.getDefaultImport() &&
|
|
1179
|
+
!declaration.getNamespaceImport() &&
|
|
1180
|
+
declaration.getNamedImports().length === 0
|
|
1181
|
+
)
|
|
1182
|
+
declaration.remove();
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
function diagnose(
|
|
1187
|
+
diagnostics: ServiceMigrationDiagnostic[],
|
|
1188
|
+
code: ServiceMigrationDiagnosticCode,
|
|
1189
|
+
descriptor: Pick<ServiceDescriptor, 'sourceFile' | 'symbol'>,
|
|
1190
|
+
message: string,
|
|
1191
|
+
): void {
|
|
1192
|
+
if (
|
|
1193
|
+
diagnostics.some(
|
|
1194
|
+
(item) =>
|
|
1195
|
+
item.code === code &&
|
|
1196
|
+
item.filePath === descriptor.sourceFile.getFilePath() &&
|
|
1197
|
+
item.symbol === descriptor.symbol,
|
|
1198
|
+
)
|
|
1199
|
+
)
|
|
1200
|
+
return;
|
|
1201
|
+
diagnostics.push({
|
|
1202
|
+
code,
|
|
1203
|
+
filePath: descriptor.sourceFile.getFilePath(),
|
|
1204
|
+
symbol: descriptor.symbol,
|
|
1205
|
+
message,
|
|
1206
|
+
manual: true,
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
function getFileReport(
|
|
1211
|
+
map: Map<string, MigratedServiceFile>,
|
|
1212
|
+
filePath: string,
|
|
1213
|
+
): MigratedServiceFile {
|
|
1214
|
+
let report = map.get(filePath);
|
|
1215
|
+
if (!report) {
|
|
1216
|
+
report = { filePath, changed: false, symbols: [], companions: [] };
|
|
1217
|
+
map.set(filePath, report);
|
|
1218
|
+
}
|
|
1219
|
+
return report;
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function rewriteThis(text: string, propertyNames: ReadonlySet<string>): string {
|
|
1223
|
+
let rewritten = text;
|
|
1224
|
+
for (const name of propertyNames) {
|
|
1225
|
+
rewritten = rewritten.replace(
|
|
1226
|
+
new RegExp(`\\bthis\\.${escapeRegex(name)}\\b`, 'g'),
|
|
1227
|
+
toInternalName(name),
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
return rewritten.replace(/\bthis\./g, '');
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
function toInternalName(name: string): string {
|
|
1234
|
+
return `_${name}`;
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
function matches(pattern: string, value: string): boolean {
|
|
1238
|
+
if (!pattern.includes('*'))
|
|
1239
|
+
return pattern === value || value.endsWith(pattern);
|
|
1240
|
+
const regex = new RegExp(
|
|
1241
|
+
`^${pattern.split('*').map(escapeRegex).join('.*')}$`,
|
|
1242
|
+
);
|
|
1243
|
+
return regex.test(value);
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
function escapeRegex(value: string): string {
|
|
1247
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
function isInside(filePath: string, rootDir: string): boolean {
|
|
1251
|
+
const path = relative(rootDir, filePath);
|
|
1252
|
+
return path === '' || (!path.startsWith('..') && !isAbsolute(path));
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function defaultTsConfig(rootDir: string): string {
|
|
1256
|
+
for (const name of ['tsconfig.app.json', 'tsconfig.json']) {
|
|
1257
|
+
const candidate = join(rootDir, name);
|
|
1258
|
+
if (existsSync(candidate)) return candidate;
|
|
1259
|
+
}
|
|
1260
|
+
return join(rootDir, 'tsconfig.json');
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
async function runEslint(files: string[], cwd: string): Promise<void> {
|
|
1264
|
+
const local = join(cwd, 'node_modules', '.bin', 'eslint');
|
|
1265
|
+
await execFileAsync(
|
|
1266
|
+
existsSync(local) ? local : 'eslint',
|
|
1267
|
+
['--fix', ...files],
|
|
1268
|
+
{ cwd },
|
|
1269
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function logSummary(
|
|
1273
|
+
result: MigrateServicesResult,
|
|
1274
|
+
log: (message: string) => void,
|
|
1275
|
+
wrote: boolean,
|
|
1276
|
+
): void {
|
|
1277
|
+
log(
|
|
1278
|
+
`${wrote ? 'Migrated' : 'Would migrate'} ${result.changedFiles.length} file(s); ${result.diagnostics.length} manual diagnostic(s).`,
|
|
1279
|
+
);
|
|
1280
|
+
for (const diagnostic of result.diagnostics)
|
|
1281
|
+
log(`[${diagnostic.code}] ${diagnostic.filePath}: ${diagnostic.message}`);
|
|
1282
|
+
}
|