@ethlete/query 6.0.0-next.45 → 6.0.0-next.48
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 +106 -0
- package/fesm2022/ethlete-query-testing.mjs +138 -52
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +1960 -984
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/migrate-query-opt-in-features/migration.js +13 -1
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +6 -4
- package/generators/migrate-to-query-v3/cleanup-migration.js +13 -52
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +9 -12
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +81 -15
- package/generators/migrate-to-query-v3/migration-scope.js +0 -3
- package/generators/migrate-to-query-v3/query-client-migration.js +97 -47
- package/generators/migrate-to-query-v3/rename-symbols.js +19 -6
- package/generators/migrate-to-query-v3/shared.js +6 -2
- package/generators/prep-for-query-v3/migration.js +68 -11
- package/package.json +5 -5
- package/types/ethlete-query-testing.d.ts +37 -5
- package/types/ethlete-query.d.ts +310 -119
|
@@ -115,17 +115,25 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
115
115
|
const sourceFile = createSourceFile(content, filePath);
|
|
116
116
|
const replacements = [];
|
|
117
117
|
const variableRenames = new Map();
|
|
118
|
+
let hasUnmigratedClient = false;
|
|
118
119
|
const visit = (node) => {
|
|
119
|
-
if (ts.isNewExpression(node) &&
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && node.expression.text === 'V2QueryClient') {
|
|
121
|
+
const configArgument = node.arguments?.[0];
|
|
122
|
+
if (!configArgument || !ts.isObjectLiteralExpression(configArgument)) {
|
|
123
|
+
hasUnmigratedClient = true;
|
|
124
|
+
report.addManualReview({
|
|
125
|
+
title: 'Migrate a V2QueryClient whose config is not an object literal',
|
|
126
|
+
summary: `\`${node.getText(sourceFile)}\` passes its config indirectly, so the migration cannot read it and left the client on v2.`,
|
|
127
|
+
action: 'Inline the config object and re-run the migration, or rewrite the client by hand as `createQueryClient({ baseUrl, name })` with provider aliases and creators.',
|
|
128
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
|
|
129
|
+
source: 'query-client-migration',
|
|
130
|
+
dedupeKey: `unmigrated-client:${filePath}:${node.getStart(sourceFile)}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
126
134
|
const oldVariableName = getVariableNameForQueryClient(node);
|
|
127
|
-
const newVariableName = ensureConfigSuffix(oldVariableName);
|
|
128
|
-
if (oldVariableName !== newVariableName) {
|
|
135
|
+
const newVariableName = oldVariableName && ensureConfigSuffix(oldVariableName);
|
|
136
|
+
if (oldVariableName && newVariableName && oldVariableName !== newVariableName) {
|
|
129
137
|
const collisionRegex = new RegExp(`\\b(?:const|let|var)\\s+${newVariableName}\\b`);
|
|
130
138
|
if (collisionRegex.test(content)) {
|
|
131
139
|
report.addWarning({
|
|
@@ -158,16 +166,20 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
158
166
|
replacements.forEach(({ start, end, replacement }) => {
|
|
159
167
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
160
168
|
});
|
|
161
|
-
result = removeQueryClientImport(result);
|
|
169
|
+
result = removeQueryClientImport(result, hasUnmigratedClient);
|
|
162
170
|
return renameVariables(result, variableRenames);
|
|
163
171
|
};
|
|
164
|
-
const removeQueryClientImport = (content) => {
|
|
172
|
+
const removeQueryClientImport = (content, keepLegacyClient) => {
|
|
165
173
|
const sourceFile = createSourceFile(content);
|
|
166
174
|
let queryImportNode;
|
|
167
175
|
ts.forEachChild(sourceFile, (node) => {
|
|
168
|
-
if (
|
|
176
|
+
if (!queryImportNode &&
|
|
177
|
+
ts.isImportDeclaration(node) &&
|
|
169
178
|
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
170
|
-
node.moduleSpecifier.text === '@ethlete/query'
|
|
179
|
+
node.moduleSpecifier.text === '@ethlete/query' &&
|
|
180
|
+
node.importClause?.namedBindings &&
|
|
181
|
+
ts.isNamedImports(node.importClause.namedBindings) &&
|
|
182
|
+
node.importClause.namedBindings.elements.some((element) => element.name.text === 'V2QueryClient')) {
|
|
171
183
|
queryImportNode = node;
|
|
172
184
|
}
|
|
173
185
|
});
|
|
@@ -183,10 +195,10 @@ const removeQueryClientImport = (content) => {
|
|
|
183
195
|
return ensureImportFromQuery(content, requiredImports);
|
|
184
196
|
}
|
|
185
197
|
const existingElements = queryImportNode.importClause.namedBindings.elements.filter((element) => {
|
|
186
|
-
return element.name.text !== 'V2QueryClient';
|
|
198
|
+
return keepLegacyClient || element.name.text !== 'V2QueryClient';
|
|
187
199
|
});
|
|
188
200
|
const nextImports = new Set(requiredImports);
|
|
189
|
-
existingElements.forEach((element) => nextImports.add(element.
|
|
201
|
+
existingElements.forEach((element) => nextImports.add(element.getText(sourceFile)));
|
|
190
202
|
const nextImportStatement = `import { ${Array.from(nextImports).sort().join(', ')} } from '@ethlete/query';`;
|
|
191
203
|
return (content.slice(0, queryImportNode.getStart(sourceFile)) +
|
|
192
204
|
nextImportStatement +
|
|
@@ -267,14 +279,37 @@ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, repo
|
|
|
267
279
|
dedupeKey: `dropped-client-option:${filePath}:${optionPath}`,
|
|
268
280
|
});
|
|
269
281
|
};
|
|
282
|
+
const raiseSpread = (spread) => {
|
|
283
|
+
const text = spread.getText(sourceFile);
|
|
284
|
+
report.addWarning({
|
|
285
|
+
title: `Reconfigure spread query client options "${text}"`,
|
|
286
|
+
summary: `The v2 client config spreads \`${text}\`. The migration cannot see which options it holds, so none of them were carried over to \`createQueryClient\`.`,
|
|
287
|
+
action: 'Inline the spread options that are still needed (`baseRoute` becomes `baseUrl`) and re-check the result.',
|
|
288
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(spread.getStart(sourceFile)).line + 1 }],
|
|
289
|
+
source: 'query-client-migration',
|
|
290
|
+
dedupeKey: `dropped-client-spread:${filePath}:${spread.getStart(sourceFile)}`,
|
|
291
|
+
});
|
|
292
|
+
};
|
|
270
293
|
configArgument.properties.forEach((property) => {
|
|
271
|
-
if (
|
|
294
|
+
if (ts.isSpreadAssignment(property)) {
|
|
295
|
+
raiseSpread(property);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
if ((!ts.isPropertyAssignment(property) && !ts.isShorthandPropertyAssignment(property)) ||
|
|
299
|
+
!ts.isIdentifier(property.name)) {
|
|
272
300
|
return;
|
|
273
301
|
}
|
|
274
302
|
const name = property.name.text;
|
|
275
|
-
if ((name === 'request' || name === 'logging') &&
|
|
303
|
+
if ((name === 'request' || name === 'logging') &&
|
|
304
|
+
ts.isPropertyAssignment(property) &&
|
|
305
|
+
ts.isObjectLiteralExpression(property.initializer)) {
|
|
276
306
|
property.initializer.properties.forEach((nested) => {
|
|
277
|
-
if (
|
|
307
|
+
if (ts.isSpreadAssignment(nested)) {
|
|
308
|
+
raiseSpread(nested);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if ((!ts.isPropertyAssignment(nested) && !ts.isShorthandPropertyAssignment(nested)) ||
|
|
312
|
+
!ts.isIdentifier(nested.name)) {
|
|
278
313
|
return;
|
|
279
314
|
}
|
|
280
315
|
if (name === 'request' && ['queryParams', 'cacheAdapter', 'retryFn'].includes(nested.name.text)) {
|
|
@@ -289,40 +324,42 @@ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, repo
|
|
|
289
324
|
}
|
|
290
325
|
});
|
|
291
326
|
};
|
|
327
|
+
const getPropertyValueText = (property, sourceFile) => {
|
|
328
|
+
if (ts.isShorthandPropertyAssignment(property)) {
|
|
329
|
+
return property.name.text;
|
|
330
|
+
}
|
|
331
|
+
return ts.isPropertyAssignment(property) ? property.initializer.getText(sourceFile) : undefined;
|
|
332
|
+
};
|
|
333
|
+
const findProperty = (objectLiteral, name) => objectLiteral.properties.find((property) => {
|
|
334
|
+
return ((ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) &&
|
|
335
|
+
ts.isIdentifier(property.name) &&
|
|
336
|
+
property.name.text === name);
|
|
337
|
+
});
|
|
292
338
|
const migrateConfigObject = (configArgument, node, sourceFile) => {
|
|
293
|
-
const variableName = getVariableNameForQueryClient(node);
|
|
339
|
+
const variableName = getVariableNameForQueryClient(node) ?? 'client';
|
|
294
340
|
const nextConfig = [];
|
|
295
|
-
const baseRouteProperty = configArgument
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const baseRouteValue = baseRouteProperty.initializer
|
|
300
|
-
.getText(sourceFile)
|
|
301
|
-
.replace('as `https://${string}`', '')
|
|
302
|
-
.trim();
|
|
303
|
-
nextConfig.push(`baseUrl: ${baseRouteValue}`);
|
|
341
|
+
const baseRouteProperty = findProperty(configArgument, 'baseRoute');
|
|
342
|
+
const baseRouteText = baseRouteProperty && getPropertyValueText(baseRouteProperty, sourceFile);
|
|
343
|
+
if (baseRouteText) {
|
|
344
|
+
nextConfig.push(`baseUrl: ${baseRouteText.replace('as `https://${string}`', '').trim()}`);
|
|
304
345
|
}
|
|
305
346
|
nextConfig.push(`name: '${variableName}'`);
|
|
306
|
-
const requestProperty = configArgument
|
|
307
|
-
return ts.isPropertyAssignment(property) && ts.isIdentifier(property.name) && property.name.text === 'request';
|
|
308
|
-
});
|
|
347
|
+
const requestProperty = findProperty(configArgument, 'request');
|
|
309
348
|
if (requestProperty &&
|
|
310
349
|
ts.isPropertyAssignment(requestProperty) &&
|
|
311
350
|
ts.isObjectLiteralExpression(requestProperty.initializer)) {
|
|
312
|
-
requestProperty.initializer
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
351
|
+
const requestConfig = requestProperty.initializer;
|
|
352
|
+
for (const [from, to] of [
|
|
353
|
+
['queryParams', 'queryString'],
|
|
354
|
+
['cacheAdapter', 'cacheAdapter'],
|
|
355
|
+
['retryFn', 'retryFn'],
|
|
356
|
+
]) {
|
|
357
|
+
const property = findProperty(requestConfig, from);
|
|
358
|
+
const valueText = property && getPropertyValueText(property, sourceFile);
|
|
359
|
+
if (valueText) {
|
|
360
|
+
nextConfig.push(`${to}: ${valueText}`);
|
|
321
361
|
}
|
|
322
|
-
|
|
323
|
-
nextConfig.push(`retryFn: ${property.initializer.getText(sourceFile)}`);
|
|
324
|
-
}
|
|
325
|
-
});
|
|
362
|
+
}
|
|
326
363
|
}
|
|
327
364
|
return `createQueryClient({\n ${nextConfig.join(',\n ')}\n})`;
|
|
328
365
|
};
|
|
@@ -334,7 +371,7 @@ const getVariableNameForQueryClient = (node) => {
|
|
|
334
371
|
}
|
|
335
372
|
parent = parent.parent;
|
|
336
373
|
}
|
|
337
|
-
return
|
|
374
|
+
return undefined;
|
|
338
375
|
};
|
|
339
376
|
const extractClientConfigNames = (content) => {
|
|
340
377
|
const sourceFile = createSourceFile(content);
|
|
@@ -433,6 +470,19 @@ const generateCreatorsForConfig = (configName) => {
|
|
|
433
470
|
`export const ${creatorBaseName}Delete = createDeleteQuery(${configVariableName});`,
|
|
434
471
|
].join('\n');
|
|
435
472
|
};
|
|
473
|
+
const declaresName = (declarations, name) => declarations.some((declaration) => declaration.name && ts.isIdentifier(declaration.name) && declaration.name.text === name);
|
|
474
|
+
const isShadowedByLocalDeclaration = (identifier) => {
|
|
475
|
+
for (let scope = identifier.parent; scope && !ts.isSourceFile(scope); scope = scope.parent) {
|
|
476
|
+
if (ts.isFunctionLike(scope) && declaresName(scope.parameters, identifier.text)) {
|
|
477
|
+
return true;
|
|
478
|
+
}
|
|
479
|
+
if (ts.isBlock(scope) &&
|
|
480
|
+
scope.statements.some((statement) => ts.isVariableStatement(statement) && declaresName(statement.declarationList.declarations, identifier.text))) {
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
return false;
|
|
485
|
+
};
|
|
436
486
|
const updateImportsInFile = (content, renames) => {
|
|
437
487
|
const sourceFile = createSourceFile(content);
|
|
438
488
|
const replacements = [];
|
|
@@ -443,7 +493,7 @@ const updateImportsInFile = (content, renames) => {
|
|
|
443
493
|
const nextElements = [];
|
|
444
494
|
let hasChanges = false;
|
|
445
495
|
node.importClause.namedBindings.elements.forEach((element) => {
|
|
446
|
-
const nextName = renames.get(element.name.text);
|
|
496
|
+
const nextName = renames.get(element.propertyName?.text ?? element.name.text);
|
|
447
497
|
if (!nextName) {
|
|
448
498
|
nextElements.push(element.getText(sourceFile));
|
|
449
499
|
return;
|
|
@@ -494,7 +544,7 @@ const updateImportsInFile = (content, renames) => {
|
|
|
494
544
|
ts.forEachChild(node, visit);
|
|
495
545
|
return;
|
|
496
546
|
}
|
|
497
|
-
if (ts.isPropertyAssignment(parent) && parent.name === node) {
|
|
547
|
+
if ((ts.isPropertyAssignment(parent) && parent.name === node) || isShadowedByLocalDeclaration(node)) {
|
|
498
548
|
ts.forEachChild(node, visit);
|
|
499
549
|
return;
|
|
500
550
|
}
|
|
@@ -17,8 +17,6 @@ const isNamePosition = (node) => {
|
|
|
17
17
|
// Object literal / type member keys, enum members, JSX-ish attribute names.
|
|
18
18
|
if (ts.isPropertyAssignment(parent) && parent.name === node)
|
|
19
19
|
return true;
|
|
20
|
-
if (ts.isShorthandPropertyAssignment(parent) && parent.name === node)
|
|
21
|
-
return true;
|
|
22
20
|
if (ts.isPropertySignature(parent) && parent.name === node)
|
|
23
21
|
return true;
|
|
24
22
|
if (ts.isMethodSignature(parent) && parent.name === node)
|
|
@@ -57,12 +55,24 @@ const isNamePosition = (node) => {
|
|
|
57
55
|
if (ts.isLabeledStatement(parent) && parent.label === node)
|
|
58
56
|
return true;
|
|
59
57
|
// Import / export clauses are rewritten by the caller, never by the reference pass.
|
|
60
|
-
if (ts.isImportSpecifier(parent)
|
|
58
|
+
if (ts.isImportSpecifier(parent))
|
|
59
|
+
return true;
|
|
60
|
+
if (ts.isExportSpecifier(parent) &&
|
|
61
|
+
(parent.parent.parent.moduleSpecifier || (parent.propertyName && parent.name === node))) {
|
|
61
62
|
return true;
|
|
63
|
+
}
|
|
62
64
|
if (ts.isImportClause(parent) || ts.isNamespaceImport(parent))
|
|
63
65
|
return true;
|
|
64
66
|
return false;
|
|
65
67
|
};
|
|
68
|
+
const renamedReference = (node, nextName) => {
|
|
69
|
+
const parent = node.parent;
|
|
70
|
+
if (ts.isShorthandPropertyAssignment(parent))
|
|
71
|
+
return `${node.text}: ${nextName}`;
|
|
72
|
+
if (ts.isExportSpecifier(parent) && !parent.propertyName)
|
|
73
|
+
return `${nextName} as ${node.text}`;
|
|
74
|
+
return nextName;
|
|
75
|
+
};
|
|
66
76
|
/** Every name the file declares itself - a local by the same name means the import is shadowed. */
|
|
67
77
|
const collectLocalDeclarations = (sourceFile) => {
|
|
68
78
|
const declared = new Set();
|
|
@@ -115,7 +125,11 @@ export const renameImportedReferences = (content, renames) => {
|
|
|
115
125
|
if (ts.isIdentifier(node)) {
|
|
116
126
|
const nextName = applicable.get(node.text);
|
|
117
127
|
if (nextName && !isNamePosition(node)) {
|
|
118
|
-
replacements.push({
|
|
128
|
+
replacements.push({
|
|
129
|
+
start: node.getStart(sourceFile),
|
|
130
|
+
end: node.getEnd(),
|
|
131
|
+
replacement: renamedReference(node, nextName),
|
|
132
|
+
});
|
|
119
133
|
}
|
|
120
134
|
}
|
|
121
135
|
ts.forEachChild(node, visit);
|
|
@@ -136,7 +150,7 @@ export const renameImportedReferences = (content, renames) => {
|
|
|
136
150
|
* the same generator invocation so they land that way. Identifiers inside template strings count as
|
|
137
151
|
* usage too - a component referenced only from an inline template must not be pruned.
|
|
138
152
|
*/
|
|
139
|
-
export const pruneUnusedNamedImports = (content
|
|
153
|
+
export const pruneUnusedNamedImports = (content) => {
|
|
140
154
|
const sourceFile = createSourceFile(content);
|
|
141
155
|
const used = new Set();
|
|
142
156
|
const addWordsFrom = (text) => {
|
|
@@ -162,7 +176,6 @@ export const pruneUnusedNamedImports = (content, moduleSpecifiers) => {
|
|
|
162
176
|
for (const node of sourceFile.statements) {
|
|
163
177
|
if (!ts.isImportDeclaration(node) ||
|
|
164
178
|
!ts.isStringLiteral(node.moduleSpecifier) ||
|
|
165
|
-
(moduleSpecifiers !== 'all' && !moduleSpecifiers.includes(node.moduleSpecifier.text)) ||
|
|
166
179
|
!node.importClause?.namedBindings ||
|
|
167
180
|
!ts.isNamedImports(node.importClause.namedBindings)) {
|
|
168
181
|
continue;
|
|
@@ -17,9 +17,13 @@ export const ensureNamedImports = ({ content, importsNeeded, moduleSpecifier })
|
|
|
17
17
|
const sourceFile = createSourceFile(content);
|
|
18
18
|
let importNode;
|
|
19
19
|
ts.forEachChild(sourceFile, (node) => {
|
|
20
|
-
if (
|
|
20
|
+
if (!importNode &&
|
|
21
|
+
ts.isImportDeclaration(node) &&
|
|
21
22
|
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
22
|
-
node.moduleSpecifier.text === moduleSpecifier
|
|
23
|
+
node.moduleSpecifier.text === moduleSpecifier &&
|
|
24
|
+
!node.importClause?.isTypeOnly &&
|
|
25
|
+
node.importClause?.namedBindings &&
|
|
26
|
+
ts.isNamedImports(node.importClause.namedBindings)) {
|
|
23
27
|
importNode = node;
|
|
24
28
|
}
|
|
25
29
|
});
|
|
@@ -76,7 +76,18 @@ function renameSymbolsInFile(content, filePath, symbolUsageCounts) {
|
|
|
76
76
|
const replacements = [];
|
|
77
77
|
// Track which symbols are imported from @ethlete/query
|
|
78
78
|
const importedSymbols = new Set();
|
|
79
|
+
const namespaceAliases = collectQueryNamespaceAliases(sourceFile);
|
|
79
80
|
function visit(node) {
|
|
81
|
+
const namespaceMember = getNamespaceMember(node, namespaceAliases)?.member;
|
|
82
|
+
const renamedMember = namespaceMember && (TYPE_RENAMES.get(namespaceMember.text) || FUNCTION_RENAMES.get(namespaceMember.text));
|
|
83
|
+
if (namespaceMember && renamedMember) {
|
|
84
|
+
replacements.push({
|
|
85
|
+
start: namespaceMember.getStart(sourceFile),
|
|
86
|
+
end: namespaceMember.getEnd(),
|
|
87
|
+
replacement: renamedMember,
|
|
88
|
+
oldName: namespaceMember.text,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
80
91
|
// Track imports from @ethlete/query
|
|
81
92
|
if (ts.isImportDeclaration(node)) {
|
|
82
93
|
const moduleSpecifier = node.moduleSpecifier;
|
|
@@ -107,7 +118,9 @@ function renameSymbolsInFile(content, filePath, symbolUsageCounts) {
|
|
|
107
118
|
return;
|
|
108
119
|
}
|
|
109
120
|
// Skip if it's the imported name in an import specifier (we'll handle that separately)
|
|
110
|
-
if (ts.isImportSpecifier(parent) && (parent.propertyName === node || parent.name === node))
|
|
121
|
+
if ((ts.isImportSpecifier(parent) && (parent.propertyName === node || parent.name === node)) ||
|
|
122
|
+
(ts.isPropertyAccessExpression(parent) && parent.name === node) ||
|
|
123
|
+
(ts.isQualifiedName(parent) && parent.right === node)) {
|
|
111
124
|
ts.forEachChild(node, visit);
|
|
112
125
|
return;
|
|
113
126
|
}
|
|
@@ -138,6 +151,33 @@ function renameSymbolsInFile(content, filePath, symbolUsageCounts) {
|
|
|
138
151
|
}
|
|
139
152
|
return result;
|
|
140
153
|
}
|
|
154
|
+
function collectQueryNamespaceAliases(sourceFile) {
|
|
155
|
+
const aliases = new Set();
|
|
156
|
+
sourceFile.statements.forEach((statement) => {
|
|
157
|
+
if (!ts.isImportDeclaration(statement)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const namedBindings = statement.importClause?.namedBindings;
|
|
161
|
+
if (namedBindings &&
|
|
162
|
+
ts.isNamespaceImport(namedBindings) &&
|
|
163
|
+
ts.isStringLiteral(statement.moduleSpecifier) &&
|
|
164
|
+
statement.moduleSpecifier.text === '@ethlete/query') {
|
|
165
|
+
aliases.add(namedBindings.name.text);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return aliases;
|
|
169
|
+
}
|
|
170
|
+
function getNamespaceMember(node, namespaceAliases) {
|
|
171
|
+
const [namespace, member] = ts.isPropertyAccessExpression(node)
|
|
172
|
+
? [node.expression, node.name]
|
|
173
|
+
: ts.isQualifiedName(node)
|
|
174
|
+
? [node.left, node.right]
|
|
175
|
+
: [];
|
|
176
|
+
if (!namespace || !member || !ts.isIdentifier(namespace) || !ts.isIdentifier(member)) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
return namespaceAliases.has(namespace.text) ? { namespace, member } : undefined;
|
|
180
|
+
}
|
|
141
181
|
function updateImports(sourceFile) {
|
|
142
182
|
const replacements = [];
|
|
143
183
|
function visit(node) {
|
|
@@ -232,9 +272,11 @@ function replaceNamespaceInFile(content, filePath) {
|
|
|
232
272
|
ts.forEachChild(node, collectImport);
|
|
233
273
|
}
|
|
234
274
|
collectImport(sourceFile);
|
|
275
|
+
const namespaceAliases = collectQueryNamespaceAliases(sourceFile);
|
|
276
|
+
const namespaceReplacements = collectNamespacedExperimentalQueryReplacements(sourceFile, namespaceAliases);
|
|
235
277
|
// If no ExperimentalQuery import found, return unchanged
|
|
236
278
|
if (!hasExperimentalQueryImport || !experimentalQueryAlias) {
|
|
237
|
-
return
|
|
279
|
+
return applyReplacements(content, namespaceReplacements, namespaceReplacements.length);
|
|
238
280
|
}
|
|
239
281
|
// Second pass: collect used symbols
|
|
240
282
|
function findUsages(node) {
|
|
@@ -313,17 +355,32 @@ function replaceNamespaceInFile(content, filePath) {
|
|
|
313
355
|
ts.forEachChild(node, createReplacements);
|
|
314
356
|
}
|
|
315
357
|
createReplacements(sourceFile);
|
|
316
|
-
//
|
|
358
|
+
// Subtract 1 for the import replacement
|
|
359
|
+
return applyReplacements(content, [...replacements, ...namespaceReplacements], replacements.length - 1 + namespaceReplacements.length);
|
|
360
|
+
}
|
|
361
|
+
function collectNamespacedExperimentalQueryReplacements(sourceFile, namespaceAliases) {
|
|
362
|
+
const replacements = [];
|
|
363
|
+
if (namespaceAliases.size === 0) {
|
|
364
|
+
return replacements;
|
|
365
|
+
}
|
|
366
|
+
function visit(node) {
|
|
367
|
+
const access = getNamespaceMember(node, namespaceAliases);
|
|
368
|
+
if (access?.member.text === 'ExperimentalQuery') {
|
|
369
|
+
replacements.push({ start: node.getStart(sourceFile), end: node.getEnd(), replacement: access.namespace.text });
|
|
370
|
+
}
|
|
371
|
+
ts.forEachChild(node, visit);
|
|
372
|
+
}
|
|
373
|
+
visit(sourceFile);
|
|
374
|
+
return replacements;
|
|
375
|
+
}
|
|
376
|
+
function applyReplacements(content, replacements, replacementCount) {
|
|
317
377
|
let result = content;
|
|
318
|
-
replacements
|
|
319
|
-
|
|
378
|
+
[...replacements]
|
|
379
|
+
.sort((a, b) => b.start - a.start)
|
|
380
|
+
.forEach(({ start, end, replacement }) => {
|
|
320
381
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
321
|
-
}
|
|
322
|
-
return {
|
|
323
|
-
content: result,
|
|
324
|
-
modified: replacements.length > 0,
|
|
325
|
-
replacementCount: replacements.length - 1, // Subtract 1 for the import replacement
|
|
326
|
-
};
|
|
382
|
+
});
|
|
383
|
+
return { content: result, modified: replacements.length > 0, replacementCount };
|
|
327
384
|
}
|
|
328
385
|
//#endregion
|
|
329
386
|
//# sourceMappingURL=migration.js.map
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethlete/query",
|
|
3
|
-
"version": "6.0.0-next.
|
|
3
|
+
"version": "6.0.0-next.48",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"peerDependencies": {
|
|
7
7
|
"@analogjs/vite-plugin-angular": "2.6.3",
|
|
8
|
-
"@angular/common": "22.
|
|
9
|
-
"@angular/core": "22.
|
|
10
|
-
"@angular/forms": "22.
|
|
11
|
-
"@angular/router": "22.
|
|
8
|
+
"@angular/common": "22.1.6",
|
|
9
|
+
"@angular/core": "22.1.6",
|
|
10
|
+
"@angular/forms": "22.1.6",
|
|
11
|
+
"@angular/router": "22.1.6",
|
|
12
12
|
"@ethlete/core": "^5.0.0-beta.11",
|
|
13
13
|
"@ethlete/types": "^2.0.0-beta.4",
|
|
14
14
|
"@nx/devkit": "23.1.0",
|
|
@@ -15,6 +15,11 @@ type QueryTestSetup = {
|
|
|
15
15
|
createPut: ReturnType<typeof createPutQuery>;
|
|
16
16
|
createPatch: ReturnType<typeof createPatchQuery>;
|
|
17
17
|
createDelete: ReturnType<typeof createDeleteQuery>;
|
|
18
|
+
/**
|
|
19
|
+
* Puts back the `console.warn` / `console.error` that were installed before the first
|
|
20
|
+
* {@link setupQueryTest} call. Idempotent, and safe to call from any of the setups a file created.
|
|
21
|
+
*/
|
|
22
|
+
restoreConsole: () => void;
|
|
18
23
|
};
|
|
19
24
|
type QueryTestSetupConfig = {
|
|
20
25
|
baseUrl?: string;
|
|
@@ -235,6 +240,8 @@ type FakeQueryPersistenceStoreHandle = {
|
|
|
235
240
|
calls: () => Record<'loadIndex' | 'read' | 'write' | 'remove' | 'clear', number>;
|
|
236
241
|
/** Makes the next `count` writes reject - a full disk, by default. */
|
|
237
242
|
failNextWrites: (count: number, error?: unknown) => void;
|
|
243
|
+
/** Makes the next `count` removals reject. */
|
|
244
|
+
failNextRemoves: (count: number, error?: unknown) => void;
|
|
238
245
|
/** Makes the next `loadIndex` reject. */
|
|
239
246
|
failNextLoadIndex: (error?: unknown) => void;
|
|
240
247
|
/**
|
|
@@ -245,6 +252,14 @@ type FakeQueryPersistenceStoreHandle = {
|
|
|
245
252
|
deferReads: () => void;
|
|
246
253
|
/** Settles the reads held by {@link deferReads} and stops deferring. */
|
|
247
254
|
flushReads: () => Promise<void>;
|
|
255
|
+
/**
|
|
256
|
+
* Holds every subsequent `loadIndex` pending until {@link flushLoadIndex} is called, for testing
|
|
257
|
+
* what happens to the store while a client's startup is still reading its index (a real disk takes
|
|
258
|
+
* milliseconds). The snapshot each held load returns is the one it would have read when it started.
|
|
259
|
+
*/
|
|
260
|
+
deferLoadIndex: () => void;
|
|
261
|
+
/** Settles the index loads held by {@link deferLoadIndex} and stops deferring. */
|
|
262
|
+
flushLoadIndex: () => Promise<void>;
|
|
248
263
|
};
|
|
249
264
|
declare const createFakeQueryPersistenceStore: () => FakeQueryPersistenceStoreHandle;
|
|
250
265
|
|
|
@@ -266,20 +281,37 @@ type WebSocketTestDouble = {
|
|
|
266
281
|
url: string;
|
|
267
282
|
transports: string[] | undefined;
|
|
268
283
|
} | null;
|
|
269
|
-
/**
|
|
284
|
+
/** The `withCredentials` the client asked the factory for, or `null` until it called it. */
|
|
285
|
+
withCredentials: () => boolean | null;
|
|
286
|
+
/** Every message the client emitted, newest last - room joins and leaves included, buffered ones too. */
|
|
270
287
|
sent: () => {
|
|
271
288
|
event: string;
|
|
272
289
|
data: unknown;
|
|
273
290
|
}[];
|
|
291
|
+
/** Every message that reached the server, newest last. An emit buffered while offline lands here on the next connect. */
|
|
292
|
+
delivered: () => {
|
|
293
|
+
event: string;
|
|
294
|
+
data: unknown;
|
|
295
|
+
}[];
|
|
274
296
|
/** Whether the client asked the socket to connect, and whether it has since disconnected it. */
|
|
275
297
|
state: () => {
|
|
276
298
|
connectRequested: boolean;
|
|
277
299
|
disconnected: boolean;
|
|
278
300
|
};
|
|
279
|
-
/**
|
|
280
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Complete the handshake: flush every buffered emit, then turn the client's `isConnected` true. Pass
|
|
303
|
+
* `recovered: true` for a connection-state-recovered reconnect, in which the server kept the rooms.
|
|
304
|
+
*/
|
|
305
|
+
serverConnect: (options?: {
|
|
306
|
+
recovered?: boolean;
|
|
307
|
+
}) => void;
|
|
281
308
|
/** Drop the connection, so `isConnected` turns false. */
|
|
282
309
|
serverDisconnect: () => void;
|
|
310
|
+
/**
|
|
311
|
+
* Let the ping expire: the socket still reports itself connected, but buffers every emit like socket.io
|
|
312
|
+
* does until it notices and closes. Follow it with {@link serverDisconnect}.
|
|
313
|
+
*/
|
|
314
|
+
serverPingExpire: () => void;
|
|
283
315
|
/** Deliver a message the way the server would. */
|
|
284
316
|
serverSend: (message: SocketMessageView) => void;
|
|
285
317
|
/** Deliver a raw frame, for the malformed-payload path. */
|
|
@@ -291,8 +323,8 @@ type WebSocketTestDouble = {
|
|
|
291
323
|
*
|
|
292
324
|
* Nothing here connects on its own: the client's own `connect()` only flips
|
|
293
325
|
* {@link WebSocketTestDouble.state}, and it is {@link WebSocketTestDouble.serverConnect} that fires
|
|
294
|
-
* the `connect` listener. That split is what makes "joined a room while disconnected, and
|
|
295
|
-
*
|
|
326
|
+
* the `connect` listener. That split is what makes "joined a room while disconnected, and it was
|
|
327
|
+
* re-joined after a reconnect" testable.
|
|
296
328
|
*/
|
|
297
329
|
declare const createWebSocketTestDouble: () => WebSocketTestDouble;
|
|
298
330
|
|