@trpc/upgrade 0.0.0-alpha.2 → 0.0.0-alpha.26

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/README.md CHANGED
@@ -2,6 +2,9 @@ run locally with source files
2
2
 
3
3
  ```sh
4
4
  DEV=1 pnpx tsx path/to/cli.ts
5
+
6
+ # example
7
+ cd examples/minimal-react/client && DEV=1 pnpx tsx ../../../packages/upgrade/src/bin/cli.ts --force --skipTanstackQuery --verbose
5
8
  ```
6
9
 
7
10
  or compiled
package/dist/cli.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- var path = require('path');
2
+ var path = require('node:path');
3
3
  var cli$1 = require('@effect/cli');
4
4
  var platform = require('@effect/platform');
5
5
  var platformNode = require('@effect/platform-node');
@@ -10,22 +10,33 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
10
 
11
11
  var path__default = /*#__PURE__*/_interopDefault(path);
12
12
 
13
- var version = "0.0.0-alpha.2";
13
+ var version = "0.0.0-alpha.25";
14
14
 
15
- const assertCleanGitTree = platform.Command.string(platform.Command.make('git', 'status')).pipe(effect.Effect.filterOrFail(effect.String.includes('nothing to commit'), ()=>'Git tree is not clean, please commit your changes and try again, or run with `--force`'));
15
+ const MakeCommand = (command, ...args)=>{
16
+ return platform.Command.make(command, ...args).pipe(platform.Command.workingDirectory(process.cwd()), platform.Command.runInShell(true));
17
+ };
18
+ const assertCleanGitTree = platform.Command.string(MakeCommand('git', 'status')).pipe().pipe(effect.Effect.filterOrFail(effect.String.includes('nothing to commit'), ()=>'Git tree is not clean, please commit your changes and try again, or run with `--force`'));
19
+ const getPackageManager = ()=>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'));
16
20
  const installPackage = (packageName)=>{
17
- 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'));
18
- return platform.Command.streamLines(platform.Command.make(packageManager, 'install', packageName)).pipe(effect.Stream.mapEffect(effect.Console.log), effect.Stream.runDrain);
21
+ const packageManager = getPackageManager();
22
+ return platform.Command.streamLines(MakeCommand(packageManager, 'install', packageName)).pipe(effect.Stream.mapEffect(effect.Console.log), effect.Stream.runDrain);
23
+ };
24
+ const uninstallPackage = (packageName)=>{
25
+ const packageManager = getPackageManager();
26
+ const uninstallCmd = packageManager === 'yarn' ? 'remove' : 'uninstall';
27
+ return platform.Command.streamLines(MakeCommand(packageManager, uninstallCmd, packageName)).pipe(effect.Stream.mapEffect(effect.Console.log), effect.Stream.runDrain);
19
28
  };
20
29
  const filterIgnored = (files)=>effect.Effect.gen(function*() {
21
- const ignores = yield* platform.Command.string(platform.Command.make('git', 'check-ignore', '**/*')).pipe(effect.Effect.map((_)=>_.split('\n')));
22
- yield* effect.Effect.log('All files in program:', files.map((_)=>_.fileName));
23
- yield* effect.Effect.log('Ignored files:', ignores);
30
+ const ignores = yield* platform.Command.string(MakeCommand('git', 'check-ignore', '**/*').pipe(platform.Command.runInShell(true))).pipe(effect.Effect.tap((_)=>effect.Effect.logDebug('Ignored files output:', _)), effect.Effect.map((_)=>_.split('\n')));
31
+ yield* effect.Effect.logDebug('cwd:', process.cwd());
32
+ yield* effect.Effect.logDebug('All files in program:', files.map((_)=>_.fileName));
33
+ yield* effect.Effect.logDebug('Ignored files:', ignores);
24
34
  // Ignore "common files"
25
35
  const filteredSourcePaths = files.filter((source)=>source.fileName.startsWith(path__default.default.resolve()) && // only look ahead of current directory
26
36
  !source.fileName.includes('/trpc/packages/') && // relative paths when running codemod locally
37
+ !source.fileName.includes('/node_modules/') && // always ignore node_modules
27
38
  !ignores.includes(source.fileName)).map((source)=>source.fileName);
28
- yield* effect.Effect.log('Filtered files:', filteredSourcePaths);
39
+ yield* effect.Effect.logDebug('Filtered files:', filteredSourcePaths);
29
40
  return filteredSourcePaths;
30
41
  });
