@ethlete/query 6.0.0-next.5 → 6.0.0-next.50

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 +766 -0
  2. package/fesm2022/ethlete-query-testing.mjs +441 -38
  3. package/fesm2022/ethlete-query-testing.mjs.map +1 -1
  4. package/fesm2022/ethlete-query.mjs +15946 -5331
  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 +194 -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 +133 -0
  16. package/generators/migrate-to-query-v3/cleanup-migration.js +111 -99
  17. package/generators/migrate-to-query-v3/http-client-check.js +93 -0
  18. package/generators/migrate-to-query-v3/legacy-prepare-migration.js +129 -40
  19. package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +237 -152
  20. package/generators/migrate-to-query-v3/migration-scope.js +38 -0
  21. package/generators/migrate-to-query-v3/migration.js +17 -9
  22. package/generators/migrate-to-query-v3/module-graph.js +153 -0
  23. package/generators/migrate-to-query-v3/query-client-migration.js +152 -53
  24. package/generators/migrate-to-query-v3/rename-symbols.js +206 -0
  25. package/generators/migrate-to-query-v3/schema.json +12 -0
  26. package/generators/migrate-to-query-v3/shared.js +51 -2
  27. package/generators/prep-for-query-v3/migration.js +71 -14
  28. package/migrations.json +44 -0
  29. package/package.json +29 -13
  30. package/types/ethlete-query-testing.d.ts +179 -12
  31. package/types/ethlete-query.d.ts +8935 -3203
@@ -1,6 +1,8 @@
1
- import { visitNotIgnoredFiles } from '@nx/devkit';
2
1
  import * as ts from 'typescript';
