@ethlete/query 6.0.0-next.3 → 6.0.0-next.30

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.
Files changed (31) hide show
  1. package/CHANGELOG.md +447 -0
  2. package/fesm2022/ethlete-query-testing.mjs +281 -5
  3. package/fesm2022/ethlete-query-testing.mjs.map +1 -1
  4. package/fesm2022/ethlete-query.mjs +9926 -5013
  5. package/fesm2022/ethlete-query.mjs.map +1 -1
  6. package/generators/deprecate-legacy-queries/migration.js +77 -0
  7. package/generators/deprecate-legacy-queries/schema.json +26 -0
  8. package/generators/generators.json +15 -0
  9. package/generators/migrate-query-client-features/migration.js +142 -0
  10. package/generators/migrate-query-client-features/report.js +136 -0
  11. package/generators/migrate-query-client-features/schema.json +26 -0
  12. package/generators/migrate-query-opt-in-features/migration.js +182 -0
  13. package/generators/migrate-query-opt-in-features/report.js +143 -0
  14. package/generators/migrate-query-opt-in-features/schema.json +31 -0
  15. package/generators/migrate-to-query-v3/auth-provider-scaffold.js +131 -0
  16. package/generators/migrate-to-query-v3/cleanup-migration.js +293 -0
  17. package/generators/migrate-to-query-v3/http-client-check.js +93 -0
  18. package/generators/migrate-to-query-v3/legacy-prepare-migration.js +865 -0
  19. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +608 -0
  20. package/generators/migrate-to-query-v3/migration-scope.js +41 -0
  21. package/generators/migrate-to-query-v3/migration.js +28 -2932
  22. package/generators/migrate-to-query-v3/module-graph.js +153 -0
  23. package/generators/migrate-to-query-v3/query-client-migration.js +526 -0
  24. package/generators/migrate-to-query-v3/rename-symbols.js +193 -0
  25. package/generators/migrate-to-query-v3/report.js +136 -0
  26. package/generators/migrate-to-query-v3/schema.json +12 -0
  27. package/generators/migrate-to-query-v3/shared.js +142 -0
  28. package/generators/prep-for-query-v3/migration.js +3 -3
  29. package/package.json +26 -13
  30. package/types/ethlete-query-testing.d.ts +139 -12
  31. package/types/ethlete-query.d.ts +6965 -3142