31
42
  const TSProgram = 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({
@@ -40,15 +51,22 @@ const force = cli$1.Options.boolean('force').pipe(cli$1.Options.withAlias('f'),
40
51
  * TODO: Instead of default values these should be detected automatically from the TS program
41
52
  */ const trpcFile = cli$1.Options.text('trpcFile').pipe(cli$1.Options.withAlias('f'), cli$1.Options.withDefault('~/trpc'), cli$1.Options.withDescription('Path to the trpc import file'));
42
53
  const trpcImportName = cli$1.Options.text('trpcImportName').pipe(cli$1.Options.withAlias('i'), cli$1.Options.withDefault('trpc'), cli$1.Options.withDescription('Name of the trpc import'));
54
+ const skipTanstackQuery = cli$1.Options.boolean('skipTanstackQuery').pipe(cli$1.Options.withAlias('q'), cli$1.Options.withDefault(false), cli$1.Options.withDescription('Skip installing @trpc/tanstack-react-query package'));
55
+ const verbose = cli$1.Options.boolean('verbose').pipe(cli$1.Options.withAlias('v'), cli$1.Options.withDefault(false), cli$1.Options.withDescription('Enable verbose logging'));
43
56
  const rootComamnd = cli$1.Command.make('upgrade', {
44
57
  force,
45
58
  trpcFile,
46
- trpcImportName
59
+ trpcImportName,
60
+ skipTanstackQuery,
61
+ verbose
47
62
  }, (args)=>effect.Effect.gen(function*() {
63
+ if (args.verbose) {
64
+ yield* effect.Effect.log('Running upgrade with args:', args);
65
+ }
48
66
  if (!args.force) {
49
67
  yield* assertCleanGitTree;
50
68
  }
51
- const transforms = yield* effect.Effect.map(cli$1.Prompt.multiSelect({
69
+ const transforms = yield* effect.pipe(cli$1.Prompt.multiSelect({
52
70
  message: 'Select transforms to run',
53
71
  choices: [
54
72
  {
@@ -60,20 +78,26 @@ const rootComamnd = cli$1.Command.make('upgrade', {
60
78
  value: require.resolve(transformPath('../transforms/provider.ts'))
61
79
  }
62
80
  ]
63
- }), // Make sure provider transform runs first if it's selected
64
- effect.Array.sortWith((a)=>!a.includes('provider.ts'), effect.Order.boolean));
81
+ }), effect.Effect.flatMap((selected)=>{
82
+ if (selected.length === 0) {
83
+ return effect.Effect.fail(new Error('Please select at least one transform to run'));
84
+ }
85
+ return effect.Effect.succeed(selected);
86
+ }), effect.Effect.map(// Make sure provider transform runs first if it's selected
87
+ effect.Array.sortWith((a)=>!a.includes('provider.ts'), effect.Order.boolean)));
65
88
  const program = yield* TSProgram;
66
89
  const sourceFiles = program.getSourceFiles();
67
90
  const commitedFiles = yield* filterIgnored(sourceFiles);
68
91
  yield* effect.Effect.forEach(transforms, (transform)=>{
69
- 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, commitedFiles, {
70
- ...args,
71
- verbose: true
72
- })))), effect.Effect.map((_)=>effect.Effect.log('Transform result', _)));
92
+ 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, commitedFiles, args)))), effect.Effect.map((_)=>effect.Effect.log('Transform result', _)));
73
93
  });
