@trpc/upgrade 0.0.0-alpha.0
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/LICENSE +21 -0
- package/README.md +15 -0
- package/dist/cli.cjs +77 -0
- package/dist/transforms/hooksToOptions.cjs +221 -0
- package/dist/transforms/provider.cjs +130 -0
- package/package.json +56 -0
- package/src/bin/cli.ts +158 -0
- package/src/transforms/hooksToOptions.ts +323 -0
- package/src/transforms/provider.ts +162 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Alex Johansson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var path = require('path');
|
|
3
|
+
var cli$1 = require('@effect/cli');
|
|
4
|
+
var platform = require('@effect/platform');
|
|
5
|
+
var platformNode = require('@effect/platform-node');
|
|
6
|
+
var effect = require('effect');
|
|
7
|
+
var ignore = require('ignore');
|
|
8
|
+
var typescript = require('typescript');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
13
|
+
var ignore__default = /*#__PURE__*/_interopDefault(ignore);
|
|
14
|
+
|
|
15
|
+
var version = "0.0.0-alpha.0";
|
|
16
|
+
|
|
17
|
+
platform.Command.string(platform.Command.make('git', 'status')).pipe(effect.Effect.filterOrFail((status)=>status.includes('nothing to commit'), ()=>'Git tree is not clean, please commit your changes before running the migrator'));
|
|
18
|
+
const installPackage = (packageName)=>{
|
|
19
|
+
const packageManager = effect.Match.value(process.env.npm_config_user_agent ?? 'npm').pipe(effect.Match.when(effect.String.startsWith('pnpm'), ()=>'pnpm'), effect.Match.when(effect.String.startsWith('yarn'), ()=>'yarn'), effect.Match.when(effect.String.startsWith('bun'), ()=>'bun'), effect.Match.orElse(()=>'npm'));
|
|
20
|
+
return platform.Command.streamLines(platform.Command.make(packageManager, 'install', packageName)).pipe(effect.Stream.mapEffect(effect.Console.log), effect.Stream.runDrain);
|
|
21
|
+
};
|
|
22
|
+
const filterIgnored = (files)=>effect.Effect.gen(function*() {
|
|
23
|
+
const fs = yield* platform.FileSystem.FileSystem;
|
|
24
|
+
const ignores = yield* fs.readFileString(path__default.default.join(process.cwd(), '.gitignore')).pipe(effect.Effect.map((content)=>content.split('\n')), effect.Effect.map((patterns)=>ignore__default.default().add(patterns)));
|
|
25
|
+
// Ignore "common files"
|
|
26
|
+
const relativeFilePaths = files.filter((source)=>!source.fileName.includes('node_modules') && !source.fileName.includes('packages/')).map((file)=>path__default.default.relative(process.cwd(), file.fileName));
|
|
27
|
+
// As well as gitignored?
|
|
28
|
+
return ignores.filter(relativeFilePaths);
|
|
29
|
+
});
|
|
30
|
+
const Program = effect.Effect.succeed(typescript.findConfigFile(process.cwd(), typescript.sys.fileExists)).pipe(effect.Effect.filterOrFail(effect.Predicate.isNotNullable, ()=>'No tsconfig found'), effect.Effect.tap((_)=>effect.Effect.logDebug('Using tsconfig', _)), effect.Effect.map((_)=>typescript.readConfigFile(_, typescript.sys.readFile)), effect.Effect.map((_)=>typescript.parseJsonConfigFileContent(_.config, typescript.sys, process.cwd())), effect.Effect.map((_)=>typescript.createProgram({
|
|
31
|
+
options: _.options,
|
|
32
|
+
rootNames: _.fileNames,
|
|
33
|
+
configFileParsingDiagnostics: _.errors
|
|
34
|
+
})));
|
|
35
|
+
const transformPath = (path)=>process.env.DEV ? path : path.replace('../', './').replace('.ts', '.cjs');
|
|
36
|
+
const prompts = cli$1.Command.prompt('transforms', cli$1.Prompt.multiSelect({
|
|
37
|
+
message: 'Select transforms to run',
|
|
38
|
+
choices: [
|
|
39
|
+
{
|
|
40
|
+
title: 'Migrate Hooks to xxxOptions API',
|
|
41
|
+
value: require.resolve(transformPath('../transforms/hooksToOptions.ts'))
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
title: 'Migrate context provider setup',
|
|
45
|
+
value: require.resolve(transformPath('../transforms/provider.ts'))
|
|
46
|
+
}
|
|
47
|
+
]
|
|
48
|
+
}), (_)=>effect.Effect.gen(function*() {
|
|
49
|
+
// yield* assertCleanGitTree;
|
|
50
|
+
const program = yield* Program;
|
|
51
|
+
const sourceFiles = program.getSourceFiles();
|
|
52
|
+
// Make sure provider transform runs first if it's selected
|
|
53
|
+
_.sort((a, b)=>a.includes('provider.ts') ? -1 : b.includes('provider.ts') ? 1 : 0);
|
|
54
|
+
/**
|
|
55
|
+
* TODO: Detect these automatically
|
|
56
|
+
*/ const appRouterImportFile = '~/server/routers/_app';
|
|
57
|
+
const appRouterImportName = 'AppRouter';
|
|
58
|
+
const trpcFile = '~/lib/trpc';
|
|
59
|
+
const trpcImportName = 'trpc';
|
|
60
|
+
const commitedFiles = yield* filterIgnored(sourceFiles);
|
|
61
|
+
yield* effect.Effect.forEach(_, (transform)=>{
|
|
62
|
+
return effect.pipe(effect.Effect.log('Running transform', transform), effect.Effect.flatMap(()=>effect.Effect.tryPromise(async ()=>import('jscodeshift/src/Runner.js').then(({ run })=>run(transform.replace('file://', ''), commitedFiles, {
|
|
63
|
+
appRouterImportFile,
|
|
64
|
+
appRouterImportName,
|
|
65
|
+
trpcFile,
|
|
66
|
+
trpcImportName
|
|
67
|
+
})))), effect.Effect.map((res)=>effect.Effect.log('Transform result', res)));
|
|
68
|
+
});
|
|
69
|
+
yield* effect.Effect.log('Installing @trpc/tanstack-react-query');
|
|
70
|
+
yield* installPackage('@trpc/tanstack-react-query');
|
|
71
|
+
// TODO: Format project
|
|
72
|
+
}));
|
|
73
|
+
const cli = cli$1.Command.run(prompts, {
|
|
74
|
+
name: 'tRPC Upgrade CLI',
|
|
75
|
+
version: `v${version}`
|
|
76
|
+
});
|
|
77
|
+
cli(process.argv).pipe(effect.Effect.provide(platformNode.NodeContext.layer), platformNode.NodeRuntime.runMain);
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
|
|
3
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable no-console */ const hookToOptions = {
|
|
4
|
+
useQuery: {
|
|
5
|
+
lib: '@tanstack/react-query',
|
|
6
|
+
fn: 'queryOptions'
|
|
7
|
+
},
|
|
8
|
+
useSuspenseQuery: {
|
|
9
|
+
lib: '@tanstack/react-query',
|
|
10
|
+
fn: 'queryOptions'
|
|
11
|
+
},
|
|
12
|
+
useInfiniteQuery: {
|
|
13
|
+
lib: '@tanstack/react-query',
|
|
14
|
+
fn: 'infiniteQueryOptions'
|
|
15
|
+
},
|
|
16
|
+
useSuspenseInfiniteQuery: {
|
|
17
|
+
lib: '@tanstack/react-query',
|
|
18
|
+
fn: 'infiniteQueryOptions'
|
|
19
|
+
},
|
|
20
|
+
useMutation: {
|
|
21
|
+
lib: '@tanstack/react-query',
|
|
22
|
+
fn: 'mutationOptions'
|
|
23
|
+
},
|
|
24
|
+
useSubscription: {
|
|
25
|
+
lib: '@trpc/tanstack-react-query',
|
|
26
|
+
fn: 'subscriptionOptions'
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
const utilMap = {
|
|
30
|
+
fetch: 'fetchQuery',
|
|
31
|
+
fetchInfinite: 'fetchInfiniteQuery',
|
|
32
|
+
prefetch: 'prefetchQuery',
|
|
33
|
+
prefetchInfinite: 'prefetchInfiniteQuery',
|
|
34
|
+
ensureData: 'ensureQueryData',
|
|
35
|
+
invalidate: 'invalidateQueries',
|
|
36
|
+
reset: 'resetQueries',
|
|
37
|
+
refetch: 'refetchQueries',
|
|
38
|
+
cancel: 'cancelQuery',
|
|
39
|
+
setData: 'setQueryData',
|
|
40
|
+
setQueriesData: 'setQueriesData',
|
|
41
|
+
setInfiniteData: 'setInfiniteQueryData',
|
|
42
|
+
getData: 'getQueryData',
|
|
43
|
+
getInfiniteData: 'getInfiniteQueryData'
|
|
44
|
+
};
|
|
45
|
+
function transform(file, api, options) {
|
|
46
|
+
const { trpcFile, trpcImportName } = options;
|
|
47
|
+
if (!trpcFile || !trpcImportName) {
|
|
48
|
+
throw new Error('trpcFile and trpcImportName are required');
|
|
49
|
+
}
|
|
50
|
+
const j = api.jscodeshift;
|
|
51
|
+
const root = j(file.source);
|
|
52
|
+
let dirtyFlag = false;
|
|
53
|
+
// Traverse all functions, and _do stuff_
|
|
54
|
+
root.find(j.FunctionDeclaration).forEach((path)=>{
|
|
55
|
+
if (j(path).find(j.Identifier, {
|
|
56
|
+
name: trpcImportName
|
|
57
|
+
}).size() > 0) {
|
|
58
|
+
updateTRPCImport(path);
|
|
59
|
+
}
|
|
60
|
+
replaceHooksWithOptions(path);
|
|
61
|
+
removeSuspenseDestructuring(path);
|
|
62
|
+
migrateUseUtils(path);
|
|
63
|
+
});
|
|
64
|
+
root.find(j.ArrowFunctionExpression).forEach((path)=>{
|
|
65
|
+
if (j(path).find(j.Identifier, {
|
|
66
|
+
name: trpcImportName
|
|
67
|
+
}).size() > 0) {
|
|
68
|
+
updateTRPCImport(path);
|
|
69
|
+
}
|
|
70
|
+
replaceHooksWithOptions(path);
|
|
71
|
+
removeSuspenseDestructuring(path);
|
|
72
|
+
migrateUseUtils(path);
|
|
73
|
+
});
|
|
74
|
+
/**
|
|
75
|
+
* === HELPER FUNCTIONS BELOW ===
|
|
76
|
+
*/ function updateTRPCImport(path) {
|
|
77
|
+
const specifier = root.find(j.ImportDeclaration, {
|
|
78
|
+
source: {
|
|
79
|
+
value: trpcFile
|
|
80
|
+
}
|
|
81
|
+
}).find(j.ImportSpecifier, {
|
|
82
|
+
imported: {
|
|
83
|
+
name: trpcImportName
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
if (specifier.size() === 0) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
specifier.replaceWith(j.importSpecifier(j.identifier('useTRPC')));
|
|
90
|
+
dirtyFlag = true;
|
|
91
|
+
const variableDeclaration = j.variableDeclaration('const', [
|
|
92
|
+
j.variableDeclarator(j.identifier(trpcImportName), j.callExpression(j.identifier('useTRPC'), []))
|
|
93
|
+
]);
|
|
94
|
+
if (j.FunctionDeclaration.check(path.node)) {
|
|
95
|
+
const body = path.node.body.body;
|
|
96
|
+
body.unshift(variableDeclaration);
|
|
97
|
+
} else if (j.BlockStatement.check(path.node.body)) {
|
|
98
|
+
path.node.body.body.unshift(variableDeclaration);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
function ensureImported(lib, specifier) {
|
|
102
|
+
if (root.find(j.ImportDeclaration, {
|
|
103
|
+
source: {
|
|
104
|
+
value: lib
|
|
105
|
+
}
|
|
106
|
+
}).find(j.ImportSpecifier, {
|
|
107
|
+
imported: {
|
|
108
|
+
name: specifier
|
|
109
|
+
}
|
|
110
|
+
}).size() === 0) {
|
|
111
|
+
root.find(j.ImportDeclaration).at(-1).insertAfter(j.importDeclaration([
|
|
112
|
+
j.importSpecifier(j.identifier(specifier))
|
|
113
|
+
], j.literal(lib)));
|
|
114
|
+
dirtyFlag = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function replaceHooksWithOptions(path) {
|
|
118
|
+
// REplace proxy-hooks with useX(options())
|
|
119
|
+
for (const [hook, { fn, lib }] of Object.entries(hookToOptions)){
|
|
120
|
+
j(path).find(j.CallExpression, {
|
|
121
|
+
callee: {
|
|
122
|
+
property: {
|
|
123
|
+
name: hook
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}).forEach((path)=>{
|
|
127
|
+
const memberExpr = path.node.callee;
|
|
128
|
+
if (!j.MemberExpression.check(memberExpr) || !j.Identifier.check(memberExpr.property)) {
|
|
129
|
+
console.warn('Failed to identify hook call expression', path.node);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
// Rename the hook to the options function
|
|
133
|
+
memberExpr.property.name = fn;
|
|
134
|
+
// Wrap it in the hook call
|
|
135
|
+
j(path).replaceWith(j.callExpression(j.identifier(hook), [
|
|
136
|
+
path.node
|
|
137
|
+
]));
|
|
138
|
+
ensureImported(lib, hook);
|
|
139
|
+
dirtyFlag = true;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// Migrate trpc.useUtils() to useQueryClient()
|
|
144
|
+
function migrateUseUtils(path) {
|
|
145
|
+
j(path).find(j.CallExpression, {
|
|
146
|
+
callee: {
|
|
147
|
+
property: {
|
|
148
|
+
name: (name)=>[
|
|
149
|
+
'useContext',
|
|
150
|
+
'useUtils'
|
|
151
|
+
].includes(name)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}).forEach((path)=>{
|
|
155
|
+
if (j.VariableDeclarator.check(path.parentPath.node) && j.Identifier.check(path.parentPath.node.id)) {
|
|
156
|
+
const oldIdentifier = path.parentPath.node.id;
|
|
157
|
+
// Find all the references to `utils` and replace with `queryClient[helperMap](trpc.PATH.queryFilter())`
|
|
158
|
+
root.find(j.Identifier, {
|
|
159
|
+
name: oldIdentifier.name
|
|
160
|
+
}).forEach((path)=>{
|
|
161
|
+
if (j.MemberExpression.check(path.parent?.parent?.node)) {
|
|
162
|
+
const callExprPath = path.parent.parent.parent;
|
|
163
|
+
const callExpr = callExprPath.node;
|
|
164
|
+
const memberExpr = callExpr.callee;
|
|
165
|
+
if (!j.CallExpression.check(callExpr) || !j.MemberExpression.check(memberExpr)) {
|
|
166
|
+
console.warn('Failed to walk up the tree to find utilMethod call expression', callExpr);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (!(j.MemberExpression.check(memberExpr.object) && j.Identifier.check(memberExpr.object.object) && j.Identifier.check(memberExpr.property) && memberExpr.property.name in utilMap)) {
|
|
170
|
+
console.warn('Failed to identify utilMethod from proxy call expression', memberExpr);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
// Replace util.PATH.proxyMethod() with trpc.PATH.queryFilter()
|
|
174
|
+
const proxyMethod = memberExpr.property.name;
|
|
175
|
+
memberExpr.object.object = j.identifier('trpc');
|
|
176
|
+
memberExpr.property = j.identifier('queryFilter');
|
|
177
|
+
// Wrap it in queryClient.utilMethod()
|
|
178
|
+
j(callExprPath).replaceWith(j.memberExpression(j.identifier('queryClient'), j.callExpression(j.identifier(utilMap[proxyMethod]), [
|
|
179
|
+
callExpr
|
|
180
|
+
])));
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
// Replace `const utils = trpc.useUtils()` with `const queryClient = useQueryClient()`
|
|
184
|
+
j(path).replaceWith(j.callExpression(j.identifier('useQueryClient'), []));
|
|
185
|
+
path.parentPath.node.id = j.identifier('queryClient');
|
|
186
|
+
ensureImported('@tanstack/react-query', 'useQueryClient');
|
|
187
|
+
}
|
|
188
|
+
dirtyFlag = true;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function removeSuspenseDestructuring(path) {
|
|
192
|
+
// Remove suspense query destructuring
|
|
193
|
+
j(path).find(j.VariableDeclaration).forEach((path)=>{
|
|
194
|
+
const declarator = j.VariableDeclarator.check(path.node.declarations[0]) ? path.node.declarations[0] : null;
|
|
195
|
+
if (!j.CallExpression.check(declarator?.init) || !j.Identifier.check(declarator.init.callee) || ![
|
|
196
|
+
'useSuspenseQuery',
|
|
197
|
+
'useSuspenseInfiniteQuery'
|
|
198
|
+
].includes(declarator.init.callee.name)) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
console.log(declarator.init.callee.name);
|
|
202
|
+
const tuple = j.ArrayPattern.check(declarator?.id) ? declarator.id : null;
|
|
203
|
+
const dataName = j.Identifier.check(tuple?.elements?.[0]) ? tuple.elements[0].name : null;
|
|
204
|
+
const queryName = j.Identifier.check(tuple?.elements?.[1]) ? tuple.elements[1].name : null;
|
|
205
|
+
if (declarator && queryName) {
|
|
206
|
+
declarator.id = j.identifier(queryName);
|
|
207
|
+
dirtyFlag = true;
|
|
208
|
+
if (dataName) {
|
|
209
|
+
j(path).insertAfter(j.variableDeclaration('const', [
|
|
210
|
+
j.variableDeclarator(j.identifier(dataName), j.memberExpression(declarator.id, j.identifier('data')))
|
|
211
|
+
]));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return dirtyFlag ? root.toSource() : undefined;
|
|
217
|
+
}
|
|
218
|
+
const parser = 'tsx'; // https://go.codemod.com/ddX54TM
|
|
219
|
+
|
|
220
|
+
exports.default = transform;
|
|
221
|
+
exports.parser = parser;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
|
|
3
|
+
function transform(file, api, options) {
|
|
4
|
+
const { trpcFile, trpcImportName, appRouterImportFile, appRouterImportName } = options;
|
|
5
|
+
const j = api.jscodeshift;
|
|
6
|
+
const root = j(file.source);
|
|
7
|
+
let dirtyFlag = false;
|
|
8
|
+
function upsertAppRouterImport() {
|
|
9
|
+
if (root.find(j.ImportDeclaration, {
|
|
10
|
+
source: {
|
|
11
|
+
value: appRouterImportFile
|
|
12
|
+
}
|
|
13
|
+
}).size() === 0) {
|
|
14
|
+
root.find(j.ImportDeclaration).at(-1).insertAfter(j.importDeclaration([
|
|
15
|
+
j.importSpecifier(j.identifier(appRouterImportName))
|
|
16
|
+
], j.literal(appRouterImportFile), 'type'));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// Find the variable declaration for `trpc`
|
|
20
|
+
root.find(j.VariableDeclaration).forEach((path)=>{
|
|
21
|
+
const declaration = path.node.declarations[0];
|
|
22
|
+
if (j.Identifier.check(declaration.id) && declaration.id.name === trpcImportName) {
|
|
23
|
+
if (j.CallExpression.check(declaration.init) && j.Identifier.check(declaration.init.callee) && declaration.init.callee.name === 'createTRPCReact') {
|
|
24
|
+
// Replace the `createTRPCReact` call with `createTRPCContext`
|
|
25
|
+
declaration.init.callee.name = 'createTRPCContext';
|
|
26
|
+
// Destructure the result into `TRPCProvider` and `useTRPC`
|
|
27
|
+
declaration.id = j.objectPattern([
|
|
28
|
+
j.property.from({
|
|
29
|
+
kind: 'init',
|
|
30
|
+
key: j.identifier('TRPCProvider'),
|
|
31
|
+
value: j.identifier('TRPCProvider'),
|
|
32
|
+
shorthand: true
|
|
33
|
+
}),
|
|
34
|
+
j.property.from({
|
|
35
|
+
kind: 'init',
|
|
36
|
+
key: j.identifier('useTRPC'),
|
|
37
|
+
value: j.identifier('useTRPC'),
|
|
38
|
+
shorthand: true
|
|
39
|
+
})
|
|
40
|
+
]);
|
|
41
|
+
dirtyFlag = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
// Update the import statement if transformation was successful
|
|
46
|
+
if (dirtyFlag) {
|
|
47
|
+
root.find(j.ImportDeclaration, {
|
|
48
|
+
source: {
|
|
49
|
+
value: '@trpc/react-query'
|
|
50
|
+
}
|
|
51
|
+
}).forEach((path)=>{
|
|
52
|
+
path.node.source.value = '@trpc/tanstack-react-query';
|
|
53
|
+
path.node.specifiers?.forEach((specifier)=>{
|
|
54
|
+
if (j.ImportSpecifier.check(specifier) && specifier.imported.name === 'createTRPCReact') {
|
|
55
|
+
specifier.imported.name = 'createTRPCContext';
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
// Replace trpc.createClient with createTRPCClient
|
|
61
|
+
root.find(j.CallExpression, {
|
|
62
|
+
callee: {
|
|
63
|
+
object: {
|
|
64
|
+
name: trpcImportName
|
|
65
|
+
},
|
|
66
|
+
property: {
|
|
67
|
+
name: 'createClient'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}).forEach((path)=>{
|
|
71
|
+
path.node.callee = j.identifier('createTRPCClient');
|
|
72
|
+
// Add the type parameter `<AppRouter>`
|
|
73
|
+
path.node.typeParameters = j.tsTypeParameterInstantiation([
|
|
74
|
+
j.tsTypeReference(j.identifier(appRouterImportName))
|
|
75
|
+
]);
|
|
76
|
+
upsertAppRouterImport();
|
|
77
|
+
dirtyFlag = true;
|
|
78
|
+
});
|
|
79
|
+
// Replace <trpc.Provider client={...} with <TRPCProvider trpcClient={...}
|
|
80
|
+
root.find(j.JSXElement, {
|
|
81
|
+
openingElement: {
|
|
82
|
+
name: {
|
|
83
|
+
object: {
|
|
84
|
+
name: trpcImportName
|
|
85
|
+
},
|
|
86
|
+
property: {
|
|
87
|
+
name: 'Provider'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}).forEach((path)=>{
|
|
92
|
+
path.node.openingElement.name = j.jsxIdentifier('TRPCProvider');
|
|
93
|
+
if (path.node.closingElement) {
|
|
94
|
+
path.node.closingElement.name = j.jsxIdentifier('TRPCProvider');
|
|
95
|
+
}
|
|
96
|
+
path.node.openingElement.attributes?.forEach((attr)=>{
|
|
97
|
+
if (j.JSXAttribute.check(attr) && attr.name.name === 'client') {
|
|
98
|
+
attr.name.name = 'trpcClient';
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
dirtyFlag = true;
|
|
102
|
+
});
|
|
103
|
+
// Update imports if transformations were applied
|
|
104
|
+
if (dirtyFlag) {
|
|
105
|
+
// Add createTRPCClient to the import from '@trpc/client'
|
|
106
|
+
root.find(j.ImportDeclaration, {
|
|
107
|
+
source: {
|
|
108
|
+
value: '@trpc/client'
|
|
109
|
+
}
|
|
110
|
+
}).forEach((path)=>{
|
|
111
|
+
const createTRPCClientImport = j.importSpecifier(j.identifier('createTRPCClient'));
|
|
112
|
+
path.node.specifiers?.push(createTRPCClientImport);
|
|
113
|
+
});
|
|
114
|
+
// Replace trpc import with TRPCProvider
|
|
115
|
+
root.find(j.ImportDeclaration, {
|
|
116
|
+
source: {
|
|
117
|
+
value: trpcFile
|
|
118
|
+
}
|
|
119
|
+
}).forEach((path)=>{
|
|
120
|
+
path.node.specifiers = [
|
|
121
|
+
j.importSpecifier(j.identifier('TRPCProvider'))
|
|
122
|
+
];
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
return dirtyFlag ? root.toSource() : undefined;
|
|
126
|
+
}
|
|
127
|
+
const parser = 'tsx';
|
|
128
|
+
|
|
129
|
+
exports.default = transform;
|
|
130
|
+
exports.parser = parser;
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@trpc/upgrade",
|
|
3
|
+
"version": "0.0.0-alpha.0",
|
|
4
|
+
"description": "Upgrade scripts for tRPC",
|
|
5
|
+
"author": "juliusmarminge",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": "./dist/cli.cjs",
|
|
8
|
+
"homepage": "https://trpc.io",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/trpc/trpc.git",
|
|
12
|
+
"directory": "packages/upgrade"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
"./transforms/hooksToOptions": {
|
|
16
|
+
"require": "./dist/transforms/hooksToOptions.cjs"
|
|
17
|
+
},
|
|
18
|
+
"./transforms/provider": {
|
|
19
|
+
"require": "./dist/transforms/provider.cjs"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist",
|
|
24
|
+
"src",
|
|
25
|
+
"README.md",
|
|
26
|
+
"package.json",
|
|
27
|
+
"!**/*.test.*",
|
|
28
|
+
"!**/__tests__"
|
|
29
|
+
],
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@effect/cli": "0.48.25",
|
|
32
|
+
"@effect/platform": "0.69.25",
|
|
33
|
+
"@effect/platform-node": "0.64.27",
|
|
34
|
+
"effect": "3.10.16",
|
|
35
|
+
"ignore": "^6.0.2",
|
|
36
|
+
"jscodeshift": "17.1.1",
|
|
37
|
+
"typescript": "^5.6.2"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/jscodeshift": "0.12.0",
|
|
41
|
+
"@types/node": "^22.9.0",
|
|
42
|
+
"bunchee": "5.6.1",
|
|
43
|
+
"esbuild": "0.19.2",
|
|
44
|
+
"tsx": "^4.0.0"
|
|
45
|
+
},
|
|
46
|
+
"publishConfig": {
|
|
47
|
+
"access": "public"
|
|
48
|
+
},
|
|
49
|
+
"funding": [
|
|
50
|
+
"https://trpc.io/sponsor"
|
|
51
|
+
],
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "bunchee",
|
|
54
|
+
"run": "tsx src/cli.ts"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/bin/cli.ts
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/unbound-method */
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { Command as CLICommand, Prompt } from '@effect/cli';
|
|
4
|
+
import { Command, FileSystem } from '@effect/platform';
|
|
5
|
+
import { NodeContext, NodeRuntime } from '@effect/platform-node';
|
|
6
|
+
import {
|
|
7
|
+
Console,
|
|
8
|
+
Effect,
|
|
9
|
+
Match,
|
|
10
|
+
pipe,
|
|
11
|
+
Predicate,
|
|
12
|
+
Stream,
|
|
13
|
+
String,
|
|
14
|
+
} from 'effect';
|
|
15
|
+
import ignore from 'ignore';
|
|
16
|
+
import type { SourceFile } from 'typescript';
|
|
17
|
+
import {
|
|
18
|
+
createProgram,
|
|
19
|
+
findConfigFile,
|
|
20
|
+
parseJsonConfigFileContent,
|
|
21
|
+
readConfigFile,
|
|
22
|
+
sys,
|
|
23
|
+
} from 'typescript';
|
|
24
|
+
import { version } from '../../package.json';
|
|
25
|
+
|
|
26
|
+
const assertCleanGitTree = Command.string(Command.make('git', 'status')).pipe(
|
|
27
|
+
Effect.filterOrFail(
|
|
28
|
+
(status) => status.includes('nothing to commit'),
|
|
29
|
+
() =>
|
|
30
|
+
'Git tree is not clean, please commit your changes before running the migrator',
|
|
31
|
+
),
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const installPackage = (packageName: string) => {
|
|
35
|
+
const packageManager = Match.value(
|
|
36
|
+
process.env.npm_config_user_agent ?? 'npm',
|
|
37
|
+
).pipe(
|
|
38
|
+
Match.when(String.startsWith('pnpm'), () => 'pnpm'),
|
|
39
|
+
Match.when(String.startsWith('yarn'), () => 'yarn'),
|
|
40
|
+
Match.when(String.startsWith('bun'), () => 'bun'),
|
|
41
|
+
Match.orElse(() => 'npm'),
|
|
42
|
+
);
|
|
43
|
+
return Command.streamLines(
|
|
44
|
+
Command.make(packageManager, 'install', packageName),
|
|
45
|
+
).pipe(Stream.mapEffect(Console.log), Stream.runDrain);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const filterIgnored = (files: readonly SourceFile[]) =>
|
|
49
|
+
Effect.gen(function* () {
|
|
50
|
+
const fs = yield* FileSystem.FileSystem;
|
|
51
|
+
const ignores = yield* fs
|
|
52
|
+
.readFileString(path.join(process.cwd(), '.gitignore'))
|
|
53
|
+
.pipe(
|
|
54
|
+
Effect.map((content) => content.split('\n')),
|
|
55
|
+
Effect.map((patterns) => ignore().add(patterns)),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Ignore "common files"
|
|
59
|
+
const relativeFilePaths = files
|
|
60
|
+
.filter(
|
|
61
|
+
(source) =>
|
|
62
|
+
!source.fileName.includes('node_modules') &&
|
|
63
|
+
!source.fileName.includes('packages/'),
|
|
64
|
+
)
|
|
65
|
+
.map((file) => path.relative(process.cwd(), file.fileName));
|
|
66
|
+
|
|
67
|
+
// As well as gitignored?
|
|
68
|
+
return ignores.filter(relativeFilePaths);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const Program = Effect.succeed(
|
|
72
|
+
findConfigFile(process.cwd(), sys.fileExists),
|
|
73
|
+
).pipe(
|
|
74
|
+
Effect.filterOrFail(Predicate.isNotNullable, () => 'No tsconfig found'),
|
|
75
|
+
Effect.tap((_) => Effect.logDebug('Using tsconfig', _)),
|
|
76
|
+
Effect.map((_) => readConfigFile(_, sys.readFile)),
|
|
77
|
+
Effect.map((_) => parseJsonConfigFileContent(_.config, sys, process.cwd())),
|
|
78
|
+
Effect.map((_) =>
|
|
79
|
+
createProgram({
|
|
80
|
+
options: _.options,
|
|
81
|
+
rootNames: _.fileNames,
|
|
82
|
+
configFileParsingDiagnostics: _.errors,
|
|
83
|
+
}),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const transformPath = (path: string) =>
|
|
88
|
+
process.env.DEV ? path : path.replace('../', './').replace('.ts', '.cjs');
|
|
89
|
+
|
|
90
|
+
const prompts = CLICommand.prompt(
|
|
91
|
+
'transforms',
|
|
92
|
+
Prompt.multiSelect({
|
|
93
|
+
message: 'Select transforms to run',
|
|
94
|
+
choices: [
|
|
95
|
+
{
|
|
96
|
+
title: 'Migrate Hooks to xxxOptions API',
|
|
97
|
+
value: require.resolve(
|
|
98
|
+
transformPath('../transforms/hooksToOptions.ts'),
|
|
99
|
+
),
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
title: 'Migrate context provider setup',
|
|
103
|
+
value: require.resolve(transformPath('../transforms/provider.ts')),
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
}),
|
|
107
|
+
(_) =>
|
|
108
|
+
Effect.gen(function* () {
|
|
109
|
+
// yield* assertCleanGitTree;
|
|
110
|
+
const program = yield* Program;
|
|
111
|
+
const sourceFiles = program.getSourceFiles();
|
|
112
|
+
|
|
113
|
+
// Make sure provider transform runs first if it's selected
|
|
114
|
+
_.sort((a, b) =>
|
|
115
|
+
a.includes('provider.ts') ? -1 : b.includes('provider.ts') ? 1 : 0,
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* TODO: Detect these automatically
|
|
120
|
+
*/
|
|
121
|
+
const appRouterImportFile = '~/server/routers/_app';
|
|
122
|
+
const appRouterImportName = 'AppRouter';
|
|
123
|
+
const trpcFile = '~/lib/trpc';
|
|
124
|
+
const trpcImportName = 'trpc';
|
|
125
|
+
|
|
126
|
+
const commitedFiles = yield* filterIgnored(sourceFiles);
|
|
127
|
+
yield* Effect.forEach(_, (transform) => {
|
|
128
|
+
return pipe(
|
|
129
|
+
Effect.log('Running transform', transform),
|
|
130
|
+
Effect.flatMap(() =>
|
|
131
|
+
Effect.tryPromise(async () =>
|
|
132
|
+
import('jscodeshift/src/Runner.js').then(({ run }) =>
|
|
133
|
+
run(transform.replace('file://', ''), commitedFiles, {
|
|
134
|
+
appRouterImportFile,
|
|
135
|
+
appRouterImportName,
|
|
136
|
+
trpcFile,
|
|
137
|
+
trpcImportName,
|
|
138
|
+
}),
|
|
139
|
+
),
|
|
140
|
+
),
|
|
141
|
+
),
|
|
142
|
+
Effect.map((res) => Effect.log('Transform result', res)),
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
yield* Effect.log('Installing @trpc/tanstack-react-query');
|
|
147
|
+
yield* installPackage('@trpc/tanstack-react-query');
|
|
148
|
+
|
|
149
|
+
// TODO: Format project
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const cli = CLICommand.run(prompts, {
|
|
154
|
+
name: 'tRPC Upgrade CLI',
|
|
155
|
+
version: `v${version}`,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
2
|
+
/* eslint-disable no-console */
|
|
3
|
+
import type {
|
|
4
|
+
API,
|
|
5
|
+
ArrowFunctionExpression,
|
|
6
|
+
ASTPath,
|
|
7
|
+
CallExpression,
|
|
8
|
+
FileInfo,
|
|
9
|
+
FunctionDeclaration,
|
|
10
|
+
Identifier,
|
|
11
|
+
MemberExpression,
|
|
12
|
+
Options,
|
|
13
|
+
} from 'jscodeshift';
|
|
14
|
+
|
|
15
|
+
interface TransformOptions extends Options {
|
|
16
|
+
trpcFile?: string;
|
|
17
|
+
trpcImportName?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const hookToOptions = {
|
|
21
|
+
useQuery: { lib: '@tanstack/react-query', fn: 'queryOptions' },
|
|
22
|
+
useSuspenseQuery: { lib: '@tanstack/react-query', fn: 'queryOptions' },
|
|
23
|
+
useInfiniteQuery: {
|
|
24
|
+
lib: '@tanstack/react-query',
|
|
25
|
+
fn: 'infiniteQueryOptions',
|
|
26
|
+
},
|
|
27
|
+
useSuspenseInfiniteQuery: {
|
|
28
|
+
lib: '@tanstack/react-query',
|
|
29
|
+
fn: 'infiniteQueryOptions',
|
|
30
|
+
},
|
|
31
|
+
useMutation: { lib: '@tanstack/react-query', fn: 'mutationOptions' },
|
|
32
|
+
useSubscription: {
|
|
33
|
+
lib: '@trpc/tanstack-react-query',
|
|
34
|
+
fn: 'subscriptionOptions',
|
|
35
|
+
},
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
const utilMap = {
|
|
39
|
+
fetch: 'fetchQuery',
|
|
40
|
+
fetchInfinite: 'fetchInfiniteQuery',
|
|
41
|
+
prefetch: 'prefetchQuery',
|
|
42
|
+
prefetchInfinite: 'prefetchInfiniteQuery',
|
|
43
|
+
ensureData: 'ensureQueryData',
|
|
44
|
+
invalidate: 'invalidateQueries',
|
|
45
|
+
reset: 'resetQueries',
|
|
46
|
+
refetch: 'refetchQueries',
|
|
47
|
+
cancel: 'cancelQuery',
|
|
48
|
+
setData: 'setQueryData',
|
|
49
|
+
setQueriesData: 'setQueriesData',
|
|
50
|
+
setInfiniteData: 'setInfiniteQueryData',
|
|
51
|
+
getData: 'getQueryData',
|
|
52
|
+
getInfiniteData: 'getInfiniteQueryData',
|
|
53
|
+
// setMutationDefaults: 'setMutationDefaults',
|
|
54
|
+
// getMutationDefaults: 'getMutationDefaults',
|
|
55
|
+
// isMutating: 'isMutating',
|
|
56
|
+
} as const;
|
|
57
|
+
type ProxyMethod = keyof typeof utilMap;
|
|
58
|
+
|
|
59
|
+
export default function transform(
|
|
60
|
+
file: FileInfo,
|
|
61
|
+
api: API,
|
|
62
|
+
options: TransformOptions,
|
|
63
|
+
) {
|
|
64
|
+
const { trpcFile, trpcImportName } = options;
|
|
65
|
+
if (!trpcFile || !trpcImportName) {
|
|
66
|
+
throw new Error('trpcFile and trpcImportName are required');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const j = api.jscodeshift;
|
|
70
|
+
const root = j(file.source);
|
|
71
|
+
let dirtyFlag = false;
|
|
72
|
+
|
|
73
|
+
// Traverse all functions, and _do stuff_
|
|
74
|
+
root.find(j.FunctionDeclaration).forEach((path) => {
|
|
75
|
+
if (j(path).find(j.Identifier, { name: trpcImportName }).size() > 0) {
|
|
76
|
+
updateTRPCImport(path);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
replaceHooksWithOptions(path);
|
|
80
|
+
removeSuspenseDestructuring(path);
|
|
81
|
+
migrateUseUtils(path);
|
|
82
|
+
});
|
|
83
|
+
root.find(j.ArrowFunctionExpression).forEach((path) => {
|
|
84
|
+
if (j(path).find(j.Identifier, { name: trpcImportName }).size() > 0) {
|
|
85
|
+
updateTRPCImport(path);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
replaceHooksWithOptions(path);
|
|
89
|
+
removeSuspenseDestructuring(path);
|
|
90
|
+
migrateUseUtils(path);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* === HELPER FUNCTIONS BELOW ===
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
function updateTRPCImport(
|
|
98
|
+
path: ASTPath<FunctionDeclaration | ArrowFunctionExpression>,
|
|
99
|
+
) {
|
|
100
|
+
const specifier = root
|
|
101
|
+
.find(j.ImportDeclaration, {
|
|
102
|
+
source: { value: trpcFile },
|
|
103
|
+
})
|
|
104
|
+
.find(j.ImportSpecifier, { imported: { name: trpcImportName } });
|
|
105
|
+
|
|
106
|
+
if (specifier.size() === 0) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
specifier.replaceWith(j.importSpecifier(j.identifier('useTRPC')));
|
|
111
|
+
dirtyFlag = true;
|
|
112
|
+
|
|
113
|
+
const variableDeclaration = j.variableDeclaration('const', [
|
|
114
|
+
j.variableDeclarator(
|
|
115
|
+
j.identifier(trpcImportName!),
|
|
116
|
+
j.callExpression(j.identifier('useTRPC'), []),
|
|
117
|
+
),
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
if (j.FunctionDeclaration.check(path.node)) {
|
|
121
|
+
const body = path.node.body.body;
|
|
122
|
+
body.unshift(variableDeclaration);
|
|
123
|
+
} else if (j.BlockStatement.check(path.node.body)) {
|
|
124
|
+
path.node.body.body.unshift(variableDeclaration);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ensureImported(lib: string, specifier: string) {
|
|
129
|
+
if (
|
|
130
|
+
root
|
|
131
|
+
.find(j.ImportDeclaration, {
|
|
132
|
+
source: { value: lib },
|
|
133
|
+
})
|
|
134
|
+
.find(j.ImportSpecifier, { imported: { name: specifier } })
|
|
135
|
+
.size() === 0
|
|
136
|
+
) {
|
|
137
|
+
root
|
|
138
|
+
.find(j.ImportDeclaration)
|
|
139
|
+
.at(-1)
|
|
140
|
+
.insertAfter(
|
|
141
|
+
j.importDeclaration(
|
|
142
|
+
[j.importSpecifier(j.identifier(specifier))],
|
|
143
|
+
j.literal(lib),
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
dirtyFlag = true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function replaceHooksWithOptions(
|
|
151
|
+
path: ASTPath<FunctionDeclaration | ArrowFunctionExpression>,
|
|
152
|
+
) {
|
|
153
|
+
// REplace proxy-hooks with useX(options())
|
|
154
|
+
for (const [hook, { fn, lib }] of Object.entries(hookToOptions)) {
|
|
155
|
+
j(path)
|
|
156
|
+
.find(j.CallExpression, {
|
|
157
|
+
callee: {
|
|
158
|
+
property: { name: hook },
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
.forEach((path) => {
|
|
162
|
+
const memberExpr = path.node.callee as MemberExpression;
|
|
163
|
+
if (
|
|
164
|
+
!j.MemberExpression.check(memberExpr) ||
|
|
165
|
+
!j.Identifier.check(memberExpr.property)
|
|
166
|
+
) {
|
|
167
|
+
console.warn('Failed to identify hook call expression', path.node);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Rename the hook to the options function
|
|
172
|
+
memberExpr.property.name = fn;
|
|
173
|
+
|
|
174
|
+
// Wrap it in the hook call
|
|
175
|
+
j(path).replaceWith(
|
|
176
|
+
j.callExpression(j.identifier(hook), [path.node]),
|
|
177
|
+
);
|
|
178
|
+
ensureImported(lib, hook);
|
|
179
|
+
|
|
180
|
+
dirtyFlag = true;
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Migrate trpc.useUtils() to useQueryClient()
|
|
186
|
+
function migrateUseUtils(
|
|
187
|
+
path: ASTPath<FunctionDeclaration | ArrowFunctionExpression>,
|
|
188
|
+
) {
|
|
189
|
+
j(path)
|
|
190
|
+
.find(j.CallExpression, {
|
|
191
|
+
callee: {
|
|
192
|
+
property: {
|
|
193
|
+
name: (name: string) => ['useContext', 'useUtils'].includes(name),
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
.forEach((path) => {
|
|
198
|
+
if (
|
|
199
|
+
j.VariableDeclarator.check(path.parentPath.node) &&
|
|
200
|
+
j.Identifier.check(path.parentPath.node.id)
|
|
201
|
+
) {
|
|
202
|
+
const oldIdentifier = path.parentPath.node.id as Identifier;
|
|
203
|
+
|
|
204
|
+
// Find all the references to `utils` and replace with `queryClient[helperMap](trpc.PATH.queryFilter())`
|
|
205
|
+
root
|
|
206
|
+
.find(j.Identifier, { name: oldIdentifier.name })
|
|
207
|
+
.forEach((path) => {
|
|
208
|
+
if (j.MemberExpression.check(path.parent?.parent?.node)) {
|
|
209
|
+
const callExprPath = path.parent.parent.parent;
|
|
210
|
+
const callExpr = callExprPath.node as CallExpression;
|
|
211
|
+
const memberExpr = callExpr.callee as MemberExpression;
|
|
212
|
+
if (
|
|
213
|
+
!j.CallExpression.check(callExpr) ||
|
|
214
|
+
!j.MemberExpression.check(memberExpr)
|
|
215
|
+
) {
|
|
216
|
+
console.warn(
|
|
217
|
+
'Failed to walk up the tree to find utilMethod call expression',
|
|
218
|
+
callExpr,
|
|
219
|
+
);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (
|
|
224
|
+
!(
|
|
225
|
+
j.MemberExpression.check(memberExpr.object) &&
|
|
226
|
+
j.Identifier.check(memberExpr.object.object) &&
|
|
227
|
+
j.Identifier.check(memberExpr.property) &&
|
|
228
|
+
memberExpr.property.name in utilMap
|
|
229
|
+
)
|
|
230
|
+
) {
|
|
231
|
+
console.warn(
|
|
232
|
+
'Failed to identify utilMethod from proxy call expression',
|
|
233
|
+
memberExpr,
|
|
234
|
+
);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Replace util.PATH.proxyMethod() with trpc.PATH.queryFilter()
|
|
239
|
+
const proxyMethod = memberExpr.property.name as ProxyMethod;
|
|
240
|
+
memberExpr.object.object = j.identifier('trpc');
|
|
241
|
+
memberExpr.property = j.identifier('queryFilter');
|
|
242
|
+
|
|
243
|
+
// Wrap it in queryClient.utilMethod()
|
|
244
|
+
j(callExprPath).replaceWith(
|
|
245
|
+
j.memberExpression(
|
|
246
|
+
j.identifier('queryClient'),
|
|
247
|
+
j.callExpression(j.identifier(utilMap[proxyMethod]), [
|
|
248
|
+
callExpr,
|
|
249
|
+
]),
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Replace `const utils = trpc.useUtils()` with `const queryClient = useQueryClient()`
|
|
256
|
+
j(path).replaceWith(
|
|
257
|
+
j.callExpression(j.identifier('useQueryClient'), []),
|
|
258
|
+
);
|
|
259
|
+
path.parentPath.node.id = j.identifier('queryClient');
|
|
260
|
+
ensureImported('@tanstack/react-query', 'useQueryClient');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
dirtyFlag = true;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function removeSuspenseDestructuring(
|
|
268
|
+
path: ASTPath<FunctionDeclaration | ArrowFunctionExpression>,
|
|
269
|
+
) {
|
|
270
|
+
// Remove suspense query destructuring
|
|
271
|
+
j(path)
|
|
272
|
+
.find(j.VariableDeclaration)
|
|
273
|
+
.forEach((path) => {
|
|
274
|
+
const declarator = j.VariableDeclarator.check(path.node.declarations[0])
|
|
275
|
+
? path.node.declarations[0]
|
|
276
|
+
: null;
|
|
277
|
+
|
|
278
|
+
if (
|
|
279
|
+
!j.CallExpression.check(declarator?.init) ||
|
|
280
|
+
!j.Identifier.check(declarator.init.callee) ||
|
|
281
|
+
!['useSuspenseQuery', 'useSuspenseInfiniteQuery'].includes(
|
|
282
|
+
declarator.init.callee.name,
|
|
283
|
+
)
|
|
284
|
+
) {
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
console.log(declarator.init.callee.name);
|
|
289
|
+
|
|
290
|
+
const tuple = j.ArrayPattern.check(declarator?.id)
|
|
291
|
+
? declarator.id
|
|
292
|
+
: null;
|
|
293
|
+
const dataName = j.Identifier.check(tuple?.elements?.[0])
|
|
294
|
+
? tuple.elements[0].name
|
|
295
|
+
: null;
|
|
296
|
+
const queryName = j.Identifier.check(tuple?.elements?.[1])
|
|
297
|
+
? tuple.elements[1].name
|
|
298
|
+
: null;
|
|
299
|
+
|
|
300
|
+
if (declarator && queryName) {
|
|
301
|
+
declarator.id = j.identifier(queryName);
|
|
302
|
+
dirtyFlag = true;
|
|
303
|
+
|
|
304
|
+
if (dataName) {
|
|
305
|
+
j(path).insertAfter(
|
|
306
|
+
j.variableDeclaration('const', [
|
|
307
|
+
j.variableDeclarator(
|
|
308
|
+
j.identifier(dataName),
|
|
309
|
+
j.memberExpression(declarator.id, j.identifier('data')),
|
|
310
|
+
),
|
|
311
|
+
]),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return dirtyFlag ? root.toSource() : undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export const parser = 'tsx';
|
|
322
|
+
|
|
323
|
+
// https://go.codemod.com/ddX54TM
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { API, FileInfo, Options } from 'jscodeshift';
|
|
2
|
+
|
|
3
|
+
interface TransformOptions extends Options {
|
|
4
|
+
trpcFile?: string;
|
|
5
|
+
trpcImportName?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default function transform(
|
|
9
|
+
file: FileInfo,
|
|
10
|
+
api: API,
|
|
11
|
+
options: TransformOptions,
|
|
12
|
+
) {
|
|
13
|
+
const { trpcFile, trpcImportName, appRouterImportFile, appRouterImportName } =
|
|
14
|
+
options;
|
|
15
|
+
|
|
16
|
+
const j = api.jscodeshift;
|
|
17
|
+
const root = j(file.source);
|
|
18
|
+
let dirtyFlag = false;
|
|
19
|
+
|
|
20
|
+
function upsertAppRouterImport() {
|
|
21
|
+
if (
|
|
22
|
+
root
|
|
23
|
+
.find(j.ImportDeclaration, { source: { value: appRouterImportFile } })
|
|
24
|
+
.size() === 0
|
|
25
|
+
) {
|
|
26
|
+
root
|
|
27
|
+
.find(j.ImportDeclaration)
|
|
28
|
+
.at(-1)
|
|
29
|
+
.insertAfter(
|
|
30
|
+
j.importDeclaration(
|
|
31
|
+
[j.importSpecifier(j.identifier(appRouterImportName))],
|
|
32
|
+
j.literal(appRouterImportFile),
|
|
33
|
+
'type',
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Find the variable declaration for `trpc`
|
|
40
|
+
root.find(j.VariableDeclaration).forEach((path) => {
|
|
41
|
+
const declaration = path.node.declarations[0];
|
|
42
|
+
if (
|
|
43
|
+
j.Identifier.check(declaration.id) &&
|
|
44
|
+
declaration.id.name === trpcImportName
|
|
45
|
+
) {
|
|
46
|
+
if (
|
|
47
|
+
j.CallExpression.check(declaration.init) &&
|
|
48
|
+
j.Identifier.check(declaration.init.callee) &&
|
|
49
|
+
declaration.init.callee.name === 'createTRPCReact'
|
|
50
|
+
) {
|
|
51
|
+
// Replace the `createTRPCReact` call with `createTRPCContext`
|
|
52
|
+
declaration.init.callee.name = 'createTRPCContext';
|
|
53
|
+
|
|
54
|
+
// Destructure the result into `TRPCProvider` and `useTRPC`
|
|
55
|
+
declaration.id = j.objectPattern([
|
|
56
|
+
j.property.from({
|
|
57
|
+
kind: 'init',
|
|
58
|
+
key: j.identifier('TRPCProvider'),
|
|
59
|
+
value: j.identifier('TRPCProvider'),
|
|
60
|
+
shorthand: true,
|
|
61
|
+
}),
|
|
62
|
+
j.property.from({
|
|
63
|
+
kind: 'init',
|
|
64
|
+
key: j.identifier('useTRPC'),
|
|
65
|
+
value: j.identifier('useTRPC'),
|
|
66
|
+
shorthand: true,
|
|
67
|
+
}),
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
dirtyFlag = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// Update the import statement if transformation was successful
|
|
76
|
+
if (dirtyFlag) {
|
|
77
|
+
root
|
|
78
|
+
.find(j.ImportDeclaration, { source: { value: '@trpc/react-query' } })
|
|
79
|
+
.forEach((path) => {
|
|
80
|
+
path.node.source.value = '@trpc/tanstack-react-query';
|
|
81
|
+
path.node.specifiers?.forEach((specifier) => {
|
|
82
|
+
if (
|
|
83
|
+
j.ImportSpecifier.check(specifier) &&
|
|
84
|
+
specifier.imported.name === 'createTRPCReact'
|
|
85
|
+
) {
|
|
86
|
+
specifier.imported.name = 'createTRPCContext';
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Replace trpc.createClient with createTRPCClient
|
|
93
|
+
root
|
|
94
|
+
.find(j.CallExpression, {
|
|
95
|
+
callee: {
|
|
96
|
+
object: { name: trpcImportName },
|
|
97
|
+
property: { name: 'createClient' },
|
|
98
|
+
},
|
|
99
|
+
})
|
|
100
|
+
.forEach((path) => {
|
|
101
|
+
path.node.callee = j.identifier('createTRPCClient');
|
|
102
|
+
// Add the type parameter `<AppRouter>`
|
|
103
|
+
path.node.typeParameters = j.tsTypeParameterInstantiation([
|
|
104
|
+
j.tsTypeReference(j.identifier(appRouterImportName)),
|
|
105
|
+
]);
|
|
106
|
+
upsertAppRouterImport();
|
|
107
|
+
dirtyFlag = true;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Replace <trpc.Provider client={...} with <TRPCProvider trpcClient={...}
|
|
111
|
+
root
|
|
112
|
+
.find(j.JSXElement, {
|
|
113
|
+
openingElement: {
|
|
114
|
+
name: {
|
|
115
|
+
object: { name: trpcImportName },
|
|
116
|
+
property: { name: 'Provider' },
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
})
|
|
120
|
+
.forEach((path) => {
|
|
121
|
+
path.node.openingElement.name = j.jsxIdentifier('TRPCProvider');
|
|
122
|
+
if (path.node.closingElement) {
|
|
123
|
+
path.node.closingElement.name = j.jsxIdentifier('TRPCProvider');
|
|
124
|
+
}
|
|
125
|
+
path.node.openingElement.attributes?.forEach((attr) => {
|
|
126
|
+
if (j.JSXAttribute.check(attr) && attr.name.name === 'client') {
|
|
127
|
+
attr.name.name = 'trpcClient';
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
dirtyFlag = true;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Update imports if transformations were applied
|
|
134
|
+
if (dirtyFlag) {
|
|
135
|
+
// Add createTRPCClient to the import from '@trpc/client'
|
|
136
|
+
root
|
|
137
|
+
.find(j.ImportDeclaration, {
|
|
138
|
+
source: { value: '@trpc/client' },
|
|
139
|
+
})
|
|
140
|
+
.forEach((path) => {
|
|
141
|
+
const createTRPCClientImport = j.importSpecifier(
|
|
142
|
+
j.identifier('createTRPCClient'),
|
|
143
|
+
);
|
|
144
|
+
path.node.specifiers?.push(createTRPCClientImport);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Replace trpc import with TRPCProvider
|
|
148
|
+
root
|
|
149
|
+
.find(j.ImportDeclaration, {
|
|
150
|
+
source: { value: trpcFile },
|
|
151
|
+
})
|
|
152
|
+
.forEach((path) => {
|
|
153
|
+
path.node.specifiers = [
|
|
154
|
+
j.importSpecifier(j.identifier('TRPCProvider')),
|
|
155
|
+
];
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return dirtyFlag ? root.toSource() : undefined;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const parser = 'tsx';
|