@ethlete/query 6.0.0-next.4 ā 6.0.0-next.41
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 +593 -0
- package/fesm2022/ethlete-query-testing.mjs +314 -5
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +13767 -4881
- 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 +182 -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 +131 -0
- package/generators/migrate-to-query-v3/cleanup-migration.js +128 -77
- package/generators/migrate-to-query-v3/http-client-check.js +93 -0
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +120 -28
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +158 -139
- package/generators/migrate-to-query-v3/migration-scope.js +41 -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 +58 -9
- package/generators/migrate-to-query-v3/rename-symbols.js +193 -0
- package/generators/migrate-to-query-v3/schema.json +12 -0
- package/generators/migrate-to-query-v3/shared.js +45 -0
- package/generators/prep-for-query-v3/migration.js +3 -3
- package/package.json +26 -13
- package/types/ethlete-query-testing.d.ts +145 -12
- package/types/ethlete-query.d.ts +8429 -3138
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { getProjects, visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
|
+
const stripTrailingSlash = (value) => (value.endsWith('/') ? value.slice(0, -1) : value);
|
|
3
|
+
/**
|
|
4
|
+
* Limits the migration to a set of projects or paths.
|
|
5
|
+
*
|
|
6
|
+
* Rewriting every app in a monorepo in one run is rarely how adoption actually goes, and a 300-file
|
|
7
|
+
* diff is not reviewable. Note the one coupling that cannot be split: a query client and the
|
|
8
|
+
* creators built on it have to be migrated together, because the creators are rewritten in terms of
|
|
9
|
+
* the client's generated `xGet` / `xPostSecure` helpers.
|
|
10
|
+
*/
|
|
11
|
+
export const createMigrationScope = (tree, options) => {
|
|
12
|
+
const roots = [];
|
|
13
|
+
for (const projectName of options.projects ?? []) {
|
|
14
|
+
const project = getProjects(tree).get(projectName);
|
|
15
|
+
if (!project) {
|
|
16
|
+
throw new Error(`Unknown project "${projectName}". Pass names as they appear in \`nx show projects\`, or use --include with a path prefix.`);
|
|
17
|
+
}
|
|
18
|
+
roots.push(stripTrailingSlash(project.root));
|
|
19
|
+
}
|
|
20
|
+
for (const includePath of options.include ?? []) {
|
|
21
|
+
roots.push(stripTrailingSlash(includePath));
|
|
22
|
+
}
|
|
23
|
+
if (roots.length === 0) {
|
|
24
|
+
return {
|
|
25
|
+
visit: (targetTree, callback) => visitNotIgnoredFiles(targetTree, '', callback),
|
|
26
|
+
includes: () => true,
|
|
27
|
+
describe: () => 'the whole workspace',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const includes = (filePath) => roots.some((root) => filePath === root || filePath.startsWith(`${root}/`));
|
|
31
|
+
return {
|
|
32
|
+
visit: (targetTree, callback) => {
|
|
33
|
+
for (const root of roots) {
|
|
34
|
+
visitNotIgnoredFiles(targetTree, root, callback);
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
includes,
|
|
38
|
+
describe: () => roots.join(', '),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=migration-scope.js.map
|
|
@@ -1,26 +1,34 @@
|
|
|
1
1
|
import { formatFiles } from '@nx/devkit';
|
|
2
|
-
import {
|
|
2
|
+
import { migrateDevtoolsUsage, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy } from './cleanup-migration.js';
|
|
3
|
+
import { reportDefaultHeaderUsages, reportMissingHttpClientProviders } from './http-client-check.js';
|
|
3
4
|
import { migrateLegacyPrepareCalls } from './legacy-prepare-migration.js';
|
|
4
5
|
import { createNewQueryCreators, updateLegacyCreatorImportsAndUsages } from './legacy-query-creator-migration.js';
|
|
6
|
+
import { createMigrationScope } from './migration-scope.js';
|
|
5
7
|
import { generateProviderAliases, generateQueryCreators, migrateQueryClients, updateImportsAcrossWorkspace, } from './query-client-migration.js';
|
|
6
8
|
import { QueryV3MigrationReport } from './report.js';
|
|
7
9
|
export default async function migrate(tree, schema) {
|
|
8
10
|
console.log('\nš Starting query v3 migration...');
|
|
11
|
+
const scope = createMigrationScope(tree, schema);
|
|
12
|
+
console.log(` Scope: ${scope.describe()}`);
|
|
9
13
|
const report = new QueryV3MigrationReport();
|
|
10
|
-
const { queryClientFiles, variableRenames } = migrateQueryClients(tree, report);
|
|
14
|
+
const { queryClientFiles, variableRenames } = migrateQueryClients(tree, report, scope);
|
|
11
15
|
if (queryClientFiles.size > 0) {
|
|
12
16
|
generateProviderAliases(tree, queryClientFiles);
|
|
13
17
|
generateQueryCreators(tree, queryClientFiles);
|
|
14
|
-
createNewQueryCreators(tree, queryClientFiles, report);
|
|
15
|
-
updateLegacyCreatorImportsAndUsages(tree);
|
|
18
|
+
createNewQueryCreators(tree, queryClientFiles, report, scope);
|
|
19
|
+
updateLegacyCreatorImportsAndUsages(tree, scope, report);
|
|
16
20
|
}
|
|
17
21
|
if (variableRenames.size > 0) {
|
|
18
|
-
updateImportsAcrossWorkspace(tree, variableRenames);
|
|
22
|
+
updateImportsAcrossWorkspace(tree, variableRenames, scope);
|
|
23
|
+
}
|
|
24
|
+
replaceAnyQueryWithLegacy(tree, scope);
|
|
25
|
+
migrateDevtoolsUsage(tree, scope, report);
|
|
26
|
+
migrateEmptyPrepareCalls(tree, scope);
|
|
27
|
+
migrateLegacyPrepareCalls(tree, report, scope);
|
|
28
|
+
if (queryClientFiles.size > 0) {
|
|
29
|
+
reportMissingHttpClientProviders(tree, report, scope);
|
|
30
|
+
reportDefaultHeaderUsages(tree, report, scope);
|
|
19
31
|
}
|
|
20
|
-
replaceAnyQueryWithLegacy(tree);
|
|
21
|
-
removeDevtoolsUsage(tree);
|
|
22
|
-
migrateEmptyPrepareCalls(tree);
|
|
23
|
-
migrateLegacyPrepareCalls(tree, report);
|
|
24
32
|
report.writeToTree(tree);
|
|
25
33
|
report.printSummary();
|
|
26
34
|
if (!schema.skipFormat) {
|
|
@@ -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
|
}
|
|
@@ -143,6 +142,7 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
143
142
|
renamesOut.set(oldVariableName, newVariableName);
|
|
144
143
|
}
|
|
145
144
|
}
|
|
145
|
+
reportDroppedClientOptions({ configArgument, sourceFile, filePath, report });
|
|
146
146
|
replacements.push({
|
|
147
147
|
start: node.getStart(sourceFile),
|
|
148
148
|
end: node.getEnd(),
|
|
@@ -241,6 +241,54 @@ const renameVariables = (content, renames) => {
|
|
|
241
241
|
});
|
|
242
242
|
return result;
|
|
243
243
|
};
|
|
244
|
+
/**
|
|
245
|
+
* What each v2 client option turns into, or why it has nowhere to go.
|
|
246
|
+
*
|
|
247
|
+
* Silently dropping these is how two apps here lost their default headers without a single warning.
|
|
248
|
+
* A dropped option that shows up in the report is a five-minute fix; one that doesn't is a bug
|
|
249
|
+
* report from production.
|
|
250
|
+
*/
|
|
251
|
+
const DROPPED_CLIENT_OPTIONS = {
|
|
252
|
+
'request.autoRefreshQueriesOnWindowFocus': 'No direct equivalent. Re-run the affected queries yourself - `withAutoRefresh({ onSignalChanges: [windowFocusSignal] })` per query, or `injectMyClient().refreshQueriesInUse()` from a focus listener.',
|
|
253
|
+
'request.enableSmartPolling': 'No direct equivalent. `withPolling({ interval })` polls unconditionally; pause it yourself when the tab is hidden if that matters.',
|
|
254
|
+
'logging.preparedQuerySubscriptions': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
255
|
+
'logging.queryStateChanges': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
256
|
+
'request.autoRefreshOn': 'No direct equivalent. Re-run the affected queries yourself with `withAutoRefresh` or `refreshQueriesInUse()`.',
|
|
257
|
+
};
|
|
258
|
+
const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, report, }) => {
|
|
259
|
+
const migrated = new Set(['baseRoute', 'queryParams', 'cacheAdapter', 'retryFn', 'request', 'logging']);
|
|
260
|
+
const raise = (optionPath, node) => {
|
|
261
|
+
report.addWarning({
|
|
262
|
+
title: `Reconfigure dropped query client option "${optionPath}"`,
|
|
263
|
+
summary: `The v2 client option \`${optionPath}\` has no place in \`createQueryClient\` and was dropped by the migration.`,
|
|
264
|
+
action: DROPPED_CLIENT_OPTIONS[optionPath] ?? 'Check whether the behaviour is still needed and reimplement it.',
|
|
265
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
|
|
266
|
+
source: 'query-client-migration',
|
|
267
|
+
dedupeKey: `dropped-client-option:${filePath}:${optionPath}`,
|
|
268
|
+
});
|
|
269
|
+
};
|
|
270
|
+
configArgument.properties.forEach((property) => {
|
|
271
|
+
if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
const name = property.name.text;
|
|
275
|
+
if ((name === 'request' || name === 'logging') && ts.isObjectLiteralExpression(property.initializer)) {
|
|
276
|
+
property.initializer.properties.forEach((nested) => {
|
|
277
|
+
if (!ts.isPropertyAssignment(nested) || !ts.isIdentifier(nested.name)) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (name === 'request' && ['queryParams', 'cacheAdapter', 'retryFn'].includes(nested.name.text)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
raise(`${name}.${nested.name.text}`, nested);
|
|
284
|
+
});
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (!migrated.has(name)) {
|
|
288
|
+
raise(name, property);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
};
|
|
244
292
|
const migrateConfigObject = (configArgument, node, sourceFile) => {
|
|
245
293
|
const variableName = getVariableNameForQueryClient(node);
|
|
246
294
|
const nextConfig = [];
|
|
@@ -326,19 +374,20 @@ const addProviderAliasesToFile = (content, configNames) => {
|
|
|
326
374
|
return;
|
|
327
375
|
}
|
|
328
376
|
const { injectName, provideName } = getProviderAliasNames(variableName);
|
|
329
|
-
if (result.includes(`export const
|
|
377
|
+
if (result.includes(`export const ${injectName} = toInjectFn(${variableName});`)) {
|
|
330
378
|
return;
|
|
331
379
|
}
|
|
332
380
|
insertions.push({
|
|
333
381
|
position,
|
|
334
|
-
text: `\n\nexport const
|
|
382
|
+
text: `\n\nexport const ${provideName} = toProvideFn(${variableName});` +
|
|
383
|
+
`\nexport const ${injectName} = toInjectFn(${variableName});`,
|
|
335
384
|
});
|
|
336
385
|
});
|
|
337
386
|
insertions.sort((left, right) => right.position - left.position);
|
|
338
387
|
insertions.forEach(({ position, text }) => {
|
|
339
388
|
result = result.slice(0, position) + text + result.slice(position);
|
|
340
389
|
});
|
|
341
|
-
return result;
|
|
390
|
+
return insertions.length > 0 ? ensureImportFromEthleteCore(result, ['toInjectFn', 'toProvideFn']) : result;
|
|
342
391
|
};
|
|
343
392
|
const getProviderAliasNames = (configVariableName) => {
|
|
344
393
|
const baseName = configVariableName.replace(/Config$/, '');
|
|
@@ -0,0 +1,193 @@
|
|
|
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.isShorthandPropertyAssignment(parent) && parent.name === node)
|
|
21
|
+
return true;
|
|
22
|
+
if (ts.isPropertySignature(parent) && parent.name === node)
|
|
23
|
+
return true;
|
|
24
|
+
if (ts.isMethodSignature(parent) && parent.name === node)
|
|
25
|
+
return true;
|
|
26
|
+
if (ts.isEnumMember(parent) && parent.name === node)
|
|
27
|
+
return true;
|
|
28
|
+
if (ts.isBindingElement(parent) && parent.propertyName === node)
|
|
29
|
+
return true;
|
|
30
|
+
// Declarations of any kind.
|
|
31
|
+
if (ts.isVariableDeclaration(parent) && parent.name === node)
|
|
32
|
+
return true;
|
|
33
|
+
if (ts.isFunctionDeclaration(parent) && parent.name === node)
|
|
34
|
+
return true;
|
|
35
|
+
if (ts.isClassDeclaration(parent) && parent.name === node)
|
|
36
|
+
return true;
|
|
37
|
+
if (ts.isInterfaceDeclaration(parent) && parent.name === node)
|
|
38
|
+
return true;
|
|
39
|
+
if (ts.isTypeAliasDeclaration(parent) && parent.name === node)
|
|
40
|
+
return true;
|
|
41
|
+
if (ts.isEnumDeclaration(parent) && parent.name === node)
|
|
42
|
+
return true;
|
|
43
|
+
if (ts.isModuleDeclaration(parent) && parent.name === node)
|
|
44
|
+
return true;
|
|
45
|
+
if (ts.isMethodDeclaration(parent) && parent.name === node)
|
|
46
|
+
return true;
|
|
47
|
+
if (ts.isPropertyDeclaration(parent) && parent.name === node)
|
|
48
|
+
return true;
|
|
49
|
+
if (ts.isGetAccessorDeclaration(parent) && parent.name === node)
|
|
50
|
+
return true;
|
|
51
|
+
if (ts.isSetAccessorDeclaration(parent) && parent.name === node)
|
|
52
|
+
return true;
|
|
53
|
+
if (ts.isParameter(parent) && parent.name === node)
|
|
54
|
+
return true;
|
|
55
|
+
if (ts.isTypeParameterDeclaration(parent) && parent.name === node)
|
|
56
|
+
return true;
|
|
57
|
+
if (ts.isLabeledStatement(parent) && parent.label === node)
|
|
58
|
+
return true;
|
|
59
|
+
// Import / export clauses are rewritten by the caller, never by the reference pass.
|
|
60
|
+
if (ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent))
|
|
61
|
+
return true;
|
|
62
|
+
if (ts.isImportClause(parent) || ts.isNamespaceImport(parent))
|
|
63
|
+
return true;
|
|
64
|
+
return false;
|
|
65
|
+
};
|
|
66
|
+
/** Every name the file declares itself - a local by the same name means the import is shadowed. */
|
|
67
|
+
const collectLocalDeclarations = (sourceFile) => {
|
|
68
|
+
const declared = new Set();
|
|
69
|
+
const add = (name) => {
|
|
70
|
+
if (name && ts.isIdentifier(name))
|
|
71
|
+
declared.add(name.text);
|
|
72
|
+
};
|
|
73
|
+
const visit = (node) => {
|
|
74
|
+
if (ts.isVariableDeclaration(node))
|
|
75
|
+
add(node.name);
|
|
76
|
+
if (ts.isFunctionDeclaration(node))
|
|
77
|
+
add(node.name);
|
|
78
|
+
if (ts.isClassDeclaration(node))
|
|
79
|
+
add(node.name);
|
|
80
|
+
if (ts.isInterfaceDeclaration(node))
|
|
81
|
+
add(node.name);
|
|
82
|
+
if (ts.isTypeAliasDeclaration(node))
|
|
83
|
+
add(node.name);
|
|
84
|
+
if (ts.isEnumDeclaration(node))
|
|
85
|
+
add(node.name);
|
|
86
|
+
if (ts.isParameter(node))
|
|
87
|
+
add(node.name);
|
|
88
|
+
if (ts.isBindingElement(node))
|
|
89
|
+
add(node.name);
|
|
90
|
+
ts.forEachChild(node, visit);
|
|
91
|
+
};
|
|
92
|
+
visit(sourceFile);
|
|
93
|
+
return declared;
|
|
94
|
+
};
|
|
95
|
+
/**
|
|
96
|
+
* Renames references to imported bindings, and only those.
|
|
97
|
+
*
|
|
98
|
+
* `renames` maps a *local binding name* to its new name. The caller is responsible for having
|
|
99
|
+
* verified that the binding really is the symbol being migrated (see `ModuleGraph`); this pass only
|
|
100
|
+
* makes sure the rewrite lands on references rather than on same-named properties, declarations or
|
|
101
|
+
* type members. A name the file also declares locally is skipped entirely and reported back, since
|
|
102
|
+
* a rename there would silently change which value the code reads.
|
|
103
|
+
*/
|
|
104
|
+
export const renameImportedReferences = (content, renames) => {
|
|
105
|
+
if (renames.size === 0)
|
|
106
|
+
return { content, shadowed: [] };
|
|
107
|
+
const sourceFile = createSourceFile(content);
|
|
108
|
+
const declaredLocally = collectLocalDeclarations(sourceFile);
|
|
109
|
+
const shadowed = Array.from(renames.keys()).filter((name) => declaredLocally.has(name));
|
|
110
|
+
const applicable = new Map(Array.from(renames.entries()).filter(([name]) => !declaredLocally.has(name)));
|
|
111
|
+
if (applicable.size === 0)
|
|
112
|
+
return { content, shadowed };
|
|
113
|
+
const replacements = [];
|
|
114
|
+
const visit = (node) => {
|
|
115
|
+
if (ts.isIdentifier(node)) {
|
|
116
|
+
const nextName = applicable.get(node.text);
|
|
117
|
+
if (nextName && !isNamePosition(node)) {
|
|
118
|
+
replacements.push({ start: node.getStart(sourceFile), end: node.getEnd(), replacement: nextName });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
ts.forEachChild(node, visit);
|
|
122
|
+
};
|
|
123
|
+
visit(sourceFile);
|
|
124
|
+
let result = content;
|
|
125
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
126
|
+
replacements.forEach(({ start, end, replacement }) => {
|
|
127
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
128
|
+
});
|
|
129
|
+
return { content: result, shadowed };
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Drops named imports whose local name no longer appears anywhere else in the file.
|
|
133
|
+
*
|
|
134
|
+
* The migration rewrites away the constructs that needed `def`, `AnyLegacyQueryCreator` and friends;
|
|
135
|
+
* leaving the imports behind turns every touched file into a lint error, and `formatFiles` runs in
|
|
136
|
+
* the same generator invocation so they land that way. Identifiers inside template strings count as
|
|
137
|
+
* usage too - a component referenced only from an inline template must not be pruned.
|
|
138
|
+
*/
|
|
139
|
+
export const pruneUnusedNamedImports = (content, moduleSpecifiers) => {
|
|
140
|
+
const sourceFile = createSourceFile(content);
|
|
141
|
+
const used = new Set();
|
|
142
|
+
const addWordsFrom = (text) => {
|
|
143
|
+
for (const word of text.match(/[A-Za-z_$][\w$]*/g) ?? []) {
|
|
144
|
+
used.add(word);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const visit = (node) => {
|
|
148
|
+
if (ts.isIdentifier(node) && !ts.isImportSpecifier(node.parent) && !ts.isImportClause(node.parent)) {
|
|
149
|
+
used.add(node.text);
|
|
150
|
+
}
|
|
151
|
+
// A symbol referenced only from an inline template or a `styleUrl`-style string is still used.
|
|
152
|
+
if (ts.isStringLiteralLike(node) && !ts.isImportDeclaration(node.parent)) {
|
|
153
|
+
addWordsFrom(node.text);
|
|
154
|
+
}
|
|
155
|
+
if (ts.isTemplateLiteralToken(node)) {
|
|
156
|
+
addWordsFrom(node.text);
|
|
157
|
+
}
|
|
158
|
+
ts.forEachChild(node, visit);
|
|
159
|
+
};
|
|
160
|
+
visit(sourceFile);
|
|
161
|
+
const replacements = [];
|
|
162
|
+
for (const node of sourceFile.statements) {
|
|
163
|
+
if (!ts.isImportDeclaration(node) ||
|
|
164
|
+
!ts.isStringLiteral(node.moduleSpecifier) ||
|
|
165
|
+
(moduleSpecifiers !== 'all' && !moduleSpecifiers.includes(node.moduleSpecifier.text)) ||
|
|
166
|
+
!node.importClause?.namedBindings ||
|
|
167
|
+
!ts.isNamedImports(node.importClause.namedBindings)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const kept = node.importClause.namedBindings.elements.filter((element) => used.has(element.name.text));
|
|
171
|
+
if (kept.length === node.importClause.namedBindings.elements.length)
|
|
172
|
+
continue;
|
|
173
|
+
const hasDefaultImport = !!node.importClause.name;
|
|
174
|
+
if (kept.length === 0 && !hasDefaultImport) {
|
|
175
|
+
const end = content[node.getEnd()] === '\n' ? node.getEnd() + 1 : node.getEnd();
|
|
176
|
+
replacements.push({ start: node.getStart(sourceFile), end, replacement: '' });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const defaultPart = hasDefaultImport ? `${node.importClause.name.text}, ` : '';
|
|
180
|
+
replacements.push({
|
|
181
|
+
start: node.getStart(sourceFile),
|
|
182
|
+
end: node.getEnd(),
|
|
183
|
+
replacement: `import ${defaultPart}{ ${kept.map((element) => element.getText(sourceFile)).join(', ')} } from '${node.moduleSpecifier.text}';`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
let result = content;
|
|
187
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
188
|
+
replacements.forEach(({ start, end, replacement }) => {
|
|
189
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
190
|
+
});
|
|
191
|
+
return result;
|
|
192
|
+
};
|
|
193
|
+
//# sourceMappingURL=rename-symbols.js.map
|
|
@@ -4,6 +4,18 @@
|
|
|
4
4
|
"title": "Migrate to Query v3",
|
|
5
5
|
"type": "object",
|
|
6
6
|
"properties": {
|
|
7
|
+
"projects": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": { "type": "string" },
|
|
10
|
+
"description": "Nx projects to migrate. Defaults to every project in the workspace. Use this to adopt v3 one app or library at a time instead of rewriting the whole monorepo in one run - but keep a query client and the creators built from it in the same run, since they are migrated together.",
|
|
11
|
+
"alias": "project"
|
|
12
|
+
},
|
|
13
|
+
"include": {
|
|
14
|
+
"type": "array",
|
|
15
|
+
"items": { "type": "string" },
|
|
16
|
+
"description": "Path prefixes to migrate, relative to the workspace root (e.g. 'libs/queries'). Unioned with `projects`. Defaults to the whole workspace.",
|
|
17
|
+
"alias": "includePath"
|
|
18
|
+
},
|
|
7
19
|
"skipFormat": {
|
|
8
20
|
"type": "boolean",
|
|
9
21
|
"description": "Skip formatting files after migration",
|
|
@@ -55,6 +55,8 @@ export const ensureNamedImports = ({ content, importsNeeded, moduleSpecifier })
|
|
|
55
55
|
};
|
|
56
56
|
export const ensureImportFromQuery = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@ethlete/query' });
|
|
57
57
|
export const ensureAngularCoreImports = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@angular/core' });
|
|
58
|
+
/** `toProvideFn` / `toInjectFn` - the provider-definition extractors every generated ref pair needs. */
|
|
59
|
+
export const ensureImportFromEthleteCore = (content, importsNeeded) => ensureNamedImports({ content, importsNeeded, moduleSpecifier: '@ethlete/core' });
|
|
58
60
|
export const getVariableStatementEnd = (sourceFile, variableName, initializerName) => {
|
|
59
61
|
let variableStatementEnd;
|
|
60
62
|
const visit = (node) => {
|
|
@@ -94,4 +96,47 @@ export const getLineNumber = (node, sourceFile) => {
|
|
|
94
96
|
return line + 1;
|
|
95
97
|
};
|
|
96
98
|
export const getLineNumberFromPosition = (content, position) => content.slice(0, position).split('\n').length;
|
|
99
|
+
const MIGRATION_GUIDE_URL = 'https://ethlete-sdk-docs.web.app/query/migrating-from-v2';
|
|
100
|
+
/**
|
|
101
|
+
* The tag every `createLegacyQueryCreator(ā¦)` wrapper carries. The wrapper is a migration
|
|
102
|
+
* scaffold, so marking it is what turns "which call sites are still on v2" from a grep into
|
|
103
|
+
* strikethrough in the editor.
|
|
104
|
+
*/
|
|
105
|
+
export const legacyQueryDeprecationTag = (creatorName) => `@deprecated Legacy (v2) query wrapper. Migrate the call sites to ${creatorName ? `\`${creatorName}\`` : 'the query creator it wraps'} and delete this wrapper - see ${MIGRATION_GUIDE_URL}.`;
|
|
106
|
+
/**
|
|
107
|
+
* Adds a tag to `node`'s JSDoc, creating the block when there is none. Returns `null` when the
|
|
108
|
+
* tag is already there, so a caller can tell "nothing to do" from "rewritten".
|
|
109
|
+
*
|
|
110
|
+
* Leading `//` comments stay glued to the node - an `eslint-disable-next-line` only works from
|
|
111
|
+
* the line directly above.
|
|
112
|
+
*/
|
|
113
|
+
export const addJsDocTag = (content, sourceFile, node, tag) => {
|
|
114
|
+
const jsDocs = node.jsDoc ?? [];
|
|
115
|
+
const existing = jsDocs[jsDocs.length - 1];
|
|
116
|
+
const tagName = /^@\w+/.exec(tag)?.[0];
|
|
117
|
+
if (existing && tagName && new RegExp(`${tagName}\\b`).test(existing.getText())) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
if (existing) {
|
|
121
|
+
const start = existing.getStart(sourceFile);
|
|
122
|
+
const indentation = ' '.repeat(sourceFile.getLineAndCharacterOfPosition(start).character);
|
|
123
|
+
const text = existing.getText();
|
|
124
|
+
const body = text.slice(0, text.lastIndexOf('*/')).replace(/\s+$/, '');
|
|
125
|
+
// A one-line `/** ⦠*/` has to become a block before a tag line can be appended to it.
|
|
126
|
+
const opened = body.includes('\n') ? body : `/**\n${indentation} * ${body.slice(3).trim()}`;
|
|
127
|
+
return (content.slice(0, start) +
|
|
128
|
+
`${opened}\n${indentation} *\n${indentation} * ${tag}\n${indentation} */` +
|
|
129
|
+
content.slice(existing.getEnd()));
|
|
130
|
+
}
|
|
131
|
+
let start = node.getStart(sourceFile, false);
|
|
132
|
+
for (const comment of (ts.getLeadingCommentRanges(content, node.getFullStart()) ?? []).reverse()) {
|
|
133
|
+
if (comment.kind !== ts.SyntaxKind.SingleLineCommentTrivia)
|
|
134
|
+
break;
|
|
135
|
+
if (!/^\n[ \t]*$/.test(content.slice(comment.end, start)))
|
|
136
|
+
break;
|
|
137
|
+
start = comment.pos;
|
|
138
|
+
}
|
|
139
|
+
const indentation = ' '.repeat(sourceFile.getLineAndCharacterOfPosition(start).character);
|
|
140
|
+
return (content.slice(0, start) + `/**\n${indentation} * ${tag}\n${indentation} */\n${indentation}` + content.slice(start));
|
|
141
|
+
};
|
|
97
142
|
//# sourceMappingURL=shared.js.map
|
|
@@ -58,7 +58,7 @@ function renameConflictingSymbols(tree) {
|
|
|
58
58
|
// Log statistics
|
|
59
59
|
if (updatedFiles.length > 0) {
|
|
60
60
|
console.log(`\nš Renamed symbols in ${updatedFiles.length} files:`);
|
|
61
|
-
const hasRenames = Array.from(symbolUsageCounts.entries()).filter(([
|
|
61
|
+
const hasRenames = Array.from(symbolUsageCounts.entries()).filter(([, count]) => count > 0);
|
|
62
62
|
if (hasRenames.length > 0) {
|
|
63
63
|
console.log('\n Symbol usage counts:');
|
|
64
64
|
hasRenames.forEach(([symbol, count]) => {
|
|
@@ -123,7 +123,7 @@ function renameSymbolsInFile(content, filePath, symbolUsageCounts) {
|
|
|
123
123
|
}
|
|
124
124
|
visit(sourceFile);
|
|
125
125
|
// Update imports in @ethlete/query import declarations
|
|
126
|
-
const importReplacements = updateImports(sourceFile
|
|
126
|
+
const importReplacements = updateImports(sourceFile);
|
|
127
127
|
replacements.push(...importReplacements);
|
|
128
128
|
// Count usages
|
|
129
129
|
replacements.forEach((r) => {
|
|
@@ -138,7 +138,7 @@ function renameSymbolsInFile(content, filePath, symbolUsageCounts) {
|
|
|
138
138
|
}
|
|
139
139
|
return result;
|
|
140
140
|
}
|
|
141
|
-
function updateImports(sourceFile
|
|
141
|
+
function updateImports(sourceFile) {
|
|
142
142
|
const replacements = [];
|
|
143
143
|
function visit(node) {
|
|
144
144
|
if (ts.isImportDeclaration(node)) {
|