74
- yield* effect.Effect.log('Installing @trpc/tanstack-react-query');
75
- yield* installPackage('@trpc/tanstack-react-query');
76
- }));
94
+ if (!args.skipTanstackQuery) {
95
+ yield* effect.Effect.log('Installing @trpc/tanstack-react-query');
96
+ yield* installPackage('@trpc/tanstack-react-query');
97
+ yield* effect.Effect.log('Uninstalling @trpc/react-query');
98
+ yield* uninstallPackage('@trpc/react-query');
99
+ }
100
+ }).pipe(effect.Logger.withMinimumLogLevel(args.verbose ? effect.LogLevel.Debug : effect.LogLevel.Info)));
77
101
  const cli = cli$1.Command.run(rootComamnd, {
78
102
  name: 'tRPC Upgrade CLI',
79
103
  version: `v${version}`
@@ -1,6 +1,41 @@
1
1
  Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
- /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable no-console */ const hookToOptions = {
3
+ /**
4
+ * Replaces the identifier for the root path key
5
+ * of a member expression
6
+ *
7
+ * For dot notation like `rootKey.x.y.z` the AST
8
+ * is constructed with the `rootKey` being nested deep
9
+ * inside a wrapper MemberExpression holding `rootKey.x`
10
+ * and so on
11
+ *
12
+ * This function helps replace the `rootKey` identifier with
13
+ * the provided identifier node
14
+ */ function replaceMemberExpressionRootIndentifier(j, expr, id) {
15
+ if (j.Identifier.check(expr.object)) {
16
+ expr.object = id;
17
+ return true;
18
+ }
19
+ if (j.MemberExpression.check(expr.object)) {
20
+ return replaceMemberExpressionRootIndentifier(j, expr.object, id);
21
+ }
22
+ return false;
23
+ }
24
+
25
+ /**
26
+ * Walks the path upwards to look for the closest parent
27
+ * of the mentioned type
28
+ */ function findParentOfType(path, type) {
29
+ if (!type.check(path.node)) {
30
+ return findParentOfType(path.parentPath, type);
31
+ }
32
+ if (!path.parent) {
33
+ return false;
34
+ }
35
+ return path;
36
+ }
37
+
38
+ const hookToOptions = {
4
39
  useQuery: {
5
40
  lib: '@tanstack/react-query',
6
41
  fn: 'queryOptions'
@@ -155,23 +190,36 @@ function transform(file, api, options) {
155
190
  name: oldIdentifier.name
156
191
  }).forEach((path)=>{
157
192
  if (j.MemberExpression.check(path.parent?.parent?.node)) {
158
- const callExprPath = path.parent.parent.parent;
193
+ const callExprPath = findParentOfType(path.parentPath, j.CallExpression);
194
+ if (!callExprPath) {
195
+ console.warn(`Failed to walk up the tree to find utilMethod call expression, on file: ${file.path}`, callExprPath, {
196
+ start: path.node.loc?.start,
197
+ end: path.node.loc?.end
198
+ });
199
+ return;
200
+ }
159
201
  const callExpr = callExprPath.node;
160
202
  const memberExpr = callExpr.callee;
161
203
  if (!j.CallExpression.check(callExpr) || !j.MemberExpression.check(memberExpr)) {
162
- console.warn('Failed to walk up the tree to find utilMethod call expression', callExpr);
204
+ console.warn(`Failed to walk up the tree to find utilMethod with a \`trpc.PATH.<call>\`, on file: ${file.path}`, callExpr, {
205
+ start: path.node.loc?.start,
206
+ end: path.node.loc?.end
207
+ });
163
208
  return;
164
209
  }
165
- if (!(j.MemberExpression.check(memberExpr.object) && j.Identifier.check(memberExpr.object.object) && j.Identifier.check(memberExpr.property) && memberExpr.property.name in utilMap)) {
210
+ if (!(j.MemberExpression.check(memberExpr.object) && j.Identifier.check(memberExpr.property) && memberExpr.property.name in utilMap)) {
166
211
  console.warn('Failed to identify utilMethod from proxy call expression', memberExpr);
167
212
  return;
168
213
  }
169
214
  // Replace util.PATH.proxyMethod() with trpc.PATH.queryFilter()
170
215
  const proxyMethod = memberExpr.property.name;
171
- memberExpr.object.object = j.identifier(trpcImportName);
216
+ const replacedPath = replaceMemberExpressionRootIndentifier(j, memberExpr, j.identifier(trpcImportName));
217
+ if (!replacedPath) {
218
+ console.warn('Failed to wrap proxy call expression', memberExpr);
219
+ }
172
220
  memberExpr.property = j.identifier('queryFilter');
173
221
  // Wrap it in queryClient.utilMethod()
174
- j(callExprPath).replaceWith(j.memberExpression(j.identifier('queryClient'), j.callExpression(j.identifier(utilMap[proxyMethod]), [
222
+ callExprPath.replace(j.memberExpression(j.identifier('queryClient'), j.callExpression(j.identifier(utilMap[proxyMethod]), [
175
223
  callExpr
176
224
  ])));
177
225
  }
@@ -2,6 +2,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
3
  function transform(file, api, options) {
4
4
  const { trpcImportName } = options;
5
+ let routerName = undefined;
5
6
  const j = api.jscodeshift;
6
7
  const root = j(file.source);
7
8
  let dirtyFlag = false;
@@ -10,6 +11,8 @@ function transform(file, api, options) {
10
11
  const declaration = path.node.declarations[0];
11
12
  if (j.Identifier.check(declaration.id) && declaration.id.name === trpcImportName) {
12
13
  if (j.CallExpression.check(declaration.init) && j.Identifier.check(declaration.init.callee) && declaration.init.callee.name === 'createTRPCReact') {
14
+ // Get router name ( TODO : should probably get this from the TS compiler along with the import path)
15
+ routerName = declaration.init.original?.typeParameters?.params?.[0]?.typeName?.name;
13
16
  // Replace the `createTRPCReact` call with `createTRPCContext`
14
17
  declaration.init.callee.name = 'createTRPCContext';
15
18
  // Destructure the result into `TRPCProvider` and `useTRPC`
@@ -46,7 +49,7 @@ function transform(file, api, options) {
46
49
  });
47
50
  });
48
51
  }
49
- // Replace trpc.createClient with createTRPCClient
52
+ // Replace trpc.createClient with createTRPCClient<TRouter>
50
53
  root.find(j.CallExpression, {
51
54
  callee: {
52
55
  object: {
@@ -59,6 +62,11 @@ function transform(file, api, options) {
59
62
  }).forEach((path)=>{
60
63
  path.node.callee = j.identifier('createTRPCClient');
61
64
  dirtyFlag = true;
65
+ if (routerName) {
66
+ path.node.typeParameters = j.tsTypeParameterInstantiation([
67
+ j.tsTypeReference(j.identifier(routerName))
68
+ ]);
69
+ }
62
70
  });
63
71
  // Replace <trpc.Provider client={...} with <TRPCProvider trpcClient={...}
64
72
  root.find(j.JSXElement, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trpc/upgrade",
3
- "version": "0.0.0-alpha.2",
3
+ "version": "0.0.0-alpha.26",
4
4
  "description": "Upgrade scripts for tRPC",
5
5
  "author": "juliusmarminge",
6
6
  "license": "MIT",
@@ -28,10 +28,10 @@
28
28
  "!**/__tests__"
29
29
  ],
30
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",
31
+ "@effect/cli": "0.55.0",
32
+ "@effect/platform": "0.76.0",
33
+ "@effect/platform-node": "0.72.0",
34
+ "effect": "3.12.11",
35
35
  "jscodeshift": "17.1.1",
36
36
  "typescript": "^5.6.2"
37
37
  },
package/src/bin/cli.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable @typescript-eslint/unbound-method */
2
- import path from 'path';
2
+ import path from 'node:path';
3
3
  import { Command as CLICommand, Options, Prompt } from '@effect/cli';
4
4
  import { Command } from '@effect/platform';
5
5
  import { NodeContext, NodeRuntime } from '@effect/platform-node';
@@ -7,6 +7,8 @@ import {
7
7
  Array,
8
8
  Console,
9
9
  Effect,
10
+ Logger,
11
+ LogLevel,
10
12
  Match,
11
13
  Order,
12
14
  pipe,
@@ -24,39 +26,61 @@ import {
24
26
  } from 'typescript';
25
27
  import { version } from '../../package.json';
26
28
 
27
- const assertCleanGitTree = Command.string(Command.make('git', 'status')).pipe(
28
- Effect.filterOrFail(
29
- String.includes('nothing to commit'),
30
- () =>
31
- 'Git tree is not clean, please commit your changes and try again, or run with `--force`',
32
- ),
33
- );
29
+ const MakeCommand = (command: string, ...args: string[]) => {
30
+ return Command.make(command, ...args).pipe(
31
+ Command.workingDirectory(process.cwd()),
32
+ Command.runInShell(true),
33
+ );
34
+ };
34
35
 
35
- const installPackage = (packageName: string) => {
36
- const packageManager = Match.value(
37
- process.env.npm_config_user_agent ?? 'npm',
38
- ).pipe(
36
+ const assertCleanGitTree = Command.string(MakeCommand('git', 'status'))
37
+ .pipe()
38
+ .pipe(
39
+ Effect.filterOrFail(
40
+ String.includes('nothing to commit'),
41
+ () =>
42
+ 'Git tree is not clean, please commit your changes and try again, or run with `--force`',
43
+ ),
44
+ );
45
+ const getPackageManager = () =>
46
+ Match.value(process.env.npm_config_user_agent ?? 'npm').pipe(
39
47
  Match.when(String.startsWith('pnpm'), () => 'pnpm'),
40
48
  Match.when(String.startsWith('yarn'), () => 'yarn'),
41
49
  Match.when(String.startsWith('bun'), () => 'bun'),
42
50
  Match.orElse(() => 'npm'),
43
51
  );
52
+
53
+ const installPackage = (packageName: string) => {
54
+ const packageManager = getPackageManager();
55
+ return Command.streamLines(
56
+ MakeCommand(packageManager, 'install', packageName),
57
+ ).pipe(Stream.mapEffect(Console.log), Stream.runDrain);
58
+ };
59
+
60
+ const uninstallPackage = (packageName: string) => {
61
+ const packageManager = getPackageManager();
62
+ const uninstallCmd = packageManager === 'yarn' ? 'remove' : 'uninstall';
44
63
  return Command.streamLines(
45
- Command.make(packageManager, 'install', packageName),
64
+ MakeCommand(packageManager, uninstallCmd, packageName),
46
65
  ).pipe(Stream.mapEffect(Console.log), Stream.runDrain);
47
66
  };
48
67
 
49
68
  const filterIgnored = (files: readonly SourceFile[]) =>
50
69
  Effect.gen(function* () {
51
70
  const ignores = yield* Command.string(
52
- Command.make('git', 'check-ignore', '**/*'),
53
- ).pipe(Effect.map((_) => _.split('\n')));
71
+ MakeCommand('git', 'check-ignore', '**/*').pipe(Command.runInShell(true)),
72
+ ).pipe(
73
+ Effect.tap((_) => Effect.logDebug('Ignored files output:', _)),
74
+ Effect.map((_) => _.split('\n')),
75
+ );
54
76
 
55
- yield* Effect.log(
77
+ yield* Effect.logDebug('cwd:', process.cwd());
78
+
79
+ yield* Effect.logDebug(
56
80
  'All files in program:',
57
81
  files.map((_) => _.fileName),
58
82
  );
59
- yield* Effect.log('Ignored files:', ignores);
83
+ yield* Effect.logDebug('Ignored files:', ignores);
60
84
 
61
85
  // Ignore "common files"
62
86
  const filteredSourcePaths = files
@@ -64,11 +88,12 @@ const filterIgnored = (files: readonly SourceFile[]) =>
64
88
  (source) =>
65
89
  source.fileName.startsWith(path.resolve()) && // only look ahead of current directory
66
90
  !source.fileName.includes('/trpc/packages/') && // relative paths when running codemod locally
91
+ !source.fileName.includes('/node_modules/') && // always ignore node_modules
67
92
  !ignores.includes(source.fileName), // ignored files
68
93
  )
69
94
  .map((source) => source.fileName);
70
95
 
71
- yield* Effect.log('Filtered files:', filteredSourcePaths);
96
+ yield* Effect.logDebug('Filtered files:', filteredSourcePaths);
72
97
 
73
98
  return filteredSourcePaths;
74
99
  });
@@ -114,20 +139,36 @@ const trpcImportName = Options.text('trpcImportName').pipe(
114
139
  Options.withDescription('Name of the trpc import'),
115
140
  );
116
141
 
142
+ const skipTanstackQuery = Options.boolean('skipTanstackQuery').pipe(
143
+ Options.withAlias('q'),
144
+ Options.withDefault(false),
145
+ Options.withDescription('Skip installing @trpc/tanstack-react-query package'),
146
+ );
147
+
148
+ const verbose = Options.boolean('verbose').pipe(
149
+ Options.withAlias('v'),
150
+ Options.withDefault(false),
151
+ Options.withDescription('Enable verbose logging'),
152
+ );
153
+
117
154
  const rootComamnd = CLICommand.make(
118
155
  'upgrade',
119
156
  {
120
157
  force,
121
158
  trpcFile,
122
159
  trpcImportName,
160
+ skipTanstackQuery,
161
+ verbose,
123
162
  },
124
163
  (args) =>
125
164
  Effect.gen(function* () {
165
+ if (args.verbose) {
166
+ yield* Effect.log('Running upgrade with args:', args);
167
+ }
126
168
  if (!args.force) {
127
169
  yield* assertCleanGitTree;
128
170
  }
129
-
130
- const transforms = yield* Effect.map(
171
+ const transforms = yield* pipe(
131
172
  Prompt.multiSelect({
132
173
  message: 'Select transforms to run',
133
174
  choices: [
@@ -145,8 +186,18 @@ const rootComamnd = CLICommand.make(
145
186
  },
146
187
  ],
147
188
  }),
148
- // Make sure provider transform runs first if it's selected
149
- Array.sortWith((a) => !a.includes('provider.ts'), Order.boolean),
189
+ Effect.flatMap((selected) => {
190
+ if (selected.length === 0) {
191
+ return Effect.fail(
192
+ new Error('Please select at least one transform to run'),
193
+ );
194
+ }
195
+ return Effect.succeed(selected);
196
+ }),
197
+ Effect.map(
198
+ // Make sure provider transform runs first if it's selected
199
+ Array.sortWith((a) => !a.includes('provider.ts'), Order.boolean),
200
+ ),
150
201
  );
151
202
 
152
203
  const program = yield* TSProgram;
@@ -159,7 +210,7 @@ const rootComamnd = CLICommand.make(
159
210
  Effect.flatMap(() =>
160
211
  Effect.tryPromise(async () =>
161
212
  import('jscodeshift/src/Runner.js').then(({ run }) =>
162
- run(transform, commitedFiles, { ...args, verbose: true }),
213
+ run(transform, commitedFiles, args),
163
214
  ),
164
215
  ),
165
216
  ),
@@ -167,9 +218,16 @@ const rootComamnd = CLICommand.make(
167
218
  );
168
219
  });
169
220
 
170
- yield* Effect.log('Installing @trpc/tanstack-react-query');
171
- yield* installPackage('@trpc/tanstack-react-query');
172
- }),
221
+ if (!args.skipTanstackQuery) {
222
+ yield* Effect.log('Installing @trpc/tanstack-react-query');
223
+ yield* installPackage('@trpc/tanstack-react-query');
224
+
225
+ yield* Effect.log('Uninstalling @trpc/react-query');
226
+ yield* uninstallPackage('@trpc/react-query');
227
+ }
228
+ }).pipe(
229
+ Logger.withMinimumLogLevel(args.verbose ? LogLevel.Debug : LogLevel.Info),
230
+ ),
173
231
  );
174
232
 
175
233
  const cli = CLICommand.run(rootComamnd, {
@@ -0,0 +1,28 @@
1
+ import type { Identifier, JSCodeshift, MemberExpression } from 'jscodeshift';
2
+
3
+ /**
4
+ * Replaces the identifier for the root path key
5
+ * of a member expression
6
+ *
7
+ * For dot notation like `rootKey.x.y.z` the AST
8
+ * is constructed with the `rootKey` being nested deep
9
+ * inside a wrapper MemberExpression holding `rootKey.x`
10
+ * and so on
11
+ *
12
+ * This function helps replace the `rootKey` identifier with
13
+ * the provided identifier node
14
+ */
15
+ export function replaceMemberExpressionRootIndentifier(
16
+ j: JSCodeshift,
17
+ expr: MemberExpression,
18
+ id: Identifier,
19
+ ) {
20
+ if (j.Identifier.check(expr.object)) {
21
+ expr.object = id;
22
+ return true;
23
+ }
24
+ if (j.MemberExpression.check(expr.object)) {
25
+ return replaceMemberExpressionRootIndentifier(j, expr.object, id);
26
+ }
27
+ return false;
28
+ }
@@ -0,0 +1,18 @@
1
+ import type { ASTPath, JSCodeshift } from 'jscodeshift';
2
+
3
+ /**
4
+ * Walks the path upwards to look for the closest parent
5
+ * of the mentioned type
6
+ */
7
+ export function findParentOfType<TPath>(
8
+ path: ASTPath<unknown>,
9
+ type: JSCodeshift['AnyType'],
10
+ ): ASTPath<unknown> | false {
11
+ if (!type.check(path.node)) {
12
+ return findParentOfType(path.parentPath, type);
13
+ }
14
+ if (!path.parent) {
15
+ return false;
16
+ }
17
+ return path as ASTPath<TPath>;
18
+ }
@@ -11,6 +11,8 @@ import type {
11
11
  MemberExpression,
12
12
  Options,
13
13
  } from 'jscodeshift';
14
+ import { replaceMemberExpressionRootIndentifier } from '../lib/ast/modifiers';
15
+ import { findParentOfType } from '../lib/ast/walkers';
14
16
 
15
17
  interface TransformOptions extends Options {
16
18
  trpcImportName?: string;
@@ -205,7 +207,18 @@ export default function transform(
205
207
  .find(j.Identifier, { name: oldIdentifier.name })
206
208
  .forEach((path) => {
207
209
  if (j.MemberExpression.check(path.parent?.parent?.node)) {
208
- const callExprPath = path.parent.parent.parent;
210
+ const callExprPath = findParentOfType<CallExpression>(
211
+ path.parentPath,
212
+ j.CallExpression,
213
+ );
214
+ if (!callExprPath) {
215
+ console.warn(
216
+ `Failed to walk up the tree to find utilMethod call expression, on file: ${file.path}`,
217
+ callExprPath,
218
+ { start: path.node.loc?.start, end: path.node.loc?.end },
219
+ );
220
+ return;
221
+ }
209
222
  const callExpr = callExprPath.node as CallExpression;
210
223
  const memberExpr = callExpr.callee as MemberExpression;
211
224
  if (
@@ -213,8 +226,9 @@ export default function transform(
213
226
  !j.MemberExpression.check(memberExpr)
214
227
  ) {
215
228
  console.warn(
216
- 'Failed to walk up the tree to find utilMethod call expression',
229
+ `Failed to walk up the tree to find utilMethod with a \`trpc.PATH.<call>\`, on file: ${file.path}`,
217
230
  callExpr,
231
+ { start: path.node.loc?.start, end: path.node.loc?.end },
218
232
  );
219
233
  return;
220
234
  }
@@ -222,7 +236,6 @@ export default function transform(
222
236
  if (
223
237
  !(
224
238
  j.MemberExpression.check(memberExpr.object) &&
225
- j.Identifier.check(memberExpr.object.object) &&
226
239
  j.Identifier.check(memberExpr.property) &&
227
240
  memberExpr.property.name in utilMap
228
241
  )
@@ -236,11 +249,21 @@ export default function transform(
236
249
 
237
250
  // Replace util.PATH.proxyMethod() with trpc.PATH.queryFilter()
238
251
  const proxyMethod = memberExpr.property.name as ProxyMethod;
239
- memberExpr.object.object = j.identifier(trpcImportName!);
252
+ const replacedPath = replaceMemberExpressionRootIndentifier(
253
+ j,
254
+ memberExpr,
255
+ j.identifier(trpcImportName!),
256
+ );
257
+ if (!replacedPath) {
258
+ console.warn(
259
+ 'Failed to wrap proxy call expression',
260
+ memberExpr,
261
+ );
262
+ }
240
263
  memberExpr.property = j.identifier('queryFilter');
241
264
 
242
265
  // Wrap it in queryClient.utilMethod()
243
- j(callExprPath).replaceWith(
266
+ callExprPath.replace(
244
267
  j.memberExpression(
245
268
  j.identifier('queryClient'),
246
269
  j.callExpression(j.identifier(utilMap[proxyMethod]), [
@@ -10,6 +10,7 @@ export default function transform(
10
10
  options: TransformOptions,
11
11
  ) {
12
12
  const { trpcImportName } = options;
13
+ let routerName: string | undefined = undefined;
13
14
 
14
15
  const j = api.jscodeshift;
15
16
  const root = j(file.source);
@@ -27,6 +28,11 @@ export default function transform(
27
28
  j.Identifier.check(declaration.init.callee) &&
28
29
  declaration.init.callee.name === 'createTRPCReact'
29
30
  ) {
31
+ // Get router name ( TODO : should probably get this from the TS compiler along with the import path)
32
+ routerName =
33
+ declaration.init.original?.typeParameters?.params?.[0]?.typeName
34
+ ?.name;
35
+
30
36
  // Replace the `createTRPCReact` call with `createTRPCContext`
31
37
  declaration.init.callee.name = 'createTRPCContext';
32
38
 
@@ -68,7 +74,7 @@ export default function transform(
68
74
  });
69
75
  }
70
76
 
71
- // Replace trpc.createClient with createTRPCClient
77
+ // Replace trpc.createClient with createTRPCClient<TRouter>
72
78
  root
73
79
  .find(j.CallExpression, {
74
80
  callee: {
@@ -79,6 +85,12 @@ export default function transform(
79
85
  .forEach((path) => {
80
86
  path.node.callee = j.identifier('createTRPCClient');
81
87
  dirtyFlag = true;
88
+
89
+ if (routerName) {
90
+ (path.node as any).typeParameters = j.tsTypeParameterInstantiation([
91
+ j.tsTypeReference(j.identifier(routerName)),
92
+ ]);
93
+ }
82
94
  });
83
95
 
84
96
  // Replace <trpc.Provider client={...} with <TRPCProvider trpcClient={...}