@ethlete/query 6.0.0-next.51 → 6.0.0-next.53
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 +78 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +393 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
- package/fesm2022/ethlete-query-testing.mjs +67 -25
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +7895 -7226
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/generators.json +5 -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-opt-in-features/migration.js +10 -9
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +29 -2
- package/generators/migrate-to-query-v3/cleanup-migration.js +163 -53
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +39 -2
- package/generators/migrate-to-query-v3/migration-scope.js +2 -0
- package/generators/migrate-to-query-v3/migration.js +5 -4
- package/generators/migrate-to-query-v3/module-graph.js +41 -29
- package/generators/migrate-to-query-v3/query-client-migration.js +209 -126
- package/generators/migrate-to-query-v3/rename-symbols.js +38 -11
- package/generators/migrate-to-query-v3/report.js +40 -7
- package/generators/migrate-to-query-v3/shared.js +46 -1
- package/generators/prep-for-query-v3/migration.js +400 -57
- package/migrations/query-field-default-types.md +43 -0
- package/migrations/search-query-field-string.md +43 -0
- package/migrations.json +16 -0
- package/package.json +5 -1
- package/types/ethlete-query-ngrx-toolkit.d.ts +143 -0
- package/types/ethlete-query-testing.d.ts +28 -11
- package/types/ethlete-query.d.ts +789 -584
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatFiles } from '@nx/devkit';
|
|
2
|
-
import { migrateDevtoolsUsage, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy } from './cleanup-migration.js';
|
|
2
|
+
import { migrateDevtoolsUsage, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy, reportRemovedExperimentalQueryHelpers, } from './cleanup-migration.js';
|
|
3
3
|
import { reportDefaultHeaderUsages, reportMissingHttpClientProviders } from './http-client-check.js';
|
|
4
4
|
import { migrateLegacyPrepareCalls } from './legacy-prepare-migration.js';
|
|
5
5
|
import { createNewQueryCreators, updateLegacyCreatorImportsAndUsages } from './legacy-query-creator-migration.js';
|
|
@@ -19,17 +19,18 @@ export default async function migrate(tree, schema) {
|
|
|
19
19
|
updateLegacyCreatorImportsAndUsages(tree, scope, report);
|
|
20
20
|
}
|
|
21
21
|
if (variableRenames.size > 0) {
|
|
22
|
-
updateImportsAcrossWorkspace(tree, variableRenames, scope);
|
|
22
|
+
updateImportsAcrossWorkspace(tree, variableRenames, scope, report);
|
|
23
23
|
}
|
|
24
24
|
replaceAnyQueryWithLegacy(tree, scope);
|
|
25
25
|
migrateDevtoolsUsage(tree, scope, report);
|
|
26
|
-
migrateEmptyPrepareCalls(tree, scope);
|
|
26
|
+
migrateEmptyPrepareCalls(tree, scope, report);
|
|
27
27
|
migrateLegacyPrepareCalls(tree, report, scope);
|
|
28
|
+
reportRemovedExperimentalQueryHelpers(tree, scope, report);
|
|
28
29
|
if (queryClientFiles.size > 0) {
|
|
29
30
|
reportMissingHttpClientProviders(tree, report, scope);
|
|
30
31
|
reportDefaultHeaderUsages(tree, report, scope);
|
|
31
32
|
}
|
|
32
|
-
report.writeToTree(tree);
|
|
33
|
+
report.writeToTree(tree, scope.isScoped ? scope.describe() : undefined);
|
|
33
34
|
report.printSummary();
|
|
34
35
|
if (!schema.skipFormat) {
|
|
35
36
|
await formatFiles(tree);
|
|
@@ -15,8 +15,9 @@ const normalizePath = (path) => {
|
|
|
15
15
|
return segments.join('/');
|
|
16
16
|
};
|
|
17
17
|
const dirName = (path) => path.slice(0, Math.max(0, path.lastIndexOf('/')));
|
|
18
|
-
const
|
|
18
|
+
const readTsConfig = (tree) => {
|
|
19
19
|
const paths = new Map();
|
|
20
|
+
let baseUrl = null;
|
|
20
21
|
for (const configPath of ['tsconfig.base.json', 'tsconfig.json']) {
|
|
21
22
|
const raw = tree.read(configPath, 'utf-8');
|
|
22
23
|
if (!raw)
|
|
@@ -25,6 +26,9 @@ const readTsConfigPaths = (tree) => {
|
|
|
25
26
|
// tsconfig files are JSONC in practice; the TS parser is the only thing that reliably reads
|
|
26
27
|
// them, comments and trailing commas included.
|
|
27
28
|
const parsed = ts.parseConfigFileTextToJson(configPath, raw).config;
|
|
29
|
+
const configBaseUrl = parsed?.compilerOptions?.baseUrl;
|
|
30
|
+
if (baseUrl === null && typeof configBaseUrl === 'string')
|
|
31
|
+
baseUrl = normalizePath(configBaseUrl);
|
|
28
32
|
Object.entries(parsed?.compilerOptions?.paths ?? {}).forEach(([key, targets]) => {
|
|
29
33
|
if (!paths.has(key))
|
|
30
34
|
paths.set(key, targets);
|
|
@@ -34,45 +38,49 @@ const readTsConfigPaths = (tree) => {
|
|
|
34
38
|
// A tsconfig we cannot parse just means fewer resolvable aliases, not a failed migration.
|
|
35
39
|
}
|
|
36
40
|
}
|
|
37
|
-
return paths;
|
|
41
|
+
return { paths, baseUrl };
|
|
38
42
|
};
|
|
43
|
+
/** The part of `specifier` the `*` of `pattern` stands for, `''` for an exact match, or `null`. */
|
|
44
|
+
const matchPathAlias = (pattern, specifier) => {
|
|
45
|
+
const wildcardIndex = pattern.indexOf('*');
|
|
46
|
+
if (wildcardIndex === -1)
|
|
47
|
+
return pattern === specifier ? '' : null;
|
|
48
|
+
const prefix = pattern.slice(0, wildcardIndex);
|
|
49
|
+
const patternSuffix = pattern.slice(wildcardIndex + 1);
|
|
50
|
+
if (!specifier.startsWith(prefix) || !specifier.endsWith(patternSuffix))
|
|
51
|
+
return null;
|
|
52
|
+
return specifier.slice(prefix.length, specifier.length - patternSuffix.length);
|
|
53
|
+
};
|
|
54
|
+
const JS_EXTENSION = /\.(m?)js$/;
|
|
39
55
|
/** The files a specifier could point at, most specific first. */
|
|
40
|
-
const candidateFiles = (base) =>
|
|
41
|
-
base.endsWith('.ts')
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
const candidateFiles = (base) => {
|
|
57
|
+
if (base.endsWith('.ts'))
|
|
58
|
+
return [base];
|
|
59
|
+
const jsExtension = JS_EXTENSION.exec(base);
|
|
60
|
+
if (jsExtension) {
|
|
61
|
+
const stem = base.slice(0, -jsExtension[0].length);
|
|
62
|
+
return [`${stem}.${jsExtension[1]}ts`, `${stem}.ts`, `${stem}.d.ts`, `${base}/index.ts`];
|
|
63
|
+
}
|
|
64
|
+
return [`${base}.ts`, `${base}/index.ts`, `${base}.d.ts`];
|
|
65
|
+
};
|
|
45
66
|
export const createModuleGraph = (tree) => {
|
|
46
|
-
const tsConfigPaths =
|
|
67
|
+
const { paths: tsConfigPaths, baseUrl } = readTsConfig(tree);
|
|
68
|
+
const findCandidate = (base) => candidateFiles(base).find((candidate) => tree.exists(candidate)) ?? null;
|
|
47
69
|
const declaringFileCache = new Map();
|
|
48
70
|
const resolveEntryFile = (fromFile, specifier) => {
|
|
49
|
-
if (specifier.startsWith('.'))
|
|
50
|
-
|
|
51
|
-
return candidateFiles(base).find((candidate) => tree.exists(candidate)) ?? null;
|
|
52
|
-
}
|
|
71
|
+
if (specifier.startsWith('.'))
|
|
72
|
+
return findCandidate(normalizePath(`${dirName(fromFile)}/${specifier}`));
|
|
53
73
|
for (const [pattern, targets] of tsConfigPaths.entries()) {
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (pattern !== specifier)
|
|
58
|
-
continue;
|
|
59
|
-
suffix = '';
|
|
60
|
-
}
|
|
61
|
-
else {
|
|
62
|
-
const prefix = pattern.slice(0, wildcardIndex);
|
|
63
|
-
const patternSuffix = pattern.slice(wildcardIndex + 1);
|
|
64
|
-
if (!specifier.startsWith(prefix) || !specifier.endsWith(patternSuffix))
|
|
65
|
-
continue;
|
|
66
|
-
suffix = specifier.slice(prefix.length, specifier.length - patternSuffix.length);
|
|
67
|
-
}
|
|
74
|
+
const wildcard = matchPathAlias(pattern, specifier);
|
|
75
|
+
if (wildcard === null)
|
|
76
|
+
continue;
|
|
68
77
|
for (const target of targets) {
|
|
69
|
-
const
|
|
70
|
-
const candidate = candidateFiles(resolvedTarget).find((option) => tree.exists(option));
|
|
78
|
+
const candidate = findCandidate(normalizePath(`${baseUrl ?? ''}/${target.replace('*', wildcard)}`));
|
|
71
79
|
if (candidate)
|
|
72
80
|
return candidate;
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
|
-
return null;
|
|
83
|
+
return baseUrl === null ? null : findCandidate(normalizePath(`${baseUrl}/${specifier}`));
|
|
76
84
|
};
|
|
77
85
|
const declaresSymbol = (sourceFile, symbolName) => {
|
|
78
86
|
let declares = false;
|
|
@@ -138,6 +146,10 @@ export const createModuleGraph = (tree) => {
|
|
|
138
146
|
return null;
|
|
139
147
|
};
|
|
140
148
|
return {
|
|
149
|
+
resolveFile: resolveEntryFile,
|
|
150
|
+
pathAliases: () => [...tsConfigPaths.keys()],
|
|
151
|
+
isWorkspaceSpecifier: (specifier) => specifier.startsWith('.') ||
|
|
152
|
+
[...tsConfigPaths.keys()].some((pattern) => matchPathAlias(pattern, specifier) !== null),
|
|
141
153
|
findDeclaringFile: (fromFile, specifier, symbolName) => {
|
|
142
154
|
const cacheKey = `${fromFile}|${specifier}|${symbolName}`;
|
|
143
155
|
const cached = declaringFileCache.get(cacheKey);
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import {
|
|
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';
|
|
3
6
|
export const migrateQueryClients = (tree, report, scope) => {
|
|
4
7
|
const queryClientFiles = new Map();
|
|
5
8
|
const variableRenames = new Map();
|
|
@@ -13,9 +16,9 @@ export const migrateQueryClients = (tree, report, scope) => {
|
|
|
13
16
|
}
|
|
14
17
|
const renames = new Map();
|
|
15
18
|
const nextContent = migrateQueryClientToConfig({ content, filePath, renamesOut: renames, report });
|
|
16
|
-
renames.
|
|
17
|
-
variableRenames.set(
|
|
18
|
-
}
|
|
19
|
+
if (renames.size > 0) {
|
|
20
|
+
variableRenames.set(filePath, { originalContent: content, renames });
|
|
21
|
+
}
|
|
19
22
|
const configNames = extractClientConfigNames(nextContent);
|
|
20
23
|
if (configNames.length > 0) {
|
|
21
24
|
queryClientFiles.set(filePath, configNames);
|
|
@@ -68,10 +71,12 @@ export const generateQueryCreators = (tree, queryClientFiles) => {
|
|
|
68
71
|
generatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
69
72
|
}
|
|
70
73
|
};
|
|
71
|
-
export const updateImportsAcrossWorkspace = (tree, renames, scope) => {
|
|
74
|
+
export const updateImportsAcrossWorkspace = (tree, renames, scope, report) => {
|
|
72
75
|
if (renames.size === 0) {
|
|
73
76
|
return;
|
|
74
77
|
}
|
|
78
|
+
const originalContents = new Map(Array.from(renames.entries()).map(([filePath, { originalContent }]) => [filePath, originalContent]));
|
|
79
|
+
const graph = createModuleGraph(withOriginalContents(tree, originalContents));
|
|
75
80
|
scope.visit(tree, (filePath) => {
|
|
76
81
|
if (!filePath.endsWith('.ts')) {
|
|
77
82
|
return;
|
|
@@ -80,12 +85,42 @@ export const updateImportsAcrossWorkspace = (tree, renames, scope) => {
|
|
|
80
85
|
if (!content) {
|
|
81
86
|
return;
|
|
82
87
|
}
|
|
83
|
-
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
|
+
}
|
|
84
99
|
if (nextContent !== content) {
|
|
100
|
+
originalContents.set(filePath, content);
|
|
85
101
|
tree.write(filePath, nextContent);
|
|
86
102
|
}
|
|
87
103
|
});
|
|
88
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
|
+
});
|
|
89
124
|
const hasQueryClientInstantiation = (content) => {
|
|
90
125
|
const sourceFile = createSourceFile(content);
|
|
91
126
|
let hasQueryClientImport = false;
|
|
@@ -151,6 +186,7 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
151
186
|
}
|
|
152
187
|
}
|
|
153
188
|
reportDroppedClientOptions({ configArgument, sourceFile, filePath, report });
|
|
189
|
+
reportChangedClientDefaults({ configArgument, sourceFile, filePath, report });
|
|
154
190
|
replacements.push({
|
|
155
191
|
start: node.getStart(sourceFile),
|
|
156
192
|
end: node.getEnd(),
|
|
@@ -190,6 +226,7 @@ const removeQueryClientImport = (content, keepLegacyClient) => {
|
|
|
190
226
|
'createPostQuery',
|
|
191
227
|
'createPutQuery',
|
|
192
228
|
'createQueryClient',
|
|
229
|
+
CLIENT_FEATURE,
|
|
193
230
|
];
|
|
194
231
|
if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
|
|
195
232
|
return ensureImportFromQuery(content, requiredImports);
|
|
@@ -199,11 +236,31 @@ const removeQueryClientImport = (content, keepLegacyClient) => {
|
|
|
199
236
|
});
|
|
200
237
|
const nextImports = new Set(requiredImports);
|
|
201
238
|
existingElements.forEach((element) => nextImports.add(element.getText(sourceFile)));
|
|
202
|
-
const nextImportStatement =
|
|
239
|
+
const nextImportStatement = printImportDeclaration(queryImportNode, sourceFile, Array.from(nextImports).sort());
|
|
203
240
|
return (content.slice(0, queryImportNode.getStart(sourceFile)) +
|
|
204
241
|
nextImportStatement +
|
|
205
242
|
content.slice(queryImportNode.getEnd()));
|
|
206
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
|
+
};
|
|
207
264
|
const renameVariables = (content, renames) => {
|
|
208
265
|
if (renames.size === 0) {
|
|
209
266
|
return content;
|
|
@@ -211,48 +268,31 @@ const renameVariables = (content, renames) => {
|
|
|
211
268
|
const sourceFile = createSourceFile(content);
|
|
212
269
|
const replacements = [];
|
|
213
270
|
const visit = (node) => {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
start: node.name.getStart(sourceFile),
|
|
219
|
-
end: node.name.getEnd(),
|
|
220
|
-
replacement: nextName,
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
else if (ts.isIdentifier(node)) {
|
|
225
|
-
const nextName = renames.get(node.text);
|
|
226
|
-
if (nextName) {
|
|
227
|
-
const parent = node.parent;
|
|
228
|
-
if (!ts.isPropertyAssignment(parent) || parent.name !== node) {
|
|
229
|
-
replacements.push({
|
|
230
|
-
start: node.getStart(sourceFile),
|
|
231
|
-
end: node.getEnd(),
|
|
232
|
-
replacement: nextName,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
}
|
|
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 });
|
|
236
275
|
}
|
|
237
276
|
ts.forEachChild(node, visit);
|
|
238
277
|
};
|
|
239
278
|
visit(sourceFile);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
if (seen.has(key)) {
|
|
244
|
-
return false;
|
|
245
|
-
}
|
|
246
|
-
seen.add(key);
|
|
247
|
-
return true;
|
|
248
|
-
});
|
|
279
|
+
return applyReplacements(content, replacements);
|
|
280
|
+
};
|
|
281
|
+
const applyReplacements = (content, replacements) => {
|
|
249
282
|
let result = content;
|
|
250
|
-
|
|
251
|
-
|
|
283
|
+
[...replacements]
|
|
284
|
+
.sort((left, right) => right.start - left.start)
|
|
285
|
+
.forEach(({ start, end, replacement }) => {
|
|
252
286
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
253
287
|
});
|
|
254
288
|
return result;
|
|
255
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
|
+
};
|
|
256
296
|
/**
|
|
257
297
|
* What each v2 client option turns into, or why it has nowhere to go.
|
|
258
298
|
*
|
|
@@ -261,8 +301,8 @@ const renameVariables = (content, renames) => {
|
|
|
261
301
|
* report from production.
|
|
262
302
|
*/
|
|
263
303
|
const DROPPED_CLIENT_OPTIONS = {
|
|
264
|
-
'request.autoRefreshQueriesOnWindowFocus':
|
|
265
|
-
'request.enableSmartPolling':
|
|
304
|
+
'request.autoRefreshQueriesOnWindowFocus': WINDOW_FOCUS_REPLACEMENT,
|
|
305
|
+
'request.enableSmartPolling': SMART_POLLING_REPLACEMENT,
|
|
266
306
|
'logging.preparedQuerySubscriptions': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
267
307
|
'logging.queryStateChanges': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
268
308
|
'request.autoRefreshOn': 'No direct equivalent. Re-run the affected queries yourself with `withAutoRefresh` or `refreshQueriesInUse()`.',
|
|
@@ -315,6 +355,12 @@ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, repo
|
|
|
315
355
|
if (name === 'request' && ['queryParams', 'cacheAdapter', 'retryFn'].includes(nested.name.text)) {
|
|
316
356
|
return;
|
|
317
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
|
+
}
|
|
318
364
|
raise(`${name}.${nested.name.text}`, nested);
|
|
319
365
|
});
|
|
320
366
|
return;
|
|
@@ -324,6 +370,59 @@ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, repo
|
|
|
324
370
|
}
|
|
325
371
|
});
|
|
326
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
|
+
};
|
|
327
426
|
const getPropertyValueText = (property, sourceFile) => {
|
|
328
427
|
if (ts.isShorthandPropertyAssignment(property)) {
|
|
329
428
|
return property.name.text;
|
|
@@ -361,6 +460,7 @@ const migrateConfigObject = (configArgument, node, sourceFile) => {
|
|
|
361
460
|
}
|
|
362
461
|
}
|
|
363
462
|
}
|
|
463
|
+
nextConfig.push(`features: [${CLIENT_FEATURE}()]`);
|
|
364
464
|
return `createQueryClient({\n ${nextConfig.join(',\n ')}\n})`;
|
|
365
465
|
};
|
|
366
466
|
const getVariableNameForQueryClient = (node) => {
|
|
@@ -470,107 +570,90 @@ const generateCreatorsForConfig = (configName) => {
|
|
|
470
570
|
`export const ${creatorBaseName}Delete = createDeleteQuery(${configVariableName});`,
|
|
471
571
|
].join('\n');
|
|
472
572
|
};
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
return true;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
return false;
|
|
485
|
-
};
|
|
486
|
-
const updateImportsInFile = (content, renames) => {
|
|
487
|
-
const sourceFile = createSourceFile(content);
|
|
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);
|
|
488
581
|
const replacements = [];
|
|
489
|
-
const
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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;
|
|
494
602
|
let hasChanges = false;
|
|
495
|
-
|
|
496
|
-
const nextName =
|
|
603
|
+
const nextElements = statement.importClause.namedBindings.elements.map((element) => {
|
|
604
|
+
const nextName = resolveRename(specifier, (element.propertyName ?? element.name).text);
|
|
497
605
|
if (!nextName) {
|
|
498
|
-
|
|
499
|
-
return;
|
|
606
|
+
return element.getText(sourceFile);
|
|
500
607
|
}
|
|
501
608
|
hasChanges = true;
|
|
502
|
-
|
|
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}`;
|
|
503
615
|
});
|
|
504
616
|
if (hasChanges) {
|
|
505
617
|
replacements.push({
|
|
506
|
-
start:
|
|
507
|
-
end:
|
|
508
|
-
replacement:
|
|
618
|
+
start: statement.getStart(sourceFile),
|
|
619
|
+
end: statement.getEnd(),
|
|
620
|
+
replacement: printImportDeclaration(statement, sourceFile, nextElements),
|
|
509
621
|
});
|
|
510
622
|
}
|
|
511
623
|
}
|
|
512
|
-
if (ts.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
ts.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
}
|
|
535
|
-
if (ts.isParameter(parent) && parent.name === node) {
|
|
536
|
-
ts.forEachChild(node, visit);
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
if (ts.isPropertyDeclaration(parent) && parent.name === node) {
|
|
540
|
-
ts.forEachChild(node, visit);
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
544
|
-
ts.forEachChild(node, visit);
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
if ((ts.isPropertyAssignment(parent) && parent.name === node) || isShadowedByLocalDeclaration(node)) {
|
|
548
|
-
ts.forEachChild(node, visit);
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
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)) {
|
|
551
646
|
replacements.push({
|
|
552
647
|
start: node.getStart(sourceFile),
|
|
553
648
|
end: node.getEnd(),
|
|
554
|
-
replacement: nextName,
|
|
649
|
+
replacement: renamedReference(node, nextName),
|
|
555
650
|
});
|
|
556
651
|
}
|
|
557
652
|
ts.forEachChild(node, visit);
|
|
558
653
|
};
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
const key = `${replacement.start}:${replacement.end}`;
|
|
564
|
-
if (seen.has(key)) {
|
|
565
|
-
return false;
|
|
566
|
-
}
|
|
567
|
-
seen.add(key);
|
|
568
|
-
return true;
|
|
569
|
-
});
|
|
570
|
-
uniqueReplacements.sort((left, right) => right.start - left.start);
|
|
571
|
-
uniqueReplacements.forEach(({ start, end, replacement }) => {
|
|
572
|
-
result = result.slice(0, start) + replacement + result.slice(end);
|
|
573
|
-
});
|
|
574
|
-
return result;
|
|
654
|
+
if (localRenames.size > 0) {
|
|
655
|
+
visit(sourceFile);
|
|
656
|
+
}
|
|
657
|
+
return { content: applyReplacements(content, replacements), renamedByName };
|
|
575
658
|
};
|
|
576
659
|
//# sourceMappingURL=query-client-migration.js.map
|