@ethlete/query 6.0.0-next.19 → 6.0.0-next.20

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.
@@ -0,0 +1,131 @@
1
+ import * as ts from 'typescript';
2
+ import { createSourceFile } from './shared.js';
3
+ const REFRESH_CONFIG_KEYS = new Set([
4
+ 'queryCreator',
5
+ 'responseAdapter',
6
+ 'requestArgsAdapter',
7
+ 'cookieName',
8
+ 'cookieDomain',
9
+ 'cookieExpiresInDays',
10
+ 'expiresInPropertyName',
11
+ ]);
12
+ export const collectV2BearerAuthConfigs = (tree, scope) => {
13
+ const configs = [];
14
+ scope.visit(tree, (filePath) => {
15
+ if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
16
+ return;
17
+ }
18
+ const content = tree.read(filePath, 'utf-8');
19
+ if (!content || !content.includes('V2BearerAuthProvider')) {
20
+ return;
21
+ }
22
+ const sourceFile = createSourceFile(content, filePath);
23
+ const visit = (node) => {
24
+ if (ts.isNewExpression(node) &&
25
+ ts.isIdentifier(node.expression) &&
26
+ node.expression.text === 'V2BearerAuthProvider') {
27
+ const configArgument = node.arguments?.[0];
28
+ if (configArgument && ts.isObjectLiteralExpression(configArgument)) {
29
+ configs.push(readConfig(configArgument, sourceFile, filePath, node));
30
+ }
31
+ }
32
+ ts.forEachChild(node, visit);
33
+ };
34
+ visit(sourceFile);
35
+ });
36
+ return configs;
37
+ };
38
+ const readConfig = (configArgument, sourceFile, filePath, node) => {
39
+ const config = {
40
+ filePath,
41
+ line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1,
42
+ };
43
+ const refreshConfig = configArgument.properties.find((property) => ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'refreshConfig');
44
+ if (!refreshConfig ||
45
+ !ts.isPropertyAssignment(refreshConfig) ||
46
+ !ts.isObjectLiteralExpression(refreshConfig.initializer)) {
47
+ return config;
48
+ }
49
+ refreshConfig.initializer.properties.forEach((property) => {
50
+ if (!ts.isPropertyAssignment(property) || !ts.isIdentifier(property.name)) {
51
+ return;
52
+ }
53
+ const key = property.name.text;
54
+ if (!REFRESH_CONFIG_KEYS.has(key)) {
55
+ return;
56
+ }
57
+ const text = property.initializer.getText(sourceFile);
58
+ if (key === 'queryCreator') {
59
+ if (ts.isIdentifier(property.initializer)) {
60
+ config.refreshCreatorName = property.initializer.text;
61
+ }
62
+ return;
63
+ }
64
+ config[key] = text;
65
+ });
66
+ return config;
67
+ };
68
+ const REFRESH_QUERY_KEY = 'tokenRefresh';
69
+ /**
70
+ * Renders the `queries` / `features` arguments of a v3 provider from a v2 config.
71
+ *
72
+ * The mapping is close to 1:1 — `queryCreator` → `withRefreshQuery`, `responseAdapter` →
73
+ * `extractTokens`, `cookieName` → `withPersistentAuth({ cookie })` — but the adapters are copied
74
+ * verbatim from v2 and their parameter shapes differ, so each one is emitted with a TODO rather
75
+ * than presented as finished.
76
+ */
77
+ export const renderAuthProviderBody = (config) => {
78
+ if (!config?.refreshCreatorName) {
79
+ return {
80
+ queries: ' queries: [],',
81
+ features: '',
82
+ importsNeeded: [],
83
+ isScaffolded: false,
84
+ };
85
+ }
86
+ const refreshEntries = [` queryCreator: ${config.refreshCreatorName},`];
87
+ if (config.responseAdapter) {
88
+ refreshEntries.push(` // TODO(query-v3): v2 responseAdapter. It must return { accessToken, refreshToken } — v2 returned { token, refreshToken }.`, ` extractTokens: ${config.responseAdapter},`);
89
+ }
90
+ if (config.requestArgsAdapter) {
91
+ refreshEntries.push(` // TODO(query-v3): v2 requestArgsAdapter took { token, refreshToken }; buildArgs takes the refresh token alone.`, ` buildArgs: ${config.requestArgsAdapter},`);
92
+ }
93
+ if (config.expiresInPropertyName) {
94
+ refreshEntries.push(` expiresInPropertyName: ${config.expiresInPropertyName},`);
95
+ }
96
+ const queries = [
97
+ ' queries: [',
98
+ ` // Derived from the v2 BearerAuthProvider in ${config.filePath}:${config.line}. Verify before shipping.`,
99
+ ` withRefreshQuery('${REFRESH_QUERY_KEY}', {`,
100
+ ...refreshEntries,
101
+ ' }),',
102
+ ` // TODO(query-v3): add withAuthenticationQuery('login', { queryCreator: … }) for the login query.`,
103
+ ' ],',
104
+ ].join('\n');
105
+ const importsNeeded = ['withRefreshQuery'];
106
+ if (!config.cookieName) {
107
+ return { queries, features: '', importsNeeded, isScaffolded: true };
108
+ }
109
+ const cookieEntries = [` name: ${config.cookieName},`];
110
+ if (config.cookieDomain)
111
+ cookieEntries.push(` domain: ${config.cookieDomain},`);
112
+ if (config.cookieExpiresInDays)
113
+ cookieEntries.push(` expiresInDays: ${config.cookieExpiresInDays},`);
114
+ const features = [
115
+ ' features: [',
116
+ ' withPersistentAuth({',
117
+ ' cookie: {',
118
+ ...cookieEntries,
119
+ ' },',
120
+ ' autoLogin: {',
121
+ ` queryKey: '${REFRESH_QUERY_KEY}',`,
122
+ ` // TODO(query-v3): shape the auto-login request from the cookie value.`,
123
+ ' buildArgs: (token) => ({ body: { refreshToken: token } }),',
124
+ ' },',
125
+ ' }),',
126
+ ' ],',
127
+ ].join('\n');
128
+ importsNeeded.push('withPersistentAuth');
129
+ return { queries, features, importsNeeded, isScaffolded: true };
130
+ };
131
+ //# sourceMappingURL=auth-provider-scaffold.js.map
@@ -1,45 +1,56 @@
1
- import { visitNotIgnoredFiles } from '@nx/devkit';
2
1
  import * as ts from 'typescript';
