@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.
- package/CHANGELOG.md +766 -0
- package/fesm2022/ethlete-query-testing.mjs +441 -38
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +15946 -5331
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/deprecate-legacy-queries/migration.js +77 -0
- package/generators/deprecate-legacy-queries/schema.json +26 -0
- package/generators/generators.json +15 -0
- package/generators/migrate-query-client-features/migration.js +142 -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 +194 -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 +133 -0
- package/generators/migrate-to-query-v3/cleanup-migration.js +111 -99
- 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 +237 -152
- package/generators/migrate-to-query-v3/migration-scope.js +38 -0
- package/generators/migrate-to-query-v3/migration.js +17 -9
- package/generators/migrate-to-query-v3/module-graph.js +153 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +152 -53
- package/generators/migrate-to-query-v3/rename-symbols.js +206 -0
- package/generators/migrate-to-query-v3/schema.json +12 -0
- package/generators/migrate-to-query-v3/shared.js +51 -2
- package/generators/prep-for-query-v3/migration.js +71 -14
- package/migrations.json +44 -0
- package/package.json +29 -13
- package/types/ethlete-query-testing.d.ts +179 -12
- package/types/ethlete-query.d.ts +8935 -3203
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { createSourceFile } from './shared.js';
|
|
3
|
+
const MAX_BARREL_DEPTH = 8;
|
|
4
|
+
const normalizePath = (path) => {
|
|
5
|
+
const segments = [];
|
|
6
|
+
for (const segment of path.split('/')) {
|
|
7
|
+
if (segment === '' || segment === '.')
|
|
8
|
+
continue;
|
|
9
|
+
if (segment === '..') {
|
|
10
|
+
segments.pop();
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
segments.push(segment);
|
|
14
|
+
}
|
|
15
|
+
return segments.join('/');
|
|
16
|
+
};
|
|
17
|
+
const dirName = (path) => path.slice(0, Math.max(0, path.lastIndexOf('/')));
|
|
18
|
+
const readTsConfigPaths = (tree) => {
|
|
19
|
+
const paths = new Map();
|
|
20
|
+
for (const configPath of ['tsconfig.base.json', 'tsconfig.json']) {
|
|
21
|
+
const raw = tree.read(configPath, 'utf-8');
|
|
22
|
+
if (!raw)
|
|
23
|
+
continue;
|
|
24
|
+
try {
|
|
25
|
+
// tsconfig files are JSONC in practice; the TS parser is the only thing that reliably reads
|
|
26
|
+
// them, comments and trailing commas included.
|
|
27
|
+
const parsed = ts.parseConfigFileTextToJson(configPath, raw).config;
|
|
28
|
+
Object.entries(parsed?.compilerOptions?.paths ?? {}).forEach(([key, targets]) => {
|
|
29
|
+
if (!paths.has(key))
|
|
30
|
+
paths.set(key, targets);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// A tsconfig we cannot parse just means fewer resolvable aliases, not a failed migration.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return paths;
|
|
38
|
+
};
|
|
39
|
+
/** The files a specifier could point at, most specific first. */
|
|
40
|
+
const candidateFiles = (base) => [
|
|
41
|
+
base.endsWith('.ts') ? base : `${base}.ts`,
|
|
42
|
+
`${base}/index.ts`,
|
|
43
|
+
`${base}.d.ts`,
|
|
44
|
+
];
|
|
45
|
+
export const createModuleGraph = (tree) => {
|
|
46
|
+
const tsConfigPaths = readTsConfigPaths(tree);
|
|
47
|
+
const declaringFileCache = new Map();
|
|
48
|
+
const resolveEntryFile = (fromFile, specifier) => {
|
|
49
|
+
if (specifier.startsWith('.')) {
|
|
50
|
+
const base = normalizePath(`${dirName(fromFile)}/${specifier}`);
|
|
51
|
+
return candidateFiles(base).find((candidate) => tree.exists(candidate)) ?? null;
|
|
52
|
+
}
|
|
53
|
+
for (const [pattern, targets] of tsConfigPaths.entries()) {
|
|
54
|
+
const wildcardIndex = pattern.indexOf('*');
|
|
55
|
+
let suffix;
|
|
56
|
+
if (wildcardIndex === -1) {
|
|
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
|
+
}
|
|
68
|
+
for (const target of targets) {
|
|
69
|
+
const resolvedTarget = normalizePath(target.replace('*', suffix));
|
|
70
|
+
const candidate = candidateFiles(resolvedTarget).find((option) => tree.exists(option));
|
|
71
|
+
if (candidate)
|
|
72
|
+
return candidate;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
};
|
|
77
|
+
const declaresSymbol = (sourceFile, symbolName) => {
|
|
78
|
+
let declares = false;
|
|
79
|
+
const isExported = (node) => ts.canHaveModifiers(node) &&
|
|
80
|
+
ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
81
|
+
ts.forEachChild(sourceFile, (node) => {
|
|
82
|
+
if (declares)
|
|
83
|
+
return;
|
|
84
|
+
if (ts.isVariableStatement(node) && isExported(node)) {
|
|
85
|
+
declares = node.declarationList.declarations.some((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === symbolName);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if ((ts.isFunctionDeclaration(node) ||
|
|
89
|
+
ts.isClassDeclaration(node) ||
|
|
90
|
+
ts.isInterfaceDeclaration(node) ||
|
|
91
|
+
ts.isTypeAliasDeclaration(node) ||
|
|
92
|
+
ts.isEnumDeclaration(node)) &&
|
|
93
|
+
isExported(node) &&
|
|
94
|
+
node.name?.text === symbolName) {
|
|
95
|
+
declares = true;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return declares;
|
|
99
|
+
};
|
|
100
|
+
const findDeclaringFileIn = (entryFile, symbolName, depth, seen) => {
|
|
101
|
+
if (depth > MAX_BARREL_DEPTH || seen.has(`${entryFile}#${symbolName}`))
|
|
102
|
+
return null;
|
|
103
|
+
seen.add(`${entryFile}#${symbolName}`);
|
|
104
|
+
const content = tree.read(entryFile, 'utf-8');
|
|
105
|
+
if (!content)
|
|
106
|
+
return null;
|
|
107
|
+
const sourceFile = createSourceFile(content, entryFile);
|
|
108
|
+
if (declaresSymbol(sourceFile, symbolName))
|
|
109
|
+
return entryFile;
|
|
110
|
+
const starExports = [];
|
|
111
|
+
for (const node of sourceFile.statements) {
|
|
112
|
+
if (!ts.isExportDeclaration(node) || !node.moduleSpecifier || !ts.isStringLiteral(node.moduleSpecifier)) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
const nextEntry = resolveEntryFile(entryFile, node.moduleSpecifier.text);
|
|
116
|
+
if (!nextEntry)
|
|
117
|
+
continue;
|
|
118
|
+
if (!node.exportClause) {
|
|
119
|
+
starExports.push(nextEntry);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (!ts.isNamedExports(node.exportClause))
|
|
123
|
+
continue;
|
|
124
|
+
for (const element of node.exportClause.elements) {
|
|
125
|
+
if (element.name.text !== symbolName)
|
|
126
|
+
continue;
|
|
127
|
+
const originalName = element.propertyName?.text ?? symbolName;
|
|
128
|
+
const found = findDeclaringFileIn(nextEntry, originalName, depth + 1, seen);
|
|
129
|
+
if (found)
|
|
130
|
+
return found;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
for (const starExport of starExports) {
|
|
134
|
+
const found = findDeclaringFileIn(starExport, symbolName, depth + 1, seen);
|
|
135
|
+
if (found)
|
|
136
|
+
return found;
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
};
|
|
140
|
+
return {
|
|
141
|
+
findDeclaringFile: (fromFile, specifier, symbolName) => {
|
|
142
|
+
const cacheKey = `${fromFile}|${specifier}|${symbolName}`;
|
|
143
|
+
const cached = declaringFileCache.get(cacheKey);
|
|
144
|
+
if (cached !== undefined)
|
|
145
|
+
return cached;
|
|
146
|
+
const entryFile = resolveEntryFile(fromFile, specifier);
|
|
147
|
+
const declaringFile = entryFile ? findDeclaringFileIn(entryFile, symbolName, 0, new Set()) : null;
|
|
148
|
+
declaringFileCache.set(cacheKey, declaringFile);
|
|
149
|
+
return declaringFile;
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
//# sourceMappingURL=module-graph.js.map
|
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
1
|
import * as ts from 'typescript';
|
|
3
|
-
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromQuery, getVariableStatementEnd, } from './shared.js';
|
|
4
|
-
export const migrateQueryClients = (tree, report) => {
|
|
2
|
+
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, getVariableStatementEnd, } from './shared.js';
|
|
3
|
+
export const migrateQueryClients = (tree, report, scope) => {
|
|
5
4
|
const queryClientFiles = new Map();
|
|
6
5
|
const variableRenames = new Map();
|
|
7
|
-
|
|
6
|
+
scope.visit(tree, (filePath) => {
|
|
8
7
|
if (!filePath.endsWith('.ts')) {
|
|
9
8
|
return;
|
|
10
9
|
}
|
|
@@ -69,11 +68,11 @@ export const generateQueryCreators = (tree, queryClientFiles) => {
|
|
|
69
68
|
generatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
70
69
|
}
|
|
71
70
|
};
|
|
72
|
-
export const updateImportsAcrossWorkspace = (tree, renames) => {
|
|
71
|
+
export const updateImportsAcrossWorkspace = (tree, renames, scope) => {
|
|
73
72
|
if (renames.size === 0) {
|
|
74
73
|
return;
|
|
75
74
|
}
|
|
76
|
-
|
|
75
|
+
scope.visit(tree, (filePath) => {
|
|
77
76
|
if (!filePath.endsWith('.ts')) {
|
|
78
77
|
return;
|
|
79
78
|
}
|
|
@@ -116,17 +115,25 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
116
115
|
const sourceFile = createSourceFile(content, filePath);
|
|
117
116
|
const replacements = [];
|
|
118
117
|
const variableRenames = new Map();
|
|
118
|
+
let hasUnmigratedClient = false;
|
|
119
119
|
const visit = (node) => {
|
|
120
|
-
if (ts.isNewExpression(node) &&
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
120
|
+
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'V2QueryClient') {
|
|
121
|
+
const configArgument = node.arguments?.[0];
|
|
122
|
+
if (!configArgument || !ts.isObjectLiteralExpression(configArgument)) {
|
|
123
|
+
hasUnmigratedClient = true;
|
|
124
|
+
report.addManualReview({
|
|
125
|
+
title: 'Migrate a V2QueryClient whose config is not an object literal',
|
|
126
|
+
summary: `\`${node.getText(sourceFile)}\` passes its config indirectly, so the migration cannot read it and left the client on v2.`,
|
|
127
|
+
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.',
|
|
128
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
|
|
129
|
+
source: 'query-client-migration',
|
|
130
|
+
dedupeKey: `unmigrated-client:${filePath}:${node.getStart(sourceFile)}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
127
134
|
const oldVariableName = getVariableNameForQueryClient(node);
|
|
128
|
-
const newVariableName = ensureConfigSuffix(oldVariableName);
|
|
129
|
-
if (oldVariableName !== newVariableName) {
|
|
135
|
+
const newVariableName = oldVariableName && ensureConfigSuffix(oldVariableName);
|
|
136
|
+
if (oldVariableName && newVariableName && oldVariableName !== newVariableName) {
|
|
130
137
|
const collisionRegex = new RegExp(`\\b(?:const|let|var)\\s+${newVariableName}\\b`);
|
|
131
138
|
if (collisionRegex.test(content)) {
|
|
132
139
|
report.addWarning({
|
|
@@ -143,6 +150,7 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
143
150
|
renamesOut.set(oldVariableName, newVariableName);
|
|
144
151
|
}
|
|
145
152
|
}
|
|
153
|
+
reportDroppedClientOptions({ configArgument, sourceFile, filePath, report });
|
|
146
154
|
replacements.push({
|
|
147
155
|
start: node.getStart(sourceFile),
|
|
148
156
|
end: node.getEnd(),
|
|
@@ -158,16 +166,20 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
158
166
|
replacements.forEach(({ start, end, replacement }) => {
|
|
159
167
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
160
168
|
});
|
|
161
|
-
result = removeQueryClientImport(result);
|
|
169
|
+
result = removeQueryClientImport(result, hasUnmigratedClient);
|
|
162
170
|
return renameVariables(result, variableRenames);
|
|
163
171
|
};
|
|
164
|
-
const removeQueryClientImport = (content) => {
|
|
172
|
+
const removeQueryClientImport = (content, keepLegacyClient) => {
|
|
165
173
|
const sourceFile = createSourceFile(content);
|
|
166
174
|
let queryImportNode;
|
|
167
175
|
ts.forEachChild(sourceFile, (node) => {
|
|
168
|
-
if (
|
|
176
|
+
if (!queryImportNode &&
|
|
177
|
+
ts.isImportDeclaration(node) &&
|
|
169
178
|
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
170
|
-
node.moduleSpecifier.text === '@ethlete/query'
|
|
179
|
+
node.moduleSpecifier.text === '@ethlete/query' &&
|
|
180
|
+
node.importClause?.namedBindings &&
|
|
181
|
+
ts.isNamedImports(node.importClause.namedBindings) &&
|
|
182
|
+
node.importClause.namedBindings.elements.some((element) => element.name.text === 'V2QueryClient')) {
|
|
171
183
|
queryImportNode = node;
|
|
172
184
|
}
|
|
173
185
|
});
|
|
@@ -183,10 +195,10 @@ const removeQueryClientImport = (content) => {
|
|
|
183
195
|
return ensureImportFromQuery(content, requiredImports);
|
|
184
196
|
}
|
|
185
197
|
const existingElements = queryImportNode.importClause.namedBindings.elements.filter((element) => {
|
|
186
|
-
return element.name.text !== 'V2QueryClient';
|
|
198
|
+
return keepLegacyClient || element.name.text !== 'V2QueryClient';
|
|
187
199
|
});
|
|
188
200
|
const nextImports = new Set(requiredImports);
|
|
189
|
-
existingElements.forEach((element) => nextImports.add(element.
|
|
201
|
+
existingElements.forEach((element) => nextImports.add(element.getText(sourceFile)));
|
|
190
202
|
const nextImportStatement = `import { ${Array.from(nextImports).sort().join(', ')} } from '@ethlete/query';`;
|
|
191
203
|
return (content.slice(0, queryImportNode.getStart(sourceFile)) +
|
|
192
204
|
nextImportStatement +
|
|
@@ -241,40 +253,113 @@ const renameVariables = (content, renames) => {
|
|
|
241
253
|
});
|
|
242
254
|
return result;
|
|
243
255
|
};
|
|
256
|
+
/**
|
|
257
|
+
* What each v2 client option turns into, or why it has nowhere to go.
|
|
258
|
+
*
|
|
259
|
+
* Silently dropping these is how two apps here lost their default headers without a single warning.
|
|
260
|
+
* A dropped option that shows up in the report is a five-minute fix; one that doesn't is a bug
|
|
261
|
+
* report from production.
|
|
262
|
+
*/
|
|
263
|
+
const DROPPED_CLIENT_OPTIONS = {
|
|
264
|
+
'request.autoRefreshQueriesOnWindowFocus': 'No direct equivalent. Re-run the affected queries yourself - `withAutoRefresh({ onSignalChanges: [windowFocusSignal] })` per query, or `injectMyClient().refreshQueriesInUse()` from a focus listener.',
|
|
265
|
+
'request.enableSmartPolling': 'No direct equivalent. `withPolling({ interval })` polls unconditionally; pause it yourself when the tab is hidden if that matters.',
|
|
266
|
+
'logging.preparedQuerySubscriptions': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
267
|
+
'logging.queryStateChanges': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
268
|
+
'request.autoRefreshOn': 'No direct equivalent. Re-run the affected queries yourself with `withAutoRefresh` or `refreshQueriesInUse()`.',
|
|
269
|
+
};
|
|
270
|
+
const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, report, }) => {
|
|
271
|
+
const migrated = new Set(['baseRoute', 'queryParams', 'cacheAdapter', 'retryFn', 'request', 'logging']);
|
|
272
|
+
const raise = (optionPath, node) => {
|
|
273
|
+
report.addWarning({
|
|
274
|
+
title: `Reconfigure dropped query client option "${optionPath}"`,
|
|
275
|
+
summary: `The v2 client option \`${optionPath}\` has no place in \`createQueryClient\` and was dropped by the migration.`,
|
|
276
|
+
action: DROPPED_CLIENT_OPTIONS[optionPath] ?? 'Check whether the behaviour is still needed and reimplement it.',
|
|
277
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
|
|
278
|
+
source: 'query-client-migration',
|
|
279
|
+
dedupeKey: `dropped-client-option:${filePath}:${optionPath}`,
|
|
280
|
+
});
|
|
281
|
+
};
|
|
282
|
+
const raiseSpread = (spread) => {
|
|
283
|
+
const text = spread.getText(sourceFile);
|
|
284
|
+
report.addWarning({
|
|
285
|
+
title: `Reconfigure spread query client options "${text}"`,
|
|
286
|
+
summary: `The v2 client config spreads \`${text}\`. The migration cannot see which options it holds, so none of them were carried over to \`createQueryClient\`.`,
|
|
287
|
+
action: 'Inline the spread options that are still needed (`baseRoute` becomes `baseUrl`) and re-check the result.',
|
|
288
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(spread.getStart(sourceFile)).line + 1 }],
|
|
289
|
+
source: 'query-client-migration',
|
|
290
|
+
dedupeKey: `dropped-client-spread:${filePath}:${spread.getStart(sourceFile)}`,
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
configArgument.properties.forEach((property) => {
|
|
294
|
+
if (ts.isSpreadAssignment(property)) {
|
|
295
|
+
raiseSpread(property);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if ((!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) ||
|
|
299
|
+
!ts.isIdentifier(property.name)) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const name = property.name.text;
|
|
303
|
+
if ((name === 'request' || name === 'logging') &&
|
|
304
|
+
ts.isPropertyAssignment(property) &&
|
|
305
|
+
ts.isObjectLiteralExpression(property.initializer)) {
|
|
306
|
+
property.initializer.properties.forEach((nested) => {
|
|
307
|
+
if (ts.isSpreadAssignment(nested)) {
|
|
308
|
+
raiseSpread(nested);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if ((!ts.isPropertyAssignment(nested) && !ts.isShorthandPropertyAssignment(nested)) ||
|
|
312
|
+
!ts.isIdentifier(nested.name)) {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (name === 'request' && ['queryParams', 'cacheAdapter', 'retryFn'].includes(nested.name.text)) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
raise(`${name}.${nested.name.text}`, nested);
|
|
319
|
+
});
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (!migrated.has(name)) {
|
|
323
|
+
raise(name, property);
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
const getPropertyValueText = (property, sourceFile) => {
|
|
328
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
329
|
+
return property.name.text;
|
|
330
|
+
}
|
|
331
|
+
return ts.isPropertyAssignment(property) ? property.initializer.getText(sourceFile) : undefined;
|
|
332
|
+
};
|
|
333
|
+
const findProperty = (objectLiteral, name) => objectLiteral.properties.find((property) => {
|
|
334
|
+
return ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
|
|
335
|
+
ts.isIdentifier(property.name) &&
|
|
336
|
+
property.name.text === name);
|
|
337
|
+
});
|
|
244
338
|
const migrateConfigObject = (configArgument, node, sourceFile) => {
|
|
245
|
-
const variableName = getVariableNameForQueryClient(node);
|
|
339
|
+
const variableName = getVariableNameForQueryClient(node) ?? 'client';
|
|
246
340
|
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}`);
|
|
341
|
+
const baseRouteProperty = findProperty(configArgument, 'baseRoute');
|
|
342
|
+
const baseRouteText = baseRouteProperty && getPropertyValueText(baseRouteProperty, sourceFile);
|
|
343
|
+
if (baseRouteText) {
|
|
344
|
+
nextConfig.push(`baseUrl: ${baseRouteText.replace('as `https://${string}`', '').trim()}`);
|
|
256
345
|
}
|
|
257
346
|
nextConfig.push(`name: '${variableName}'`);
|
|
258
|
-
const requestProperty = configArgument
|
|
259
|
-
return ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'request';
|
|
260
|
-
});
|
|
347
|
+
const requestProperty = findProperty(configArgument, 'request');
|
|
261
348
|
if (requestProperty &&
|
|
262
349
|
ts.isPropertyAssignment(requestProperty) &&
|
|
263
350
|
ts.isObjectLiteralExpression(requestProperty.initializer)) {
|
|
264
|
-
requestProperty.initializer
|
|
265
|
-
|
|
266
|
-
|
|
351
|
+
const requestConfig = requestProperty.initializer;
|
|
352
|
+
for (const [from, to] of [
|
|
353
|
+
['queryParams', 'queryString'],
|
|
354
|
+
['cacheAdapter', 'cacheAdapter'],
|
|
355
|
+
['retryFn', 'retryFn'],
|
|
356
|
+
]) {
|
|
357
|
+
const property = findProperty(requestConfig, from);
|
|
358
|
+
const valueText = property && getPropertyValueText(property, sourceFile);
|
|
359
|
+
if (valueText) {
|
|
360
|
+
nextConfig.push(`${to}: ${valueText}`);
|
|
267
361
|
}
|
|
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
|
-
});
|
|
362
|
+
}
|
|
278
363
|
}
|
|
279
364
|
return `createQueryClient({\n ${nextConfig.join(',\n ')}\n})`;
|
|
280
365
|
};
|
|
@@ -286,7 +371,7 @@ const getVariableNameForQueryClient = (node) => {
|
|
|
286
371
|
}
|
|
287
372
|
parent = parent.parent;
|
|
288
373
|
}
|
|
289
|
-
return
|
|
374
|
+
return undefined;
|
|
290
375
|
};
|
|
291
376
|
const extractClientConfigNames = (content) => {
|
|
292
377
|
const sourceFile = createSourceFile(content);
|
|
@@ -326,19 +411,20 @@ const addProviderAliasesToFile = (content, configNames) => {
|
|
|
326
411
|
return;
|
|
327
412
|
}
|
|
328
413
|
const { injectName, provideName } = getProviderAliasNames(variableName);
|
|
329
|
-
if (result.includes(`export const
|
|
414
|
+
if (result.includes(`export const ${injectName} = toInjectFn(${variableName});`)) {
|
|
330
415
|
return;
|
|
331
416
|
}
|
|
332
417
|
insertions.push({
|
|
333
418
|
position,
|
|
334
|
-
text: `\n\nexport const
|
|
419
|
+
text: `\n\nexport const ${provideName} = toProvideFn(${variableName});` +
|
|
420
|
+
`\nexport const ${injectName} = toInjectFn(${variableName});`,
|
|
335
421
|
});
|
|
336
422
|
});
|
|
337
423
|
insertions.sort((left, right) => right.position - left.position);
|
|
338
424
|
insertions.forEach(({ position, text }) => {
|
|
339
425
|
result = result.slice(0, position) + text + result.slice(position);
|
|
340
426
|
});
|
|
341
|
-
return result;
|
|
427
|
+
return insertions.length > 0 ? ensureImportFromEthleteCore(result, ['toInjectFn', 'toProvideFn']) : result;
|
|
342
428
|
};
|
|
343
429
|
const getProviderAliasNames = (configVariableName) => {
|
|
344
430
|
const baseName = configVariableName.replace(/Config$/, '');
|
|
@@ -384,6 +470,19 @@ const generateCreatorsForConfig = (configName) => {
|
|
|
384
470
|
`export const ${creatorBaseName}Delete = createDeleteQuery(${configVariableName});`,
|
|
385
471
|
].join('\n');
|
|
386
472
|
};
|
|
473
|
+
const declaresName = (declarations, name) => declarations.some((declaration) => declaration.name && ts.isIdentifier(declaration.name) && declaration.name.text === name);
|
|
474
|
+
const isShadowedByLocalDeclaration = (identifier) => {
|
|
475
|
+
for (let scope = identifier.parent; scope && !ts.isSourceFile(scope); scope = scope.parent) {
|
|
476
|
+
if (ts.isFunctionLike(scope) && declaresName(scope.parameters, identifier.text)) {
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
if (ts.isBlock(scope) &&
|
|
480
|
+
scope.statements.some((statement) => ts.isVariableStatement(statement) && declaresName(statement.declarationList.declarations, identifier.text))) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return false;
|
|
485
|
+
};
|
|
387
486
|
const updateImportsInFile = (content, renames) => {
|
|
388
487
|
const sourceFile = createSourceFile(content);
|
|
389
488
|
const replacements = [];
|
|
@@ -394,7 +493,7 @@ const updateImportsInFile = (content, renames) => {
|
|
|
394
493
|
const nextElements = [];
|
|
395
494
|
let hasChanges = false;
|
|
396
495
|
node.importClause.namedBindings.elements.forEach((element) => {
|
|
397
|
-
const nextName = renames.get(element.name.text);
|
|
496
|
+
const nextName = renames.get(element.propertyName?.text ?? element.name.text);
|
|
398
497
|
if (!nextName) {
|
|
399
498
|
nextElements.push(element.getText(sourceFile));
|
|
400
499
|
return;
|
|
@@ -445,7 +544,7 @@ const updateImportsInFile = (content, renames) => {
|
|
|
445
544
|
ts.forEachChild(node, visit);
|
|
446
545
|
return;
|
|
447
546
|
}
|
|
448
|
-
if (ts.isPropertyAssignment(parent) && parent.name === node) {
|
|
547
|
+
if ((ts.isPropertyAssignment(parent) && parent.name === node) || isShadowedByLocalDeclaration(node)) {
|
|
449
548
|
ts.forEachChild(node, visit);
|
|
450
549
|
return;
|
|
451
550
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { createSourceFile } from './shared.js';
|
|
3
|
+
/**
|
|
4
|
+
* Whether an identifier is a *name* rather than a *reference* - a declaration, a property key, the
|
|
5
|
+
* right-hand side of a member access. Renaming those is what turned `PeopleDetailDataSource.getPerson`
|
|
6
|
+
* and the `postLogin` property of a config type into `legacy…` during the first migration run.
|
|
7
|
+
*/
|
|
8
|
+
const isNamePosition = (node) => {
|
|
9
|
+
const parent = node.parent;
|
|
10
|
+
if (!parent)
|
|
11
|
+
return false;
|
|
12
|
+
// `foo.bar` / `Foo.Bar` - the part after the dot is never the imported binding.
|
|
13
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node)
|
|
14
|
+
return true;
|
|
15
|
+
if (ts.isQualifiedName(parent) && parent.right === node)
|
|
16
|
+
return true;
|
|
17
|
+
// Object literal / type member keys, enum members, JSX-ish attribute names.
|
|
18
|
+
if (ts.isPropertyAssignment(parent) && parent.name === node)
|
|
19
|
+
return true;
|
|
20
|
+
if (ts.isPropertySignature(parent) && parent.name === node)
|
|
21
|
+
return true;
|
|
22
|
+
if (ts.isMethodSignature(parent) && parent.name === node)
|
|
23
|
+
return true;
|
|
24
|
+
if (ts.isEnumMember(parent) && parent.name === node)
|
|
25
|
+
return true;
|
|
26
|
+
if (ts.isBindingElement(parent) && parent.propertyName === node)
|
|
27
|
+
return true;
|
|
28
|
+
// Declarations of any kind.
|
|
29
|
+
if (ts.isVariableDeclaration(parent) && parent.name === node)
|
|
30
|
+
return true;
|
|
31
|
+
if (ts.isFunctionDeclaration(parent) && parent.name === node)
|
|
32
|
+
return true;
|
|
33
|
+
if (ts.isClassDeclaration(parent) && parent.name === node)
|
|
34
|
+
return true;
|
|
35
|
+
if (ts.isInterfaceDeclaration(parent) && parent.name === node)
|
|
36
|
+
return true;
|
|
37
|
+
if (ts.isTypeAliasDeclaration(parent) && parent.name === node)
|
|
38
|
+
return true;
|
|
39
|
+
if (ts.isEnumDeclaration(parent) && parent.name === node)
|
|
40
|
+
return true;
|
|
41
|
+
if (ts.isModuleDeclaration(parent) && parent.name === node)
|
|
42
|
+
return true;
|
|
43
|
+
if (ts.isMethodDeclaration(parent) && parent.name === node)
|
|
44
|
+
return true;
|
|
45
|
+
if (ts.isPropertyDeclaration(parent) && parent.name === node)
|
|
46
|
+
return true;
|
|
47
|
+
if (ts.isGetAccessorDeclaration(parent) && parent.name === node)
|
|
48
|
+
return true;
|
|
49
|
+
if (ts.isSetAccessorDeclaration(parent) && parent.name === node)
|
|
50
|
+
return true;
|
|
51
|
+
if (ts.isParameter(parent) && parent.name === node)
|
|
52
|
+
return true;
|
|
53
|
+
if (ts.isTypeParameterDeclaration(parent) && parent.name === node)
|
|
54
|
+
return true;
|
|
55
|
+
if (ts.isLabeledStatement(parent) && parent.label === node)
|
|
56
|
+
return true;
|
|
57
|
+
// Import / export clauses are rewritten by the caller, never by the reference pass.
|
|
58
|
+
if (ts.isImportSpecifier(parent))
|
|
59
|
+
return true;
|
|
60
|
+
if (ts.isExportSpecifier(parent) &&
|
|
61
|
+
(parent.parent.parent.moduleSpecifier || (parent.propertyName && parent.name === node))) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (ts.isImportClause(parent) || ts.isNamespaceImport(parent))
|
|
65
|
+
return true;
|
|
66
|
+
return false;
|
|
67
|
+
};
|
|
68
|
+
const renamedReference = (node, nextName) => {
|
|
69
|
+
const parent = node.parent;
|
|
70
|
+
if (ts.isShorthandPropertyAssignment(parent))
|
|
71
|
+
return `${node.text}: ${nextName}`;
|
|
72
|
+
if (ts.isExportSpecifier(parent) && !parent.propertyName)
|
|
73
|
+
return `${nextName} as ${node.text}`;
|
|
74
|
+
return nextName;
|
|
75
|
+
};
|
|
76
|
+
/** Every name the file declares itself - a local by the same name means the import is shadowed. */
|
|
77
|
+
const collectLocalDeclarations = (sourceFile) => {
|
|
78
|
+
const declared = new Set();
|
|
79
|
+
const add = (name) => {
|
|
80
|
+
if (name && ts.isIdentifier(name))
|
|
81
|
+
declared.add(name.text);
|
|
82
|
+
};
|
|
83
|
+
const visit = (node) => {
|
|
84
|
+
if (ts.isVariableDeclaration(node))
|
|
85
|
+
add(node.name);
|
|
86
|
+
if (ts.isFunctionDeclaration(node))
|
|
87
|
+
add(node.name);
|
|
88
|
+
if (ts.isClassDeclaration(node))
|
|
89
|
+
add(node.name);
|
|
90
|
+
if (ts.isInterfaceDeclaration(node))
|
|
91
|
+
add(node.name);
|
|
92
|
+
if (ts.isTypeAliasDeclaration(node))
|
|
93
|
+
add(node.name);
|
|
94
|
+
if (ts.isEnumDeclaration(node))
|
|
95
|
+
add(node.name);
|
|
96
|
+
if (ts.isParameter(node))
|
|
97
|
+
add(node.name);
|
|
98
|
+
if (ts.isBindingElement(node))
|
|
99
|
+
add(node.name);
|
|
100
|
+
ts.forEachChild(node, visit);
|
|
101
|
+
};
|
|
102
|
+
visit(sourceFile);
|
|
103
|
+
return declared;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Renames references to imported bindings, and only those.
|
|
107
|
+
*
|
|
108
|
+
* `renames` maps a *local binding name* to its new name. The caller is responsible for having
|
|
109
|
+
* verified that the binding really is the symbol being migrated (see `ModuleGraph`); this pass only
|
|
110
|
+
* makes sure the rewrite lands on references rather than on same-named properties, declarations or
|
|
111
|
+
* type members. A name the file also declares locally is skipped entirely and reported back, since
|
|
112
|
+
* a rename there would silently change which value the code reads.
|
|
113
|
+
*/
|
|
114
|
+
export const renameImportedReferences = (content, renames) => {
|
|
115
|
+
if (renames.size === 0)
|
|
116
|
+
return { content, shadowed: [] };
|
|
117
|
+
const sourceFile = createSourceFile(content);
|
|
118
|
+
const declaredLocally = collectLocalDeclarations(sourceFile);
|
|
119
|
+
const shadowed = Array.from(renames.keys()).filter((name) => declaredLocally.has(name));
|
|
120
|
+
const applicable = new Map(Array.from(renames.entries()).filter(([name]) => !declaredLocally.has(name)));
|
|
121
|
+
if (applicable.size === 0)
|
|
122
|
+
return { content, shadowed };
|
|
123
|
+
const replacements = [];
|
|
124
|
+
const visit = (node) => {
|
|
125
|
+
if (ts.isIdentifier(node)) {
|
|
126
|
+
const nextName = applicable.get(node.text);
|
|
127
|
+
if (nextName && !isNamePosition(node)) {
|
|
128
|
+
replacements.push({
|
|
129
|
+
start: node.getStart(sourceFile),
|
|
130
|
+
end: node.getEnd(),
|
|
131
|
+
replacement: renamedReference(node, nextName),
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
ts.forEachChild(node, visit);
|
|
136
|
+
};
|
|
137
|
+
visit(sourceFile);
|
|
138
|
+
let result = content;
|
|
139
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
140
|
+
replacements.forEach(({ start, end, replacement }) => {
|
|
141
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
142
|
+
});
|
|
143
|
+
return { content: result, shadowed };
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Drops named imports whose local name no longer appears anywhere else in the file.
|
|
147
|
+
*
|
|
148
|
+
* The migration rewrites away the constructs that needed `def`, `AnyLegacyQueryCreator` and friends;
|
|
149
|
+
* leaving the imports behind turns every touched file into a lint error, and `formatFiles` runs in
|
|
150
|
+
* the same generator invocation so they land that way. Identifiers inside template strings count as
|
|
151
|
+
* usage too - a component referenced only from an inline template must not be pruned.
|
|
152
|
+
*/
|
|
153
|
+
export const pruneUnusedNamedImports = (content) => {
|
|
154
|
+
const sourceFile = createSourceFile(content);
|
|
155
|
+
const used = new Set();
|
|
156
|
+
const addWordsFrom = (text) => {
|
|
157
|
+
for (const word of text.match(/[A-Za-z_$][\w$]*/g) ?? []) {
|
|
158
|
+
used.add(word);
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
const visit = (node) => {
|
|
162
|
+
if (ts.isIdentifier(node) && !ts.isImportSpecifier(node.parent) && !ts.isImportClause(node.parent)) {
|
|
163
|
+
used.add(node.text);
|
|
164
|
+
}
|
|
165
|
+
// A symbol referenced only from an inline template or a `styleUrl`-style string is still used.
|
|
166
|
+
if (ts.isStringLiteralLike(node) && !ts.isImportDeclaration(node.parent)) {
|
|
167
|
+
addWordsFrom(node.text);
|
|
168
|
+
}
|
|
169
|
+
if (ts.isTemplateLiteralToken(node)) {
|
|
170
|
+
addWordsFrom(node.text);
|
|
171
|
+
}
|
|
172
|
+
ts.forEachChild(node, visit);
|
|
173
|
+
};
|
|
174
|
+
visit(sourceFile);
|
|
175
|
+
const replacements = [];
|
|
176
|
+
for (const node of sourceFile.statements) {
|
|
177
|
+
if (!ts.isImportDeclaration(node) ||
|
|
178
|
+
!ts.isStringLiteral(node.moduleSpecifier) ||
|
|
179
|
+
!node.importClause?.namedBindings ||
|
|
180
|
+
!ts.isNamedImports(node.importClause.namedBindings)) {
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
const kept = node.importClause.namedBindings.elements.filter((element) => used.has(element.name.text));
|
|
184
|
+
if (kept.length === node.importClause.namedBindings.elements.length)
|
|
185
|
+
continue;
|
|
186
|
+
const hasDefaultImport = !!node.importClause.name;
|
|
187
|
+
if (kept.length === 0 && !hasDefaultImport) {
|
|
188
|
+
const end = content[node.getEnd()] === '\n' ? node.getEnd() + 1 : node.getEnd();
|
|
189
|
+
replacements.push({ start: node.getStart(sourceFile), end, replacement: '' });
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const defaultPart = hasDefaultImport ? `${node.importClause.name.text}, ` : '';
|
|
193
|
+
replacements.push({
|
|
194
|
+
start: node.getStart(sourceFile),
|
|
195
|
+
end: node.getEnd(),
|
|
196
|
+
replacement: `import ${defaultPart}{ ${kept.map((element) => element.getText(sourceFile)).join(', ')} } from '${node.moduleSpecifier.text}';`,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
let result = content;
|
|
200
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
201
|
+
replacements.forEach(({ start, end, replacement }) => {
|
|
202
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
203
|
+
});
|
|
204
|
+
return result;
|
|
205
|
+
};
|
|
206
|
+
//# sourceMappingURL=rename-symbols.js.map
|