3
- import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromQuery } from './shared.js';
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';
4
6
  const LEGACY_HTTP_METHOD = {
5
7
  GET: 'get',
6
8
  POST: 'post',
@@ -8,19 +10,21 @@ const LEGACY_HTTP_METHOD = {
8
10
  PATCH: 'patch',
9
11
  DELETE: 'delete',
10
12
  };
13
+ const HTTP_OPTION_NAMES = ['reportProgress', 'responseType', 'transferCache', 'withCredentials'];
11
14
  const isLegacyHttpMethod = (value) => {
12
15
  return Object.values(LEGACY_HTTP_METHOD).includes(value);
13
16
  };
14
- export const createNewQueryCreators = (tree, queryClientFiles, report) => {
17
+ export const createNewQueryCreators = (tree, queryClientFiles, report, scope) => {
15
18
  const createdFiles = [];
16
19
  const authProvidersNeeded = new Map();
17
20
  const clientNameToFilePath = new Map();
21
+ const creatorToClient = new Map();
18
22
  queryClientFiles.forEach((configNames, filePath) => {
19
23
  configNames.forEach((configName) => {
20
24
  clientNameToFilePath.set(configName, filePath);
21
25
  });
22
26
  });
23
- visitNotIgnoredFiles(tree, '', (filePath) => {
27
+ scope.visit(tree, (filePath) => {
24
28
  if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
25
29
  return;
26
30
  }
@@ -33,6 +37,7 @@ export const createNewQueryCreators = (tree, queryClientFiles, report) => {
33
37
  return;
34
38
  }
35
39
  legacyCreators.forEach((creator) => {
40
+ creatorToClient.set(creator.name, creator.clientName);
36
41
  if (!creator.secure) {
37
42
  return;
38
43
  }
@@ -48,17 +53,23 @@ export const createNewQueryCreators = (tree, queryClientFiles, report) => {
48
53
  }
49
54
  });
50
55
  if (authProvidersNeeded.size > 0) {
51
- createAuthProviders(tree, authProvidersNeeded, report);
56
+ createAuthProviders({
57
+ tree,
58
+ authProvidersNeeded,
59
+ report,
60
+ v2Configs: collectV2BearerAuthConfigs(tree, scope),
61
+ creatorToClient,
62
+ });
52
63
  }
53
64
  if (createdFiles.length > 0) {
54
65
  console.log('\n✅ Created new query creators in:');
55
66
  createdFiles.forEach((filePath) => console.log(` - ${filePath}`));
56
67
  }
57
68
  };
58
- export const updateLegacyCreatorImportsAndUsages = (tree) => {
69
+ export const updateLegacyCreatorImportsAndUsages = (tree, scope, report) => {
59
70
  const legacyCreators = new Map();
60
71
  const updatedFiles = [];
61
- visitNotIgnoredFiles(tree, '', (filePath) => {
72
+ scope.visit(tree, (filePath) => {
62
73
  if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
63
74
  return;
64
75
  }
@@ -66,16 +77,17 @@ export const updateLegacyCreatorImportsAndUsages = (tree) => {
66
77
  if (!content) {
67
78
  return;
68
79
  }
69
- const wrappers = findLegacyWrappers(content);
70
- wrappers.forEach((legacyName, originalName) => {
71
- legacyCreators.set(originalName, legacyName);
80
+ findLegacyWrappers(content).forEach((legacyName, originalName) => {
81
+ legacyCreators.set(originalName, { legacyName, definedIn: filePath });
72
82
  });
73
83
  });
74
84
  if (legacyCreators.size === 0) {
75
85
  return;
76
86
  }
77
87
  console.log(`\n✅ Found ${legacyCreators.size} legacy query creator wrappers`);
78
- visitNotIgnoredFiles(tree, '', (filePath) => {
88
+ const moduleGraph = createModuleGraph(tree);
89
+ const pendingWrites = new Map();
90
+ scope.visit(tree, (filePath) => {
79
91
  if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
80
92
  return;
81
93
  }
@@ -83,18 +95,110 @@ export const updateLegacyCreatorImportsAndUsages = (tree) => {
83
95
  if (!content || containsLegacyWrapperDefinitions(content)) {
84
96
  return;
85
97
  }
86
- const nextImports = updateLegacyCreatorImports(content, legacyCreators);
87
- const nextContent = updateLegacyCreatorUsages(nextImports, legacyCreators);
98
+ const nextContent = pointFileAtLegacyWrappers({
99
+ content,
100
+ filePath,
101
+ legacyCreators,
102
+ moduleGraph,
103
+ report,
104
+ });
88
105
  if (nextContent !== content) {
89
- tree.write(filePath, nextContent);
90
- updatedFiles.push(filePath);
106
+ pendingWrites.set(filePath, nextContent);
91
107
  }
92
108
  });
109
+ // Writing a barrel mid-pass would hide its original re-exports from consumers visited after it.
110
+ pendingWrites.forEach((nextContent, filePath) => {
111
+ tree.write(filePath, nextContent);
112
+ updatedFiles.push(filePath);
113
+ });
93
114
  if (updatedFiles.length > 0) {
94
115
  console.log('\n✅ Updated legacy creator imports and usages in:');
95
116
  updatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
96
117
  }
97
118
  };
119
+ /**
120
+ * Rewrites this file's imports of migrated creators to the `legacy*` wrappers.
121
+ *
122
+ * Two rules keep the rewrite honest, and both exist because the first version of this pass did not
123
+ * have them:
124
+ *
125
+ * - An import is only touched when the module it comes from really does resolve to the file that
126
+ * defines the wrapper. A same-named export from an unrelated package stays as it is.
127
+ * - An alias is preserved (`legacyGetPerson as getPersonQuery`) instead of being dropped, so the
128
+ * call sites using that alias keep compiling.
129
+ */
130
+ const pointFileAtLegacyWrappers = ({ content, filePath, legacyCreators, moduleGraph, report, }) => {
131
+ const sourceFile = createSourceFile(content, filePath);
132
+ const replacements = [];
133
+ const referenceRenames = new Map();
134
+ const resolvesToWrapper = (specifier, importedName, registration) => moduleGraph.findDeclaringFile(filePath, specifier, importedName) === registration.definedIn;
135
+ const rewriteSpecifiers = (elements, moduleSpecifier) => {
136
+ let changed = false;
137
+ const nextElements = elements.map((element) => {
138
+ const importedName = element.propertyName?.text ?? element.name.text;
139
+ const registration = legacyCreators.get(importedName);
140
+ if (!registration || !resolvesToWrapper(moduleSpecifier, importedName, registration)) {
141
+ return element.getText(sourceFile);
142
+ }
143
+ changed = true;
144
+ // Aliased: only the imported half changes, so nothing in the file body has to move.
145
+ if (element.propertyName) {
146
+ return `${registration.legacyName} as ${element.name.text}`;
147
+ }
148
+ referenceRenames.set(element.name.text, registration.legacyName);
149
+ return registration.legacyName;
150
+ });
151
+ return changed ? nextElements : null;
152
+ };
153
+ for (const node of sourceFile.statements) {
154
+ if (ts.isImportDeclaration(node) &&
155
+ ts.isStringLiteral(node.moduleSpecifier) &&
156
+ node.importClause?.namedBindings &&
157
+ ts.isNamedImports(node.importClause.namedBindings)) {
158
+ const nextElements = rewriteSpecifiers(node.importClause.namedBindings.elements, node.moduleSpecifier.text);
159
+ if (nextElements) {
160
+ replacements.push({
161
+ start: node.getStart(sourceFile),
162
+ end: node.getEnd(),
163
+ replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
164
+ });
165
+ }
166
+ continue;
167
+ }
168
+ // Re-exports need the same treatment, or a barrel keeps pointing at the v3 creator.
169
+ if (ts.isExportDeclaration(node) &&
170
+ node.moduleSpecifier &&
171
+ ts.isStringLiteral(node.moduleSpecifier) &&
172
+ node.exportClause &&
173
+ ts.isNamedExports(node.exportClause)) {
174
+ const nextElements = rewriteSpecifiers(node.exportClause.elements, node.moduleSpecifier.text);
175
+ if (nextElements) {
176
+ replacements.push({
177
+ start: node.getStart(sourceFile),
178
+ end: node.getEnd(),
179
+ replacement: `export { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
180
+ });
181
+ }
182
+ }
183
+ }
184
+ let result = content;
185
+ replacements.sort((left, right) => right.start - left.start);
186
+ replacements.forEach(({ start, end, replacement }) => {
187
+ result = result.slice(0, start) + replacement + result.slice(end);
188
+ });
189
+ const renamed = renameImportedReferences(result, referenceRenames);
190
+ renamed.shadowed.forEach((name) => {
191
+ report.addManualReview({
192
+ title: `Point ${name} at its legacy wrapper by hand`,
193
+ 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.`,
194
+ action: `Rename the local declaration or the import alias, then point the query usages at \`${legacyCreators.get(name)?.legacyName ?? `legacy${capitalizeFirstLetter(name)}`}\`.`,
195
+ locations: [{ filePath }],
196
+ source: 'legacy-query-creator-migration',
197
+ dedupeKey: `shadowed-rename:${filePath}:${name}`,
198
+ });
199
+ });
200
+ return renamed.content;
201
+ };
98
202
  const toLegacyName = (name) => `legacy${capitalizeFirstLetter(name)}`;
99
203
  const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report) => {
100
204
  const sourceFile = createSourceFile(content, filePath);
@@ -128,17 +232,62 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
128
232
  secure: false,
129
233
  httpOptions: new Map(),
130
234
  };
235
+ const warnUnreadableConfigValue = (property, passedAs) => {
236
+ const propertyName = property.name.getText(sourceFile);
237
+ report.addWarning({
238
+ title: `Carry over \`${propertyName}\` of ${creatorName}`,
239
+ summary: `${creatorName} passes \`${propertyName}\` ${passedAs} in its v2 config. The migration cannot read its value, so it was not carried over to the v3 creator.`,
240
+ action: propertyName === 'secure'
241
+ ? 'If the value is true, switch the generated creator to the matching *Secure creator of the client by hand.'
242
+ : 'Move what it configures onto the generated creator by hand.',
243
+ locations: [
244
+ { filePath, line: sourceFile.getLineAndCharacterOfPosition(property.getStart(sourceFile)).line + 1 },
245
+ ],
246
+ source: 'legacy-query-creator-migration',
247
+ dedupeKey: `unreadable-config:${filePath}:${creatorName}:${propertyName}`,
248
+ });
249
+ };
131
250
  const configObject = node.initializer.arguments[0];
132
251
  if (configObject && ts.isObjectLiteralExpression(configObject)) {
133
252
  configObject.properties.forEach((property) => {
253
+ if (ts.isSpreadAssignment(property)) {
254
+ report.addWarning({
255
+ title: `Restore the spread config of ${creatorName}`,
256
+ summary: `${creatorName} spreads \`${property.expression.getText(sourceFile)}\` into its v2 config. The migration cannot see what it contains, so none of it was carried over to the v3 creator.`,
257
+ action: 'Move the route, secure flag, types and http options it provides onto the generated creator by hand.',
258
+ locations: [
259
+ { filePath, line: sourceFile.getLineAndCharacterOfPosition(property.getStart(sourceFile)).line + 1 },
260
+ ],
261
+ source: 'legacy-query-creator-migration',
262
+ dedupeKey: `spread-config:${filePath}:${creatorName}`,
263
+ });
264
+ return;
265
+ }
266
+ if (ts.isShorthandPropertyAssignment(property)) {
267
+ if (property.name.text === 'route') {
268
+ info.route = property.name.text;
269
+ }
270
+ if (HTTP_OPTION_NAMES.includes(property.name.text)) {
271
+ info.httpOptions.set(property.name.text, property.name.text);
272
+ }
273
+ if (property.name.text !== 'route' && !HTTP_OPTION_NAMES.includes(property.name.text)) {
274
+ warnUnreadableConfigValue(property, 'as a shorthand property');
275
+ }
276
+ return;
277
+ }
134
278
  if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
135
279
  return;
136
280
  }
137
281
  if (property.name.text === 'route') {
138
282
  info.route = property.initializer.getText(sourceFile);
139
283
  }
140
- if (property.name.text === 'secure' && property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
141
- info.secure = true;
284
+ if (property.name.text === 'secure') {
285
+ if (property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
286
+ info.secure = true;
287
+ }
288
+ else if (property.initializer.kind !== ts.SyntaxKind.FalseKeyword) {
289
+ warnUnreadableConfigValue(property, 'with a value that is not a literal');
290
+ }
142
291
  }
143
292
  if (property.name.text === 'types' && ts.isObjectLiteralExpression(property.initializer)) {
144
293
  property.initializer.properties.forEach((typeProperty) => {
@@ -173,7 +322,7 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
173
322
  }
174
323
  });
175
324
  }
176
- if (['reportProgress', 'responseType', 'transferCache', 'withCredentials'].includes(property.name.text)) {
325
+ if (HTTP_OPTION_NAMES.includes(property.name.text)) {
177
326
  info.httpOptions.set(property.name.text, property.initializer.getText(sourceFile));
178
327
  }
179
328
  });
@@ -205,27 +354,42 @@ const transformLegacyQueryCreators = (content, legacyCreators) => {
205
354
  }
206
355
  creatorsByClient.get(creator.clientName).push(creator);
207
356
  });
357
+ const wrappersByStatement = new Map();
208
358
  legacyCreators.forEach((creator) => {
209
- const nextCreator = generateNewQueryCreator(creator);
210
- const legacyWrapper = generateLegacyWrapper(creator);
211
359
  let variableStatement;
360
+ let declaration;
212
361
  const visit = (node) => {
362
+ if (variableStatement)
363
+ return;
213
364
  if (ts.isVariableStatement(node)) {
214
- const declaration = node.declarationList.declarations[0];
215
- if (declaration && ts.isIdentifier(declaration.name) && declaration.name.text === creator.name) {
365
+ declaration = node.declarationList.declarations.find((candidate) => ts.isIdentifier(candidate.name) && candidate.name.text === creator.name);
366
+ if (declaration) {
216
367
  variableStatement = node;
368
+ return;
217
369
  }
218
370
  }
219
371
  ts.forEachChild(node, visit);
220
372
  };
221
373
  visit(sourceFile);
222
- if (variableStatement) {
374
+ if (!variableStatement || !declaration)
375
+ return;
376
+ const isTopLevel = ts.isSourceFile(variableStatement.parent);
377
+ const exportPrefix = isTopLevel ? 'export ' : '';
378
+ const nextCreator = `${creator.name} = ${generateNewQueryCreator(creator)}`;
379
+ const legacyWrapper = generateLegacyWrapper(creator, exportPrefix);
380
+ if (variableStatement.declarationList.declarations.length === 1) {
223
381
  replacements.push({
224
382
  start: variableStatement.getStart(sourceFile),
225
383
  end: variableStatement.getEnd(),
226
- replacement: `${nextCreator}\n${legacyWrapper}`,
384
+ replacement: `${exportPrefix}const ${nextCreator};\n${legacyWrapper}`,
227
385
  });
386
+ return;
228
387
  }
388
+ replacements.push({ start: declaration.getStart(sourceFile), end: declaration.getEnd(), replacement: nextCreator });
389
+ wrappersByStatement.set(variableStatement, [...(wrappersByStatement.get(variableStatement) ?? []), legacyWrapper]);
390
+ });
391
+ wrappersByStatement.forEach((wrappers, statement) => {
392
+ replacements.push({ start: statement.getEnd(), end: statement.getEnd(), replacement: `\n${wrappers.join('\n')}` });
229
393
  });
230
394
  let result = content;
231
395
  replacements.sort((left, right) => right.start - left.start);
@@ -233,7 +397,10 @@ const transformLegacyQueryCreators = (content, legacyCreators) => {
233
397
  result = result.slice(0, start) + replacement + result.slice(end);
234
398
  });
235
399
  result = ensureImportFromQuery(result, ['createLegacyQueryCreator']);
236
- return addCreatorImports(result, creatorsByClient);
400
+ result = addCreatorImports(result, creatorsByClient);
401
+ // The rewritten creators no longer go through `def<T>()` or the v2 client object, so whatever is
402
+ // left over from those constructs would land as a lint error the moment `formatFiles` runs.
403
+ return pruneUnusedNamedImports(result);
237
404
  };
238
405
  const addCreatorImports = (content, creatorsByClient) => {
239
406
  const sourceFile = createSourceFile(content);
@@ -311,7 +478,7 @@ const generateNewQueryCreator = (creator) => {
311
478
  const typeParameter = getTypeParameter(creator);
312
479
  const options = Array.from(creator.httpOptions.entries()).map(([key, value]) => `${key}: ${value}`);
313
480
  const optionsBlock = options.length > 0 ? `, {\n ${options.join(',\n ')}\n}` : '';
314
- return `export const ${creator.name} = ${creatorFunction}${typeParameter}(${creator.route}${optionsBlock});`;
481
+ return `${creatorFunction}${typeParameter}(${creator.route}${optionsBlock})`;
315
482
  };
316
483
  const getTypeParameter = (creator) => {
317
484
  if (creator.typeArgs && (creator.typeBody || creator.typeResponse)) {
@@ -339,10 +506,12 @@ const getTypeParameter = (creator) => {
339
506
  }
340
507
  return '';
341
508
  };
342
- const generateLegacyWrapper = (creator) => {
343
- return `export const ${toLegacyName(creator.name)} = createLegacyQueryCreator({ creator: ${creator.name} });`;
509
+ const generateLegacyWrapper = (creator, exportPrefix) => {
510
+ // `name` is what lets a `prepare()` called outside an injection context name itself in the error
511
+ // instead of surfacing as a bare NG0203.
512
+ return `/**\n * ${legacyQueryDeprecationTag(creator.name)}\n */\n${exportPrefix}const ${toLegacyName(creator.name)} = createLegacyQueryCreator({ name: '${toLegacyName(creator.name)}', creator: ${creator.name} });`;
344
513
  };
345
- const createAuthProviders = (tree, authProvidersNeeded, report) => {
514
+ const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, creatorToClient, }) => {
346
515
  const createdProviders = [];
347
516
  authProvidersNeeded.forEach((filePath, clientName) => {
348
517
  const content = tree.read(filePath, 'utf-8');
@@ -353,19 +522,46 @@ const createAuthProviders = (tree, authProvidersNeeded, report) => {
353
522
  if (content.includes(`export const ${authProviderName} = createBearerAuthProvider(`)) {
354
523
  return;
355
524
  }
356
- const nextContent = generateSecureQueryCreators(addAuthProviderToFile(content, clientName), clientName);
525
+ // Match on the refresh creator: it is the one thing a v2 provider config always names, and it
526
+ // belongs to exactly one client.
527
+ const v2Config = v2Configs.find((candidate) => candidate.refreshCreatorName && creatorToClient.get(candidate.refreshCreatorName) === clientName);
528
+ const body = renderAuthProviderBody(v2Config);
529
+ const nextContent = generateSecureQueryCreators(addAuthProviderToFile(content, clientName, body), clientName);
357
530
  if (nextContent === content) {
358
531
  return;
359
532
  }
360
533
  tree.write(filePath, nextContent);
361
534
  createdProviders.push(filePath);
362
- report.addFollowUp({
363
- title: `Configure auth queries for ${authProviderName}`,
364
- summary: `The migration created ${authProviderName} with an empty \`queries\` array because it cannot infer login or refresh builders safely.`,
365
- action: 'Add authQuery/tokenRefreshQuery builders, verify token extraction, and remove the placeholder empty array once the provider is configured.',
535
+ if (body.isScaffolded) {
536
+ report.addFollowUp({
537
+ title: `Finish the scaffolded auth provider ${authProviderName}`,
538
+ 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.`,
539
+ 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.',
540
+ locations: [{ filePath }, ...(v2Config ? [{ filePath: v2Config.filePath, line: v2Config.line }] : [])],
541
+ source: 'legacy-query-creator-migration',
542
+ dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
543
+ });
544
+ }
545
+ else {
546
+ report.addFollowUp({
547
+ title: `Configure auth queries for ${authProviderName}`,
548
+ summary: `The migration created ${authProviderName} with an empty \`queries\` array because no v2 BearerAuthProvider config could be matched to this client.`,
549
+ action: 'Add authQuery/tokenRefreshQuery builders, verify token extraction, and remove the placeholder empty array once the provider is configured.',
550
+ locations: [{ filePath }],
551
+ source: 'legacy-query-creator-migration',
552
+ dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
553
+ });
554
+ }
555
+ // The generated layout only works if the auth queries live next to the provider: the provider
556
+ // needs the login/refresh creators, and the secure creators need the provider. Leaving them in
557
+ // an `auth.queries.ts` that imports the client's `post` creator closes the loop.
558
+ report.addManualReview({
559
+ title: `Keep the auth queries in ${filePath}`,
560
+ 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.`,
561
+ action: `Move the login/refresh creators into ${filePath} above the provider, and have the auth queries file import them back for its legacy* wrappers.`,
366
562
  locations: [{ filePath }],
367
563
  source: 'legacy-query-creator-migration',
368
- dedupeKey: `auth-provider:${filePath}:${authProviderName}`,
564
+ dedupeKey: `auth-provider-cycle:${filePath}`,
369
565
  });
370
566
  });
371
567
  if (createdProviders.length > 0) {
@@ -373,8 +569,8 @@ const createAuthProviders = (tree, authProvidersNeeded, report) => {
373
569
  createdProviders.forEach((filePath) => console.log(` - ${filePath}`));
374
570
  }
375
571
  };
376
- const addAuthProviderToFile = (content, clientName) => {
377
- const nextContent = ensureImportFromQuery(content, ['createBearerAuthProvider']);
572
+ const addAuthProviderToFile = (content, clientName, body) => {
573
+ const nextContent = ensureImportFromEthleteCore(ensureImportFromQuery(content, ['createBearerAuthProvider', ...body.importsNeeded]), ['toInjectFn', 'toProvideFn']);
378
574
  const sourceFile = createSourceFile(nextContent);
379
575
  const configName = ensureConfigSuffix(clientName);
380
576
  const configPosition = findVariableStatementEnd(sourceFile, configName, 'createQueryClient');
@@ -388,10 +584,12 @@ const addAuthProviderToFile = (content, clientName) => {
388
584
  `export const ${authProviderName} = createBearerAuthProvider({`,
389
585
  ` name: '${clientName}',`,
390
586
  ` queryClientRef: ${configName},`,
391
- ` queries: [],`,
587
+ body.queries,
588
+ ...(body.features ? [body.features] : []),
392
589
  '});',
393
590
  '',
394
- `export const [provide${capitalizedClientName}AuthProvider, inject${capitalizedClientName}AuthProvider] = ${authProviderName};`,
591
+ `export const provide${capitalizedClientName}AuthProvider = toProvideFn(${authProviderName});`,
592
+ `export const inject${capitalizedClientName}AuthProvider = toInjectFn(${authProviderName});`,
395
593
  ].join('\n');
396
594
  return nextContent.slice(0, configPosition) + authProviderBlock + nextContent.slice(configPosition);
397
595
  };
@@ -473,117 +671,4 @@ const findLegacyWrappers = (content) => {
473
671
  visit(sourceFile);
474
672
  return wrappers;
475
673
  };
476
- const updateLegacyCreatorImports = (content, legacyCreators) => {
477
- const sourceFile = createSourceFile(content);
478
- const replacements = [];
479
- const visit = (node) => {
480
- if (ts.isImportDeclaration(node) &&
481
- node.importClause?.namedBindings &&
482
- ts.isNamedImports(node.importClause.namedBindings)) {
483
- const nextElements = node.importClause.namedBindings.elements.map((element) => {
484
- const importedName = element.propertyName ? element.propertyName.text : element.name.text;
485
- const legacyName = legacyCreators.get(importedName);
486
- return legacyName ?? element.getText(sourceFile);
487
- });
488
- const hasChanges = node.importClause.namedBindings.elements.some((element) => {
489
- const importedName = element.propertyName ? element.propertyName.text : element.name.text;
490
- return legacyCreators.has(importedName);
491
- });
492
- if (hasChanges) {
493
- replacements.push({
494
- start: node.getStart(sourceFile),
495
- end: node.getEnd(),
496
- replacement: `import { ${nextElements.join(', ')} } from '${node.moduleSpecifier.text}';`,
497
- });
498
- }
499
- }
500
- ts.forEachChild(node, visit);
501
- };
502
- visit(sourceFile);
503
- let result = content;
504
- replacements.sort((left, right) => right.start - left.start);
505
- replacements.forEach(({ start, end, replacement }) => {
506
- result = result.slice(0, start) + replacement + result.slice(end);
507
- });
508
- return result;
509
- };
510
- const updateLegacyCreatorUsages = (content, legacyCreators) => {
511
- const sourceFile = createSourceFile(content);
512
- const replacements = [];
513
- const visit = (node) => {
514
- if (ts.isObjectLiteralExpression(node)) {
515
- node.properties.forEach((property) => {
516
- if (ts.isPropertyAssignment(property) && ts.isIdentifier(property.initializer)) {
517
- const nextName = legacyCreators.get(property.initializer.text);
518
- if (nextName) {
519
- replacements.push({
520
- start: property.initializer.getStart(sourceFile),
521
- end: property.initializer.getEnd(),
522
- replacement: nextName,
523
- });
524
- }
525
- }
526
- if (ts.isShorthandPropertyAssignment(property) && legacyCreators.has(property.name.text)) {
527
- replacements.push({
528
- start: property.getStart(sourceFile),
529
- end: property.getEnd(),
530
- replacement: `${property.name.text}: ${legacyCreators.get(property.name.text)}`,
531
- });
532
- }
533
- });
534
- }
535
- if (ts.isIdentifier(node) && legacyCreators.has(node.text)) {
536
- const parent = node.parent;
537
- if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
538
- ts.forEachChild(node, visit);
539
- return;
540
- }
541
- if (ts.isPropertyAssignment(parent) && (parent.name === node || parent.initializer === node)) {
542
- ts.forEachChild(node, visit);
543
- return;
544
- }
545
- if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) {
546
- ts.forEachChild(node, visit);
547
- return;
548
- }
549
- if (ts.isVariableDeclaration(parent) && parent.name === node) {
550
- ts.forEachChild(node, visit);
551
- return;
552
- }
553
- if (ts.isImportSpecifier(parent)) {
554
- ts.forEachChild(node, visit);
555
- return;
556
- }
557
- if (ts.isFunctionDeclaration(parent) && parent.name === node) {
558
- ts.forEachChild(node, visit);
559
- return;
560
- }
561
- if (ts.isMethodDeclaration(parent) && parent.name === node) {
562
- ts.forEachChild(node, visit);
563
- return;
564
- }
565
- if (ts.isParameter(parent) && parent.name === node) {
566
- ts.forEachChild(node, visit);
567
- return;
568
- }
569
- if (ts.isPropertyDeclaration(parent) && parent.name === node) {
570
- ts.forEachChild(node, visit);
571
- return;
572
- }
573
- replacements.push({
574
- start: node.getStart(sourceFile),
575
- end: node.getEnd(),
576
- replacement: legacyCreators.get(node.text),
577
- });
578
- }
579
- ts.forEachChild(node, visit);
580
- };
581
- visit(sourceFile);
582
- let result = content;
583
- replacements.sort((left, right) => right.start - left.start);
584
- replacements.forEach(({ start, end, replacement }) => {
585
- result = result.slice(0, start) + replacement + result.slice(end);
586
- });
587
- return result;
588
- };
589
674
  //# sourceMappingURL=legacy-query-creator-migration.js.map
@@ -0,0 +1,38 @@
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
+ describe: () => 'the whole workspace',
27
+ };
28
+ }
29
+ return {
30
+ visit: (targetTree, callback) => {
31
+ for (const root of roots) {
32
+ visitNotIgnoredFiles(targetTree, root, callback);
33
+ }
34
+ },
35
+ describe: () => roots.join(', '),
36
+ };
37
+ };
38
+ //# sourceMappingURL=migration-scope.js.map
@@ -1,26 +1,34 @@
1
1
  import { formatFiles } from '@nx/devkit';
2
- import { migrateEmptyPrepareCalls, removeDevtoolsUsage, replaceAnyQueryWithLegacy } from './cleanup-migration.js';
2
+ import { migrateDevtoolsUsage, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy } from './cleanup-migration.js';
3
+ import { reportDefaultHeaderUsages, reportMissingHttpClientProviders } from './http-client-check.js';
3
4
  import { migrateLegacyPrepareCalls } from './legacy-prepare-migration.js';
4
5
  import { createNewQueryCreators, updateLegacyCreatorImportsAndUsages } from './legacy-query-creator-migration.js';
6
+ import { createMigrationScope } from './migration-scope.js';
5
7
  import { generateProviderAliases, generateQueryCreators, migrateQueryClients, updateImportsAcrossWorkspace, } from './query-client-migration.js';
6
8
  import { QueryV3MigrationReport } from './report.js';
7
9
  export default async function migrate(tree, schema) {
8
10
  console.log('\n🔄 Starting query v3 migration...');
11
+ const scope = createMigrationScope(tree, schema);
12
+ console.log(` Scope: ${scope.describe()}`);
9
13
  const report = new QueryV3MigrationReport();
10
- const { queryClientFiles, variableRenames } = migrateQueryClients(tree, report);
14
+ const { queryClientFiles, variableRenames } = migrateQueryClients(tree, report, scope);
11
15
  if (queryClientFiles.size > 0) {
12
16
  generateProviderAliases(tree, queryClientFiles);
13
17
  generateQueryCreators(tree, queryClientFiles);
14
- createNewQueryCreators(tree, queryClientFiles, report);
15
- updateLegacyCreatorImportsAndUsages(tree);
18
+ createNewQueryCreators(tree, queryClientFiles, report, scope);
19
+ updateLegacyCreatorImportsAndUsages(tree, scope, report);
16
20
  }
17
21
  if (variableRenames.size > 0) {
18
- updateImportsAcrossWorkspace(tree, variableRenames);
22
+ updateImportsAcrossWorkspace(tree, variableRenames, scope);
23
+ }
24
+ replaceAnyQueryWithLegacy(tree, scope);
25
+ migrateDevtoolsUsage(tree, scope, report);
26
+ migrateEmptyPrepareCalls(tree, scope);
27
+ migrateLegacyPrepareCalls(tree, report, scope);
28
+ if (queryClientFiles.size > 0) {
29
+ reportMissingHttpClientProviders(tree, report, scope);
30
+ reportDefaultHeaderUsages(tree, report, scope);
19
31
  }
20
- replaceAnyQueryWithLegacy(tree);
21
- removeDevtoolsUsage(tree);
22
- migrateEmptyPrepareCalls(tree);
23
- migrateLegacyPrepareCalls(tree, report);
24
32
  report.writeToTree(tree);
25
33
  report.printSummary();
26
34
  if (!schema.skipFormat) {