@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.
@@ -1,10 +1,9 @@
1
- import { visitNotIgnoredFiles } from '@nx/devkit';
2
1
  import * as ts from 'typescript';
3
2
  import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromQuery, getVariableStatementEnd, } from './shared.js';
4
- export const migrateQueryClients = (tree, report) => {
3
+ export const migrateQueryClients = (tree, report, scope) => {
5
4
  const queryClientFiles = new Map();
6
5
  const variableRenames = new Map();
7
- visitNotIgnoredFiles(tree, '', (filePath) => {
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
- visitNotIgnoredFiles(tree, '', (filePath) => {
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 = [];
@@ -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",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethlete/query",
3
- "version": "6.0.0-next.19",
3
+ "version": "6.0.0-next.20",
4
4
  "license": "MIT",
5
5
  "sideEffects": false,
6
6
  "peerDependencies": {