@@ -0,0 +1,608 @@
1
+ import * as ts from 'typescript';
2
+ import { collectV2BearerAuthConfigs, renderAuthProviderBody } from './auth-provider-scaffold.js';
3
+ import { createModuleGraph } from './module-graph.js';
4
+ import { pruneUnusedNamedImports, renameImportedReferences } from './rename-symbols.js';
5
+ import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, legacyQueryDeprecationTag, } from './shared.js';
6
+ const LEGACY_HTTP_METHOD = {
7
+ GET: 'get',
8
+ POST: 'post',
9
+ PUT: 'put',
10
+ PATCH: 'patch',
11
+ DELETE: 'delete',
12
+ };
13
+ const isLegacyHttpMethod = (value) => {
14
+ return Object.values(LEGACY_HTTP_METHOD).includes(value);
15
+ };
16
+ export const createNewQueryCreators = (tree, queryClientFiles, report, scope) => {
17
+ const createdFiles = [];
18
+ const authProvidersNeeded = new Map();
19
+ const clientNameToFilePath = new Map();
20
+ const creatorToClient = new Map();
21
+ queryClientFiles.forEach((configNames, filePath) => {
22
+ configNames.forEach((configName) => {
23
+ clientNameToFilePath.set(configName, filePath);
24
+ });
25
+ });
26
+ scope.visit(tree, (filePath) => {
27
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
28
+ return;
29
+ }
30
+ const content = tree.read(filePath, 'utf-8');
31
+ if (!content) {
32
+ return;
33
+ }
34
+ const legacyCreators = analyzeLegacyQueryCreators(content, queryClientFiles, filePath, report);
35
+ if (legacyCreators.length === 0) {
36
+ return;
37
+ }
38
+ legacyCreators.forEach((creator) => {
39
+ creatorToClient.set(creator.name, creator.clientName);
40
+ if (!creator.secure) {
41
+ return;
42
+ }
43
+ const clientFilePath = clientNameToFilePath.get(creator.clientName);
44
+ if (clientFilePath) {
45
+ authProvidersNeeded.set(creator.clientName, clientFilePath);
46
+ }
47
+ });
48
+ const nextContent = transformLegacyQueryCreators(content, legacyCreators);
49
+ if (nextContent !== content) {
50
+ tree.write(filePath, nextContent);
51
+ createdFiles.push(filePath);
52
+ }
53
+ });
54
+ if (authProvidersNeeded.size > 0) {
55
+ createAuthProviders({
56
+ tree,
57
+ authProvidersNeeded,
58
+ report,
59
+ v2Configs: collectV2BearerAuthConfigs(tree, scope),
60
+ creatorToClient,
61
+ });
62
+ }
63
+ if (createdFiles.length > 0) {
64
+ console.log('\n✅ Created new query creators in:');
65
+ createdFiles.forEach((filePath) => console.log(` - ${filePath}`));
66
+ }
67
+ };
68
+ export const updateLegacyCreatorImportsAndUsages = (tree, scope, report) => {
69
+ const legacyCreators = new Map();
70
+ const updatedFiles = [];
71
+ scope.visit(tree, (filePath) => {
72
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
73
+ return;
74
+ }
75
+ const content = tree.read(filePath, 'utf-8');
76
+ if (!content) {
77
+ return;
78
+ }
79
+ findLegacyWrappers(content).forEach((legacyName, originalName) => {
80
+ legacyCreators.set(originalName, { legacyName, definedIn: filePath });
81
+ });
82
+ });
83
+ if (legacyCreators.size === 0) {
84
+ return;
85
+ }
86
+ console.log(`\n✅ Found ${legacyCreators.size} legacy query creator wrappers`);
87
+ const moduleGraph = createModuleGraph(tree);
88
+ scope.visit(tree, (filePath) => {
89
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
90
+ return;
91
+ }
92
+ const content = tree.read(filePath, 'utf-8');
93
+ if (!content || containsLegacyWrapperDefinitions(content)) {
94
+ return;
95
+ }
96
+ const nextContent = pointFileAtLegacyWrappers({
97
+ content,
98
+ filePath,
99
+ legacyCreators,
100
+ moduleGraph,
101
+ report,
102
+ });
103
+ if (nextContent !== content) {
104
+ tree.write(filePath, nextContent);
105
+ updatedFiles.push(filePath);
106
+ }
107
+ });
108
+ if (updatedFiles.length > 0) {
109
+ console.log('\n✅ Updated legacy creator imports and usages in:');
110
+ updatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
111
+ }
112
+ };
113
+ /**
114
+ * Rewrites this file's imports of migrated creators to the `legacy*` wrappers.
115
+ *
116
+ * Two rules keep the rewrite honest, and both exist because the first version of this pass did not
117
+ * have them:
118
+ *
119
+ * - An import is only touched when the module it comes from really does resolve to the file that
120
+ * defines the wrapper. A same-named export from an unrelated package stays as it is.
121
+ * - An alias is preserved (`legacyGetPerson as getPersonQuery`) instead of being dropped, so the
122
+ * call sites using that alias keep compiling.
123
+ */
124
+ const pointFileAtLegacyWrappers = ({ content, filePath, legacyCreators, moduleGraph, report, }) => {
125
+ const sourceFile = createSourceFile(content, filePath);
126
+ const replacements = [];
127
+ const referenceRenames = new Map();
128
+ const resolvesToWrapper = (specifier, importedName, registration) => moduleGraph.findDeclaringFile(filePath, specifier, importedName) === registration.definedIn;
129
+ const rewriteSpecifiers = (elements, moduleSpecifier) => {
130
+ let changed = false;
131
+ const nextElements = elements.map((element) => {
132
+ const importedName = element.propertyName?.text ?? element.name.text;
133
+ const registration = legacyCreators.get(importedName);
134
+ if (!registration || !resolvesToWrapper(moduleSpecifier, importedName, registration)) {
135
+ return element.getText(sourceFile);
136
+ }
137
+ changed = true;
138
+ // Aliased: only the imported half changes, so nothing in the file body has to move.
139
+ if (element.propertyName) {
140
+ return `${registration.legacyName} as ${element.name.text}`;
141
+ }
142
+ referenceRenames.set(element.name.text, registration.legacyName);
143
+ return registration.legacyName;
144
+ });
145
+ return changed ? nextElements : null;
146
+ };
147
+ for (const node of sourceFile.statements) {
148
+ if (ts.isImportDeclaration(node) &&
149
+ ts.isStringLiteral(node.moduleSpecifier) &&
150
+ node.importClause?.namedBindings &&
151
+ ts.isNamedImports(node.importClause.namedBindings)) {
152
+ const nextElements = rewriteSpecifiers(node.importClause.namedBindings.elements, node.moduleSpecifier.text);
153
+ if (nextElements) {
154
+ replacements.push({
155
+ start: node.getStart(sourceFile),
156
+ end: node.getEnd(),
157
+ replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
158
+ });
159
+ }
160
+ continue;
161
+ }
162
+ // Re-exports need the same treatment, or a barrel keeps pointing at the v3 creator.
163
+ if (ts.isExportDeclaration(node) &&
164
+ node.moduleSpecifier &&
165
+ ts.isStringLiteral(node.moduleSpecifier) &&
166
+ node.exportClause &&
167
+ ts.isNamedExports(node.exportClause)) {
168
+ const nextElements = rewriteSpecifiers(node.exportClause.elements, node.moduleSpecifier.text);
169
+ if (nextElements) {
170
+ replacements.push({
171
+ start: node.getStart(sourceFile),
172
+ end: node.getEnd(),
173
+ replacement: `export { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
174
+ });
175
+ }
176
+ }
177
+ }
178
+ let result = content;
179
+ replacements.sort((left, right) => right.start - left.start);
180
+ replacements.forEach(({ start, end, replacement }) => {
181
+ result = result.slice(0, start) + replacement + result.slice(end);
182
+ });
183
+ const renamed = renameImportedReferences(result, referenceRenames);
184
+ renamed.shadowed.forEach((name) => {
185
+ report.addManualReview({
186
+ title: `Point ${name} at its legacy wrapper by hand`,
187
+ summary: `${filePath} imports the migrated creator \`${name}\` but also declares something with that name, so the migration left its usages alone to avoid rewriting the wrong symbol.`,
188
+ action: `Rename the local declaration or the import alias, then point the query usages at \`${legacyCreators.get(name)?.legacyName ?? `legacy${capitalizeFirstLetter(name)}`}\`.`,
189
+ locations: [{ filePath }],
190
+ source: 'legacy-query-creator-migration',
191
+ dedupeKey: `shadowed-rename:${filePath}:${name}`,
192
+ });
193
+ });
194
+ return renamed.content;
195
+ };
196
+ const toLegacyName = (name) => `legacy${capitalizeFirstLetter(name)}`;
197
+ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report) => {
198
+ const sourceFile = createSourceFile(content, filePath);
199
+ const creators = [];
200
+ const oldClientNames = new Set();
201
+ queryClientFiles.forEach((configNames) => {
202
+ configNames.forEach((configName) => oldClientNames.add(configName));
203
+ });
204
+ const visit = (node) => {
205
+ if (ts.isVariableDeclaration(node) &&
206
+ ts.isIdentifier(node.name) &&
207
+ node.initializer &&
208
+ ts.isCallExpression(node.initializer) &&
209
+ ts.isPropertyAccessExpression(node.initializer.expression)) {
210
+ const objectExpression = node.initializer.expression.expression;
211
+ const methodExpression = node.initializer.expression.name;
212
+ if (!ts.isIdentifier(objectExpression) || !ts.isIdentifier(methodExpression)) {
213
+ ts.forEachChild(node, visit);
214
+ return;
215
+ }
216
+ if (!oldClientNames.has(objectExpression.text) || !isLegacyHttpMethod(methodExpression.text)) {
217
+ ts.forEachChild(node, visit);
218
+ return;
219
+ }
220
+ const creatorName = node.name.text;
221
+ const info = {
222
+ name: creatorName,
223
+ clientName: objectExpression.text,
224
+ method: methodExpression.text,
225
+ route: '',
226
+ secure: false,
227
+ httpOptions: new Map(),
228
+ };
229
+ const configObject = node.initializer.arguments[0];
230
+ if (configObject && ts.isObjectLiteralExpression(configObject)) {
231
+ configObject.properties.forEach((property) => {
232
+ if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
233
+ return;
234
+ }
235
+ if (property.name.text === 'route') {
236
+ info.route = property.initializer.getText(sourceFile);
237
+ }
238
+ if (property.name.text === 'secure' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
239
+ info.secure = true;
240
+ }
241
+ if (property.name.text === 'types' && ts.isObjectLiteralExpression(property.initializer)) {
242
+ property.initializer.properties.forEach((typeProperty) => {
243
+ if (!ts.isPropertyAssignment(typeProperty) || !ts.isIdentifier(typeProperty.name)) {
244
+ return;
245
+ }
246
+ const extractedType = extractTypeFromDef(typeProperty.initializer, sourceFile);
247
+ if (!extractedType) {
248
+ report.addManualReview({
249
+ title: `Verify generated types for ${creatorName}`,
250
+ summary: `The migration could not extract the \`${typeProperty.name.text}\` type for ${creatorName} automatically.`,
251
+ action: 'Check the generated creator type arguments and restore any missing args/body/response types manually.',
252
+ locations: [
253
+ {
254
+ filePath,
255
+ line: sourceFile.getLineAndCharacterOfPosition(typeProperty.getStart(sourceFile)).line + 1,
256
+ },
257
+ ],
258
+ source: 'legacy-query-creator-migration',
259
+ dedupeKey: `type-extraction:${filePath}:${creatorName}:${typeProperty.name.text}`,
260
+ });
261
+ return;
262
+ }
263
+ if (typeProperty.name.text === 'args') {
264
+ info.typeArgs = extractedType;
265
+ }
266
+ if (typeProperty.name.text === 'response') {
267
+ info.typeResponse = extractedType;
268
+ }
269
+ if (typeProperty.name.text === 'body') {
270
+ info.typeBody = extractedType;
271
+ }
272
+ });
273
+ }
274
+ if (['reportProgress', 'responseType', 'transferCache', 'withCredentials'].includes(property.name.text)) {
275
+ info.httpOptions.set(property.name.text, property.initializer.getText(sourceFile));
276
+ }
277
+ });
278
+ }
279
+ if (info.route) {
280
+ creators.push(info);
281
+ }
282
+ }
283
+ ts.forEachChild(node, visit);
284
+ };
285
+ visit(sourceFile);
286
+ return creators;
287
+ };
288
+ const extractTypeFromDef = (node, sourceFile) => {
289
+ if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'def') {
290
+ if (node.typeArguments && node.typeArguments.length > 0) {
291
+ return node.typeArguments[0].getText(sourceFile);
292
+ }
293
+ }
294
+ return undefined;
295
+ };
296
+ const transformLegacyQueryCreators = (content, legacyCreators) => {
297
+ const sourceFile = createSourceFile(content);
298
+ const creatorsByClient = new Map();
299
+ const replacements = [];
300
+ legacyCreators.forEach((creator) => {
301
+ if (!creatorsByClient.has(creator.clientName)) {
302
+ creatorsByClient.set(creator.clientName, []);
303
+ }
304
+ creatorsByClient.get(creator.clientName).push(creator);
305
+ });
306
+ legacyCreators.forEach((creator) => {
307
+ const nextCreator = generateNewQueryCreator(creator);
308
+ const legacyWrapper = generateLegacyWrapper(creator);
309
+ let variableStatement;
310
+ const visit = (node) => {
311
+ if (ts.isVariableStatement(node)) {
312
+ const declaration = node.declarationList.declarations[0];
313
+ if (declaration && ts.isIdentifier(declaration.name) && declaration.name.text === creator.name) {
314
+ variableStatement = node;
315
+ }
316
+ }
317
+ ts.forEachChild(node, visit);
318
+ };
319
+ visit(sourceFile);
320
+ if (variableStatement) {
321
+ replacements.push({
322
+ start: variableStatement.getStart(sourceFile),
323
+ end: variableStatement.getEnd(),
324
+ replacement: `${nextCreator}\n${legacyWrapper}`,
325
+ });
326
+ }
327
+ });
328
+ let result = content;
329
+ replacements.sort((left, right) => right.start - left.start);
330
+ replacements.forEach(({ start, end, replacement }) => {
331
+ result = result.slice(0, start) + replacement + result.slice(end);
332
+ });
333
+ result = ensureImportFromQuery(result, ['createLegacyQueryCreator']);
334
+ result = addCreatorImports(result, creatorsByClient);
335
+ // The rewritten creators no longer go through `def<T>()` or the v2 client object, so whatever is
336
+ // left over from those constructs would land as a lint error the moment `formatFiles` runs.
337
+ return pruneUnusedNamedImports(result, 'all');
338
+ };
339
+ const addCreatorImports = (content, creatorsByClient) => {
340
+ const sourceFile = createSourceFile(content);
341
+ const replacements = [];
342
+ const importsByClient = new Map();
343
+ creatorsByClient.forEach((creators, clientName) => {
344
+ const clientBaseName = clientName.replace(/Client$/, '');
345
+ const importsNeeded = new Set([ensureConfigSuffix(clientName)]);
346
+ let hasSecureCreators = false;
347
+ creators.forEach((creator) => {
348
+ hasSecureCreators ||= creator.secure;
349
+ const capitalizedMethod = capitalizeFirstLetter(creator.method);
350
+ importsNeeded.add(`${clientBaseName}${capitalizedMethod}`);
351
+ if (creator.secure) {
352
+ importsNeeded.add(`${clientBaseName}${capitalizedMethod}Secure`);
353
+ }
354
+ });
355
+ if (hasSecureCreators) {
356
+ ['Get', 'Post', 'Put', 'Patch', 'Delete'].forEach((methodName) => {
357
+ const secureImport = `${clientBaseName}${methodName}Secure`;
358
+ if (creators.some((creator) => capitalizeFirstLetter(creator.method) === methodName && creator.secure)) {
359
+ importsNeeded.add(secureImport);
360
+ }
361
+ });
362
+ }
363
+ importsByClient.set(clientName, importsNeeded);
364
+ });
365
+ const visit = (node) => {
366
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
367
+ const clientsInImport = [];
368
+ const existingImports = new Set();
369
+ if (node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)) {
370
+ node.importClause.namedBindings.elements.forEach((element) => {
371
+ existingImports.add(element.name.text);
372
+ if (importsByClient.has(element.name.text)) {
373
+ clientsInImport.push(element.name.text);
374
+ }
375
+ });
376
+ }
377
+ if (clientsInImport.length === 0) {
378
+ ts.forEachChild(node, visit);
379
+ return;
380
+ }
381
+ const nextImports = new Set();
382
+ clientsInImport.forEach((clientName) => {
383
+ importsByClient.get(clientName)?.forEach((importName) => nextImports.add(importName));
384
+ });
385
+ existingImports.forEach((importName) => {
386
+ if (!clientsInImport.includes(importName)) {
387
+ nextImports.add(importName);
388
+ }
389
+ });
390
+ replacements.push({
391
+ start: node.getStart(sourceFile),
392
+ end: node.getEnd(),
393
+ replacement: `import { ${Array.from(nextImports).sort().join(', ')} } from '${node.moduleSpecifier.text}';`,
394
+ });
395
+ }
396
+ ts.forEachChild(node, visit);
397
+ };
398
+ visit(sourceFile);
399
+ let result = content;
400
+ replacements.sort((left, right) => right.start - left.start);
401
+ replacements.forEach(({ start, end, replacement }) => {
402
+ result = result.slice(0, start) + replacement + result.slice(end);
403
+ });
404
+ return result;
405
+ };
406
+ const generateNewQueryCreator = (creator) => {
407
+ const clientBaseName = creator.clientName.replace(/Client$/, '');
408
+ const capitalizedMethod = capitalizeFirstLetter(creator.method);
409
+ const creatorFunction = creator.secure
410
+ ? `${clientBaseName}${capitalizedMethod}Secure`
411
+ : `${clientBaseName}${capitalizedMethod}`;
412
+ const typeParameter = getTypeParameter(creator);
413
+ const options = Array.from(creator.httpOptions.entries()).map(([key, value]) => `${key}: ${value}`);
414
+ const optionsBlock = options.length > 0 ? `, {\n ${options.join(',\n ')}\n}` : '';
415
+ return `export const ${creator.name} = ${creatorFunction}${typeParameter}(${creator.route}${optionsBlock});`;
416
+ };
417
+ const getTypeParameter = (creator) => {
418
+ if (creator.typeArgs && (creator.typeBody || creator.typeResponse)) {
419
+ const additionalTypes = [];
420
+ if (creator.typeBody) {
421
+ additionalTypes.push(`body: ${creator.typeBody}`);
422
+ }
423
+ if (creator.typeResponse) {
424
+ additionalTypes.push(`response: ${creator.typeResponse}`);
425
+ }
426
+ return `<${creator.typeArgs} & { ${additionalTypes.join('; ')} }>`;
427
+ }
428
+ if (creator.typeArgs) {
429
+ return `<${creator.typeArgs}>`;
430
+ }
431
+ if (creator.typeBody || creator.typeResponse) {
432
+ const typeProperties = [];
433
+ if (creator.typeBody) {
434
+ typeProperties.push(`body: ${creator.typeBody}`);
435
+ }
436
+ if (creator.typeResponse) {
437
+ typeProperties.push(`response: ${creator.typeResponse}`);
438
+ }
439
+ return `<{ ${typeProperties.join('; ')} }>`;
440
+ }
441
+ return '';
442
+ };
443
+ const generateLegacyWrapper = (creator) => {
444
+ // `name` is what lets a `prepare()` called outside an injection context name itself in the error
445
+ // instead of surfacing as a bare NG0203.
446
+ return `/**\n * ${legacyQueryDeprecationTag(creator.name)}\n */\nexport const ${toLegacyName(creator.name)} = createLegacyQueryCreator({ name: '${toLegacyName(creator.name)}', creator: ${creator.name} });`;
447
+ };
448
+ const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, creatorToClient, }) => {
449
+ const createdProviders = [];
450
+ authProvidersNeeded.forEach((filePath, clientName) => {
451
+ const content = tree.read(filePath, 'utf-8');
452
+ if (!content) {
453
+ return;
454
+ }
455
+ const authProviderName = `${clientName}AuthProvider`;
456
+ if (content.includes(`export const ${authProviderName} = createBearerAuthProvider(`)) {
457
+ return;
458
+ }
459
+ // Match on the refresh creator: it is the one thing a v2 provider config always names, and it
460
+ // belongs to exactly one client.
461
+ const v2Config = v2Configs.find((candidate) => candidate.refreshCreatorName && creatorToClient.get(candidate.refreshCreatorName) === clientName);
462
+ const body = renderAuthProviderBody(v2Config);
463
+ const nextContent = generateSecureQueryCreators(addAuthProviderToFile(content, clientName, body), clientName);
464
+ if (nextContent === content) {
465
+ return;
466
+ }
467
+ tree.write(filePath, nextContent);
468
+ createdProviders.push(filePath);
469
+ if (body.isScaffolded) {
470
+ report.addFollowUp({
471
+ title: `Finish the scaffolded auth provider ${authProviderName}`,
472
+ summary: `${authProviderName} was scaffolded from the v2 BearerAuthProvider at ${v2Config?.filePath}:${v2Config?.line}. The refresh query and cookie config carried over; the adapters were copied verbatim and their parameter shapes changed between v2 and v3.`,
473
+ action: 'Resolve the TODO(query-v3) comments in the generated provider, add the login query via withAuthenticationQuery, and delete the v2 provider once the app logs in again.',
474
+ locations: [{ filePath }, ...(v2Config ? [{ filePath: v2Config.filePath, line: v2Config.line }] : [])],
475
+ source: 'legacy-query-creator-migration',
476
+ dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
477
+ });
478
+ }
479
+ else {
480
+ report.addFollowUp({
481
+ title: `Configure auth queries for ${authProviderName}`,
482
+ summary: `The migration created ${authProviderName} with an empty \`queries\` array because no v2 BearerAuthProvider config could be matched to this client.`,
483
+ action: 'Add authQuery/tokenRefreshQuery builders, verify token extraction, and remove the placeholder empty array once the provider is configured.',
484
+ locations: [{ filePath }],
485
+ source: 'legacy-query-creator-migration',
486
+ dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
487
+ });
488
+ }
489
+ // The generated layout only works if the auth queries live next to the provider: the provider
490
+ // needs the login/refresh creators, and the secure creators need the provider. Leaving them in
491
+ // an `auth.queries.ts` that imports the client's `post` creator closes the loop.
492
+ report.addManualReview({
493
+ title: `Keep the auth queries in ${filePath}`,
494
+ summary: `${authProviderName} needs the login and refresh creators, and the secure creators below it need the provider. If those creators stay in a separate auth queries file that imports this client, the two files form an import cycle.`,
495
+ action: `Move the login/refresh creators into ${filePath} above the provider, and have the auth queries file import them back for its legacy* wrappers.`,
496
+ locations: [{ filePath }],
497
+ source: 'legacy-query-creator-migration',
498
+ dedupeKey: `auth-provider-cycle:${filePath}`,
499
+ });
500
+ });
501
+ if (createdProviders.length > 0) {
502
+ console.log('\n✅ Created auth providers in:');
503
+ createdProviders.forEach((filePath) => console.log(` - ${filePath}`));
504
+ }
505
+ };
506
+ const addAuthProviderToFile = (content, clientName, body) => {
507
+ const nextContent = ensureImportFromEthleteCore(ensureImportFromQuery(content, ['createBearerAuthProvider', ...body.importsNeeded]), ['toInjectFn', 'toProvideFn']);
508
+ const sourceFile = createSourceFile(nextContent);
509
+ const configName = ensureConfigSuffix(clientName);
510
+ const configPosition = findVariableStatementEnd(sourceFile, configName, 'createQueryClient');
511
+ if (!configPosition) {
512
+ return content;
513
+ }
514
+ const authProviderName = `${clientName}AuthProvider`;
515
+ const capitalizedClientName = capitalizeFirstLetter(clientName);
516
+ const authProviderBlock = [
517
+ '',
518
+ `export const ${authProviderName} = createBearerAuthProvider({`,
519
+ ` name: '${clientName}',`,
520
+ ` queryClientRef: ${configName},`,
521
+ body.queries,
522
+ ...(body.features ? [body.features] : []),
523
+ '});',
524
+ '',
525
+ `export const provide${capitalizedClientName}AuthProvider = toProvideFn(${authProviderName});`,
526
+ `export const inject${capitalizedClientName}AuthProvider = toInjectFn(${authProviderName});`,
527
+ ].join('\n');
528
+ return nextContent.slice(0, configPosition) + authProviderBlock + nextContent.slice(configPosition);
529
+ };
530
+ const findVariableStatementEnd = (sourceFile, variableName, initializerName) => {
531
+ let variableStatementEnd;
532
+ const visit = (node) => {
533
+ if (ts.isVariableDeclaration(node) &&
534
+ ts.isIdentifier(node.name) &&
535
+ node.name.text === variableName &&
536
+ node.initializer &&
537
+ ts.isCallExpression(node.initializer) &&
538
+ ts.isIdentifier(node.initializer.expression) &&
539
+ node.initializer.expression.text === initializerName) {
540
+ let parent = node.parent;
541
+ while (parent && !ts.isVariableStatement(parent)) {
542
+ parent = parent.parent;
543
+ }
544
+ if (parent && ts.isVariableStatement(parent)) {
545
+ variableStatementEnd = parent.getEnd();
546
+ }
547
+ }
548
+ ts.forEachChild(node, visit);
549
+ };
550
+ visit(sourceFile);
551
+ return variableStatementEnd;
552
+ };
553
+ const generateSecureQueryCreators = (content, clientName) => {
554
+ const nextContent = ensureImportFromQuery(content, [
555
+ 'createSecureDeleteQuery',
556
+ 'createSecureGetQuery',
557
+ 'createSecurePatchQuery',
558
+ 'createSecurePostQuery',
559
+ 'createSecurePutQuery',
560
+ ]);
561
+ const sourceFile = createSourceFile(nextContent);
562
+ const authProviderName = `${clientName}AuthProvider`;
563
+ const authProviderPosition = findVariableStatementEnd(sourceFile, authProviderName, 'createBearerAuthProvider');
564
+ if (!authProviderPosition) {
565
+ return content;
566
+ }
567
+ const clientBaseName = clientName.replace(/Client$/, '');
568
+ const configName = ensureConfigSuffix(clientName);
569
+ const secureCreatorBlock = [
570
+ `export const ${clientBaseName}GetSecure = createSecureGetQuery(${configName}, ${authProviderName});`,
571
+ `export const ${clientBaseName}PostSecure = createSecurePostQuery(${configName}, ${authProviderName});`,
572
+ `export const ${clientBaseName}PutSecure = createSecurePutQuery(${configName}, ${authProviderName});`,
573
+ `export const ${clientBaseName}PatchSecure = createSecurePatchQuery(${configName}, ${authProviderName});`,
574
+ `export const ${clientBaseName}DeleteSecure = createSecureDeleteQuery(${configName}, ${authProviderName});`,
575
+ ].join('\n');
576
+ return (nextContent.slice(0, authProviderPosition) + `\n\n${secureCreatorBlock}` + nextContent.slice(authProviderPosition));
577
+ };
578
+ const containsLegacyWrapperDefinitions = (content) => content.includes('createLegacyQueryCreator');
579
+ const findLegacyWrappers = (content) => {
580
+ const wrappers = new Map();
581
+ const sourceFile = createSourceFile(content);
582
+ const visit = (node) => {
583
+ if (ts.isVariableDeclaration(node) &&
584
+ ts.isIdentifier(node.name) &&
585
+ node.name.text.startsWith('legacy') &&
586
+ node.initializer &&
587
+ ts.isCallExpression(node.initializer) &&
588
+ ts.isIdentifier(node.initializer.expression) &&
589
+ node.initializer.expression.text === 'createLegacyQueryCreator') {
590
+ const legacyWrapperName = node.name.text;
591
+ const configObject = node.initializer.arguments[0];
592
+ if (configObject && ts.isObjectLiteralExpression(configObject)) {
593
+ configObject.properties.forEach((property) => {
594
+ if (ts.isPropertyAssignment(property) &&
595
+ ts.isIdentifier(property.name) &&
596
+ property.name.text === 'creator' &&
597
+ ts.isIdentifier(property.initializer)) {
598
+ wrappers.set(property.initializer.text, legacyWrapperName);
599
+ }
600
+ });
601
+ }
602
+ }
603
+ ts.forEachChild(node, visit);
604
+ };
605
+ visit(sourceFile);
606
+ return wrappers;
607
+ };
608
+ //# sourceMappingURL=legacy-query-creator-migration.js.map
@@ -0,0 +1,41 @@
1
+ import { getProjects, visitNotIgnoredFiles } from '@nx/devkit';
2
+ const stripTrailingSlash = (value) => (value.endsWith('/') ? value.slice(0, -1) : value);
3
+ /**
4
+ * Limits the migration to a set of projects or paths.
5
+ *
6
+ * Rewriting every app in a monorepo in one run is rarely how adoption actually goes, and a 300-file
7
+ * diff is not reviewable. Note the one coupling that cannot be split: a query client and the
8
+ * creators built on it have to be migrated together, because the creators are rewritten in terms of
9
+ * the client's generated `xGet` / `xPostSecure` helpers.
10
+ */
11
+ export const createMigrationScope = (tree, options) => {
12
+ const roots = [];
13
+ for (const projectName of options.projects ?? []) {
14
+ const project = getProjects(tree).get(projectName);
15
+ if (!project) {
16
+ throw new Error(`Unknown project "${projectName}". Pass names as they appear in \`nx show projects\`, or use --include with a path prefix.`);
17
+ }
18
+ roots.push(stripTrailingSlash(project.root));
19
+ }
20
+ for (const includePath of options.include ?? []) {
21
+ roots.push(stripTrailingSlash(includePath));
22
+ }
23
+ if (roots.length === 0) {
24
+ return {
25
+ visit: (targetTree, callback) => visitNotIgnoredFiles(targetTree, '', callback),
26
+ includes: () => true,
27
+ describe: () => 'the whole workspace',
28
+ };
29
+ }
30
+ const includes = (filePath) => roots.some((root) => filePath === root || filePath.startsWith(`${root}/`));
31
+ return {
32
+ visit: (targetTree, callback) => {
33
+ for (const root of roots) {
34
+ visitNotIgnoredFiles(targetTree, root, callback);
35
+ }
36
+ },
37
+ includes,
38
+ describe: () => roots.join(', '),
39
+ };
40
+ };
41
+ //# sourceMappingURL=migration-scope.js.map