@ethlete/query 6.0.0-next.6 → 6.0.0-next.60
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/CHANGELOG.md +939 -0
- package/fesm2022/ethlete-query-devtools-contract.mjs +1 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +411 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
- package/fesm2022/ethlete-query-testing.mjs +548 -169
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +14680 -7172
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/deprecate-legacy-queries/migration.js +85 -0
- package/generators/deprecate-legacy-queries/schema.json +26 -0
- package/generators/generators.json +30 -0
- package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
- package/generators/migrate-from-ngrx-toolkit/consumers.js +521 -0
- package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
- package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
- package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
- package/generators/migrate-from-ngrx-toolkit/migration.js +140 -0
- package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
- package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
- package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
- package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
- package/generators/migrate-query-client-features/migration.js +144 -0
- package/generators/migrate-query-client-features/report.js +136 -0
- package/generators/migrate-query-client-features/schema.json +26 -0
- package/generators/migrate-query-opt-in-features/migration.js +195 -0
- package/generators/migrate-query-opt-in-features/report.js +143 -0
- package/generators/migrate-query-opt-in-features/schema.json +31 -0
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +160 -0
- package/generators/migrate-to-query-v3/cleanup-migration.js +247 -125
- package/generators/migrate-to-query-v3/http-client-check.js +93 -0
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +129 -40
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +274 -152
- package/generators/migrate-to-query-v3/migration-scope.js +40 -0
- package/generators/migrate-to-query-v3/migration.js +19 -10
- package/generators/migrate-to-query-v3/module-graph.js +165 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +342 -160
- package/generators/migrate-to-query-v3/rename-symbols.js +233 -0
- package/generators/migrate-to-query-v3/report.js +40 -7
- package/generators/migrate-to-query-v3/schema.json +12 -0
- package/generators/migrate-to-query-v3/shared.js +97 -3
- package/generators/move-devtools-contract-imports/migration.js +155 -0
- package/generators/move-devtools-contract-imports/moved-names.js +153 -0
- package/generators/move-devtools-contract-imports/schema.json +26 -0
- package/generators/prep-for-query-v3/migration.js +465 -65
- package/generators/report-legacy-query-apis/migration.js +256 -0
- package/generators/report-legacy-query-apis/schema.json +25 -0
- package/migrations/query-field-default-types.md +43 -0
- package/migrations/search-query-field-string.md +43 -0
- package/migrations.json +87 -0
- package/package.json +34 -14
- package/types/ethlete-query-devtools-contract.d.ts +2 -0
- package/types/ethlete-query-ngrx-toolkit.d.ts +131 -0
- package/types/ethlete-query-testing.d.ts +295 -160
- package/types/ethlete-query.d.ts +9755 -3682
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
1
|
import * as ts from 'typescript';
|
|
3
|
-
import {
|
|
4
|
-
|
|
2
|
+
import { CLIENT_FEATURE, reportClientErrorPipelineAdded } from '../migrate-query-opt-in-features/migration.js';
|
|
3
|
+
import { createModuleGraph } from './module-graph.js';
|
|
4
|
+
import { isNamePosition, isShadowedInNestedScope, renamedReference } from './rename-symbols.js';
|
|
5
|
+
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, getVariableStatementEnd, printImportDeclaration, } from './shared.js';
|
|
6
|
+
export const migrateQueryClients = (tree, report, scope) => {
|
|
5
7
|
const queryClientFiles = new Map();
|
|
6
8
|
const variableRenames = new Map();
|
|
7
|
-
|
|
9
|
+
scope.visit(tree, (filePath) => {
|
|
8
10
|
if (!filePath.endsWith('.ts')) {
|
|
9
11
|
return;
|
|
10
12
|
}
|
|
@@ -14,9 +16,9 @@ export const migrateQueryClients = (tree, report) => {
|
|
|
14
16
|
}
|
|
15
17
|
const renames = new Map();
|
|
16
18
|
const nextContent = migrateQueryClientToConfig({ content, filePath, renamesOut: renames, report });
|
|
17
|
-
renames.
|
|
18
|
-
variableRenames.set(
|
|
19
|
-
}
|
|
19
|
+
if (renames.size > 0) {
|
|
20
|
+
variableRenames.set(filePath, { originalContent: content, renames });
|
|
21
|
+
}
|
|
20
22
|
const configNames = extractClientConfigNames(nextContent);
|
|
21
23
|
if (configNames.length > 0) {
|
|
22
24
|
queryClientFiles.set(filePath, configNames);
|
|
@@ -69,11 +71,13 @@ export const generateQueryCreators = (tree, queryClientFiles) => {
|
|
|
69
71
|
generatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
70
72
|
}
|
|
71
73
|
};
|
|
72
|
-
export const updateImportsAcrossWorkspace = (tree, renames) => {
|
|
74
|
+
export const updateImportsAcrossWorkspace = (tree, renames, scope, report) => {
|
|
73
75
|
if (renames.size === 0) {
|
|
74
76
|
return;
|
|
75
77
|
}
|
|
76
|
-
|
|
78
|
+
const originalContents = new Map(Array.from(renames.entries()).map(([filePath, { originalContent }]) => [filePath, originalContent]));
|
|
79
|
+
const graph = createModuleGraph(withOriginalContents(tree, originalContents));
|
|
80
|
+
scope.visit(tree, (filePath) => {
|
|
77
81
|
if (!filePath.endsWith('.ts')) {
|
|
78
82
|
return;
|
|
79
83
|
}
|
|
@@ -81,12 +85,42 @@ export const updateImportsAcrossWorkspace = (tree, renames) => {
|
|
|
81
85
|
if (!content) {
|
|
82
86
|
return;
|
|
83
87
|
}
|
|
84
|
-
const nextContent = updateImportsInFile(content, renames);
|
|
88
|
+
const { content: nextContent, renamedByName } = updateImportsInFile(content, filePath, renames, graph);
|
|
89
|
+
if (renamedByName.size > 0) {
|
|
90
|
+
report.addManualReview({
|
|
91
|
+
title: `Check the client rename in ${filePath}`,
|
|
92
|
+
summary: `The migration could not resolve ${[...renamedByName].map((specifier) => `\`${specifier}\``).join(', ')} to a file, so it renamed the imported client by its name alone.`,
|
|
93
|
+
action: 'Confirm the renamed import really is the migrated query client, and revert it if it is not.',
|
|
94
|
+
locations: [{ filePath }],
|
|
95
|
+
source: 'query-client-migration',
|
|
96
|
+
dedupeKey: `client-rename-by-name:${filePath}`,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
85
99
|
if (nextContent !== content) {
|
|
100
|
+
originalContents.set(filePath, content);
|
|
86
101
|
tree.write(filePath, nextContent);
|
|
87
102
|
}
|
|
88
103
|
});
|
|
89
104
|
};
|
|
105
|
+
/**
|
|
106
|
+
* A view of `tree` that still reads the pre-migration content of `originalContents`, so the module
|
|
107
|
+
* graph resolves a client import by the name it had when the import was written.
|
|
108
|
+
*/
|
|
109
|
+
const withOriginalContents = (tree, originalContents) => new Proxy(tree, {
|
|
110
|
+
get: (target, property) => {
|
|
111
|
+
if (property === 'read') {
|
|
112
|
+
return (filePath, encoding) => {
|
|
113
|
+
const original = originalContents.get(filePath);
|
|
114
|
+
if (original === undefined) {
|
|
115
|
+
return encoding ? target.read(filePath, encoding) : target.read(filePath);
|
|
116
|
+
}
|
|
117
|
+
return encoding ? original : Buffer.from(original);
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
const value = Reflect.get(target, property, target);
|
|
121
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
122
|
+
},
|
|
123
|
+
});
|
|
90
124
|
const hasQueryClientInstantiation = (content) => {
|
|
91
125
|
const sourceFile = createSourceFile(content);
|
|
92
126
|
let hasQueryClientImport = false;
|
|
@@ -116,17 +150,25 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
116
150
|
const sourceFile = createSourceFile(content, filePath);
|
|
117
151
|
const replacements = [];
|
|
118
152
|
const variableRenames = new Map();
|
|
153
|
+
let hasUnmigratedClient = false;
|
|
119
154
|
const visit = (node) => {
|
|
120
|
-
if (ts.isNewExpression(node) &&
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
155
|
+
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'V2QueryClient') {
|
|
156
|
+
const configArgument = node.arguments?.[0];
|
|
157
|
+
if (!configArgument || !ts.isObjectLiteralExpression(configArgument)) {
|
|
158
|
+
hasUnmigratedClient = true;
|
|
159
|
+
report.addManualReview({
|
|
160
|
+
title: 'Migrate a V2QueryClient whose config is not an object literal',
|
|
161
|
+
summary: `\`${node.getText(sourceFile)}\` passes its config indirectly, so the migration cannot read it and left the client on v2.`,
|
|
162
|
+
action: 'Inline the config object and re-run the migration, or rewrite the client by hand as `createQueryClient({ baseUrl, name })` with provider aliases and creators.',
|
|
163
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
|
|
164
|
+
source: 'query-client-migration',
|
|
165
|
+
dedupeKey: `unmigrated-client:${filePath}:${node.getStart(sourceFile)}`,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
127
169
|
const oldVariableName = getVariableNameForQueryClient(node);
|
|
128
|
-
const newVariableName = ensureConfigSuffix(oldVariableName);
|
|
129
|
-
if (oldVariableName !== newVariableName) {
|
|
170
|
+
const newVariableName = oldVariableName && ensureConfigSuffix(oldVariableName);
|
|
171
|
+
if (oldVariableName && newVariableName && oldVariableName !== newVariableName) {
|
|
130
172
|
const collisionRegex = new RegExp(`\\b(?:const|let|var)\\s+${newVariableName}\\b`);
|
|
131
173
|
if (collisionRegex.test(content)) {
|
|
132
174
|
report.addWarning({
|
|
@@ -143,6 +185,8 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
143
185
|
renamesOut.set(oldVariableName, newVariableName);
|
|
144
186
|
}
|
|
145
187
|
}
|
|
188
|
+
reportDroppedClientOptions({ configArgument, sourceFile, filePath, report });
|
|
189
|
+
reportChangedClientDefaults({ configArgument, sourceFile, filePath, report });
|
|
146
190
|
replacements.push({
|
|
147
191
|
start: node.getStart(sourceFile),
|
|
148
192
|
end: node.getEnd(),
|
|
@@ -158,16 +202,20 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
158
202
|
replacements.forEach(({ start, end, replacement }) => {
|
|
159
203
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
160
204
|
});
|
|
161
|
-
result = removeQueryClientImport(result);
|
|
205
|
+
result = removeQueryClientImport(result, hasUnmigratedClient);
|
|
162
206
|
return renameVariables(result, variableRenames);
|
|
163
207
|
};
|
|
164
|
-
const removeQueryClientImport = (content) => {
|
|
208
|
+
const removeQueryClientImport = (content, keepLegacyClient) => {
|
|
165
209
|
const sourceFile = createSourceFile(content);
|
|
166
210
|
let queryImportNode;
|
|
167
211
|
ts.forEachChild(sourceFile, (node) => {
|
|
168
|
-
if (
|
|
212
|
+
if (!queryImportNode &&
|
|
213
|
+
ts.isImportDeclaration(node) &&
|
|
169
214
|
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
170
|
-
node.moduleSpecifier.text === '@ethlete/query'
|
|
215
|
+
node.moduleSpecifier.text === '@ethlete/query' &&
|
|
216
|
+
node.importClause?.namedBindings &&
|
|
217
|
+
ts.isNamedImports(node.importClause.namedBindings) &&
|
|
218
|
+
node.importClause.namedBindings.elements.some((element) => element.name.text === 'V2QueryClient')) {
|
|
171
219
|
queryImportNode = node;
|
|
172
220
|
}
|
|
173
221
|
});
|
|
@@ -178,20 +226,41 @@ const removeQueryClientImport = (content) => {
|
|
|
178
226
|
'createPostQuery',
|
|
179
227
|
'createPutQuery',
|
|
180
228
|
'createQueryClient',
|
|
229
|
+
CLIENT_FEATURE,
|
|
181
230
|
];
|
|
182
231
|
if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
|
|
183
232
|
return ensureImportFromQuery(content, requiredImports);
|
|
184
233
|
}
|
|
185
234
|
const existingElements = queryImportNode.importClause.namedBindings.elements.filter((element) => {
|
|
186
|
-
return element.name.text !== 'V2QueryClient';
|
|
235
|
+
return keepLegacyClient || element.name.text !== 'V2QueryClient';
|
|
187
236
|
});
|
|
188
237
|
const nextImports = new Set(requiredImports);
|
|
189
|
-
existingElements.forEach((element) => nextImports.add(element.
|
|
190
|
-
const nextImportStatement =
|
|
238
|
+
existingElements.forEach((element) => nextImports.add(element.getText(sourceFile)));
|
|
239
|
+
const nextImportStatement = printImportDeclaration(queryImportNode, sourceFile, Array.from(nextImports).sort());
|
|
191
240
|
return (content.slice(0, queryImportNode.getStart(sourceFile)) +
|
|
192
241
|
nextImportStatement +
|
|
193
242
|
content.slice(queryImportNode.getEnd()));
|
|
194
243
|
};
|
|
244
|
+
const isTopLevelVariableName = (node) => {
|
|
245
|
+
const declaration = node.parent;
|
|
246
|
+
return (ts.isVariableDeclaration(declaration) &&
|
|
247
|
+
declaration.name === node &&
|
|
248
|
+
ts.isVariableStatement(declaration.parent.parent) &&
|
|
249
|
+
ts.isSourceFile(declaration.parent.parent.parent));
|
|
250
|
+
};
|
|
251
|
+
const renameInDeclaringFile = (node, nextName) => {
|
|
252
|
+
const parent = node.parent;
|
|
253
|
+
if (isTopLevelVariableName(node)) {
|
|
254
|
+
return nextName;
|
|
255
|
+
}
|
|
256
|
+
if (ts.isExportSpecifier(parent) && !parent.parent.parent.moduleSpecifier) {
|
|
257
|
+
return (parent.propertyName ?? parent.name) === node ? nextName : undefined;
|
|
258
|
+
}
|
|
259
|
+
if (isNamePosition(node) || isShadowedInNestedScope(node)) {
|
|
260
|
+
return undefined;
|
|
261
|
+
}
|
|
262
|
+
return renamedReference(node, nextName);
|
|
263
|
+
};
|
|
195
264
|
const renameVariables = (content, renames) => {
|
|
196
265
|
if (renames.size === 0) {
|
|
197
266
|
return content;
|
|
@@ -199,83 +268,199 @@ const renameVariables = (content, renames) => {
|
|
|
199
268
|
const sourceFile = createSourceFile(content);
|
|
200
269
|
const replacements = [];
|
|
201
270
|
const visit = (node) => {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
start: node.name.getStart(sourceFile),
|
|
207
|
-
end: node.name.getEnd(),
|
|
208
|
-
replacement: nextName,
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
else if (ts.isIdentifier(node)) {
|
|
213
|
-
const nextName = renames.get(node.text);
|
|
214
|
-
if (nextName) {
|
|
215
|
-
const parent = node.parent;
|
|
216
|
-
if (!ts.isPropertyAssignment(parent) || parent.name !== node) {
|
|
217
|
-
replacements.push({
|
|
218
|
-
start: node.getStart(sourceFile),
|
|
219
|
-
end: node.getEnd(),
|
|
220
|
-
replacement: nextName,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
271
|
+
const nextName = ts.isIdentifier(node) ? renames.get(node.text) : undefined;
|
|
272
|
+
const replacement = nextName && renameInDeclaringFile(node, nextName);
|
|
273
|
+
if (replacement) {
|
|
274
|
+
replacements.push({ start: node.getStart(sourceFile), end: node.getEnd(), replacement });
|
|
224
275
|
}
|
|
225
276
|
ts.forEachChild(node, visit);
|
|
226
277
|
};
|
|
227
278
|
visit(sourceFile);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (seen.has(key)) {
|
|
232
|
-
return false;
|
|
233
|
-
}
|
|
234
|
-
seen.add(key);
|
|
235
|
-
return true;
|
|
236
|
-
});
|
|
279
|
+
return applyReplacements(content, replacements);
|
|
280
|
+
};
|
|
281
|
+
const applyReplacements = (content, replacements) => {
|
|
237
282
|
let result = content;
|
|
238
|
-
|
|
239
|
-
|
|
283
|
+
[...replacements]
|
|
284
|
+
.sort((left, right) => right.start - left.start)
|
|
285
|
+
.forEach(({ start, end, replacement }) => {
|
|
240
286
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
241
287
|
});
|
|
242
288
|
return result;
|
|
243
289
|
};
|
|
290
|
+
const WINDOW_FOCUS_REPLACEMENT = 'Opt in per query: `withPolling({ interval, refetchOnFocus: true })` on a polled query, `withAutoRefresh({ onSignalChanges: [windowFocusSignal] })` on any other, or `injectMyClient().refreshQueriesInUse()` from a focus listener.';
|
|
291
|
+
const SMART_POLLING_REPLACEMENT = 'Opt in per query: `withPolling({ interval, pauseWhileHidden: true })`. Without it v3 keeps polling in a hidden tab.';
|
|
292
|
+
const V2_ON_BY_DEFAULT_REQUEST_OPTIONS = {
|
|
293
|
+
autoRefreshQueriesOnWindowFocus: WINDOW_FOCUS_REPLACEMENT,
|
|
294
|
+
enableSmartPolling: SMART_POLLING_REPLACEMENT,
|
|
295
|
+
};
|
|
296
|
+
/**
|
|
297
|
+
* What each v2 client option turns into, or why it has nowhere to go.
|
|
298
|
+
*
|
|
299
|
+
* Silently dropping these is how two apps here lost their default headers without a single warning.
|
|
300
|
+
* A dropped option that shows up in the report is a five-minute fix; one that doesn't is a bug
|
|
301
|
+
* report from production.
|
|
302
|
+
*/
|
|
303
|
+
const DROPPED_CLIENT_OPTIONS = {
|
|
304
|
+
'request.autoRefreshQueriesOnWindowFocus': WINDOW_FOCUS_REPLACEMENT,
|
|
305
|
+
'request.enableSmartPolling': SMART_POLLING_REPLACEMENT,
|
|
306
|
+
'logging.preparedQuerySubscriptions': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
307
|
+
'logging.queryStateChanges': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
308
|
+
'request.autoRefreshOn': 'No direct equivalent. Re-run the affected queries yourself with `withAutoRefresh` or `refreshQueriesInUse()`.',
|
|
309
|
+
};
|
|
310
|
+
const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, report, }) => {
|
|
311
|
+
const migrated = new Set(['baseRoute', 'queryParams', 'cacheAdapter', 'retryFn', 'request', 'logging']);
|
|
312
|
+
const raise = (optionPath, node) => {
|
|
313
|
+
report.addWarning({
|
|
314
|
+
title: `Reconfigure dropped query client option "${optionPath}"`,
|
|
315
|
+
summary: `The v2 client option \`${optionPath}\` has no place in \`createQueryClient\` and was dropped by the migration.`,
|
|
316
|
+
action: DROPPED_CLIENT_OPTIONS[optionPath] ?? 'Check whether the behaviour is still needed and reimplement it.',
|
|
317
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
|
|
318
|
+
source: 'query-client-migration',
|
|
319
|
+
dedupeKey: `dropped-client-option:${filePath}:${optionPath}`,
|
|
320
|
+
});
|
|
321
|
+
};
|
|
322
|
+
const raiseSpread = (spread) => {
|
|
323
|
+
const text = spread.getText(sourceFile);
|
|
324
|
+
report.addWarning({
|
|
325
|
+
title: `Reconfigure spread query client options "${text}"`,
|
|
326
|
+
summary: `The v2 client config spreads \`${text}\`. The migration cannot see which options it holds, so none of them were carried over to \`createQueryClient\`.`,
|
|
327
|
+
action: 'Inline the spread options that are still needed (`baseRoute` becomes `baseUrl`) and re-check the result.',
|
|
328
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(spread.getStart(sourceFile)).line + 1 }],
|
|
329
|
+
source: 'query-client-migration',
|
|
330
|
+
dedupeKey: `dropped-client-spread:${filePath}:${spread.getStart(sourceFile)}`,
|
|
331
|
+
});
|
|
332
|
+
};
|
|
333
|
+
configArgument.properties.forEach((property) => {
|
|
334
|
+
if (ts.isSpreadAssignment(property)) {
|
|
335
|
+
raiseSpread(property);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if ((!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) ||
|
|
339
|
+
!ts.isIdentifier(property.name)) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const name = property.name.text;
|
|
343
|
+
if ((name === 'request' || name === 'logging') &&
|
|
344
|
+
ts.isPropertyAssignment(property) &&
|
|
345
|
+
ts.isObjectLiteralExpression(property.initializer)) {
|
|
346
|
+
property.initializer.properties.forEach((nested) => {
|
|
347
|
+
if (ts.isSpreadAssignment(nested)) {
|
|
348
|
+
raiseSpread(nested);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if ((!ts.isPropertyAssignment(nested) && !ts.isShorthandPropertyAssignment(nested)) ||
|
|
352
|
+
!ts.isIdentifier(nested.name)) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (name === 'request' && ['queryParams', 'cacheAdapter', 'retryFn'].includes(nested.name.text)) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
if (name === 'request' &&
|
|
359
|
+
nested.name.text in V2_ON_BY_DEFAULT_REQUEST_OPTIONS &&
|
|
360
|
+
ts.isPropertyAssignment(nested) &&
|
|
361
|
+
nested.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
raise(`${name}.${nested.name.text}`, nested);
|
|
365
|
+
});
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (!migrated.has(name)) {
|
|
369
|
+
raise(name, property);
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
const reportChangedClientDefaults = ({ configArgument, sourceFile, filePath, report, }) => {
|
|
374
|
+
const location = {
|
|
375
|
+
filePath,
|
|
376
|
+
line: sourceFile.getLineAndCharacterOfPosition(configArgument.getStart(sourceFile)).line + 1,
|
|
377
|
+
};
|
|
378
|
+
const requestProperty = findProperty(configArgument, 'request');
|
|
379
|
+
const requestConfig = requestProperty &&
|
|
380
|
+
ts.isPropertyAssignment(requestProperty) &&
|
|
381
|
+
ts.isObjectLiteralExpression(requestProperty.initializer)
|
|
382
|
+
? requestProperty.initializer
|
|
383
|
+
: undefined;
|
|
384
|
+
const hasUnreadableRequestConfig = (!!requestProperty && !requestConfig) || !!requestConfig?.properties.some(ts.isSpreadAssignment);
|
|
385
|
+
reportClientErrorPipelineAdded(report, location, 'query-client-migration', `\`${CLIENT_FEATURE}()\` was added so failed requests still retry and Symfony / HTML error bodies are still parsed, as they were by default in v2.`);
|
|
386
|
+
if (!requestConfig || !findProperty(requestConfig, 'retryFn')) {
|
|
387
|
+
report.addWarning({
|
|
388
|
+
title: 'Check the retry behaviour of the migrated client',
|
|
389
|
+
summary: 'v2 retried every request method on a 5xx (500 included), 408, 425 and 429, up to four times with a linear delay. The v3 default policy that `withEthleteApiErrors()` installs retries three times with an exponential, jittered delay, never retries a `POST`, a `PATCH` or a plain `500`, and also retries a connection failure.',
|
|
390
|
+
action: 'Accept the v3 policy, or pass `withEthleteApiErrors({ retry: { maxAttempts, retryableStatusCodes, retryNonIdempotent } })` - only set `retryNonIdempotent` for endpoints that deduplicate on their own.',
|
|
391
|
+
locations: [location],
|
|
392
|
+
source: 'query-client-migration',
|
|
393
|
+
dedupeKey: `changed-retry-default:${filePath}`,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
const cacheAdapter = requestConfig && findProperty(requestConfig, 'cacheAdapter');
|
|
397
|
+
if (cacheAdapter) {
|
|
398
|
+
report.addWarning({
|
|
399
|
+
title: 'Check the carried-over cacheAdapter',
|
|
400
|
+
summary: 'The v2 `cacheAdapter` was carried over as is and still sets the freshness TTL: a TTL of `0` means an `execute({ options: { allowCache: true } })` always hits the server, as a v2 `execute()` did. Two things differ: v3 passes Angular `HttpHeaders` (read them with `headers.get(name)`) where v2 passed a plain record, and v3 keeps an entry for `keepUnusedFor` (5 minutes) after its last consumer, rendering the old response on a remount while it refetches.',
|
|
401
|
+
action: 'Rewrite an adapter that reads headers for `HttpHeaders`. If the adapter was meant to switch caching off, also set `keepUnusedFor: 0` on the client.',
|
|
402
|
+
locations: [
|
|
403
|
+
{ filePath, line: sourceFile.getLineAndCharacterOfPosition(cacheAdapter.getStart(sourceFile)).line + 1 },
|
|
404
|
+
],
|
|
405
|
+
source: 'query-client-migration',
|
|
406
|
+
dedupeKey: `carried-cache-adapter:${filePath}`,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
if (hasUnreadableRequestConfig) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
Object.entries(V2_ON_BY_DEFAULT_REQUEST_OPTIONS).forEach(([option, action]) => {
|
|
413
|
+
if (requestConfig && findProperty(requestConfig, option)) {
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
report.addWarning({
|
|
417
|
+
title: `Restore the v2 default "request.${option}"`,
|
|
418
|
+
summary: `The v2 client did not set \`request.${option}\`, so it ran with the v2 default \`true\`. v3 has no client-wide equivalent and the behaviour is off for every migrated query.`,
|
|
419
|
+
action,
|
|
420
|
+
locations: [location],
|
|
421
|
+
source: 'query-client-migration',
|
|
422
|
+
dedupeKey: `dropped-client-default:${filePath}:${option}`,
|
|
423
|
+
});
|
|
424
|
+
});
|
|
425
|
+
};
|
|
426
|
+
const getPropertyValueText = (property, sourceFile) => {
|
|
427
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
428
|
+
return property.name.text;
|
|
429
|
+
}
|
|
430
|
+
return ts.isPropertyAssignment(property) ? property.initializer.getText(sourceFile) : undefined;
|
|
431
|
+
};
|
|
432
|
+
const findProperty = (objectLiteral, name) => objectLiteral.properties.find((property) => {
|
|
433
|
+
return ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
|
|
434
|
+
ts.isIdentifier(property.name) &&
|
|
435
|
+
property.name.text === name);
|
|
436
|
+
});
|
|
244
437
|
const migrateConfigObject = (configArgument, node, sourceFile) => {
|
|
245
|
-
const variableName = getVariableNameForQueryClient(node);
|
|
438
|
+
const variableName = getVariableNameForQueryClient(node) ?? 'client';
|
|
246
439
|
const nextConfig = [];
|
|
247
|
-
const baseRouteProperty = configArgument
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const baseRouteValue = baseRouteProperty.initializer
|
|
252
|
-
.getText(sourceFile)
|
|
253
|
-
.replace('as `https://${string}`', '')
|
|
254
|
-
.trim();
|
|
255
|
-
nextConfig.push(`baseUrl: ${baseRouteValue}`);
|
|
440
|
+
const baseRouteProperty = findProperty(configArgument, 'baseRoute');
|
|
441
|
+
const baseRouteText = baseRouteProperty && getPropertyValueText(baseRouteProperty, sourceFile);
|
|
442
|
+
if (baseRouteText) {
|
|
443
|
+
nextConfig.push(`baseUrl: ${baseRouteText.replace('as `https://${string}`', '').trim()}`);
|
|
256
444
|
}
|
|
257
445
|
nextConfig.push(`name: '${variableName}'`);
|
|
258
|
-
const requestProperty = configArgument
|
|
259
|
-
return ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'request';
|
|
260
|
-
});
|
|
446
|
+
const requestProperty = findProperty(configArgument, 'request');
|
|
261
447
|
if (requestProperty &&
|
|
262
448
|
ts.isPropertyAssignment(requestProperty) &&
|
|
263
449
|
ts.isObjectLiteralExpression(requestProperty.initializer)) {
|
|
264
|
-
requestProperty.initializer
|
|
265
|
-
|
|
266
|
-
|
|
450
|
+
const requestConfig = requestProperty.initializer;
|
|
451
|
+
for (const [from, to] of [
|
|
452
|
+
['queryParams', 'queryString'],
|
|
453
|
+
['cacheAdapter', 'cacheAdapter'],
|
|
454
|
+
['retryFn', 'retryFn'],
|
|
455
|
+
]) {
|
|
456
|
+
const property = findProperty(requestConfig, from);
|
|
457
|
+
const valueText = property && getPropertyValueText(property, sourceFile);
|
|
458
|
+
if (valueText) {
|
|
459
|
+
nextConfig.push(`${to}: ${valueText}`);
|
|
267
460
|
}
|
|
268
|
-
|
|
269
|
-
nextConfig.push(`queryString: ${property.initializer.getText(sourceFile)}`);
|
|
270
|
-
}
|
|
271
|
-
if (property.name.text === 'cacheAdapter') {
|
|
272
|
-
nextConfig.push(`cacheAdapter: ${property.initializer.getText(sourceFile)}`);
|
|
273
|
-
}
|
|
274
|
-
if (property.name.text === 'retryFn') {
|
|
275
|
-
nextConfig.push(`retryFn: ${property.initializer.getText(sourceFile)}`);
|
|
276
|
-
}
|
|
277
|
-
});
|
|
461
|
+
}
|
|
278
462
|
}
|
|
463
|
+
nextConfig.push(`features: [${CLIENT_FEATURE}()]`);
|
|
279
464
|
return `createQueryClient({\n ${nextConfig.join(',\n ')}\n})`;
|
|
280
465
|
};
|
|
281
466
|
const getVariableNameForQueryClient = (node) => {
|
|
@@ -286,7 +471,7 @@ const getVariableNameForQueryClient = (node) => {
|
|
|
286
471
|
}
|
|
287
472
|
parent = parent.parent;
|
|
288
473
|
}
|
|
289
|
-
return
|
|
474
|
+
return undefined;
|
|
290
475
|
};
|
|
291
476
|
const extractClientConfigNames = (content) => {
|
|
292
477
|
const sourceFile = createSourceFile(content);
|
|
@@ -326,19 +511,20 @@ const addProviderAliasesToFile = (content, configNames) => {
|
|
|
326
511
|
return;
|
|
327
512
|
}
|
|
328
513
|
const { injectName, provideName } = getProviderAliasNames(variableName);
|
|
329
|
-
if (result.includes(`export const
|
|
514
|
+
if (result.includes(`export const ${injectName} = toInjectFn(${variableName});`)) {
|
|
330
515
|
return;
|
|
331
516
|
}
|
|
332
517
|
insertions.push({
|
|
333
518
|
position,
|
|
334
|
-
text: `\n\nexport const
|
|
519
|
+
text: `\n\nexport const ${provideName} = toProvideFn(${variableName});` +
|
|
520
|
+
`\nexport const ${injectName} = toInjectFn(${variableName});`,
|
|
335
521
|
});
|
|
336
522
|
});
|
|
337
523
|
insertions.sort((left, right) => right.position - left.position);
|
|
338
524
|
insertions.forEach(({ position, text }) => {
|
|
339
525
|
result = result.slice(0, position) + text + result.slice(position);
|
|
340
526
|
});
|
|
341
|
-
return result;
|
|
527
|
+
return insertions.length > 0 ? ensureImportFromEthleteCore(result, ['toInjectFn', 'toProvideFn']) : result;
|
|
342
528
|
};
|
|
343
529
|
const getProviderAliasNames = (configVariableName) => {
|
|
344
530
|
const baseName = configVariableName.replace(/Config$/, '');
|
|
@@ -384,94 +570,90 @@ const generateCreatorsForConfig = (configName) => {
|
|
|
384
570
|
`export const ${creatorBaseName}Delete = createDeleteQuery(${configVariableName});`,
|
|
385
571
|
].join('\n');
|
|
386
572
|
};
|
|
387
|
-
|
|
388
|
-
|
|
573
|
+
/**
|
|
574
|
+
* Points imports and re-exports of a renamed client at its new name, and renames the references to
|
|
575
|
+
* each unaliased import. Only a binding the module graph resolves to the client's declaring file is
|
|
576
|
+
* touched; a same-named symbol from any other module stays as it is. A workspace specifier the graph
|
|
577
|
+
* cannot resolve falls back to the rename by name and is returned in `renamedByName` for the report.
|
|
578
|
+
*/
|
|
579
|
+
const updateImportsInFile = (content, filePath, renames, graph) => {
|
|
580
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
389
581
|
const replacements = [];
|
|
390
|
-
const
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
582
|
+
const localRenames = new Map();
|
|
583
|
+
const renamedByName = new Set();
|
|
584
|
+
const resolveRename = (specifier, importedName) => {
|
|
585
|
+
const declaringFile = graph.findDeclaringFile(filePath, specifier, importedName);
|
|
586
|
+
if (declaringFile) {
|
|
587
|
+
return declaringFile !== filePath ? renames.get(declaringFile)?.renames.get(importedName) : undefined;
|
|
588
|
+
}
|
|
589
|
+
if (!graph.isWorkspaceSpecifier(specifier))
|
|
590
|
+
return undefined;
|
|
591
|
+
const nextName = [...renames.values()].find((entry) => entry.renames.has(importedName))?.renames.get(importedName);
|
|
592
|
+
if (nextName)
|
|
593
|
+
renamedByName.add(specifier);
|
|
594
|
+
return nextName;
|
|
595
|
+
};
|
|
596
|
+
for (const statement of sourceFile.statements) {
|
|
597
|
+
if (ts.isImportDeclaration(statement) &&
|
|
598
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
599
|
+
statement.importClause?.namedBindings &&
|
|
600
|
+
ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
601
|
+
const specifier = statement.moduleSpecifier.text;
|
|
395
602
|
let hasChanges = false;
|
|
396
|
-
|
|
397
|
-
const nextName =
|
|
603
|
+
const nextElements = statement.importClause.namedBindings.elements.map((element) => {
|
|
604
|
+
const nextName = resolveRename(specifier, (element.propertyName ?? element.name).text);
|
|
398
605
|
if (!nextName) {
|
|
399
|
-
|
|
400
|
-
return;
|
|
606
|
+
return element.getText(sourceFile);
|
|
401
607
|
}
|
|
402
608
|
hasChanges = true;
|
|
403
|
-
|
|
609
|
+
const typePrefix = element.isTypeOnly ? 'type ' : '';
|
|
610
|
+
if (element.propertyName) {
|
|
611
|
+
return `${typePrefix}${nextName} as ${element.name.text}`;
|
|
612
|
+
}
|
|
613
|
+
localRenames.set(element.name.text, nextName);
|
|
614
|
+
return `${typePrefix}${nextName}`;
|
|
404
615
|
});
|
|
405
616
|
if (hasChanges) {
|
|
406
617
|
replacements.push({
|
|
407
|
-
start:
|
|
408
|
-
end:
|
|
409
|
-
replacement:
|
|
618
|
+
start: statement.getStart(sourceFile),
|
|
619
|
+
end: statement.getEnd(),
|
|
620
|
+
replacement: printImportDeclaration(statement, sourceFile, nextElements),
|
|
410
621
|
});
|
|
411
622
|
}
|
|
412
623
|
}
|
|
413
|
-
if (ts.
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
ts.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
}
|
|
436
|
-
if (ts.isParameter(parent) && parent.name === node) {
|
|
437
|
-
ts.forEachChild(node, visit);
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
|
-
if (ts.isPropertyDeclaration(parent) && parent.name === node) {
|
|
441
|
-
ts.forEachChild(node, visit);
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
445
|
-
ts.forEachChild(node, visit);
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
|
-
if (ts.isPropertyAssignment(parent) && parent.name === node) {
|
|
449
|
-
ts.forEachChild(node, visit);
|
|
450
|
-
return;
|
|
451
|
-
}
|
|
624
|
+
if (ts.isExportDeclaration(statement) &&
|
|
625
|
+
statement.moduleSpecifier &&
|
|
626
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
627
|
+
statement.exportClause &&
|
|
628
|
+
ts.isNamedExports(statement.exportClause)) {
|
|
629
|
+
const specifier = statement.moduleSpecifier.text;
|
|
630
|
+
statement.exportClause.elements.forEach((element) => {
|
|
631
|
+
const exportedFrom = element.propertyName ?? element.name;
|
|
632
|
+
const nextName = ts.isIdentifier(exportedFrom) ? resolveRename(specifier, exportedFrom.text) : undefined;
|
|
633
|
+
if (nextName) {
|
|
634
|
+
replacements.push({
|
|
635
|
+
start: exportedFrom.getStart(sourceFile),
|
|
636
|
+
end: exportedFrom.getEnd(),
|
|
637
|
+
replacement: nextName,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const visit = (node) => {
|
|
644
|
+
const nextName = ts.isIdentifier(node) ? localRenames.get(node.text) : undefined;
|
|
645
|
+
if (nextName && !isNamePosition(node) && !isShadowedInNestedScope(node)) {
|
|
452
646
|
replacements.push({
|
|
453
647
|
start: node.getStart(sourceFile),
|
|
454
648
|
end: node.getEnd(),
|
|
455
|
-
replacement: nextName,
|
|
649
|
+
replacement: renamedReference(node, nextName),
|
|
456
650
|
});
|
|
457
651
|
}
|
|
458
652
|
ts.forEachChild(node, visit);
|
|
459
653
|
};
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const key = `${replacement.start}:${replacement.end}`;
|
|
465
|
-
if (seen.has(key)) {
|
|
466
|
-
return false;
|
|
467
|
-
}
|
|
468
|
-
seen.add(key);
|
|
469
|
-
return true;
|
|
470
|
-
});
|
|
471
|
-
uniqueReplacements.sort((left, right) => right.start - left.start);
|
|
472
|
-
uniqueReplacements.forEach(({ start, end, replacement }) => {
|
|
473
|
-
result = result.slice(0, start) + replacement + result.slice(end);
|
|
474
|
-
});
|
|
475
|
-
return result;
|
|
654
|
+
if (localRenames.size > 0) {
|
|
655
|
+
visit(sourceFile);
|
|
656
|
+
}
|
|
657
|
+
return { content: applyReplacements(content, replacements), renamedByName };
|
|
476
658
|
};
|
|
477
659
|
//# sourceMappingURL=query-client-migration.js.map
|