@ethlete/query 6.0.0-next.50 → 6.0.0-next.52
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 +41 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs +321 -0
- package/fesm2022/ethlete-query-ngrx-toolkit.mjs.map +1 -0
- package/fesm2022/ethlete-query.mjs +359 -139
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/generators.json +5 -0
- package/generators/migrate-from-ngrx-toolkit/args-assignability.js +140 -0
- package/generators/migrate-from-ngrx-toolkit/consumers.js +453 -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 +139 -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-opt-in-features/migration.js +10 -9
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +29 -2
- package/generators/migrate-to-query-v3/cleanup-migration.js +22 -1
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +39 -2
- package/generators/migrate-to-query-v3/migration.js +2 -1
- package/generators/migrate-to-query-v3/module-graph.js +2 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +71 -2
- package/generators/migrate-to-query-v3/shared.js +27 -0
- package/generators/prep-for-query-v3/migration.js +83 -1
- package/migrations/query-field-default-types.md +43 -0
- package/migrations/search-query-field-string.md +43 -0
- package/migrations.json +16 -0
- package/package.json +5 -1
- package/types/ethlete-query-ngrx-toolkit.d.ts +143 -0
- package/types/ethlete-query.d.ts +129 -34
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { pruneUnusedNamedImports } from '../migrate-to-query-v3/rename-symbols.js';
|
|
3
|
+
import { createSourceFile } from '../migrate-to-query-v3/shared.js';
|
|
4
|
+
/** Applies edits back to front. An edit nested inside an already applied one is dropped. */
|
|
5
|
+
export const applyEdits = (content, edits) => {
|
|
6
|
+
const sorted = [...edits].sort((left, right) => right.start - left.start || right.end - left.end);
|
|
7
|
+
let result = content;
|
|
8
|
+
let floor = Number.POSITIVE_INFINITY;
|
|
9
|
+
for (const edit of sorted) {
|
|
10
|
+
if (edit.end > floor)
|
|
11
|
+
continue;
|
|
12
|
+
result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
|
|
13
|
+
floor = edit.start;
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
};
|
|
17
|
+
/** Removes `nodes[index]` from a comma separated list, together with the comma that belongs to it. */
|
|
18
|
+
export const removeListElement = (nodes, index, sourceFile) => {
|
|
19
|
+
const node = nodes[index];
|
|
20
|
+
if (index < nodes.length - 1) {
|
|
21
|
+
return { start: node.getStart(sourceFile), end: nodes[index + 1].getStart(sourceFile), text: '' };
|
|
22
|
+
}
|
|
23
|
+
if (index > 0) {
|
|
24
|
+
return { start: nodes[index - 1].getEnd(), end: node.getEnd(), text: '' };
|
|
25
|
+
}
|
|
26
|
+
return { start: node.getStart(sourceFile), end: node.getEnd(), text: '' };
|
|
27
|
+
};
|
|
28
|
+
/** Removes a whole statement or class member, including its leading comments and the line break after it. */
|
|
29
|
+
export const removeStatement = (node, content) => {
|
|
30
|
+
const comments = ts.getLeadingCommentRanges(content, node.getFullStart()) ?? [];
|
|
31
|
+
let start = comments[0]?.pos ?? node.getStart();
|
|
32
|
+
while (start > 0 && (content[start - 1] === ' ' || content[start - 1] === '\t'))
|
|
33
|
+
start -= 1;
|
|
34
|
+
let end = node.getEnd();
|
|
35
|
+
while (content[end] === ' ' || content[end] === '\t')
|
|
36
|
+
end += 1;
|
|
37
|
+
if (content[end] === '\r')
|
|
38
|
+
end += 1;
|
|
39
|
+
if (content[end] === '\n')
|
|
40
|
+
end += 1;
|
|
41
|
+
return { start, end, text: '' };
|
|
42
|
+
};
|
|
43
|
+
export const lineOf = (sourceFile, position) => sourceFile.getLineAndCharacterOfPosition(position).line + 1;
|
|
44
|
+
export const collectUsedIdentifiers = (sourceFile) => {
|
|
45
|
+
const used = new Set();
|
|
46
|
+
const visit = (node) => {
|
|
47
|
+
if (ts.isImportDeclaration(node))
|
|
48
|
+
return;
|
|
49
|
+
if (ts.isIdentifier(node))
|
|
50
|
+
used.add(node.text);
|
|
51
|
+
ts.forEachChild(node, visit);
|
|
52
|
+
};
|
|
53
|
+
visit(sourceFile);
|
|
54
|
+
return used;
|
|
55
|
+
};
|
|
56
|
+
/** Collapses runs of blank lines left behind by removed statements, outside template literals. */
|
|
57
|
+
export const collapseBlankLines = (content) => {
|
|
58
|
+
const sourceFile = createSourceFile(content);
|
|
59
|
+
const templates = [];
|
|
60
|
+
const visit = (node) => {
|
|
61
|
+
if (ts.isTemplateExpression(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
62
|
+
templates.push([node.getStart(sourceFile), node.getEnd()]);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
ts.forEachChild(node, visit);
|
|
66
|
+
};
|
|
67
|
+
visit(sourceFile);
|
|
68
|
+
return content.replace(/\n[ \t]*\n(?:[ \t]*\n)+/g, (match, offset) => templates.some(([start, end]) => offset >= start && offset < end) ? match : '\n\n');
|
|
69
|
+
};
|
|
70
|
+
/** Drops unused namespace, default and named imports. Side-effect imports stay. */
|
|
71
|
+
export const pruneUnusedImports = (content) => {
|
|
72
|
+
const sourceFile = createSourceFile(content);
|
|
73
|
+
const used = collectUsedIdentifiers(sourceFile);
|
|
74
|
+
const edits = [];
|
|
75
|
+
for (const statement of sourceFile.statements) {
|
|
76
|
+
if (!ts.isImportDeclaration(statement) || !statement.importClause)
|
|
77
|
+
continue;
|
|
78
|
+
const { name, namedBindings } = statement.importClause;
|
|
79
|
+
if (namedBindings && ts.isNamespaceImport(namedBindings) && !used.has(namedBindings.name.text)) {
|
|
80
|
+
if (!name || !used.has(name.text))
|
|
81
|
+
edits.push(removeStatement(statement, content));
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
if (!namedBindings && name && !used.has(name.text)) {
|
|
85
|
+
edits.push(removeStatement(statement, content));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return pruneUnusedNamedImports(applyEdits(content, edits));
|
|
89
|
+
};
|
|
90
|
+
/** A relative module specifier from one workspace file to another, without the `.ts` extension. */
|
|
91
|
+
export const relativeSpecifier = (fromFile, toFile) => {
|
|
92
|
+
const fromSegments = fromFile.split('/').slice(0, -1);
|
|
93
|
+
const toSegments = toFile.replace(/\.ts$/, '').split('/');
|
|
94
|
+
let shared = 0;
|
|
95
|
+
while (shared < fromSegments.length &&
|
|
96
|
+
shared < toSegments.length - 1 &&
|
|
97
|
+
fromSegments[shared] === toSegments[shared]) {
|
|
98
|
+
shared += 1;
|
|
99
|
+
}
|
|
100
|
+
const ups = fromSegments.length - shared;
|
|
101
|
+
const rest = toSegments.slice(shared).join('/');
|
|
102
|
+
return ups === 0 ? `./${rest}` : `${'../'.repeat(ups)}${rest}`;
|
|
103
|
+
};
|
|
104
|
+
export const hasExportModifier = (node) => ts.canHaveModifiers(node) &&
|
|
105
|
+
!!ts.getModifiers(node)?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword);
|
|
106
|
+
/** The names a top-level statement declares. */
|
|
107
|
+
export const declaredNames = (statement) => {
|
|
108
|
+
if (ts.isVariableStatement(statement)) {
|
|
109
|
+
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
110
|
+
if (ts.isIdentifier(declaration.name))
|
|
111
|
+
return [declaration.name.text];
|
|
112
|
+
if (ts.isObjectBindingPattern(declaration.name)) {
|
|
113
|
+
return declaration.name.elements.flatMap((element) => ts.isIdentifier(element.name) ? [element.name.text] : []);
|
|
114
|
+
}
|
|
115
|
+
return [];
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if ((ts.isFunctionDeclaration(statement) ||
|
|
119
|
+
ts.isClassDeclaration(statement) ||
|
|
120
|
+
ts.isInterfaceDeclaration(statement) ||
|
|
121
|
+
ts.isTypeAliasDeclaration(statement) ||
|
|
122
|
+
ts.isEnumDeclaration(statement)) &&
|
|
123
|
+
statement.name) {
|
|
124
|
+
return [statement.name.text];
|
|
125
|
+
}
|
|
126
|
+
return [];
|
|
127
|
+
};
|
|
128
|
+
//# sourceMappingURL=ts-edits.js.map
|
|
@@ -4,8 +4,16 @@ import { createMigrationScope } from '../migrate-to-query-v3/migration-scope.js'
|
|
|
4
4
|
import { createSourceFile, ensureImportFromQuery, getLineNumber } from '../migrate-to-query-v3/shared.js';
|
|
5
5
|
import { QueryOptInFeaturesMigrationReport } from './report.js';
|
|
6
6
|
const SOURCE = 'query-opt-in-features';
|
|
7
|
-
const CLIENT_FEATURE = 'withEthleteApiErrors';
|
|
7
|
+
export const CLIENT_FEATURE = 'withEthleteApiErrors';
|
|
8
8
|
const AUTH_FEATURE = 'withBearerAuthMultiTabSync';
|
|
9
|
+
export const reportClientErrorPipelineAdded = (report, location, source, summary = `\`${CLIENT_FEATURE}()\` was added so error parsing and retrying keep working exactly as before.`) => report.addFollowUp({
|
|
10
|
+
title: 'Client kept the full error pipeline',
|
|
11
|
+
summary,
|
|
12
|
+
action: 'Narrow it: `withHtmlErrorParsing()` only for an API behind a proxy that answers HTML, `withSymfonyErrors()` only for violation-list responses, `withDefaultRetry()` only if failed requests should retry themselves.',
|
|
13
|
+
locations: [location],
|
|
14
|
+
source,
|
|
15
|
+
dedupeKey: 'client-error-pipeline',
|
|
16
|
+
});
|
|
9
17
|
const isMigratableFile = (filePath) => /\.(ts|mts|cts)$/.test(filePath) && !filePath.endsWith('.d.ts');
|
|
10
18
|
const propertyNamed = (config, sourceFile, name) => config.properties.find((property) => ts.isPropertyAssignment(property) && property.name.getText(sourceFile) === name);
|
|
11
19
|
const hasFeature = (config, sourceFile, feature) => {
|
|
@@ -87,14 +95,7 @@ const collectEdits = (filePath, sourceFile, report) => {
|
|
|
87
95
|
}
|
|
88
96
|
else if (isClient) {
|
|
89
97
|
const feature = `${CLIENT_FEATURE}()`;
|
|
90
|
-
report
|
|
91
|
-
title: 'Client kept the full error pipeline',
|
|
92
|
-
summary: `\`${CLIENT_FEATURE}()\` was added so error parsing and retrying keep working exactly as before.`,
|
|
93
|
-
action: 'Narrow it: `withHtmlErrorParsing()` only for an API behind a proxy that answers HTML, `withSymfonyErrors()` only for violation-list responses, `withDefaultRetry()` only if failed requests should retry themselves.',
|
|
94
|
-
locations: [{ filePath, line }],
|
|
95
|
-
source: SOURCE,
|
|
96
|
-
dedupeKey: 'client-error-pipeline',
|
|
97
|
-
});
|
|
98
|
+
reportClientErrorPipelineAdded(report, { filePath, line }, SOURCE);
|
|
98
99
|
edits.push({
|
|
99
100
|
start: config.getStart(sourceFile),
|
|
100
101
|
end: config.getEnd(),
|
|
@@ -8,6 +8,10 @@ const REFRESH_CONFIG_KEYS = new Set([
|
|
|
8
8
|
'cookieDomain',
|
|
9
9
|
'cookieExpiresInDays',
|
|
10
10
|
'expiresInPropertyName',
|
|
11
|
+
'cookiePath',
|
|
12
|
+
'cookieSameSite',
|
|
13
|
+
'cookieEnabled',
|
|
14
|
+
'refreshBuffer',
|
|
11
15
|
]);
|
|
12
16
|
export const collectV2BearerAuthConfigs = (tree, scope) => {
|
|
13
17
|
const configs = [];
|
|
@@ -77,12 +81,14 @@ const REFRESH_QUERY_KEY = 'tokenRefresh';
|
|
|
77
81
|
* than presented as finished.
|
|
78
82
|
*/
|
|
79
83
|
export const renderAuthProviderBody = (config) => {
|
|
84
|
+
const warnings = [];
|
|
80
85
|
if (!config?.refreshCreatorName) {
|
|
81
86
|
return {
|
|
82
87
|
queries: ' queries: [],',
|
|
83
88
|
features: '',
|
|
84
89
|
importsNeeded: [],
|
|
85
90
|
isScaffolded: false,
|
|
91
|
+
warnings,
|
|
86
92
|
};
|
|
87
93
|
}
|
|
88
94
|
const refreshEntries = [` queryCreator: ${config.refreshCreatorName},`];
|
|
@@ -95,6 +101,16 @@ export const renderAuthProviderBody = (config) => {
|
|
|
95
101
|
if (config.expiresInPropertyName) {
|
|
96
102
|
refreshEntries.push(` expiresInPropertyName: ${config.expiresInPropertyName},`);
|
|
97
103
|
}
|
|
104
|
+
if (config.refreshBuffer) {
|
|
105
|
+
refreshEntries.push(` refreshStrategy: ${renderRefreshStrategy(config.refreshBuffer)},`);
|
|
106
|
+
}
|
|
107
|
+
if (config.cookieName && config.cookieEnabled && config.cookieEnabled !== 'true') {
|
|
108
|
+
warnings.push({
|
|
109
|
+
title: 'Replace the v2 cookieEnabled switch',
|
|
110
|
+
summary: `The v2 provider set \`cookieEnabled: ${config.cookieEnabled}\`. \`withPersistentAuth\` has no switch for it: it always keeps the refresh token in the cookie, and \`setRememberMe(false)\` only turns that cookie into a session cookie. v2's \`enableCookie()\` / \`disableCookie()\` have no v3 counterpart either.`,
|
|
111
|
+
action: 'If the cookie was only written after an opt-in (a "remember me" box), drive `setRememberMe()` from it. If it was never written, remove `withPersistentAuth` from the scaffold.',
|
|
112
|
+
});
|
|
113
|
+
}
|
|
98
114
|
const queries = [
|
|
99
115
|
' queries: [',
|
|
100
116
|
` // Derived from the v2 BearerAuthProvider in ${config.filePath}:${config.line}. Verify before shipping.`,
|
|
@@ -106,13 +122,17 @@ export const renderAuthProviderBody = (config) => {
|
|
|
106
122
|
].join('\n');
|
|
107
123
|
const importsNeeded = ['withRefreshQuery'];
|
|
108
124
|
if (!config.cookieName) {
|
|
109
|
-
return { queries, features: '', importsNeeded, isScaffolded: true };
|
|
125
|
+
return { queries, features: '', importsNeeded, isScaffolded: true, warnings };
|
|
110
126
|
}
|
|
111
127
|
const cookieEntries = [` name: ${config.cookieName},`];
|
|
112
128
|
if (config.cookieDomain)
|
|
113
129
|
cookieEntries.push(` domain: ${config.cookieDomain},`);
|
|
114
130
|
if (config.cookieExpiresInDays)
|
|
115
131
|
cookieEntries.push(` expiresInDays: ${config.cookieExpiresInDays},`);
|
|
132
|
+
if (config.cookiePath)
|
|
133
|
+
cookieEntries.push(` path: ${config.cookiePath},`);
|
|
134
|
+
if (config.cookieSameSite)
|
|
135
|
+
cookieEntries.push(` sameSite: ${config.cookieSameSite},`);
|
|
116
136
|
const features = [
|
|
117
137
|
' features: [',
|
|
118
138
|
' withPersistentAuth({',
|
|
@@ -128,6 +148,13 @@ export const renderAuthProviderBody = (config) => {
|
|
|
128
148
|
' ],',
|
|
129
149
|
].join('\n');
|
|
130
150
|
importsNeeded.push('withPersistentAuth');
|
|
131
|
-
return { queries, features, importsNeeded, isScaffolded: true };
|
|
151
|
+
return { queries, features, importsNeeded, isScaffolded: true, warnings };
|
|
152
|
+
};
|
|
153
|
+
// A v3 `refreshStrategy` number from 0 to 1 is a fraction of the token lifetime, not milliseconds.
|
|
154
|
+
const renderRefreshStrategy = (refreshBuffer) => {
|
|
155
|
+
const value = Number(refreshBuffer.replace(/_/g, ''));
|
|
156
|
+
return Number.isFinite(value) && value > 1
|
|
157
|
+
? refreshBuffer
|
|
158
|
+
: `{ minBufferMs: ${refreshBuffer}, maxBufferMs: ${refreshBuffer} }`;
|
|
132
159
|
};
|
|
133
160
|
//# sourceMappingURL=auth-provider-scaffold.js.map
|
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
import { createSourceFile, ensureImportFromQuery, ensureNamedImports } from './shared.js';
|
|
2
|
+
import { REMOVED_EXPERIMENTAL_QUERY_HELPERS, createSourceFile, ensureImportFromQuery, ensureNamedImports, findRemovedExperimentalQueryHelpers, } from './shared.js';
|
|
3
|
+
export const reportRemovedExperimentalQueryHelpers = (tree, scope, report) => {
|
|
4
|
+
scope.visit(tree, (filePath) => {
|
|
5
|
+
if (!filePath.endsWith('.ts')) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const content = tree.read(filePath, 'utf-8');
|
|
9
|
+
if (!content?.includes('@ethlete/query')) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
findRemovedExperimentalQueryHelpers(createSourceFile(content, filePath)).forEach(({ name, line }) => {
|
|
13
|
+
report.addWarning({
|
|
14
|
+
title: `Replace the removed helper ${name}`,
|
|
15
|
+
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.`,
|
|
16
|
+
action: REMOVED_EXPERIMENTAL_QUERY_HELPERS[name],
|
|
17
|
+
locations: [{ filePath, line }],
|
|
18
|
+
source: 'cleanup-migration',
|
|
19
|
+
dedupeKey: `removed-experimental-helper:${filePath}:${name}`,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
};
|
|
3
24
|
/**
|
|
4
25
|
* Points existing devtools usage at the v3 components instead of deleting it.
|
|
5
26
|
*
|
|
@@ -10,6 +10,7 @@ const LEGACY_HTTP_METHOD = {
|
|
|
10
10
|
PATCH: 'patch',
|
|
11
11
|
DELETE: 'delete',
|
|
12
12
|
};
|
|
13
|
+
const LEGACY_GQL_METHODS = ['gqlQuery', 'gqlMutate'];
|
|
13
14
|
const HTTP_OPTION_NAMES = ['reportProgress', 'responseType', 'transferCache', 'withCredentials'];
|
|
14
15
|
const isLegacyHttpMethod = (value) => {
|
|
15
16
|
return Object.values(LEGACY_HTTP_METHOD).includes(value);
|
|
@@ -219,6 +220,16 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
|
|
|
219
220
|
ts.forEachChild(node, visit);
|
|
220
221
|
return;
|
|
221
222
|
}
|
|
223
|
+
if (oldClientNames.has(objectExpression.text) && LEGACY_GQL_METHODS.includes(methodExpression.text)) {
|
|
224
|
+
report.addManualReview({
|
|
225
|
+
title: `Rewrite the GraphQL creator ${node.name.text} by hand`,
|
|
226
|
+
summary: `${node.name.text} is built with \`${objectExpression.text}.${methodExpression.text}()\`. The migration only rewrites REST creators, so this one still calls a method the migrated client no longer has.`,
|
|
227
|
+
action: `Rebuild it on the v3 client with \`${methodExpression.text === 'gqlQuery' ? 'createGqlQueryViaPost' : 'createGqlMutationViaPost'}\` (or the \`ViaGet\` variant) and a \`gql\` document, then wrap it in \`createLegacyQueryCreator\` if its call sites stay on v2. See https://ethlete-sdk-docs.web.app/query/gql.`,
|
|
228
|
+
locations: [{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 }],
|
|
229
|
+
source: 'legacy-query-creator-migration',
|
|
230
|
+
dedupeKey: `gql-creator:${filePath}:${node.name.text}`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
222
233
|
if (!oldClientNames.has(objectExpression.text) || !isLegacyHttpMethod(methodExpression.text)) {
|
|
223
234
|
ts.forEachChild(node, visit);
|
|
224
235
|
return;
|
|
@@ -270,7 +281,10 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
|
|
|
270
281
|
if (HTTP_OPTION_NAMES.includes(property.name.text)) {
|
|
271
282
|
info.httpOptions.set(property.name.text, property.name.text);
|
|
272
283
|
}
|
|
273
|
-
if (property.name.text
|
|
284
|
+
if (property.name.text === 'entity') {
|
|
285
|
+
info.entity = property.name.text;
|
|
286
|
+
}
|
|
287
|
+
if (!['route', 'entity'].includes(property.name.text) && !HTTP_OPTION_NAMES.includes(property.name.text)) {
|
|
274
288
|
warnUnreadableConfigValue(property, 'as a shorthand property');
|
|
275
289
|
}
|
|
276
290
|
return;
|
|
@@ -281,6 +295,9 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
|
|
|
281
295
|
if (property.name.text === 'route') {
|
|
282
296
|
info.route = property.initializer.getText(sourceFile);
|
|
283
297
|
}
|
|
298
|
+
if (property.name.text === 'entity') {
|
|
299
|
+
info.entity = property.initializer.getText(sourceFile);
|
|
300
|
+
}
|
|
284
301
|
if (property.name.text === 'secure') {
|
|
285
302
|
if (property.initializer.kind === ts.SyntaxKind.TrueKeyword) {
|
|
286
303
|
info.secure = true;
|
|
@@ -329,6 +346,18 @@ const analyzeLegacyQueryCreators = (content, queryClientFiles, filePath, report)
|
|
|
329
346
|
}
|
|
330
347
|
if (info.route) {
|
|
331
348
|
creators.push(info);
|
|
349
|
+
if (info.entity) {
|
|
350
|
+
report.addFollowUp({
|
|
351
|
+
title: `Check the entity config carried onto ${toLegacyName(creatorName)}`,
|
|
352
|
+
summary: `${creatorName} had an \`entity\` config. It was moved onto the \`${toLegacyName(creatorName)}\` interop wrapper, which still writes to the EntityStore; the v3 creator \`${creatorName}\` does not, and v3 has no EntityStore.`,
|
|
353
|
+
action: 'Keep the call sites that rely on the store on the legacy wrapper. `set` now only runs on a real response, never on `prepare()` or a failure.',
|
|
354
|
+
locations: [
|
|
355
|
+
{ filePath, line: sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1 },
|
|
356
|
+
],
|
|
357
|
+
source: 'legacy-query-creator-migration',
|
|
358
|
+
dedupeKey: `entity-config:${filePath}:${creatorName}`,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
332
361
|
}
|
|
333
362
|
}
|
|
334
363
|
ts.forEachChild(node, visit);
|
|
@@ -509,7 +538,7 @@ const getTypeParameter = (creator) => {
|
|
|
509
538
|
const generateLegacyWrapper = (creator, exportPrefix) => {
|
|
510
539
|
// `name` is what lets a `prepare()` called outside an injection context name itself in the error
|
|
511
540
|
// instead of surfacing as a bare NG0203.
|
|
512
|
-
return `/**\n * ${legacyQueryDeprecationTag(creator.name)}\n */\n${exportPrefix}const ${toLegacyName(creator.name)} = createLegacyQueryCreator({ name: '${toLegacyName(creator.name)}', creator: ${creator.name} });`;
|
|
541
|
+
return `/**\n * ${legacyQueryDeprecationTag(creator.name)}\n */\n${exportPrefix}const ${toLegacyName(creator.name)} = createLegacyQueryCreator({ name: '${toLegacyName(creator.name)}', creator: ${creator.name}${creator.entity ? `, entity: ${creator.entity}` : ''} });`;
|
|
513
542
|
};
|
|
514
543
|
const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, creatorToClient, }) => {
|
|
515
544
|
const createdProviders = [];
|
|
@@ -532,6 +561,14 @@ const createAuthProviders = ({ tree, authProvidersNeeded, report, v2Configs, cre
|
|
|
532
561
|
}
|
|
533
562
|
tree.write(filePath, nextContent);
|
|
534
563
|
createdProviders.push(filePath);
|
|
564
|
+
body.warnings.forEach((warning) => {
|
|
565
|
+
report.addWarning({
|
|
566
|
+
...warning,
|
|
567
|
+
locations: [{ filePath }, ...(v2Config ? [{ filePath: v2Config.filePath, line: v2Config.line }] : [])],
|
|
568
|
+
source: 'legacy-query-creator-migration',
|
|
569
|
+
dedupeKey: `auth-provider-warning:${filePath}:${authProviderName}:${warning.title}`,
|
|
570
|
+
});
|
|
571
|
+
});
|
|
535
572
|
if (body.isScaffolded) {
|
|
536
573
|
report.addFollowUp({
|
|
537
574
|
title: `Finish the scaffolded auth provider ${authProviderName}`,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { formatFiles } from '@nx/devkit';
|
|
2
|
-
import { migrateDevtoolsUsage, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy } from './cleanup-migration.js';
|
|
2
|
+
import { migrateDevtoolsUsage, migrateEmptyPrepareCalls, replaceAnyQueryWithLegacy, reportRemovedExperimentalQueryHelpers, } from './cleanup-migration.js';
|
|
3
3
|
import { reportDefaultHeaderUsages, reportMissingHttpClientProviders } from './http-client-check.js';
|
|
4
4
|
import { migrateLegacyPrepareCalls } from './legacy-prepare-migration.js';
|
|
5
5
|
import { createNewQueryCreators, updateLegacyCreatorImportsAndUsages } from './legacy-query-creator-migration.js';
|
|
@@ -25,6 +25,7 @@ export default async function migrate(tree, schema) {
|
|
|
25
25
|
migrateDevtoolsUsage(tree, scope, report);
|
|
26
26
|
migrateEmptyPrepareCalls(tree, scope);
|
|
27
27
|
migrateLegacyPrepareCalls(tree, report, scope);
|
|
28
|
+
reportRemovedExperimentalQueryHelpers(tree, scope, report);
|
|
28
29
|
if (queryClientFiles.size > 0) {
|
|
29
30
|
reportMissingHttpClientProviders(tree, report, scope);
|
|
30
31
|
reportDefaultHeaderUsages(tree, report, scope);
|
|
@@ -138,6 +138,8 @@ export const createModuleGraph = (tree) => {
|
|
|
138
138
|
return null;
|
|
139
139
|
};
|
|
140
140
|
return {
|
|
141
|
+
resolveFile: resolveEntryFile,
|
|
142
|
+
pathAliases: () => [...tsConfigPaths.keys()],
|
|
141
143
|
findDeclaringFile: (fromFile, specifier, symbolName) => {
|
|
142
144
|
const cacheKey = `${fromFile}|${specifier}|${symbolName}`;
|
|
143
145
|
const cached = declaringFileCache.get(cacheKey);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
+
import { CLIENT_FEATURE, reportClientErrorPipelineAdded } from '../migrate-query-opt-in-features/migration.js';
|
|
2
3
|
import { capitalizeFirstLetter, createSourceFile, ensureConfigSuffix, ensureImportFromEthleteCore, ensureImportFromQuery, getVariableStatementEnd, } from './shared.js';
|
|
3
4
|
export const migrateQueryClients = (tree, report, scope) => {
|
|
4
5
|
const queryClientFiles = new Map();
|
|
@@ -151,6 +152,7 @@ const migrateQueryClientToConfig = ({ content, filePath, renamesOut, report }) =
|
|
|
151
152
|
}
|
|
152
153
|
}
|
|
153
154
|
reportDroppedClientOptions({ configArgument, sourceFile, filePath, report });
|
|
155
|
+
reportChangedClientDefaults({ configArgument, sourceFile, filePath, report });
|
|
154
156
|
replacements.push({
|
|
155
157
|
start: node.getStart(sourceFile),
|
|
156
158
|
end: node.getEnd(),
|
|
@@ -190,6 +192,7 @@ const removeQueryClientImport = (content, keepLegacyClient) => {
|
|
|
190
192
|
'createPostQuery',
|
|
191
193
|
'createPutQuery',
|
|
192
194
|
'createQueryClient',
|
|
195
|
+
CLIENT_FEATURE,
|
|
193
196
|
];
|
|
194
197
|
if (!queryImportNode?.importClause?.namedBindings || !ts.isNamedImports(queryImportNode.importClause.namedBindings)) {
|
|
195
198
|
return ensureImportFromQuery(content, requiredImports);
|
|
@@ -253,6 +256,12 @@ const renameVariables = (content, renames) => {
|
|
|
253
256
|
});
|
|
254
257
|
return result;
|
|
255
258
|
};
|
|
259
|
+
const WINDOW_FOCUS_REPLACEMENT = 'Opt in per query: `withPolling({ interval, refetchOnFocus: true })` on a polled query, `withAutoRefresh({ onSignalChanges: [windowFocusSignal] })` on any other, or `injectMyClient().refreshQueriesInUse()` from a focus listener.';
|
|
260
|
+
const SMART_POLLING_REPLACEMENT = 'Opt in per query: `withPolling({ interval, pauseWhileHidden: true })`. Without it v3 keeps polling in a hidden tab.';
|
|
261
|
+
const V2_ON_BY_DEFAULT_REQUEST_OPTIONS = {
|
|
262
|
+
autoRefreshQueriesOnWindowFocus: WINDOW_FOCUS_REPLACEMENT,
|
|
263
|
+
enableSmartPolling: SMART_POLLING_REPLACEMENT,
|
|
264
|
+
};
|
|
256
265
|
/**
|
|
257
266
|
* What each v2 client option turns into, or why it has nowhere to go.
|
|
258
267
|
*
|
|
@@ -261,8 +270,8 @@ const renameVariables = (content, renames) => {
|
|
|
261
270
|
* report from production.
|
|
262
271
|
*/
|
|
263
272
|
const DROPPED_CLIENT_OPTIONS = {
|
|
264
|
-
'request.autoRefreshQueriesOnWindowFocus':
|
|
265
|
-
'request.enableSmartPolling':
|
|
273
|
+
'request.autoRefreshQueriesOnWindowFocus': WINDOW_FOCUS_REPLACEMENT,
|
|
274
|
+
'request.enableSmartPolling': SMART_POLLING_REPLACEMENT,
|
|
266
275
|
'logging.preparedQuerySubscriptions': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
267
276
|
'logging.queryStateChanges': 'Removed. Use the v3 devtools (`provideQueryDevtools()`) or `withLogging({ logFn })` on the queries you want to trace.',
|
|
268
277
|
'request.autoRefreshOn': 'No direct equivalent. Re-run the affected queries yourself with `withAutoRefresh` or `refreshQueriesInUse()`.',
|
|
@@ -315,6 +324,12 @@ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, repo
|
|
|
315
324
|
if (name === 'request' && ['queryParams', 'cacheAdapter', 'retryFn'].includes(nested.name.text)) {
|
|
316
325
|
return;
|
|
317
326
|
}
|
|
327
|
+
if (name === 'request' &&
|
|
328
|
+
nested.name.text in V2_ON_BY_DEFAULT_REQUEST_OPTIONS &&
|
|
329
|
+
ts.isPropertyAssignment(nested) &&
|
|
330
|
+
nested.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
318
333
|
raise(`${name}.${nested.name.text}`, nested);
|
|
319
334
|
});
|
|
320
335
|
return;
|
|
@@ -324,6 +339,59 @@ const reportDroppedClientOptions = ({ configArgument, sourceFile, filePath, repo
|
|
|
324
339
|
}
|
|
325
340
|
});
|
|
326
341
|
};
|
|
342
|
+
const reportChangedClientDefaults = ({ configArgument, sourceFile, filePath, report, }) => {
|
|
343
|
+
const location = {
|
|
344
|
+
filePath,
|
|
345
|
+
line: sourceFile.getLineAndCharacterOfPosition(configArgument.getStart(sourceFile)).line + 1,
|
|
346
|
+
};
|
|
347
|
+
const requestProperty = findProperty(configArgument, 'request');
|
|
348
|
+
const requestConfig = requestProperty &&
|
|
349
|
+
ts.isPropertyAssignment(requestProperty) &&
|
|
350
|
+
ts.isObjectLiteralExpression(requestProperty.initializer)
|
|
351
|
+
? requestProperty.initializer
|
|
352
|
+
: undefined;
|
|
353
|
+
const hasUnreadableRequestConfig = (!!requestProperty && !requestConfig) || !!requestConfig?.properties.some(ts.isSpreadAssignment);
|
|
354
|
+
reportClientErrorPipelineAdded(report, location, 'query-client-migration', `\`${CLIENT_FEATURE}()\` was added so failed requests still retry and Symfony / HTML error bodies are still parsed, as they were by default in v2.`);
|
|
355
|
+
if (!requestConfig || !findProperty(requestConfig, 'retryFn')) {
|
|
356
|
+
report.addWarning({
|
|
357
|
+
title: 'Check the retry behaviour of the migrated client',
|
|
358
|
+
summary: 'v2 retried every request method on a 5xx (500 included), 408, 425 and 429, up to four times with a linear delay. The v3 default policy that `withEthleteApiErrors()` installs retries three times with an exponential, jittered delay, never retries a `POST`, a `PATCH` or a plain `500`, and also retries a connection failure.',
|
|
359
|
+
action: 'Accept the v3 policy, or pass `withEthleteApiErrors({ retry: { maxAttempts, retryableStatusCodes, retryNonIdempotent } })` - only set `retryNonIdempotent` for endpoints that deduplicate on their own.',
|
|
360
|
+
locations: [location],
|
|
361
|
+
source: 'query-client-migration',
|
|
362
|
+
dedupeKey: `changed-retry-default:${filePath}`,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
const cacheAdapter = requestConfig && findProperty(requestConfig, 'cacheAdapter');
|
|
366
|
+
if (cacheAdapter) {
|
|
367
|
+
report.addWarning({
|
|
368
|
+
title: 'Check the carried-over cacheAdapter',
|
|
369
|
+
summary: 'The v2 `cacheAdapter` was carried over as is and still sets the freshness TTL: a TTL of `0` means an `execute({ options: { allowCache: true } })` always hits the server, as a v2 `execute()` did. Two things differ: v3 passes Angular `HttpHeaders` (read them with `headers.get(name)`) where v2 passed a plain record, and v3 keeps an entry for `keepUnusedFor` (5 minutes) after its last consumer, rendering the old response on a remount while it refetches.',
|
|
370
|
+
action: 'Rewrite an adapter that reads headers for `HttpHeaders`. If the adapter was meant to switch caching off, also set `keepUnusedFor: 0` on the client.',
|
|
371
|
+
locations: [
|
|
372
|
+
{ filePath, line: sourceFile.getLineAndCharacterOfPosition(cacheAdapter.getStart(sourceFile)).line + 1 },
|
|
373
|
+
],
|
|
374
|
+
source: 'query-client-migration',
|
|
375
|
+
dedupeKey: `carried-cache-adapter:${filePath}`,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
if (hasUnreadableRequestConfig) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
Object.entries(V2_ON_BY_DEFAULT_REQUEST_OPTIONS).forEach(([option, action]) => {
|
|
382
|
+
if (requestConfig && findProperty(requestConfig, option)) {
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
report.addWarning({
|
|
386
|
+
title: `Restore the v2 default "request.${option}"`,
|
|
387
|
+
summary: `The v2 client did not set \`request.${option}\`, so it ran with the v2 default \`true\`. v3 has no client-wide equivalent and the behaviour is off for every migrated query.`,
|
|
388
|
+
action,
|
|
389
|
+
locations: [location],
|
|
390
|
+
source: 'query-client-migration',
|
|
391
|
+
dedupeKey: `dropped-client-default:${filePath}:${option}`,
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
};
|
|
327
395
|
const getPropertyValueText = (property, sourceFile) => {
|
|
328
396
|
if (ts.isShorthandPropertyAssignment(property)) {
|
|
329
397
|
return property.name.text;
|
|
@@ -361,6 +429,7 @@ const migrateConfigObject = (configArgument, node, sourceFile) => {
|
|
|
361
429
|
}
|
|
362
430
|
}
|
|
363
431
|
}
|
|
432
|
+
nextConfig.push(`features: [${CLIENT_FEATURE}()]`);
|
|
364
433
|
return `createQueryClient({\n ${nextConfig.join(',\n ')}\n})`;
|
|
365
434
|
};
|
|
366
435
|
const getVariableNameForQueryClient = (node) => {
|
|
@@ -143,4 +143,31 @@ export const addJsDocTag = (content, sourceFile, node, tag) => {
|
|
|
143
143
|
const indentation = ' '.repeat(sourceFile.getLineAndCharacterOfPosition(start).character);
|
|
144
144
|
return (content.slice(0, start) + `/**\n${indentation} * ${tag}\n${indentation} */\n${indentation}` + content.slice(start));
|
|
145
145
|
};
|
|
146
|
+
const TUPLE_PROVIDER_ACTION = 'Delete the call: a v3 client, auth provider or socket is root-provided the moment something injects it. For a test or an override, provide it with `toProvideFn(ref)` from `@ethlete/core`.';
|
|
147
|
+
export const REMOVED_EXPERIMENTAL_QUERY_HELPERS = {
|
|
148
|
+
createQueryClientConfig: 'Replace it with `createQueryClient({ name, baseUrl })`, which returns the provider tuple, and derive `toProvideFn` / `toInjectFn` from it.',
|
|
149
|
+
createBearerAuthProviderConfig: 'Replace it with `createBearerAuthProvider({ name, queryClientRef, queries, features })`, which returns the provider tuple, and derive `toProvideFn` / `toInjectFn` from it.',
|
|
150
|
+
provideQueryClient: TUPLE_PROVIDER_ACTION,
|
|
151
|
+
provideBearerAuthProvider: TUPLE_PROVIDER_ACTION,
|
|
152
|
+
provideWebSocketClient: `Build the socket with \`createWebSocketClient({ name, url, io })\`. ${TUPLE_PROVIDER_ACTION}`,
|
|
153
|
+
};
|
|
154
|
+
export const findRemovedExperimentalQueryHelpers = (sourceFile) => {
|
|
155
|
+
const found = [];
|
|
156
|
+
sourceFile.statements.forEach((statement) => {
|
|
157
|
+
if (!ts.isImportDeclaration(statement) ||
|
|
158
|
+
!ts.isStringLiteral(statement.moduleSpecifier) ||
|
|
159
|
+
statement.moduleSpecifier.text !== '@ethlete/query' ||
|
|
160
|
+
!statement.importClause?.namedBindings ||
|
|
161
|
+
!ts.isNamedImports(statement.importClause.namedBindings)) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
statement.importClause.namedBindings.elements.forEach((element) => {
|
|
165
|
+
const name = (element.propertyName ?? element.name).text;
|
|
166
|
+
if (name in REMOVED_EXPERIMENTAL_QUERY_HELPERS) {
|
|
167
|
+
found.push({ name, line: getLineNumber(element, sourceFile) });
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
return found;
|
|
172
|
+
};
|
|
146
173
|
//# sourceMappingURL=shared.js.map
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { formatFiles, visitNotIgnoredFiles } from '@nx/devkit';
|
|
1
|
+
import { formatFiles, joinPathFragments, readJson, visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
2
|
import * as ts from 'typescript';
|
|
3
|
+
import { REMOVED_EXPERIMENTAL_QUERY_HELPERS, createSourceFile, findRemovedExperimentalQueryHelpers, } from '../migrate-to-query-v3/shared.js';
|
|
3
4
|
export default async function migrate(tree, schema) {
|
|
4
5
|
console.log('\n🔄 Starting Query v3 symbol conflict migration...');
|
|
5
6
|
renameConflictingSymbols(tree);
|
|
6
7
|
replaceExperimentalQueryNamespace(tree);
|
|
8
|
+
reportPrebuiltPackagesImportingRenamedSymbols(tree);
|
|
7
9
|
if (!schema.skipFormat) {
|
|
8
10
|
await formatFiles(tree);
|
|
9
11
|
}
|
|
@@ -224,6 +226,7 @@ function updateImports(sourceFile) {
|
|
|
224
226
|
//#region ExperimentalQuery namespace replacement
|
|
225
227
|
function replaceExperimentalQueryNamespace(tree) {
|
|
226
228
|
const updatedFiles = [];
|
|
229
|
+
const removedHelpers = [];
|
|
227
230
|
let totalReplacements = 0;
|
|
228
231
|
visitNotIgnoredFiles(tree, '', (filePath) => {
|
|
229
232
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts'))
|
|
@@ -240,7 +243,13 @@ function replaceExperimentalQueryNamespace(tree) {
|
|
|
240
243
|
updatedFiles.push(filePath);
|
|
241
244
|
totalReplacements += result.replacementCount;
|
|
242
245
|
}
|
|
246
|
+
findRemovedExperimentalQueryHelpers(createSourceFile(result.content, filePath)).forEach(({ name, line }) => {
|
|
247
|
+
removedHelpers.push(` - ${filePath}:${line} ${name}: ${REMOVED_EXPERIMENTAL_QUERY_HELPERS[name]}`);
|
|
248
|
+
});
|
|
243
249
|
});
|
|
250
|
+
if (removedHelpers.length > 0) {
|
|
251
|
+
console.warn(`\n⚠️ These ExperimentalQuery helpers no longer exist in v3 and were left for you to replace by hand (migrate-to-query-v3 lists them again in its report):\n${removedHelpers.join('\n')}`);
|
|
252
|
+
}
|
|
244
253
|
if (updatedFiles.length > 0) {
|
|
245
254
|
console.log(`\n📦 Replaced ExperimentalQuery namespace in ${updatedFiles.length} files (${totalReplacements} usages)`);
|
|
246
255
|
}
|
|
@@ -382,5 +391,78 @@ function applyReplacements(content, replacements, replacementCount) {
|
|
|
382
391
|
});
|
|
383
392
|
return { content: result, modified: replacements.length > 0, replacementCount };
|
|
384
393
|
}
|
|
394
|
+
const isRenamedSymbol = (name) => TYPE_RENAMES.has(name) || FUNCTION_RENAMES.has(name);
|
|
395
|
+
function reportPrebuiltPackagesImportingRenamedSymbols(tree) {
|
|
396
|
+
if (!tree.exists('package.json'))
|
|
397
|
+
return;
|
|
398
|
+
const packageJson = readJson(tree, 'package.json');
|
|
399
|
+
const packageNames = new Set([packageJson.dependencies, packageJson.devDependencies, packageJson.optionalDependencies].flatMap((deps) => Object.keys(deps ?? {})));
|
|
400
|
+
const hits = [];
|
|
401
|
+
[...packageNames]
|
|
402
|
+
.filter((name) => !name.startsWith('@ethlete/'))
|
|
403
|
+
.sort()
|
|
404
|
+
.forEach((packageName) => {
|
|
405
|
+
const importedNames = new Set();
|
|
406
|
+
collectDeclarationFiles(tree, joinPathFragments('node_modules', packageName)).forEach((filePath) => {
|
|
407
|
+
const content = tree.read(filePath, 'utf-8');
|
|
408
|
+
if (!content?.includes('@ethlete/query'))
|
|
409
|
+
return;
|
|
410
|
+
findRenamedSymbolsInDeclaration(createSourceFile(content, filePath)).forEach((name) => importedNames.add(name));
|
|
411
|
+
});
|
|
412
|
+
if (importedNames.size > 0) {
|
|
413
|
+
hits.push(` - ${packageName}: ${[...importedNames].sort().join(', ')}`);
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
if (hits.length > 0) {
|
|
417
|
+
console.warn(`\n⚠️ These installed packages were built against @ethlete/query v2 and import names v3 no longer exports. No codemod can fix them here: run prep-for-query-v3 in each package's own repository and release a rebuild before upgrading this workspace:\n${hits.join('\n')}`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
function collectDeclarationFiles(tree, dir) {
|
|
421
|
+
if (!tree.exists(dir))
|
|
422
|
+
return [];
|
|
423
|
+
return tree.children(dir).flatMap((child) => {
|
|
424
|
+
if (child === 'node_modules')
|
|
425
|
+
return [];
|
|
426
|
+
const childPath = joinPathFragments(dir, child);
|
|
427
|
+
if (tree.isFile(childPath)) {
|
|
428
|
+
return /\.d\.(c|m)?ts$/.test(child) ? [childPath] : [];
|
|
429
|
+
}
|
|
430
|
+
return collectDeclarationFiles(tree, childPath);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
function findRenamedSymbolsInDeclaration(sourceFile) {
|
|
434
|
+
const names = new Set();
|
|
435
|
+
const namespaceAliases = collectQueryNamespaceAliases(sourceFile);
|
|
436
|
+
const isQueryModule = (node) => !!node && ts.isStringLiteral(node) && node.text === '@ethlete/query';
|
|
437
|
+
function visit(node) {
|
|
438
|
+
if (ts.isImportDeclaration(node) && isQueryModule(node.moduleSpecifier)) {
|
|
439
|
+
const namedBindings = node.importClause?.namedBindings;
|
|
440
|
+
if (namedBindings && ts.isNamedImports(namedBindings)) {
|
|
441
|
+
namedBindings.elements.forEach((element) => names.add((element.propertyName ?? element.name).text));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (ts.isExportDeclaration(node) && isQueryModule(node.moduleSpecifier) && node.exportClause) {
|
|
445
|
+
if (ts.isNamedExports(node.exportClause)) {
|
|
446
|
+
node.exportClause.elements.forEach((element) => names.add((element.propertyName ?? element.name).text));
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (ts.isImportTypeNode(node) &&
|
|
450
|
+
ts.isLiteralTypeNode(node.argument) &&
|
|
451
|
+
isQueryModule(node.argument.literal) &&
|
|
452
|
+
node.qualifier) {
|
|
453
|
+
names.add(getLeftmostIdentifier(node.qualifier).text);
|
|
454
|
+
}
|
|
455
|
+
const namespaceMember = getNamespaceMember(node, namespaceAliases)?.member;
|
|
456
|
+
if (namespaceMember) {
|
|
457
|
+
names.add(namespaceMember.text);
|
|
458
|
+
}
|
|
459
|
+
ts.forEachChild(node, visit);
|
|
460
|
+
}
|
|
461
|
+
visit(sourceFile);
|
|
462
|
+
return new Set([...names].filter(isRenamedSymbol));
|
|
463
|
+
}
|
|
464
|
+
function getLeftmostIdentifier(name) {
|
|
465
|
+
return ts.isIdentifier(name) ? name : getLeftmostIdentifier(name.left);
|
|
466
|
+
}
|
|
385
467
|
//#endregion
|
|
386
468
|
//# sourceMappingURL=migration.js.map
|