@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 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
@@ -0,0 +1,15 @@
1
+ run locally with source files
2
+
3
+ ```sh
4
+ DEV=1 pnpx tsx path/to/cli.ts
5
+ ```
6
+
7
+ or compiled
8
+
9
+ ```sh
10
+ cd packages/upgrade
11
+ pnpm build && pnpm link .
12
+
13
+ # in an app somewhere
14
+ pnpm link @trpc/upgrade && pnpm trpc-upgrade
15
+ ```
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';