@ethlete/query 6.0.0-next.5 → 6.0.0-next.51
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 +772 -0
- package/fesm2022/ethlete-query-testing.mjs +441 -38
- package/fesm2022/ethlete-query-testing.mjs.map +1 -1
- package/fesm2022/ethlete-query.mjs +15961 -5330
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/generators/deprecate-legacy-queries/migration.js +77 -0
- package/generators/deprecate-legacy-queries/schema.json +26 -0
- package/generators/generators.json +15 -0
- package/generators/migrate-query-client-features/migration.js +142 -0
- package/generators/migrate-query-client-features/report.js +136 -0
- package/generators/migrate-query-client-features/schema.json +26 -0
- package/generators/migrate-query-opt-in-features/migration.js +194 -0
- package/generators/migrate-query-opt-in-features/report.js +143 -0
- package/generators/migrate-query-opt-in-features/schema.json +31 -0
- package/generators/migrate-to-query-v3/auth-provider-scaffold.js +133 -0
- package/generators/migrate-to-query-v3/cleanup-migration.js +111 -99
- package/generators/migrate-to-query-v3/http-client-check.js +93 -0
- package/generators/migrate-to-query-v3/legacy-prepare-migration.js +129 -40
- package/generators/migrate-to-query-v3/legacy-query-creator-migration.js +237 -152
- package/generators/migrate-to-query-v3/migration-scope.js +38 -0
- package/generators/migrate-to-query-v3/migration.js +17 -9
- package/generators/migrate-to-query-v3/module-graph.js +153 -0
- package/generators/migrate-to-query-v3/query-client-migration.js +152 -53
- package/generators/migrate-to-query-v3/rename-symbols.js +206 -0
- package/generators/migrate-to-query-v3/schema.json +12 -0
- package/generators/migrate-to-query-v3/shared.js +51 -2
- package/generators/prep-for-query-v3/migration.js +71 -14
- package/migrations.json +44 -0
- package/package.json +29 -13
- package/types/ethlete-query-testing.d.ts +179 -12
- package/types/ethlete-query.d.ts +8935 -3203
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export const QUERY_OPT_IN_FEATURES_REPORT_PATH = 'query-opt-in-features-migration-tasks.md';
|
|
2
|
+
export const MIGRATION_TASK_KIND = {
|
|
3
|
+
WARNING: 'warning',
|
|
4
|
+
MANUAL_REVIEW: 'manual-review',
|
|
5
|
+
FOLLOW_UP: 'follow-up',
|
|
6
|
+
};
|
|
7
|
+
const renderTaskLocations = (locations) => {
|
|
8
|
+
if (locations.length === 0) {
|
|
9
|
+
return '- None recorded';
|
|
10
|
+
}
|
|
11
|
+
return locations.map((location) => `- ${location.filePath}${location.line ? `:${location.line}` : ''}`).join('\n');
|
|
12
|
+
};
|
|
13
|
+
export class QueryOptInFeaturesMigrationReport {
|
|
14
|
+
constructor() {
|
|
15
|
+
this.tasks = [];
|
|
16
|
+
}
|
|
17
|
+
addTask(input) {
|
|
18
|
+
const dedupeKey = input.dedupeKey ?? `${input.kind}:${input.title}:${input.summary}:${input.action}`;
|
|
19
|
+
const existing = this.tasks.find((task) => task.dedupeKey === dedupeKey);
|
|
20
|
+
if (existing) {
|
|
21
|
+
existing.locations.push(...(input.locations ?? []));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
this.tasks.push({
|
|
25
|
+
id: `QOF-${String(this.tasks.length + 1).padStart(3, '0')}`,
|
|
26
|
+
kind: input.kind,
|
|
27
|
+
title: input.title,
|
|
28
|
+
summary: input.summary,
|
|
29
|
+
action: input.action,
|
|
30
|
+
locations: input.locations ?? [],
|
|
31
|
+
source: input.source,
|
|
32
|
+
dedupeKey,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
addWarning(input) {
|
|
36
|
+
this.addTask({ ...input, kind: MIGRATION_TASK_KIND.WARNING });
|
|
37
|
+
}
|
|
38
|
+
addManualReview(input) {
|
|
39
|
+
this.addTask({ ...input, kind: MIGRATION_TASK_KIND.MANUAL_REVIEW });
|
|
40
|
+
}
|
|
41
|
+
addFollowUp(input) {
|
|
42
|
+
this.addTask({ ...input, kind: MIGRATION_TASK_KIND.FOLLOW_UP });
|
|
43
|
+
}
|
|
44
|
+
printSummary() {
|
|
45
|
+
if (this.tasks.length === 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.warn(`\n⚠️ Generated ${this.tasks.length} follow-up task${this.tasks.length === 1 ? '' : 's'}:`);
|
|
49
|
+
this.tasks.forEach((task) => {
|
|
50
|
+
const locations = task.locations
|
|
51
|
+
.map((location) => `${location.filePath}${location.line ? `:${location.line}` : ''}`)
|
|
52
|
+
.join(', ');
|
|
53
|
+
console.warn(` - ${task.id} [${task.kind}] ${task.title}${locations ? ` (${locations})` : ''}`);
|
|
54
|
+
});
|
|
55
|
+
console.warn(`\n📄 See ${QUERY_OPT_IN_FEATURES_REPORT_PATH} for the AI-ready follow-up list.`);
|
|
56
|
+
}
|
|
57
|
+
writeToTree(tree) {
|
|
58
|
+
tree.write(QUERY_OPT_IN_FEATURES_REPORT_PATH, renderMigrationReportMarkdown(this.tasks));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export const renderMigrationReportMarkdown = (tasks) => {
|
|
62
|
+
const header = [
|
|
63
|
+
'# Query Opt-In Features Migration Follow-Up',
|
|
64
|
+
'',
|
|
65
|
+
'This file is generated by the `migrate-query-opt-in-features` generator.',
|
|
66
|
+
'',
|
|
67
|
+
'## Context',
|
|
68
|
+
'',
|
|
69
|
+
'Two things that used to be on by default are now opt-in features:',
|
|
70
|
+
'',
|
|
71
|
+
'- error-response parsing (HTML error pages, Symfony / class-validator violation lists) and the',
|
|
72
|
+
' default retry policy, now `withEthleteApiErrors()` on `createQueryClient`,',
|
|
73
|
+
'- bearer-auth multi-tab sync and leader election, now `withBearerAuthMultiTabSync()` on',
|
|
74
|
+
' `createBearerAuthProvider` (the `multiTabSync` config option is gone).',
|
|
75
|
+
'',
|
|
76
|
+
'The codemod is **behavior preserving**: it adds the features every call site used to get for free.',
|
|
77
|
+
'That is rarely what an app wants long term — an API that never answers with an HTML error page, or',
|
|
78
|
+
'an app that is only ever open in one tab, ships that code for nothing. Review each added feature',
|
|
79
|
+
'and delete the ones you do not need; `withEthleteApiErrors()` in particular splits into',
|
|
80
|
+
'`withHtmlErrorParsing()`, `withSymfonyErrors()` and `withDefaultRetry()`.',
|
|
81
|
+
'',
|
|
82
|
+
'## AI Instructions',
|
|
83
|
+
'',
|
|
84
|
+
'1. Re-read the referenced files before changing anything. The migration output is only a starting point.',
|
|
85
|
+
'2. Prefer minimal fixes that align with the current runtime APIs in `@ethlete/query`.',
|
|
86
|
+
'3. Update or remove a task only after the code and tests confirm that it is resolved.',
|
|
87
|
+
'4. If a task is still ambiguous after inspection, keep it open and append a short note instead of guessing.',
|
|
88
|
+
'',
|
|
89
|
+
'## Task Format',
|
|
90
|
+
'',
|
|
91
|
+
'- `Status`: always treat generated tasks as `open` until verified manually or by tests.',
|
|
92
|
+
'- `Kind`: `warning`, `manual-review`, or `follow-up`.',
|
|
93
|
+
'- `Source`: which migration phase raised the task.',
|
|
94
|
+
'',
|
|
95
|
+
].join('\n');
|
|
96
|
+
if (tasks.length === 0) {
|
|
97
|
+
return `${header}## Tasks\n\nNo open follow-up tasks were recorded during this migration run.\n`;
|
|
98
|
+
}
|
|
99
|
+
const tasksSection = tasks
|
|
100
|
+
.map((task) => [
|
|
101
|
+
`### ${task.id} - ${task.title}`,
|
|
102
|
+
'',
|
|
103
|
+
'- Status: open',
|
|
104
|
+
`- Kind: ${task.kind}`,
|
|
105
|
+
`- Source: ${task.source}`,
|
|
106
|
+
`- Summary: ${task.summary}`,
|
|
107
|
+
`- Suggested action: ${task.action}`,
|
|
108
|
+
'- Locations:',
|
|
109
|
+
renderTaskLocations(task.locations),
|
|
110
|
+
'',
|
|
111
|
+
].join('\n'))
|
|
112
|
+
.join('');
|
|
113
|
+
const machineSection = [
|
|
114
|
+
'## Machine Summary',
|
|
115
|
+
'',
|
|
116
|
+
'```yaml',
|
|
117
|
+
'tasks:',
|
|
118
|
+
...tasks.flatMap((task) => {
|
|
119
|
+
const nestedLocations = task.locations.flatMap((location) => {
|
|
120
|
+
const lines = [` - filePath: ${location.filePath}`];
|
|
121
|
+
if (location.line) {
|
|
122
|
+
lines.push(` line: ${location.line}`);
|
|
123
|
+
}
|
|
124
|
+
return lines;
|
|
125
|
+
});
|
|
126
|
+
return [
|
|
127
|
+
` - id: ${task.id}`,
|
|
128
|
+
` status: open`,
|
|
129
|
+
` kind: ${task.kind}`,
|
|
130
|
+
` source: ${task.source}`,
|
|
131
|
+
` title: ${task.title}`,
|
|
132
|
+
` summary: ${task.summary}`,
|
|
133
|
+
` action: ${task.action}`,
|
|
134
|
+
` locations:`,
|
|
135
|
+
...(nestedLocations.length > 0 ? nestedLocations : [' []']),
|
|
136
|
+
];
|
|
137
|
+
}),
|
|
138
|
+
'```',
|
|
139
|
+
'',
|
|
140
|
+
].join('\n');
|
|
141
|
+
return `${header}## Tasks\n\n${tasksSection}${machineSection}`;
|
|
142
|
+
};
|
|
143
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/schema",
|
|
3
|
+
"$id": "migrate-query-opt-in-features",
|
|
4
|
+
"title": "Migrate query clients and auth providers to opt-in features",
|
|
5
|
+
"type": "object",
|
|
6
|
+
"properties": {
|
|
7
|
+
"projects": {
|
|
8
|
+
"type": "array",
|
|
9
|
+
"items": { "type": "string" },
|
|
10
|
+
"description": "Nx projects to migrate. Defaults to every project in the workspace.",
|
|
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
|
+
},
|
|
19
|
+
"reportOnly": {
|
|
20
|
+
"type": "boolean",
|
|
21
|
+
"description": "Only list the affected call sites in the report file, without changing any code.",
|
|
22
|
+
"default": false
|
|
23
|
+
},
|
|
24
|
+
"skipFormat": {
|
|
25
|
+
"type": "boolean",
|
|
26
|
+
"description": "Skip formatting files after migration",
|
|
27
|
+
"default": false
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"required": []
|
|
31
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
const isShorthand = ts.isShorthandPropertyAssignment(property);
|
|
51
|
+
if ((!ts.isPropertyAssignment(property) && !isShorthand) || !ts.isIdentifier(property.name)) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const key = property.name.text;
|
|
55
|
+
if (!REFRESH_CONFIG_KEYS.has(key)) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const value = isShorthand ? property.name : property.initializer;
|
|
59
|
+
const text = value.getText(sourceFile);
|
|
60
|
+
if (key === 'queryCreator') {
|
|
61
|
+
if (ts.isIdentifier(value)) {
|
|
62
|
+
config.refreshCreatorName = value.text;
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
config[key] = text;
|
|
67
|
+
});
|
|
68
|
+
return config;
|
|
69
|
+
};
|
|
70
|
+
const REFRESH_QUERY_KEY = 'tokenRefresh';
|
|
71
|
+
/**
|
|
72
|
+
* Renders the `queries` / `features` arguments of a v3 provider from a v2 config.
|
|
73
|
+
*
|
|
74
|
+
* The mapping is close to 1:1 - `queryCreator` → `withRefreshQuery`, `responseAdapter` →
|
|
75
|
+
* `extractTokens`, `cookieName` → `withPersistentAuth({ cookie })` - but the adapters are copied
|
|
76
|
+
* verbatim from v2 and their parameter shapes differ, so each one is emitted with a TODO rather
|
|
77
|
+
* than presented as finished.
|
|
78
|
+
*/
|
|
79
|
+
export const renderAuthProviderBody = (config) => {
|
|
80
|
+
if (!config?.refreshCreatorName) {
|
|
81
|
+
return {
|
|
82
|
+
queries: ' queries: [],',
|
|
83
|
+
features: '',
|
|
84
|
+
importsNeeded: [],
|
|
85
|
+
isScaffolded: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
const refreshEntries = [` queryCreator: ${config.refreshCreatorName},`];
|
|
89
|
+
if (config.responseAdapter) {
|
|
90
|
+
refreshEntries.push(` // TODO(query-v3): v2 responseAdapter. It must return { accessToken, refreshToken } - v2 returned { token, refreshToken }.`, ` extractTokens: ${config.responseAdapter},`);
|
|
91
|
+
}
|
|
92
|
+
if (config.requestArgsAdapter) {
|
|
93
|
+
refreshEntries.push(` // TODO(query-v3): v2 requestArgsAdapter took { token, refreshToken }; buildArgs takes the refresh token alone.`, ` buildArgs: ${config.requestArgsAdapter},`);
|
|
94
|
+
}
|
|
95
|
+
if (config.expiresInPropertyName) {
|
|
96
|
+
refreshEntries.push(` expiresInPropertyName: ${config.expiresInPropertyName},`);
|
|
97
|
+
}
|
|
98
|
+
const queries = [
|
|
99
|
+
' queries: [',
|
|
100
|
+
` // Derived from the v2 BearerAuthProvider in ${config.filePath}:${config.line}. Verify before shipping.`,
|
|
101
|
+
` withRefreshQuery('${REFRESH_QUERY_KEY}', {`,
|
|
102
|
+
...refreshEntries,
|
|
103
|
+
' }),',
|
|
104
|
+
` // TODO(query-v3): add withAuthenticationQuery('login', { queryCreator: … }) for the login query.`,
|
|
105
|
+
' ],',
|
|
106
|
+
].join('\n');
|
|
107
|
+
const importsNeeded = ['withRefreshQuery'];
|
|
108
|
+
if (!config.cookieName) {
|
|
109
|
+
return { queries, features: '', importsNeeded, isScaffolded: true };
|
|
110
|
+
}
|
|
111
|
+
const cookieEntries = [` name: ${config.cookieName},`];
|
|
112
|
+
if (config.cookieDomain)
|
|
113
|
+
cookieEntries.push(` domain: ${config.cookieDomain},`);
|
|
114
|
+
if (config.cookieExpiresInDays)
|
|
115
|
+
cookieEntries.push(` expiresInDays: ${config.cookieExpiresInDays},`);
|
|
116
|
+
const features = [
|
|
117
|
+
' features: [',
|
|
118
|
+
' withPersistentAuth({',
|
|
119
|
+
' cookie: {',
|
|
120
|
+
...cookieEntries,
|
|
121
|
+
' },',
|
|
122
|
+
' autoLogin: {',
|
|
123
|
+
` queryKey: '${REFRESH_QUERY_KEY}',`,
|
|
124
|
+
` // TODO(query-v3): shape the auto-login request from the cookie value.`,
|
|
125
|
+
' buildArgs: (token) => ({ body: { refreshToken: token } }),',
|
|
126
|
+
' },',
|
|
127
|
+
' }),',
|
|
128
|
+
' ],',
|
|
129
|
+
].join('\n');
|
|
130
|
+
importsNeeded.push('withPersistentAuth');
|
|
131
|
+
return { queries, features, importsNeeded, isScaffolded: true };
|
|
132
|
+
};
|
|
133
|
+
//# sourceMappingURL=auth-provider-scaffold.js.map
|
|
@@ -1,45 +1,57 @@
|
|
|
1
|
-
import { visitNotIgnoredFiles } from '@nx/devkit';
|
|
2
1
|
import * as ts from 'typescript';
|
|
3
|
-
import { createSourceFile,
|
|
4
|
-
|
|
2
|
+
import { createSourceFile, ensureImportFromQuery, ensureNamedImports } 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
|
-
|
|
7
|
-
|
|
13
|
+
const filesNeedingDevtoolsPackage = [];
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
filesNeedingDevtoolsPackage.push(filePath);
|
|
33
31
|
}
|
|
34
32
|
});
|
|
35
|
-
if (updatedFiles.length
|
|
36
|
-
|
|
37
|
-
|
|
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 (filesNeedingDevtoolsPackage.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
|
|
41
|
+
// `@ethlete/query-devtools`.
|
|
42
|
+
report.addManualReview({
|
|
43
|
+
title: 'Add @ethlete/query-devtools for the query devtools',
|
|
44
|
+
summary: '`QueryDevtoolsComponent` now lives in `@ethlete/query-devtools`. The imports were rewritten, but the package may not be a dependency of these projects yet.',
|
|
45
|
+
action: 'Run `yarn add @ethlete/query-devtools` where needed, then `yarn install` and re-lint the affected libs so the `@nx/dependency-checks` rule sees the new dependency.',
|
|
46
|
+
locations: filesNeedingDevtoolsPackage.map((filePath) => ({ filePath })),
|
|
47
|
+
source: 'cleanup-migration',
|
|
48
|
+
dedupeKey: 'devtools-package-dependency',
|
|
49
|
+
});
|
|
38
50
|
}
|
|
39
51
|
};
|
|
40
|
-
export const replaceAnyQueryWithLegacy = (tree) => {
|
|
52
|
+
export const replaceAnyQueryWithLegacy = (tree, scope) => {
|
|
41
53
|
const updatedFiles = [];
|
|
42
|
-
|
|
54
|
+
scope.visit(tree, (filePath) => {
|
|
43
55
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
44
56
|
return;
|
|
45
57
|
}
|
|
@@ -57,9 +69,9 @@ export const replaceAnyQueryWithLegacy = (tree) => {
|
|
|
57
69
|
console.log(`\n✅ Replaced legacy AnyV2Query aliases in ${updatedFiles.length} files`);
|
|
58
70
|
}
|
|
59
71
|
};
|
|
60
|
-
export const migrateEmptyPrepareCalls = (tree) => {
|
|
72
|
+
export const migrateEmptyPrepareCalls = (tree, scope) => {
|
|
61
73
|
const updatedFiles = [];
|
|
62
|
-
|
|
74
|
+
scope.visit(tree, (filePath) => {
|
|
63
75
|
if (!filePath.endsWith('.ts') || filePath.endsWith('.spec.ts')) {
|
|
64
76
|
return;
|
|
65
77
|
}
|
|
@@ -77,42 +89,63 @@ export const migrateEmptyPrepareCalls = (tree) => {
|
|
|
77
89
|
console.log(`\n✅ Migrated empty .prepare() calls in ${updatedFiles.length} files`);
|
|
78
90
|
}
|
|
79
91
|
};
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
92
|
+
const migrateDevtoolsInFile = (content) => {
|
|
93
|
+
const importsComponent = importsFromQuery(content, 'QueryDevtoolsComponent');
|
|
94
|
+
let result = replaceDevtoolsProviderCalls(content);
|
|
95
|
+
const hasProvider = result.includes('provideQueryDevtools()');
|
|
96
|
+
result = dropLegacyDevtoolsImports(result);
|
|
97
|
+
if (hasProvider) {
|
|
98
|
+
result = ensureImportFromQuery(result, ['provideQueryDevtools']);
|
|
99
|
+
}
|
|
100
|
+
if (importsComponent) {
|
|
101
|
+
result = ensureNamedImports({
|
|
102
|
+
content: result,
|
|
103
|
+
importsNeeded: ['QueryDevtoolsComponent'],
|
|
104
|
+
moduleSpecifier: '@ethlete/query-devtools',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return { content: result, importsComponent };
|
|
86
108
|
};
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
return
|
|
109
|
+
const importsFromQuery = (content, name) => {
|
|
110
|
+
const sourceFile = createSourceFile(content);
|
|
111
|
+
return sourceFile.statements.some((node) => ts.isImportDeclaration(node) &&
|
|
112
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
113
|
+
node.moduleSpecifier.text === '@ethlete/query' &&
|
|
114
|
+
!!node.importClause?.namedBindings &&
|
|
115
|
+
ts.isNamedImports(node.importClause.namedBindings) &&
|
|
116
|
+
node.importClause.namedBindings.elements.some((element) => element.name.text === name));
|
|
90
117
|
};
|
|
91
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Turns every `provideQueryClientForDevtools({ client, displayName })` into a single
|
|
120
|
+
* `provideQueryDevtools()`.
|
|
121
|
+
*
|
|
122
|
+
* v3 registers every client and auth provider at once, so N per-client calls collapse to one. The
|
|
123
|
+
* first call site keeps its position - it is already where the app wanted its devtools - and the
|
|
124
|
+
* rest are removed along with the comma that separated them.
|
|
125
|
+
*/
|
|
126
|
+
const replaceDevtoolsProviderCalls = (content) => {
|
|
92
127
|
const sourceFile = createSourceFile(content);
|
|
93
|
-
const
|
|
128
|
+
const calls = [];
|
|
94
129
|
const visit = (node) => {
|
|
95
|
-
if (ts.
|
|
96
|
-
ts.isIdentifier(node.
|
|
97
|
-
node.
|
|
98
|
-
|
|
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
|
-
});
|
|
130
|
+
if (ts.isCallExpression(node) &&
|
|
131
|
+
ts.isIdentifier(node.expression) &&
|
|
132
|
+
node.expression.text === 'provideQueryClientForDevtools') {
|
|
133
|
+
calls.push(node);
|
|
112
134
|
}
|
|
113
135
|
ts.forEachChild(node, visit);
|
|
114
136
|
};
|
|
115
137
|
visit(sourceFile);
|
|
138
|
+
if (calls.length === 0) {
|
|
139
|
+
return content;
|
|
140
|
+
}
|
|
141
|
+
const replacements = calls.map((call, index) => {
|
|
142
|
+
const start = call.getStart(sourceFile);
|
|
143
|
+
const end = call.getEnd();
|
|
144
|
+
if (index === 0) {
|
|
145
|
+
return { start, end, replacement: 'provideQueryDevtools()' };
|
|
146
|
+
}
|
|
147
|
+
return { ...withSurroundingComma(content, start, end), replacement: '' };
|
|
148
|
+
});
|
|
116
149
|
let result = content;
|
|
117
150
|
replacements.sort((left, right) => right.start - left.start);
|
|
118
151
|
replacements.forEach(({ start, end, replacement }) => {
|
|
@@ -120,7 +153,24 @@ const removeQueryDevtoolsImportsFromMetadata = (content) => {
|
|
|
120
153
|
});
|
|
121
154
|
return result;
|
|
122
155
|
};
|
|
123
|
-
|
|
156
|
+
/** Widens a range to swallow the comma that separated the element from its neighbours. */
|
|
157
|
+
const withSurroundingComma = (content, start, end) => {
|
|
158
|
+
let nextEnd = end;
|
|
159
|
+
while (nextEnd < content.length && /\s/.test(content[nextEnd]))
|
|
160
|
+
nextEnd += 1;
|
|
161
|
+
if (content[nextEnd] === ',') {
|
|
162
|
+
return { start, end: nextEnd + 1 };
|
|
163
|
+
}
|
|
164
|
+
let nextStart = start;
|
|
165
|
+
while (nextStart > 0 && /\s/.test(content[nextStart - 1]))
|
|
166
|
+
nextStart -= 1;
|
|
167
|
+
if (content[nextStart - 1] === ',') {
|
|
168
|
+
return { start: nextStart - 1, end };
|
|
169
|
+
}
|
|
170
|
+
return { start, end };
|
|
171
|
+
};
|
|
172
|
+
/** Drops the v2 devtools names from the `@ethlete/query` import; both moved or were renamed. */
|
|
173
|
+
const dropLegacyDevtoolsImports = (content) => {
|
|
124
174
|
const sourceFile = createSourceFile(content);
|
|
125
175
|
let importNode;
|
|
126
176
|
ts.forEachChild(sourceFile, (node) => {
|
|
@@ -133,9 +183,7 @@ const removeDevtoolsImports = (content) => {
|
|
|
133
183
|
if (!importNode?.importClause?.namedBindings || !ts.isNamedImports(importNode.importClause.namedBindings)) {
|
|
134
184
|
return content;
|
|
135
185
|
}
|
|
136
|
-
const nextElements = importNode.importClause.namedBindings.elements.filter((element) =>
|
|
137
|
-
return element.name.text !== 'QueryDevtoolsComponent' && element.name.text !== 'provideQueryClientForDevtools';
|
|
138
|
-
});
|
|
186
|
+
const nextElements = importNode.importClause.namedBindings.elements.filter((element) => element.name.text !== 'QueryDevtoolsComponent' && element.name.text !== 'provideQueryClientForDevtools');
|
|
139
187
|
if (nextElements.length === importNode.importClause.namedBindings.elements.length) {
|
|
140
188
|
return content;
|
|
141
189
|
}
|
|
@@ -146,12 +194,6 @@ const removeDevtoolsImports = (content) => {
|
|
|
146
194
|
const nextImport = `import { ${nextElements.map((element) => element.getText(sourceFile)).join(', ')} } from '@ethlete/query';`;
|
|
147
195
|
return content.slice(0, importNode.getStart(sourceFile)) + nextImport + content.slice(importNode.getEnd());
|
|
148
196
|
};
|
|
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
197
|
const replaceAnyQueryInFile = (content) => {
|
|
156
198
|
const sourceFile = createSourceFile(content);
|
|
157
199
|
const replacements = [];
|
|
@@ -180,33 +222,7 @@ const replaceAnyQueryInFile = (content) => {
|
|
|
180
222
|
replacements.forEach(({ start, end, replacement }) => {
|
|
181
223
|
result = result.slice(0, start) + replacement + result.slice(end);
|
|
182
224
|
});
|
|
183
|
-
return
|
|
184
|
-
};
|
|
185
|
-
const removeAnyQueryFromImports = (content) => {
|
|
186
|
-
const sourceFile = createSourceFile(content);
|
|
187
|
-
let importNode;
|
|
188
|
-
ts.forEachChild(sourceFile, (node) => {
|
|
189
|
-
if (ts.isImportDeclaration(node) &&
|
|
190
|
-
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
191
|
-
node.moduleSpecifier.text === '@ethlete/query') {
|
|
192
|
-
importNode = node;
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
if (!importNode?.importClause?.namedBindings || !ts.isNamedImports(importNode.importClause.namedBindings)) {
|
|
196
|
-
return content;
|
|
197
|
-
}
|
|
198
|
-
const nextElements = importNode.importClause.namedBindings.elements.filter((element) => {
|
|
199
|
-
return element.name.text !== 'AnyV2Query' && element.name.text !== 'AnyV2QueryCreator';
|
|
200
|
-
});
|
|
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');
|
|
207
|
-
}
|
|
208
|
-
const nextImport = `import { ${names.sort().join(', ')} } from '@ethlete/query';`;
|
|
209
|
-
return content.slice(0, importNode.getStart(sourceFile)) + nextImport + content.slice(importNode.getEnd());
|
|
225
|
+
return result;
|
|
210
226
|
};
|
|
211
227
|
const transformEmptyPrepareCalls = (content) => {
|
|
212
228
|
const sourceFile = createSourceFile(content);
|
|
@@ -235,8 +251,4 @@ const transformEmptyPrepareCalls = (content) => {
|
|
|
235
251
|
});
|
|
236
252
|
return result;
|
|
237
253
|
};
|
|
238
|
-
export const describeTemplateLine = (content, position, suffix) => {
|
|
239
|
-
const lineNumber = getLineNumberFromPosition(content, position);
|
|
240
|
-
return `${lineNumber} (${suffix})`;
|
|
241
|
-
};
|
|
242
254
|
//# sourceMappingURL=cleanup-migration.js.map
|