@ethlete/query 6.0.0-next.6 → 6.0.0-next.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +939 -0
- package/fesm2022/ethlete-query-devtools-contract.mjs +1 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +411 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
- package/fesm2022/ethlete-query-testing.mjs +548 -169
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +14680 -7172
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/deprecate-legacy-queries/migration.js +85 -0
- package/generators/deprecate-legacy-queries/schema.json +26 -0
- package/generators/generators.json +30 -0
- package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
- package/generators/migrate-from-ngrx-toolkit/consumers.js +521 -0
- package/generators/migrate-from-ngrx-toolkit/creators.js +301 -0
- package/generators/migrate-from-ngrx-toolkit/facade.js +295 -0
- package/generators/migrate-from-ngrx-toolkit/feature.js +513 -0
- package/generators/migrate-from-ngrx-toolkit/migration.js +140 -0
- package/generators/migrate-from-ngrx-toolkit/report.js +154 -0
- package/generators/migrate-from-ngrx-toolkit/schema.json +42 -0
- package/generators/migrate-from-ngrx-toolkit/store-removal.js +633 -0
- package/generators/migrate-from-ngrx-toolkit/ts-edits.js +128 -0
- package/generators/migrate-query-client-features/migration.js +144 -0
- package/generators/migrate-query-client-features/report.js +136 -0
- package/generators/migrate-query-client-features/schema.json +26 -0
- package/generators/migrate-query-opt-in-features/migration.js +195 -0
- package/generators/migrate-query-opt-in-features/report.js +143 -0
- package/generators/migrate-query-opt-in-features/schema.json +31 -0
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +160 -0
- package/generators/migrate-to-query-v3/cleanup-migration.js +247 -125
- package/generators/migrate-to-query-v3/http-client-check.js +93 -0
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +129 -40
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +274 -152
- package/generators/migrate-to-query-v3/migration-scope.js +40 -0
- package/generators/migrate-to-query-v3/migration.js +19 -10
- package/generators/migrate-to-query-v3/module-graph.js +165 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +342 -160
- package/generators/migrate-to-query-v3/rename-symbols.js +233 -0
- package/generators/migrate-to-query-v3/report.js +40 -7
- package/generators/migrate-to-query-v3/schema.json +12 -0
- package/generators/migrate-to-query-v3/shared.js +97 -3
- package/generators/move-devtools-contract-imports/migration.js +155 -0
- package/generators/move-devtools-contract-imports/moved-names.js +153 -0
- package/generators/move-devtools-contract-imports/schema.json +26 -0
- package/generators/prep-for-query-v3/migration.js +465 -65
- package/generators/report-legacy-query-apis/migration.js +256 -0
- package/generators/report-legacy-query-apis/schema.json +25 -0
- package/migrations/query-field-default-types.md +43 -0
- package/migrations/search-query-field-string.md +43 -0
- package/migrations.json +87 -0
- package/package.json +34 -14
- package/types/ethlete-query-devtools-contract.d.ts +2 -0
- package/types/ethlete-query-ngrx-toolkit.d.ts +131 -0
- package/types/ethlete-query-testing.d.ts +295 -160
- package/types/ethlete-query.d.ts +9755 -3682
|
@@ -1,45 +1,80 @@
|
|
|
1
|
-
import { visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
1
|
import * as ts from 'typescript';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { createModuleGraph } from './module-graph.js';
|
|
3
|
+
import { isShadowedInNestedScope } from './rename-symbols.js';
|
|
4
|
+
import { REMOVED_EXPERIMENTAL_QUERY_HELPERS, createSourceFile, ensureImportFromQuery, ensureNamedImports, findRemovedExperimentalQueryHelpers, getLineNumber, printImportDeclaration, } from './shared.js';
|
|
5
|
+
export const reportRemovedExperimentalQueryHelpers = (tree, scope, report) => {
|
|
6
|
+
scope.visit(tree, (filePath) => {
|
|
7
|
+
if (!filePath.endsWith('.ts')) {
|
|
8
8
|
return;
|
|
9
9
|
}
|
|
10
10
|
const content = tree.read(filePath, 'utf-8');
|
|
11
|
-
if (!content) {
|
|
11
|
+
if (!content?.includes('@ethlete/query')) {
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
findRemovedExperimentalQueryHelpers(createSourceFile(content, filePath)).forEach(({ name, line }) => {
|
|
15
|
+
report.addWarning({
|
|
16
|
+
title: `Replace the removed helper ${name}`,
|
|
17
|
+
summary: `\`${name}\` came from the pre-v3 \`ExperimentalQuery\` namespace and is no longer exported by \`@ethlete/query\`, so this import does not compile. The migration does not rewrite it.`,
|
|
18
|
+
action: REMOVED_EXPERIMENTAL_QUERY_HELPERS[name],
|
|
19
|
+
locations: [{ filePath, line }],
|
|
20
|
+
source: 'cleanup-migration',
|
|
21
|
+
dedupeKey: `removed-experimental-helper:${filePath}:${name}`,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Points existing devtools usage at the v3 components instead of deleting it.
|
|
28
|
+
*
|
|
29
|
+
* Both versions render `<et-query-devtools>`, so templates need no change at all - only the provider
|
|
30
|
+
* call and the component's import move. Stripping the markup (as this phase used to) threw away
|
|
31
|
+
* something that would have kept working, and left every migrated app without devtools until someone
|
|
32
|
+
* noticed they were gone.
|
|
33
|
+
*/
|
|
34
|
+
export const migrateDevtoolsUsage = (tree, scope, report) => {
|
|
35
|
+
const updatedFiles = [];
|
|
36
|
+
const filesNeedingDevtoolsPackage = [];
|
|
37
|
+
scope.visit(tree, (filePath) => {
|
|
38
|
+
if (!filePath.endsWith('.ts')) {
|
|
24
39
|
return;
|
|
25
40
|
}
|
|
26
|
-
|
|
41
|
+
const content = tree.read(filePath, 'utf-8');
|
|
42
|
+
if (!content ||
|
|
43
|
+
(!content.includes('provideQueryClientForDevtools') && !content.includes('QueryDevtoolsComponent'))) {
|
|
27
44
|
return;
|
|
28
45
|
}
|
|
29
|
-
const nextContent =
|
|
30
|
-
if (nextContent
|
|
31
|
-
|
|
32
|
-
|
|
46
|
+
const { content: nextContent, importsComponent } = migrateDevtoolsInFile(content);
|
|
47
|
+
if (nextContent === content) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
tree.write(filePath, nextContent);
|
|
51
|
+
updatedFiles.push(filePath);
|
|
52
|
+
if (importsComponent) {
|
|
53
|
+
filesNeedingDevtoolsPackage.push(filePath);
|
|
33
54
|
}
|
|
34
55
|
});
|
|
35
|
-
if (updatedFiles.length
|
|
36
|
-
|
|
37
|
-
|
|
56
|
+
if (updatedFiles.length === 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
console.log('\n✅ Migrated query devtools usage to v3 in:');
|
|
60
|
+
updatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
|
|
61
|
+
if (filesNeedingDevtoolsPackage.length > 0) {
|
|
62
|
+
// The component moved packages, which is the one part of this rewrite that can fail outside the
|
|
63
|
+
// file being edited: an app that only ever depended on `@ethlete/query` now needs
|
|
64
|
+
// `@ethlete/query-devtools`.
|
|
65
|
+
report.addManualReview({
|
|
66
|
+
title: 'Add @ethlete/query-devtools for the query devtools',
|
|
67
|
+
summary: '`QueryDevtoolsComponent` now lives in `@ethlete/query-devtools`. The imports were rewritten, but the package may not be a dependency of these projects yet.',
|
|
68
|
+
action: 'Run `yarn add @ethlete/query-devtools` where needed, then `yarn install` and re-lint the affected libs so the `@nx/dependency-checks` rule sees the new dependency.',
|
|
69
|
+
locations: filesNeedingDevtoolsPackage.map((filePath) => ({ filePath })),
|
|
70
|
+
source: 'cleanup-migration',
|
|
71
|
+
dedupeKey: 'devtools-package-dependency',
|
|
72
|
+
});
|
|
38
73
|
}
|
|
39
74
|
};
|
|
40
|
-
export const replaceAnyQueryWithLegacy = (tree) => {
|
|
75
|
+
export const replaceAnyQueryWithLegacy = (tree, scope) => {
|
|
41
76
|
const updatedFiles = [];
|
|
42
|
-
|
|
77
|
+
scope.visit(tree, (filePath) => {
|
|
43
78
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
44
79
|
return;
|
|
45
80
|
}
|
|
@@ -57,9 +92,10 @@ export const replaceAnyQueryWithLegacy = (tree) => {
|
|
|
57
92
|
console.log(`\n✅ Replaced legacy AnyV2Query aliases in ${updatedFiles.length} files`);
|
|
58
93
|
}
|
|
59
94
|
};
|
|
60
|
-
export const migrateEmptyPrepareCalls = (tree) => {
|
|
95
|
+
export const migrateEmptyPrepareCalls = (tree, scope, report) => {
|
|
61
96
|
const updatedFiles = [];
|
|
62
|
-
|
|
97
|
+
const graph = createModuleGraph(tree);
|
|
98
|
+
scope.visit(tree, (filePath) => {
|
|
63
99
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
64
100
|
return;
|
|
65
101
|
}
|
|
@@ -67,7 +103,17 @@ export const migrateEmptyPrepareCalls = (tree) => {
|
|
|
67
103
|
if (!content || !content.includes('.prepare()')) {
|
|
68
104
|
return;
|
|
69
105
|
}
|
|
70
|
-
const nextContent = transformEmptyPrepareCalls(content);
|
|
106
|
+
const { content: nextContent, unresolved } = transformEmptyPrepareCalls(tree, graph, filePath, content);
|
|
107
|
+
unresolved.forEach(({ line, receiver }) => {
|
|
108
|
+
report.addManualReview({
|
|
109
|
+
title: `Check the empty prepare() call on ${receiver}`,
|
|
110
|
+
summary: `The migration could not tell whether \`${receiver}\` is a legacy query creator, so it left \`${receiver}.prepare()\` as is. A legacy creator needs \`.prepare({})\` in v3.`,
|
|
111
|
+
action: `Change the call to \`${receiver}.prepare({})\` if \`${receiver}\` is a legacy query creator, otherwise leave it.`,
|
|
112
|
+
locations: [{ filePath, line }],
|
|
113
|
+
source: 'cleanup-migration',
|
|
114
|
+
dedupeKey: `unresolved-empty-prepare:${filePath}:${line}`,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
71
117
|
if (nextContent !== content) {
|
|
72
118
|
tree.write(filePath, nextContent);
|
|
73
119
|
updatedFiles.push(filePath);
|
|
@@ -77,42 +123,66 @@ export const migrateEmptyPrepareCalls = (tree) => {
|
|
|
77
123
|
console.log(`\n✅ Migrated empty .prepare() calls in ${updatedFiles.length} files`);
|
|
78
124
|
}
|
|
79
125
|
};
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
126
|
+
const migrateDevtoolsInFile = (content) => {
|
|
127
|
+
const componentNames = localNamesImportedFromQuery(content, 'QueryDevtoolsComponent');
|
|
128
|
+
const providerNames = new Set([
|
|
129
|
+
'provideQueryClientForDevtools',
|
|
130
|
+
...localNamesImportedFromQuery(content, 'provideQueryClientForDevtools'),
|
|
131
|
+
]);
|
|
132
|
+
let result = replaceDevtoolsProviderCalls(content, providerNames);
|
|
133
|
+
const hasProvider = result.includes('provideQueryDevtools()');
|
|
134
|
+
result = dropLegacyDevtoolsImports(result);
|
|
135
|
+
if (hasProvider) {
|
|
136
|
+
result = ensureImportFromQuery(result, ['provideQueryDevtools']);
|
|
137
|
+
}
|
|
138
|
+
if (componentNames.length > 0) {
|
|
139
|
+
result = ensureNamedImports({
|
|
140
|
+
content: result,
|
|
141
|
+
importsNeeded: componentNames.map((name) => name === 'QueryDevtoolsComponent' ? name : `QueryDevtoolsComponent as ${name}`),
|
|
142
|
+
moduleSpecifier: '@ethlete/query-devtools',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
return { content: result, importsComponent: componentNames.length > 0 };
|
|
90
146
|
};
|
|
91
|
-
const
|
|
147
|
+
const queryImports = (sourceFile) => sourceFile.statements.filter((node) => ts.isImportDeclaration(node) &&
|
|
148
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
149
|
+
node.moduleSpecifier.text === '@ethlete/query');
|
|
150
|
+
const namedImportElements = (node) => node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings)
|
|
151
|
+
? node.importClause.namedBindings.elements
|
|
152
|
+
: [];
|
|
153
|
+
const importedName = (element) => (element.propertyName ?? element.name).text;
|
|
154
|
+
const localNamesImportedFromQuery = (content, name) => queryImports(createSourceFile(content)).flatMap((node) => namedImportElements(node)
|
|
155
|
+
.filter((element) => importedName(element) === name)
|
|
156
|
+
.map((element) => element.name.text));
|
|
157
|
+
/**
|
|
158
|
+
* Turns every `provideQueryClientForDevtools({ client, displayName })` into a single
|
|
159
|
+
* `provideQueryDevtools()`.
|
|
160
|
+
*
|
|
161
|
+
* v3 registers every client and auth provider at once, so N per-client calls collapse to one. The
|
|
162
|
+
* first call site keeps its position - it is already where the app wanted its devtools - and the
|
|
163
|
+
* rest are removed along with the comma that separated them.
|
|
164
|
+
*/
|
|
165
|
+
const replaceDevtoolsProviderCalls = (content, providerNames) => {
|
|
92
166
|
const sourceFile = createSourceFile(content);
|
|
93
|
-
const
|
|
167
|
+
const calls = [];
|
|
94
168
|
const visit = (node) => {
|
|
95
|
-
if (ts.
|
|
96
|
-
|
|
97
|
-
node.name.text === 'imports' &&
|
|
98
|
-
ts.isArrayLiteralExpression(node.initializer)) {
|
|
99
|
-
const nextElements = node.initializer.elements.filter((element) => {
|
|
100
|
-
return !(ts.isIdentifier(element) && element.text === 'QueryDevtoolsComponent');
|
|
101
|
-
});
|
|
102
|
-
if (nextElements.length === node.initializer.elements.length) {
|
|
103
|
-
ts.forEachChild(node, visit);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
const replacement = `imports: [${nextElements.map((element) => element.getText(sourceFile)).join(', ')}]`;
|
|
107
|
-
replacements.push({
|
|
108
|
-
start: node.getStart(sourceFile),
|
|
109
|
-
end: node.getEnd(),
|
|
110
|
-
replacement,
|
|
111
|
-
});
|
|
169
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && providerNames.has(node.expression.text)) {
|
|
170
|
+
calls.push(node);
|
|
112
171
|
}
|
|
113
172
|
ts.forEachChild(node, visit);
|
|
114
173
|
};
|
|
115
174
|
visit(sourceFile);
|
|
175
|
+
if (calls.length === 0) {
|
|
176
|
+
return content;
|
|
177
|
+
}
|
|
178
|
+
const replacements = calls.map((call, index) => {
|
|
179
|
+
const start = call.getStart(sourceFile);
|
|
180
|
+
const end = call.getEnd();
|
|
181
|
+
if (index === 0) {
|
|
182
|
+
return { start, end, replacement: 'provideQueryDevtools()' };
|
|
183
|
+
}
|
|
184
|
+
return { ...withSurroundingComma(content, start, end), replacement: '' };
|
|
185
|
+
});
|
|
116
186
|
let result = content;
|
|
117
187
|
replacements.sort((left, right) => right.start - left.start);
|
|
118
188
|
replacements.forEach(({ start, end, replacement }) => {
|
|
@@ -120,37 +190,43 @@ const removeQueryDevtoolsImportsFromMetadata = (content) => {
|
|
|
120
190
|
});
|
|
121
191
|
return result;
|
|
122
192
|
};
|
|
123
|
-
|
|
193
|
+
/** Widens a range to swallow the comma that separated the element from its neighbours. */
|
|
194
|
+
const withSurroundingComma = (content, start, end) => {
|
|
195
|
+
let nextEnd = end;
|
|
196
|
+
while (nextEnd < content.length && /\s/.test(content[nextEnd]))
|
|
197
|
+
nextEnd += 1;
|
|
198
|
+
if (content[nextEnd] === ',') {
|
|
199
|
+
return { start, end: nextEnd + 1 };
|
|
200
|
+
}
|
|
201
|
+
let nextStart = start;
|
|
202
|
+
while (nextStart > 0 && /\s/.test(content[nextStart - 1]))
|
|
203
|
+
nextStart -= 1;
|
|
204
|
+
if (content[nextStart - 1] === ',') {
|
|
205
|
+
return { start: nextStart - 1, end };
|
|
206
|
+
}
|
|
207
|
+
return { start, end };
|
|
208
|
+
};
|
|
209
|
+
const LEGACY_DEVTOOLS_IMPORTS = new Set(['QueryDevtoolsComponent', 'provideQueryClientForDevtools']);
|
|
210
|
+
/** Drops the v2 devtools names from the `@ethlete/query` imports; both moved or were renamed. */
|
|
211
|
+
const dropLegacyDevtoolsImports = (content) => {
|
|
124
212
|
const sourceFile = createSourceFile(content);
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
213
|
+
const replacements = [];
|
|
214
|
+
for (const node of queryImports(sourceFile)) {
|
|
215
|
+
const elements = namedImportElements(node);
|
|
216
|
+
const nextElements = elements.filter((element) => !LEGACY_DEVTOOLS_IMPORTS.has(importedName(element)));
|
|
217
|
+
if (nextElements.length === elements.length) {
|
|
218
|
+
continue;
|
|
131
219
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
220
|
+
const nextImport = printImportDeclaration(node, sourceFile, nextElements.map((element) => element.getText(sourceFile)));
|
|
221
|
+
const end = !nextImport && content[node.getEnd()] === '\n' ? node.getEnd() + 1 : node.getEnd();
|
|
222
|
+
replacements.push({ start: node.getStart(sourceFile), end, replacement: nextImport });
|
|
135
223
|
}
|
|
136
|
-
|
|
137
|
-
|
|
224
|
+
let result = content;
|
|
225
|
+
replacements.sort((left, right) => right.start - left.start);
|
|
226
|
+
replacements.forEach(({ start, end, replacement }) => {
|
|
227
|
+
result = result.slice(0, start) + replacement + result.slice(end);
|
|
138
228
|
});
|
|
139
|
-
|
|
140
|
-
return content;
|
|
141
|
-
}
|
|
142
|
-
if (nextElements.length === 0) {
|
|
143
|
-
const nextLineEnd = content[importNode.getEnd()] === '\n' ? importNode.getEnd() + 1 : importNode.getEnd();
|
|
144
|
-
return content.slice(0, importNode.getStart(sourceFile)) + content.slice(nextLineEnd);
|
|
145
|
-
}
|
|
146
|
-
const nextImport = `import { ${nextElements.map((element) => element.getText(sourceFile)).join(', ')} } from '@ethlete/query';`;
|
|
147
|
-
return content.slice(0, importNode.getStart(sourceFile)) + nextImport + content.slice(importNode.getEnd());
|
|
148
|
-
};
|
|
149
|
-
const removeDevtoolsFromHtml = (content) => {
|
|
150
|
-
return content
|
|
151
|
-
.replace(/<et-query-devtools\s*\/>/g, '')
|
|
152
|
-
.replace(/<et-query-devtools[^>]*>[\s\S]*?<\/et-query-devtools>/g, '')
|
|
153
|
-
.replace(/\n\s*\n\s*\n/g, '\n\n');
|
|
229
|
+
return result;
|
|
154
230
|
};
|
|
155
231
|
const replaceAnyQueryInFile = (content) => {
|
|
156
232
|
const sourceFile = createSourceFile(content);
|
|
@@ -180,48 +256,98 @@ const replaceAnyQueryInFile = (content) => {
|
|
|
180
256
|
replacements.forEach(({ start, end, replacement }) => {
|
|
181
257
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
182
258
|
});
|
|
183
|
-
return
|
|
259
|
+
return result;
|
|
184
260
|
};
|
|
185
|
-
const
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
importNode = node;
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
if (!importNode?.importClause?.namedBindings || !ts.isNamedImports(importNode.importClause.namedBindings)) {
|
|
196
|
-
return content;
|
|
261
|
+
const findTopLevelVariable = (sourceFile, name) => {
|
|
262
|
+
for (const statement of sourceFile.statements) {
|
|
263
|
+
if (!ts.isVariableStatement(statement))
|
|
264
|
+
continue;
|
|
265
|
+
const declaration = statement.declarationList.declarations.find((candidate) => ts.isIdentifier(candidate.name) && candidate.name.text === name);
|
|
266
|
+
if (declaration)
|
|
267
|
+
return declaration;
|
|
197
268
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
269
|
+
return undefined;
|
|
270
|
+
};
|
|
271
|
+
const findNamedImport = (sourceFile, localName) => {
|
|
272
|
+
for (const statement of sourceFile.statements) {
|
|
273
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
274
|
+
continue;
|
|
275
|
+
const element = namedImportElements(statement).find((candidate) => candidate.name.text === localName);
|
|
276
|
+
if (element)
|
|
277
|
+
return { specifier: statement.moduleSpecifier.text, importedName: importedName(element) };
|
|
204
278
|
}
|
|
205
|
-
|
|
206
|
-
|
|
279
|
+
return undefined;
|
|
280
|
+
};
|
|
281
|
+
const QUERY_CLIENT_CONSTRUCTORS = new Set(['V2QueryClient', 'QueryClient']);
|
|
282
|
+
/**
|
|
283
|
+
* Classifies what a top-level binding of `sourceFile` holds: a query creator (a call to an
|
|
284
|
+
* `@ethlete/query` export such as `createLegacyQueryCreator`, or a method call on a v2 client), something
|
|
285
|
+
* provably unrelated, or `unknown` when the binding cannot be followed.
|
|
286
|
+
*/
|
|
287
|
+
const classifyBinding = (tree, graph, filePath, sourceFile, name, isClient, depth) => {
|
|
288
|
+
if (depth > 4)
|
|
289
|
+
return 'unknown';
|
|
290
|
+
const declaration = findTopLevelVariable(sourceFile, name);
|
|
291
|
+
if (declaration) {
|
|
292
|
+
const initializer = declaration.initializer;
|
|
293
|
+
if (isClient) {
|
|
294
|
+
return initializer &&
|
|
295
|
+
ts.isNewExpression(initializer) &&
|
|
296
|
+
ts.isIdentifier(initializer.expression) &&
|
|
297
|
+
QUERY_CLIENT_CONSTRUCTORS.has(initializer.expression.text) &&
|
|
298
|
+
findNamedImport(sourceFile, initializer.expression.text)?.specifier === '@ethlete/query'
|
|
299
|
+
? 'query'
|
|
300
|
+
: 'foreign';
|
|
301
|
+
}
|
|
302
|
+
if (!initializer || !ts.isCallExpression(initializer))
|
|
303
|
+
return 'foreign';
|
|
304
|
+
const callee = initializer.expression;
|
|
305
|
+
if (ts.isIdentifier(callee)) {
|
|
306
|
+
return findNamedImport(sourceFile, callee.text)?.specifier === '@ethlete/query' ? 'query' : 'foreign';
|
|
307
|
+
}
|
|
308
|
+
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.expression)) {
|
|
309
|
+
return classifyBinding(tree, graph, filePath, sourceFile, callee.expression.text, true, depth + 1);
|
|
310
|
+
}
|
|
311
|
+
return 'foreign';
|
|
207
312
|
}
|
|
208
|
-
const
|
|
209
|
-
|
|
313
|
+
const binding = findNamedImport(sourceFile, name);
|
|
314
|
+
if (!binding || binding.specifier === '@ethlete/query')
|
|
315
|
+
return 'unknown';
|
|
316
|
+
const declaringFile = graph.findDeclaringFile(filePath, binding.specifier, binding.importedName);
|
|
317
|
+
if (!declaringFile)
|
|
318
|
+
return graph.isWorkspaceSpecifier(binding.specifier) ? 'unknown' : 'foreign';
|
|
319
|
+
const declaringContent = tree.read(declaringFile, 'utf-8');
|
|
320
|
+
if (!declaringContent)
|
|
321
|
+
return 'unknown';
|
|
322
|
+
return classifyBinding(tree, graph, declaringFile, createSourceFile(declaringContent, declaringFile), binding.importedName, isClient, depth + 1);
|
|
210
323
|
};
|
|
211
|
-
const
|
|
212
|
-
|
|
324
|
+
const classifyPrepareReceiver = (tree, graph, filePath, sourceFile, receiver) => {
|
|
325
|
+
if (!ts.isIdentifier(receiver) || isShadowedInNestedScope(receiver))
|
|
326
|
+
return 'unknown';
|
|
327
|
+
return classifyBinding(tree, graph, filePath, sourceFile, receiver.text, false, 0);
|
|
328
|
+
};
|
|
329
|
+
const transformEmptyPrepareCalls = (tree, graph, filePath, content) => {
|
|
330
|
+
const sourceFile = createSourceFile(content, filePath);
|
|
213
331
|
const replacements = [];
|
|
332
|
+
const unresolved = [];
|
|
214
333
|
const visit = (node) => {
|
|
215
|
-
if (ts.isCallExpression(node) &&
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
334
|
+
if (ts.isCallExpression(node) &&
|
|
335
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
336
|
+
ts.isIdentifier(node.expression.name) &&
|
|
337
|
+
node.expression.name.text === 'prepare' &&
|
|
338
|
+
node.arguments.length === 0) {
|
|
339
|
+
const kind = classifyPrepareReceiver(tree, graph, filePath, sourceFile, node.expression.expression);
|
|
340
|
+
if (kind === 'query') {
|
|
221
341
|
replacements.push({
|
|
222
|
-
start: node.
|
|
223
|
-
end: node.
|
|
224
|
-
replacement:
|
|
342
|
+
start: node.arguments.pos,
|
|
343
|
+
end: node.arguments.end,
|
|
344
|
+
replacement: '{}',
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
else if (kind === 'unknown') {
|
|
348
|
+
unresolved.push({
|
|
349
|
+
line: getLineNumber(node, sourceFile),
|
|
350
|
+
receiver: node.expression.expression.getText(sourceFile),
|
|
225
351
|
});
|
|
226
352
|
}
|
|
227
353
|
}
|
|
@@ -233,10 +359,6 @@ const transformEmptyPrepareCalls = (content) => {
|
|
|
233
359
|
replacements.forEach(({ start, end, replacement }) => {
|
|
234
360
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
235
361
|
});
|
|
236
|
-
return result;
|
|
237
|
-
};
|
|
238
|
-
export const describeTemplateLine = (content, position, suffix) => {
|
|
239
|
-
const lineNumber = getLineNumberFromPosition(content, position);
|
|
240
|
-
return `${lineNumber} (${suffix})`;
|
|
362
|
+
return { content: result, unresolved };
|
|
241
363
|
};
|
|
242
364
|
//# sourceMappingURL=cleanup-migration.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { getLineNumberFromPosition } from './shared.js';
|
|
2
|
+
const APP_CONFIG_MARKERS = ['bootstrapApplication(', 'ApplicationConfig', 'provideServerRendering('];
|
|
3
|
+
/**
|
|
4
|
+
* Flags the v2 default-header APIs, which the client migration drops on the floor.
|
|
5
|
+
*
|
|
6
|
+
* `setDefaultHeaders` is not cosmetic - an API token or a preview credential lives there, so losing
|
|
7
|
+
* it means every request goes out unauthenticated. v3 has a home for it (`headers` on
|
|
8
|
+
* `createQueryClient`), but the call sites are imperative and scattered, so this is a pointer rather
|
|
9
|
+
* than a rewrite.
|
|
10
|
+
*/
|
|
11
|
+
export const reportDefaultHeaderUsages = (tree, report, scope) => {
|
|
12
|
+
const locations = [];
|
|
13
|
+
scope.visit(tree, (filePath) => {
|
|
14
|
+
if (!filePath.endsWith('.ts')) {
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const content = tree.read(filePath, 'utf-8');
|
|
18
|
+
if (!content) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
for (const marker of ['setDefaultHeaders', 'globalHeaders']) {
|
|
22
|
+
const index = content.indexOf(marker);
|
|
23
|
+
if (index >= 0) {
|
|
24
|
+
locations.push({ filePath, line: getLineNumberFromPosition(content, index) });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
if (locations.length === 0) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
report.addWarning({
|
|
32
|
+
title: 'Move default headers onto the query client',
|
|
33
|
+
summary: 'This workspace sets client-wide headers through the v2 `setDefaultHeaders` / `globalHeaders` API, which `createQueryClient` does not have. Left as is, those headers are simply not sent.',
|
|
34
|
+
action: 'Pass `headers` to `createQueryClient` - a function form re-reads on every request, so a signal can drive it. `refreshQueriesInUse: true` becomes `injectMyClient().refreshQueriesInUse()` after the value changes.',
|
|
35
|
+
locations,
|
|
36
|
+
source: 'http-client-check',
|
|
37
|
+
dedupeKey: 'default-headers',
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Flags applications that will build cleanly and then fail on their first request.
|
|
42
|
+
*
|
|
43
|
+
* The v2 client shipped its own transport; v3 does `inject(HttpClient)`. An app that never needed
|
|
44
|
+
* `provideHttpClient()` therefore keeps compiling after the migration and dies at runtime instead -
|
|
45
|
+
* the one failure mode a codemod should never leave behind silently.
|
|
46
|
+
*/
|
|
47
|
+
export const reportMissingHttpClientProviders = (tree, report, scope) => {
|
|
48
|
+
const appConfigFiles = [];
|
|
49
|
+
const filesWithProvider = [];
|
|
50
|
+
const filesWithUploadProgress = [];
|
|
51
|
+
scope.visit(tree, (filePath) => {
|
|
52
|
+
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const content = tree.read(filePath, 'utf-8');
|
|
56
|
+
if (!content) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
if (content.includes('provideHttpClient(')) {
|
|
60
|
+
filesWithProvider.push(filePath);
|
|
61
|
+
}
|
|
62
|
+
const markerIndex = APP_CONFIG_MARKERS.map((marker) => content.indexOf(marker)).find((index) => index >= 0);
|
|
63
|
+
if (markerIndex !== undefined) {
|
|
64
|
+
appConfigFiles.push({ filePath, line: getLineNumberFromPosition(content, markerIndex) });
|
|
65
|
+
}
|
|
66
|
+
const progressIndex = content.indexOf('reportProgress');
|
|
67
|
+
if (progressIndex >= 0) {
|
|
68
|
+
filesWithUploadProgress.push({ filePath, line: getLineNumberFromPosition(content, progressIndex) });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (appConfigFiles.length > 0 && filesWithProvider.length === 0) {
|
|
72
|
+
report.addWarning({
|
|
73
|
+
title: 'Add provideHttpClient() to the application providers',
|
|
74
|
+
summary: 'v3 queries run on Angular’s `HttpClient`, but no `provideHttpClient()` was found. The app compiles and then throws on the first request - `@ethlete/query` never provides it, that is the application’s job.',
|
|
75
|
+
action: 'Add `provideHttpClient()` to every app config that uses a query client. If anything relies on upload progress, use `provideHttpClient(withXhr())` - on Angular ≥ 22 the default is fetch, which emits no upload progress events.',
|
|
76
|
+
locations: appConfigFiles,
|
|
77
|
+
source: 'http-client-check',
|
|
78
|
+
dedupeKey: 'missing-provide-http-client',
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (filesWithUploadProgress.length > 0) {
|
|
83
|
+
report.addManualReview({
|
|
84
|
+
title: 'Confirm the HttpClient backend supports upload progress',
|
|
85
|
+
summary: 'Queries in this workspace set `reportProgress`. On Angular ≥ 22 `provideHttpClient()` defaults to the fetch backend, which never emits upload progress events, so those queries would report download progress only.',
|
|
86
|
+
action: 'Switch the affected app to `provideHttpClient(withXhr())`, or drop `reportProgress` where it is unused.',
|
|
87
|
+
locations: filesWithUploadProgress,
|
|
88
|
+
source: 'http-client-check',
|
|
89
|
+
dedupeKey: 'upload-progress-backend',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=http-client-check.js.map
|