3
- import { createSourceFile, getLineNumberFromPosition } from './shared.js';
4
- export const removeDevtoolsUsage = (tree) => {
2
+ import { createSourceFile, ensureImportFromQuery, ensureNamedImports, getLineNumberFromPosition } from './shared.js';
3
+ /**
4
+ * Points existing devtools usage at the v3 components instead of deleting it.
5
+ *
6
+ * Both versions render `<et-query-devtools>`, so templates need no change at all — only the provider
7
+ * call and the component's import move. Stripping the markup (as this phase used to) threw away
8
+ * something that would have kept working, and left every migrated app without devtools until someone
9
+ * noticed they were gone.
10
+ */
11
+ export const migrateDevtoolsUsage = (tree, scope, report) => {
5
12
  const updatedFiles = [];
6
- visitNotIgnoredFiles(tree, '', (filePath) => {
7
- if (!filePath.endsWith('.ts') && !filePath.endsWith('.html')) {
13
+ const filesNeedingComponentsPackage = [];
14
+ scope.visit(tree, (filePath) => {
15
+ if (!filePath.endsWith('.ts')) {
8
16
  return;
9
17
  }
10
18
  const content = tree.read(filePath, 'utf-8');
11
- if (!content) {
19
+ if (!content ||
20
+ (!content.includes('provideQueryClientForDevtools') && !content.includes('QueryDevtoolsComponent'))) {
12
21
  return;
13
22
  }
14
- if (filePath.endsWith('.ts')) {
15
- if (!content.includes('provideQueryClientForDevtools') && !content.includes('QueryDevtoolsComponent')) {
16
- return;
17
- }
18
- const withoutProviders = removeProvideQueryClientForDevtools(content);
19
- const nextContent = removeQueryDevtoolsComponent(withoutProviders);
20
- if (nextContent !== content) {
21
- tree.write(filePath, nextContent);
22
- updatedFiles.push(filePath);
23
- }
23
+ const { content: nextContent, importsComponent } = migrateDevtoolsInFile(content);
24
+ if (nextContent === content) {
24
25
  return;
25
26
  }
26
- if (!content.includes('et-query-devtools')) {
27
- return;
28
- }
29
- const nextContent = removeDevtoolsFromHtml(content);
30
- if (nextContent !== content) {
31
- tree.write(filePath, nextContent);
32
- updatedFiles.push(filePath);
27
+ tree.write(filePath, nextContent);
28
+ updatedFiles.push(filePath);
29
+ if (importsComponent) {
30
+ filesNeedingComponentsPackage.push(filePath);
33
31
  }
34
32
  });
35
- if (updatedFiles.length > 0) {
36
- console.log('\n✅ Removed legacy query devtools usage in:');
37
- updatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
33
+ if (updatedFiles.length === 0) {
34
+ return;
35
+ }
36
+ console.log('\n✅ Migrated query devtools usage to v3 in:');
37
+ updatedFiles.forEach((filePath) => console.log(` - ${filePath}`));
38
+ if (filesNeedingComponentsPackage.length > 0) {
39
+ // The component moved packages, which is the one part of this rewrite that can fail outside the
40
+ // file being edited: an app that only ever depended on `@ethlete/query` now needs `@ethlete/components`.
41
+ report.addManualReview({
42
+ title: 'Add @ethlete/components for the query devtools',
43
+ summary: '`QueryDevtoolsComponent` now lives in `@ethlete/components`. The imports were rewritten, but the package may not be a dependency of these projects yet.',
44
+ action: 'Run `yarn add @ethlete/components` where needed, then `yarn install` and re-lint the affected libs so the `@nx/dependency-checks` rule sees the new dependency.',
45
+ locations: filesNeedingComponentsPackage.map((filePath) => ({ filePath })),
46
+ source: 'cleanup-migration',
47
+ dedupeKey: 'devtools-components-dependency',
48
+ });
38
49
  }
39
50
  };
40
- export const replaceAnyQueryWithLegacy = (tree) => {
51
+ export const replaceAnyQueryWithLegacy = (tree, scope) => {
41
52
  const updatedFiles = [];
42
- visitNotIgnoredFiles(tree, '', (filePath) => {
53
+ scope.visit(tree, (filePath) => {
43
54
  if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
44
55
  return;
45
56
  }
@@ -57,9 +68,9 @@ export const replaceAnyQueryWithLegacy = (tree) => {
57
68
  console.log(`\n✅ Replaced legacy AnyV2Query aliases in ${updatedFiles.length} files`);
58
69
  }
59
70
  };
60
- export const migrateEmptyPrepareCalls = (tree) => {
71
+ export const migrateEmptyPrepareCalls = (tree, scope) => {
61
72
  const updatedFiles = [];
62
- visitNotIgnoredFiles(tree, '', (filePath) => {
73
+ scope.visit(tree, (filePath) => {
63
74
  if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
64
75
  return;
65
76
  }
@@ -77,42 +88,63 @@ export const migrateEmptyPrepareCalls = (tree) => {
77
88
  console.log(`\n✅ Migrated empty .prepare() calls in ${updatedFiles.length} files`);
78
89
  }
79
90
  };
80
- const removeProvideQueryClientForDevtools = (content) => {
81
- return content
82
- .replace(/,?\s*provideQueryClientForDevtools\([^)]*\)/g, '')
83
- .replace(/\[\s*,/g, '[')
84
- .replace(/,\s*\]/g, ']')
85
- .replace(/\n\s*\n\s*\n/g, '\n\n');
91
+ const migrateDevtoolsInFile = (content) => {
92
+ const importsComponent = importsFromQuery(content, 'QueryDevtoolsComponent');
93
+ let result = replaceDevtoolsProviderCalls(content);
94
+ const hasProvider = result.includes('provideQueryDevtools()');
95
+ result = dropLegacyDevtoolsImports(result);
96
+ if (hasProvider) {
97
+ result = ensureImportFromQuery(result, ['provideQueryDevtools']);
98
+ }
99
+ if (importsComponent) {
100
+ result = ensureNamedImports({
101
+ content: result,
102
+ importsNeeded: ['QueryDevtoolsComponent'],
103
+ moduleSpecifier: '@ethlete/components',
104
+ });
105
+ }
106
+ return { content: result, importsComponent };
86
107
  };
87
- const removeQueryDevtoolsComponent = (content) => {
88
- const withoutImportsArray = removeQueryDevtoolsImportsFromMetadata(content);
89
- return removeDevtoolsImports(withoutImportsArray);
108
+ const importsFromQuery = (content, name) => {
109
+ const sourceFile = createSourceFile(content);
110
+ return sourceFile.statements.some((node) => ts.isImportDeclaration(node) &&
111
+ ts.isStringLiteral(node.moduleSpecifier) &&
112
+ node.moduleSpecifier.text === '@ethlete/query' &&
113
+ !!node.importClause?.namedBindings &&
114
+ ts.isNamedImports(node.importClause.namedBindings) &&
115
+ node.importClause.namedBindings.elements.some((element) => element.name.text === name));
90
116
  };
91
- const removeQueryDevtoolsImportsFromMetadata = (content) => {
117
+ /**
118
+ * Turns every `provideQueryClientForDevtools({ client, displayName })` into a single
119
+ * `provideQueryDevtools()`.
120
+ *
121
+ * v3 registers every client and auth provider at once, so N per-client calls collapse to one. The
122
+ * first call site keeps its position — it is already where the app wanted its devtools — and the
123
+ * rest are removed along with the comma that separated them.
124
+ */
125
+ const replaceDevtoolsProviderCalls = (content) => {
92
126
  const sourceFile = createSourceFile(content);
93
- const replacements = [];
127
+ const calls = [];
94
128
  const visit = (node) => {
95
- if (ts.isPropertyAssignment(node) &&
96
- ts.isIdentifier(node.name) &&
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
- });
129
+ if (ts.isCallExpression(node) &&
130
+ ts.isIdentifier(node.expression) &&
131
+ node.expression.text === 'provideQueryClientForDevtools') {
132
+ calls.push(node);
112
133
  }
113
134
  ts.forEachChild(node, visit);
114
135
  };
115
136
  visit(sourceFile);
137
+ if (calls.length === 0) {
138
+ return content;
139
+ }
140
+ const replacements = calls.map((call, index) => {
141
+ const start = call.getStart(sourceFile);
142
+ const end = call.getEnd();
143
+ if (index === 0) {
144
+ return { start, end, replacement: 'provideQueryDevtools()' };
145
+ }
146
+ return { ...withSurroundingComma(content, start, end), replacement: '' };
147
+ });
116
148
  let result = content;
117
149
  replacements.sort((left, right) => right.start - left.start);
118
150
  replacements.forEach(({ start, end, replacement }) => {
@@ -120,7 +152,24 @@ const removeQueryDevtoolsImportsFromMetadata = (content) => {
120
152
  });
121
153
  return result;
122
154
  };
123
- const removeDevtoolsImports = (content) => {
155
+ /** Widens a range to swallow the comma that separated the element from its neighbours. */
156
+ const withSurroundingComma = (content, start, end) => {
157
+ let nextEnd = end;
158
+ while (nextEnd < content.length && /\s/.test(content[nextEnd]))
159
+ nextEnd += 1;
160
+ if (content[nextEnd] === ',') {
161
+ return { start, end: nextEnd + 1 };
162
+ }
163
+ let nextStart = start;
164
+ while (nextStart > 0 && /\s/.test(content[nextStart - 1]))
165
+ nextStart -= 1;
166
+ if (content[nextStart - 1] === ',') {
167
+ return { start: nextStart - 1, end };
168
+ }
169
+ return { start, end };
170
+ };
171
+ /** Drops the v2 devtools names from the `@ethlete/query` import; both moved or were renamed. */
172
+ const dropLegacyDevtoolsImports = (content) => {
124
173
  const sourceFile = createSourceFile(content);
125
174
  let importNode;
126
175
  ts.forEachChild(sourceFile, (node) => {
@@ -133,9 +182,7 @@ const removeDevtoolsImports = (content) => {
133
182
  if (!importNode?.importClause?.namedBindings || !ts.isNamedImports(importNode.importClause.namedBindings)) {
134
183
  return content;
135
184
  }
136
- const nextElements = importNode.importClause.namedBindings.elements.filter((element) => {
137
- return element.name.text !== 'QueryDevtoolsComponent' && element.name.text !== 'provideQueryClientForDevtools';
138
- });
185
+ const nextElements = importNode.importClause.namedBindings.elements.filter((element) => element.name.text !== 'QueryDevtoolsComponent' && element.name.text !== 'provideQueryClientForDevtools');
139
186
  if (nextElements.length === importNode.importClause.namedBindings.elements.length) {
140
187
  return content;
141
188
  }
@@ -146,12 +193,6 @@ const removeDevtoolsImports = (content) => {
146
193
  const nextImport = `import { ${nextElements.map((element) => element.getText(sourceFile)).join(', ')} } from '@ethlete/query';`;
147
194
  return content.slice(0, importNode.getStart(sourceFile)) + nextImport + content.slice(importNode.getEnd());
148
195
  };
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');
154
- };
155
196
  const replaceAnyQueryInFile = (content) => {
156
197
  const sourceFile = createSourceFile(content);
157
198
  const replacements = [];
@@ -195,17 +236,27 @@ const removeAnyQueryFromImports = (content) => {
195
236
  if (!importNode?.importClause?.namedBindings || !ts.isNamedImports(importNode.importClause.namedBindings)) {
196
237
  return content;
197
238
  }
198
- const nextElements = importNode.importClause.namedBindings.elements.filter((element) => {
199
- return element.name.text !== 'AnyV2Query' && element.name.text !== 'AnyV2QueryCreator';
239
+ const replaced = new Map([
240
+ ['AnyV2Query', 'AnyLegacyQuery'],
241
+ ['AnyV2QueryCreator', 'AnyLegacyQueryCreator'],
242
+ ]);
243
+ const names = new Set();
244
+ let changed = false;
245
+ importNode.importClause.namedBindings.elements.forEach((element) => {
246
+ const replacement = replaced.get(element.name.text);
247
+ if (!replacement) {
248
+ names.add(element.getText(sourceFile));
249
+ return;
250
+ }
251
+ // Only add the alias that was actually imported. Adding both unconditionally is what put
252
+ // `AnyLegacyQuery` / `AnyLegacyQueryCreator` into hundreds of files that never referenced them.
253
+ changed = true;
254
+ names.add(replacement);
200
255
  });
201
- const names = nextElements.map((element) => element.getText(sourceFile));
202
- if (!names.includes('AnyLegacyQuery')) {
203
- names.push('AnyLegacyQuery');
204
- }
205
- if (!names.includes('AnyLegacyQueryCreator')) {
206
- names.push('AnyLegacyQueryCreator');
256
+ if (!changed) {
257
+ return content;
207
258
  }
208
- const nextImport = `import { ${names.sort().join(', ')} } from '@ethlete/query';`;
259
+ const nextImport = `import { ${Array.from(names).sort().join(', ')} } from '@ethlete/query';`;
209
260
  return content.slice(0, importNode.getStart(sourceFile)) + nextImport + content.slice(importNode.getEnd());
210
261
  };
211
262
  const transformEmptyPrepareCalls = (content) => {
@@ -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
@@ -1,10 +1,9 @@
1
- import { visitNotIgnoredFiles } from '@nx/devkit';
2
1
  import * as ts from 'typescript';
3
2
  import { createSourceFile, ensureAngularCoreImports, getIndentation, getLineNumber, getLineNumberFromPosition, } from './shared.js';
4
- export const migrateLegacyPrepareCalls = (tree, report) => {
3
+ export const migrateLegacyPrepareCalls = (tree, report, scope) => {
5
4
  console.log('\n🔍 Checking for legacy query creator usages...');
6
5
  const allUsages = [];
7
- visitNotIgnoredFiles(tree, '', (filePath) => {
6
+ scope.visit(tree, (filePath) => {
8
7
  if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
9
8
  return;
10
9
  }
@@ -27,7 +26,7 @@ export const migrateLegacyPrepareCalls = (tree, report) => {
27
26
  console.log(` - ${context}: ${count} usages`);
28
27
  });
29
28
  console.log('\n🔍 Detecting polling usage...');
30
- const pollingInfo = detectPollingUsage(tree);
29
+ const pollingInfo = detectPollingUsage(tree, scope);
31
30
  if (pollingInfo.size > 0) {
32
31
  console.log(`\n⚠️ Found ${pollingInfo.size} queries with polling:`);
33
32
  pollingInfo.forEach((info) => {
@@ -38,7 +37,7 @@ export const migrateLegacyPrepareCalls = (tree, report) => {
38
37
  }
39
38
  const classInjectors = findOrCreateInjectorMembers(tree, allUsages);
40
39
  createInjectorMembers(tree, classInjectors);
41
- transformLegacyCreatorPrepareCalls(tree, classInjectors, pollingInfo);
40
+ transformLegacyCreatorPrepareCalls(tree, classInjectors, pollingInfo, scope);
42
41
  reportManualReviewLocations(tree, allUsages, report);
43
42
  };
44
43
  const findLegacyCreatorUsages = (content, filePath) => {
@@ -283,7 +282,7 @@ const addInjectorMemberToClass = (content, classInfo) => {
283
282
  const injectorMember = `${indentation}private ${classInfo.injectorMemberName} = inject(Injector);\n\n`;
284
283
  return content.slice(0, classBodyStart) + injectorMember + content.slice(classBodyStart);
285
284
  };
286
- const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo) => {
285
+ const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo, scope) => {
287
286
  const updatedFiles = [];
288
287
  const filesByPath = new Map();
289
288
  classInjectors.forEach((classInfo) => {
@@ -292,7 +291,7 @@ const transformLegacyCreatorPrepareCalls = (tree, classInjectors, pollingInfo) =
292
291
  }
293
292
  filesByPath.get(classInfo.filePath).push(classInfo);
294
293
  });
295
- visitNotIgnoredFiles(tree, '', (filePath) => {
294
+ scope.visit(tree, (filePath) => {
296
295
  if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts') || filesByPath.has(filePath)) {
297
296
  return;
298
297
  }
@@ -663,9 +662,9 @@ const containsInjectCall = (node) => {
663
662
  visit(node);
664
663
  return hasInject;
665
664
  };
666
- const detectPollingUsage = (tree) => {
665
+ const detectPollingUsage = (tree, scope) => {
667
666
  const pollingInfo = new Map();
668
- visitNotIgnoredFiles(tree, '', (filePath) => {
667
+ scope.visit(tree, (filePath) => {
669
668
  if (filePath.endsWith('.spec.ts')) {
670
669
  return;
671
670
  }