@trpc/upgrade 0.0.0-alpha.1 → 0.0.0-alpha.10

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
@@ -10,68 +10,94 @@ 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.0";
13
+ var version = "0.0.0-alpha.10";
14
14
 
15
- 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'));
15
+ const MakeCommand = (command, ...args)=>{
16
+ console.log('MakeCommand', command, args, process.cwd());
17
+ return platform.Command.workingDirectory(process.cwd())(platform.Command.make(command, ...args));
18
+ };
19
+ 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`'));
20
+ 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
21
  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);
22
+ const packageManager = getPackageManager();
23
+ return platform.Command.streamLines(MakeCommand(packageManager, 'install', packageName)).pipe(effect.Stream.mapEffect(effect.Console.log), effect.Stream.runDrain);
24
+ };
25
+ const uninstallPackage = (packageName)=>{
26
+ const packageManager = getPackageManager();
27
+ const uninstallCmd = packageManager === 'yarn' ? 'remove' : 'uninstall';
28
+ return platform.Command.streamLines(MakeCommand(packageManager, uninstallCmd, packageName)).pipe(effect.Stream.mapEffect(effect.Console.log), effect.Stream.runDrain);
19
29
  };
20
30
  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);
31
+ const ignores = yield* platform.Command.string(MakeCommand('git', 'check-ignore', '**/*')).pipe(effect.Effect.tap(effect.Effect.log), effect.Effect.map((_)=>_.split('\n')));
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
27
37
  !ignores.includes(source.fileName)).map((source)=>source.fileName);
28
- yield* effect.Effect.log('Filtered files:', filteredSourcePaths);
38
+ yield* effect.Effect.logDebug('Filtered files:', filteredSourcePaths);
29
39
  return filteredSourcePaths;
30
40
  });
31
- 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({
41
+ 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({
32
42
  options: _.options,
33
43
  rootNames: _.fileNames,
34
44
  configFileParsingDiagnostics: _.errors
35
45
  })));
46
+ // FIXME :: hacky
36
47
  const transformPath = (path)=>process.env.DEV ? path : path.replace('../', './').replace('.ts', '.cjs');
37
- const prompts = cli$1.Command.prompt('transforms', cli$1.Prompt.multiSelect({
38
- message: 'Select transforms to run',
39
- choices: [
40
- {
41
- title: 'Migrate Hooks to xxxOptions API',
42
- value: require.resolve(transformPath('../transforms/hooksToOptions.ts'))
43
- },
44
- {
45
- title: 'Migrate context provider setup',
46
- value: require.resolve(transformPath('../transforms/provider.ts'))
48
+ const force = cli$1.Options.boolean('force').pipe(cli$1.Options.withAlias('f'), cli$1.Options.withDefault(false), cli$1.Options.withDescription('Skip git status check, use with caution'));
49
+ /**
50
+ * TODO: Instead of default values these should be detected automatically from the TS program
51
+ */ 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'));
52
+ 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'));
53
+ 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'));
54
+ 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'));
55
+ const rootComamnd = cli$1.Command.make('upgrade', {
56
+ force,
57
+ trpcFile,
58
+ trpcImportName,
59
+ skipTanstackQuery,
60
+ verbose
61
+ }, (args)=>effect.Effect.gen(function*() {
62
+ if (args.verbose) {
63
+ yield* effect.Effect.log('Running upgrade with args:', args);
47
64
  }
48
- ]
49
- }), (_)=>effect.Effect.gen(function*() {
50
- // yield* assertCleanGitTree;
51
- const program = yield* Program;
65
+ if (!args.force) {
66
+ yield* assertCleanGitTree;
67
+ }
68
+ const transforms = yield* effect.pipe(cli$1.Prompt.multiSelect({
69
+ message: 'Select transforms to run',
70
+ choices: [
71
+ {
72
+ title: 'Migrate Hooks to xxxOptions API',
73
+ value: require.resolve(transformPath('../transforms/hooksToOptions.ts'))
74
+ },
75
+ {
76
+ title: 'Migrate context provider setup',
77
+ value: require.resolve(transformPath('../transforms/provider.ts'))
78
+ }
79
+ ]
80
+ }), effect.Effect.flatMap((selected)=>{
81
+ if (selected.length === 0) {
82
+ return effect.Effect.fail(new Error('Please select at least one transform to run'));
83
+ }
84
+ return effect.Effect.succeed(selected);
85
+ }), effect.Effect.map(// Make sure provider transform runs first if it's selected
86
+ effect.Array.sortWith((a)=>!a.includes('provider.ts'), effect.Order.boolean)));
87
+ const program = yield* TSProgram;
52
88
  const sourceFiles = program.getSourceFiles();
53
- // Make sure provider transform runs first if it's selected
54
- _.sort((a, b)=>a.includes('provider.ts') ? -1 : b.includes('provider.ts') ? 1 : 0);
55
- /**
56
- * TODO: Detect these automatically
57
- */ const appRouterImportFile = '~/server/routers/_app';
58
- const appRouterImportName = 'AppRouter';
59
- const trpcFile = '~/lib/trpc';
60
- const trpcImportName = 'trpc';
61
89
  const commitedFiles = yield* filterIgnored(sourceFiles);
62
- yield* effect.Effect.forEach(_, (transform)=>{
63
- 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, {
64
- appRouterImportFile,
65
- appRouterImportName,
66
- trpcFile,
67
- trpcImportName
68
- })))), effect.Effect.map((res)=>effect.Effect.log('Transform result', res)));
90
+ yield* effect.Effect.forEach(transforms, (transform)=>{
91
+ 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', _)));
69
92
  });
70
- yield* effect.Effect.log('Installing @trpc/tanstack-react-query');
71
- yield* installPackage('@trpc/tanstack-react-query');
72
- // TODO: Format project
73
- }));
74
- const cli = cli$1.Command.run(prompts, {
93
+ if (!args.skipTanstackQuery) {
94
+ yield* effect.Effect.log('Installing @trpc/tanstack-react-query');
95
+ yield* installPackage('@trpc/tanstack-react-query');
96
+ yield* effect.Effect.log('Uninstalling @trpc/react-query');
97
+ yield* uninstallPackage('@trpc/react-query');
98
+ }
99
+ }).pipe(effect.Logger.withMinimumLogLevel(args.verbose ? effect.LogLevel.Debug : effect.LogLevel.Info)));
100
+ const cli = cli$1.Command.run(rootComamnd, {
75
101
  name: 'tRPC Upgrade CLI',
76
102
  version: `v${version}`
77
103
  });
@@ -43,59 +43,50 @@ const utilMap = {
43
43
  getInfiniteData: 'getInfiniteQueryData'
44
44
  };
45
45
  function transform(file, api, options) {
46
- const { trpcFile, trpcImportName } = options;
47
- if (!trpcFile || !trpcImportName) {
48
- throw new Error('trpcFile and trpcImportName are required');
46
+ const { trpcImportName } = options;
47
+ if (!trpcImportName) {
48
+ throw new Error('trpcImportName is required');
49
49
  }
50
50
  const j = api.jscodeshift;
51
51
  const root = j(file.source);
52
52
  let dirtyFlag = false;
53
53
  // Traverse all functions, and _do stuff_
54
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
55
  replaceHooksWithOptions(path);
61
56
  removeSuspenseDestructuring(path);
62
57
  migrateUseUtils(path);
63
58
  });
64
59
  root.find(j.ArrowFunctionExpression).forEach((path)=>{
65
- if (j(path).find(j.Identifier, {
66
- name: trpcImportName
67
- }).size() > 0) {
68
- updateTRPCImport(path);
69
- }
70
60
  replaceHooksWithOptions(path);
71
61
  removeSuspenseDestructuring(path);
72
62
  migrateUseUtils(path);
73
63
  });
64
+ if (dirtyFlag) {
65
+ updateTRPCImport();
66
+ }
74
67
  /**
75
68
  * === 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;
69
+ */ function ensureUseTRPCCall(path) {
91
70
  const variableDeclaration = j.variableDeclaration('const', [
92
71
  j.variableDeclarator(j.identifier(trpcImportName), j.callExpression(j.identifier('useTRPC'), []))
93
72
  ]);
94
73
  if (j.FunctionDeclaration.check(path.node)) {
95
- const body = path.node.body.body;
96
- body.unshift(variableDeclaration);
74
+ path.node.body.body.unshift(variableDeclaration);
75
+ dirtyFlag = true;
97
76
  } else if (j.BlockStatement.check(path.node.body)) {
98
77
  path.node.body.body.unshift(variableDeclaration);
78
+ dirtyFlag = true;
79
+ }
80
+ }
81
+ function updateTRPCImport() {
82
+ const specifier = root.find(j.ImportSpecifier, {
83
+ imported: {
84
+ name: trpcImportName
85
+ }
86
+ });
87
+ if (specifier.size() > 0) {
88
+ specifier.replaceWith(j.importSpecifier(j.identifier('useTRPC')));
89
+ dirtyFlag = true;
99
90
  }
100
91
  }
101
92
  function ensureImported(lib, specifier) {
@@ -114,10 +105,11 @@ function transform(file, api, options) {
114
105
  dirtyFlag = true;
115
106
  }
116
107
  }
117
- function replaceHooksWithOptions(path) {
108
+ function replaceHooksWithOptions(fnPath) {
118
109
  // REplace proxy-hooks with useX(options())
110
+ let hasInserted = false;
119
111
  for (const [hook, { fn, lib }] of Object.entries(hookToOptions)){
120
- j(path).find(j.CallExpression, {
112
+ j(fnPath).find(j.CallExpression, {
121
113
  callee: {
122
114
  property: {
123
115
  name: hook
@@ -131,6 +123,10 @@ function transform(file, api, options) {
131
123
  }
132
124
  // Rename the hook to the options function
133
125
  memberExpr.property.name = fn;
126
+ if (!hasInserted) {
127
+ ensureUseTRPCCall(fnPath);
128
+ hasInserted = true;
129
+ }
134
130
  // Wrap it in the hook call
135
131
  j(path).replaceWith(j.callExpression(j.identifier(hook), [
136
132
  path.node
@@ -172,7 +168,7 @@ function transform(file, api, options) {
172
168
  }
173
169
  // Replace util.PATH.proxyMethod() with trpc.PATH.queryFilter()
174
170
  const proxyMethod = memberExpr.property.name;
175
- memberExpr.object.object = j.identifier('trpc');
171
+ memberExpr.object.object = j.identifier(trpcImportName);
176
172
  memberExpr.property = j.identifier('queryFilter');
177
173
  // Wrap it in queryClient.utilMethod()
178
174
  j(callExprPath).replaceWith(j.memberExpression(j.identifier('queryClient'), j.callExpression(j.identifier(utilMap[proxyMethod]), [
@@ -198,11 +194,10 @@ function transform(file, api, options) {
198
194
  ].includes(declarator.init.callee.name)) {
199
195
  return;
200
196
  }
201
- console.log(declarator.init.callee.name);
202
197
  const tuple = j.ArrayPattern.check(declarator?.id) ? declarator.id : null;
203
198
  const dataName = j.Identifier.check(tuple?.elements?.[0]) ? tuple.elements[0].name : null;
204
199
  const queryName = j.Identifier.check(tuple?.elements?.[1]) ? tuple.elements[1].name : null;
205
- if (declarator && queryName) {
200
+ if (queryName) {
206
201
  declarator.id = j.identifier(queryName);
207
202
  dirtyFlag = true;
208
203
  if (dataName) {
@@ -210,12 +205,18 @@ function transform(file, api, options) {
210
205
  j.variableDeclarator(j.identifier(dataName), j.memberExpression(declarator.id, j.identifier('data')))
211
206
  ]));
212
207
  }
208
+ } else if (dataName) {
209
+ // const [dataName] = ... => const { data: dataName } = ...
210
+ declarator.id = j.objectPattern([
211
+ j.property('init', j.identifier('data'), j.identifier(dataName))
212
+ ]);
213
+ dirtyFlag = true;
213
214
  }
214
215
  });
215
216
  }
216
217
  return dirtyFlag ? root.toSource() : undefined;
217
218
  }
218
- const parser = 'tsx'; // https://go.codemod.com/ddX54TM
219
+ const parser = 'tsx';
219
220
 
220
221
  exports.default = transform;
221
222
  exports.parser = parser;
@@ -1,26 +1,18 @@
1
1
  Object.defineProperty(exports, '__esModule', { value: true });
2
2
 
3
3
  function transform(file, api, options) {
4
- const { trpcFile, trpcImportName, appRouterImportFile, appRouterImportName } = options;
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;
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
9
  // Find the variable declaration for `trpc`
20
10
  root.find(j.VariableDeclaration).forEach((path)=>{
21
11
  const declaration = path.node.declarations[0];
22
12
  if (j.Identifier.check(declaration.id) && declaration.id.name === trpcImportName) {
23
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;
24
16
  // Replace the `createTRPCReact` call with `createTRPCContext`
25
17
  declaration.init.callee.name = 'createTRPCContext';
26
18
  // Destructure the result into `TRPCProvider` and `useTRPC`
@@ -57,7 +49,7 @@ function transform(file, api, options) {
57
49
  });
58
50
  });
59
51
  }
60
- // Replace trpc.createClient with createTRPCClient
52
+ // Replace trpc.createClient with createTRPCClient<TRouter>
61
53
  root.find(j.CallExpression, {
62
54
  callee: {
63
55
  object: {
@@ -69,12 +61,12 @@ function transform(file, api, options) {
69
61
  }
70
62
  }).forEach((path)=>{
71
63
  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
64
  dirtyFlag = true;
65
+ if (routerName) {
66
+ path.node.typeParameters = j.tsTypeParameterInstantiation([
67
+ j.tsTypeReference(j.identifier(routerName))
68
+ ]);
69
+ }
78
70
  });
79
71
  // Replace <trpc.Provider client={...} with <TRPCProvider trpcClient={...}
80
72
  root.find(j.JSXElement, {
@@ -112,14 +104,12 @@ function transform(file, api, options) {
112
104
  path.node.specifiers?.push(createTRPCClientImport);
113
105
  });
114
106
  // Replace trpc import with TRPCProvider
115
- root.find(j.ImportDeclaration, {
116
- source: {
117
- value: trpcFile
107
+ root.find(j.ImportSpecifier, {
108
+ imported: {
109
+ name: trpcImportName
118
110
  }
119
111
  }).forEach((path)=>{
120
- path.node.specifiers = [
121
- j.importSpecifier(j.identifier('TRPCProvider'))
122
- ];
112
+ path.node.name = j.identifier('TRPCProvider');
123
113
  });
124
114
  }
125
115
  return dirtyFlag ? root.toSource() : undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trpc/upgrade",
3
- "version": "0.0.0-alpha.1",
3
+ "version": "0.0.0-alpha.10",
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.54.4",
32
+ "@effect/platform": "0.75.4",
33
+ "@effect/platform-node": "0.71.4",
34
+ "effect": "3.12.10",
35
35
  "jscodeshift": "17.1.1",
36
36
  "typescript": "^5.6.2"
37
37
  },
package/src/bin/cli.ts CHANGED
@@ -1,12 +1,16 @@
1
1
  /* eslint-disable @typescript-eslint/unbound-method */
2
2
  import path from 'path';
3
- import { Command as CLICommand, Prompt } from '@effect/cli';
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';
6
6
  import {
7
+ Array,
7
8
  Console,
8
9
  Effect,
10
+ Logger,
11
+ LogLevel,
9
12
  Match,
13
+ Order,
10
14
  pipe,
11
15
  Predicate,
12
16
  Stream,
@@ -22,39 +26,59 @@ import {
22
26
  } from 'typescript';
23
27
  import { version } from '../../package.json';
24
28
 
25
- const assertCleanGitTree = Command.string(Command.make('git', 'status')).pipe(
26
- Effect.filterOrFail(
27
- (status) => status.includes('nothing to commit'),
28
- () =>
29
- 'Git tree is not clean, please commit your changes before running the migrator',
30
- ),
31
- );
29
+ const MakeCommand = (command: string, ...args: string[]) => {
30
+ console.log('MakeCommand', command, args, process.cwd());
31
+ return Command.workingDirectory(process.cwd())(
32
+ Command.make(command, ...args),
33
+ );
34
+ };
32
35
 
33
- const installPackage = (packageName: string) => {
34
- const packageManager = Match.value(
35
- process.env.npm_config_user_agent ?? 'npm',
36
- ).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(
37
47
  Match.when(String.startsWith('pnpm'), () => 'pnpm'),
38
48
  Match.when(String.startsWith('yarn'), () => 'yarn'),
39
49
  Match.when(String.startsWith('bun'), () => 'bun'),
40
50
  Match.orElse(() => 'npm'),
41
51
  );
52
+
53
+ const installPackage = (packageName: string) => {
54
+ const packageManager = getPackageManager();
42
55
  return Command.streamLines(
43
- Command.make(packageManager, 'install', packageName),
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';
63
+ return Command.streamLines(
64
+ MakeCommand(packageManager, uninstallCmd, packageName),
44
65
  ).pipe(Stream.mapEffect(Console.log), Stream.runDrain);
45
66
  };
46
67
 
47
68
  const filterIgnored = (files: readonly SourceFile[]) =>
48
69
  Effect.gen(function* () {
49
70
  const ignores = yield* Command.string(
50
- Command.make('git', 'check-ignore', '**/*'),
51
- ).pipe(Effect.map((_) => _.split('\n')));
71
+ MakeCommand('git', 'check-ignore', '**/*'),
72
+ ).pipe(
73
+ Effect.tap(Effect.log),
74
+ Effect.map((_) => _.split('\n')),
75
+ );
52
76
 
53
- yield* Effect.log(
77
+ yield* Effect.logDebug(
54
78
  'All files in program:',
55
79
  files.map((_) => _.fileName),
56
80
  );
57
- yield* Effect.log('Ignored files:', ignores);
81
+ yield* Effect.logDebug('Ignored files:', ignores);
58
82
 
59
83
  // Ignore "common files"
60
84
  const filteredSourcePaths = files
@@ -66,12 +90,12 @@ const filterIgnored = (files: readonly SourceFile[]) =>
66
90
  )
67
91
  .map((source) => source.fileName);
68
92
 
69
- yield* Effect.log('Filtered files:', filteredSourcePaths);
93
+ yield* Effect.logDebug('Filtered files:', filteredSourcePaths);
70
94
 
71
95
  return filteredSourcePaths;
72
96
  });
73
97
 
74
- const Program = Effect.succeed(
98
+ const TSProgram = Effect.succeed(
75
99
  findConfigFile(process.cwd(), sys.fileExists),
76
100
  ).pipe(
77
101
  Effect.filterOrFail(Predicate.isNotNullable, () => 'No tsconfig found'),
@@ -87,73 +111,123 @@ const Program = Effect.succeed(
87
111
  ),
88
112
  );
89
113
 
114
+ // FIXME :: hacky
90
115
  const transformPath = (path: string) =>
91
116
  process.env.DEV ? path : path.replace('../', './').replace('.ts', '.cjs');
92
117
 
93
- const prompts = CLICommand.prompt(
94
- 'transforms',
95
- Prompt.multiSelect({
96
- message: 'Select transforms to run',
97
- choices: [
98
- {
99
- title: 'Migrate Hooks to xxxOptions API',
100
- value: require.resolve(
101
- transformPath('../transforms/hooksToOptions.ts'),
102
- ),
103
- },
104
- {
105
- title: 'Migrate context provider setup',
106
- value: require.resolve(transformPath('../transforms/provider.ts')),
107
- },
108
- ],
109
- }),
110
- (_) =>
111
- Effect.gen(function* () {
112
- // yield* assertCleanGitTree;
113
- const program = yield* Program;
114
- const sourceFiles = program.getSourceFiles();
118
+ const force = Options.boolean('force').pipe(
119
+ Options.withAlias('f'),
120
+ Options.withDefault(false),
121
+ Options.withDescription('Skip git status check, use with caution'),
122
+ );
123
+
124
+ /**
125
+ * TODO: Instead of default values these should be detected automatically from the TS program
126
+ */
127
+ const trpcFile = Options.text('trpcFile').pipe(
128
+ Options.withAlias('f'),
129
+ Options.withDefault('~/trpc'),
130
+ Options.withDescription('Path to the trpc import file'),
131
+ );
132
+
133
+ const trpcImportName = Options.text('trpcImportName').pipe(
134
+ Options.withAlias('i'),
135
+ Options.withDefault('trpc'),
136
+ Options.withDescription('Name of the trpc import'),
137
+ );
138
+
139
+ const skipTanstackQuery = Options.boolean('skipTanstackQuery').pipe(
140
+ Options.withAlias('q'),
141
+ Options.withDefault(false),
142
+ Options.withDescription('Skip installing @trpc/tanstack-react-query package'),
143
+ );
115
144
 
116
- // Make sure provider transform runs first if it's selected
117
- _.sort((a, b) =>
118
- a.includes('provider.ts') ? -1 : b.includes('provider.ts') ? 1 : 0,
145
+ const verbose = Options.boolean('verbose').pipe(
146
+ Options.withAlias('v'),
147
+ Options.withDefault(false),
148
+ Options.withDescription('Enable verbose logging'),
149
+ );
150
+
151
+ const rootComamnd = CLICommand.make(
152
+ 'upgrade',
153
+ {
154
+ force,
155
+ trpcFile,
156
+ trpcImportName,
157
+ skipTanstackQuery,
158
+ verbose,
159
+ },
160
+ (args) =>
161
+ Effect.gen(function* () {
162
+ if (args.verbose) {
163
+ yield* Effect.log('Running upgrade with args:', args);
164
+ }
165
+ if (!args.force) {
166
+ yield* assertCleanGitTree;
167
+ }
168
+ const transforms = yield* pipe(
169
+ Prompt.multiSelect({
170
+ message: 'Select transforms to run',
171
+ choices: [
172
+ {
173
+ title: 'Migrate Hooks to xxxOptions API',
174
+ value: require.resolve(
175
+ transformPath('../transforms/hooksToOptions.ts'),
176
+ ),
177
+ },
178
+ {
179
+ title: 'Migrate context provider setup',
180
+ value: require.resolve(
181
+ transformPath('../transforms/provider.ts'),
182
+ ),
183
+ },
184
+ ],
185
+ }),
186
+ Effect.flatMap((selected) => {
187
+ if (selected.length === 0) {
188
+ return Effect.fail(
189
+ new Error('Please select at least one transform to run'),
190
+ );
191
+ }
192
+ return Effect.succeed(selected);
193
+ }),
194
+ Effect.map(
195
+ // Make sure provider transform runs first if it's selected
196
+ Array.sortWith((a) => !a.includes('provider.ts'), Order.boolean),
197
+ ),
119
198
  );
120
199
 
121
- /**
122
- * TODO: Detect these automatically
123
- */
124
- const appRouterImportFile = '~/server/routers/_app';
125
- const appRouterImportName = 'AppRouter';
126
- const trpcFile = '~/lib/trpc';
127
- const trpcImportName = 'trpc';
200
+ const program = yield* TSProgram;
201
+ const sourceFiles = program.getSourceFiles();
128
202
 
129
203
  const commitedFiles = yield* filterIgnored(sourceFiles);
130
- yield* Effect.forEach(_, (transform) => {
204
+ yield* Effect.forEach(transforms, (transform) => {
131
205
  return pipe(
132
206
  Effect.log('Running transform', transform),
133
207
  Effect.flatMap(() =>
134
208
  Effect.tryPromise(async () =>
135
209
  import('jscodeshift/src/Runner.js').then(({ run }) =>
136
- run(transform.replace('file://', ''), commitedFiles, {
137
- appRouterImportFile,
138
- appRouterImportName,
139
- trpcFile,
140
- trpcImportName,
141
- }),
210
+ run(transform, commitedFiles, args),
142
211
  ),
143
212
  ),
144
213
  ),
145
- Effect.map((res) => Effect.log('Transform result', res)),
214
+ Effect.map((_) => Effect.log('Transform result', _)),
146
215
  );
147
216
  });
148
217
 
149
- yield* Effect.log('Installing @trpc/tanstack-react-query');
150
- yield* installPackage('@trpc/tanstack-react-query');
218
+ if (!args.skipTanstackQuery) {
219
+ yield* Effect.log('Installing @trpc/tanstack-react-query');
220
+ yield* installPackage('@trpc/tanstack-react-query');
151
221
 
152
- // TODO: Format project
153
- }),
222
+ yield* Effect.log('Uninstalling @trpc/react-query');
223
+ yield* uninstallPackage('@trpc/react-query');
224
+ }
225
+ }).pipe(
226
+ Logger.withMinimumLogLevel(args.verbose ? LogLevel.Debug : LogLevel.Info),
227
+ ),
154
228
  );
155
229
 
156
- const cli = CLICommand.run(prompts, {
230
+ const cli = CLICommand.run(rootComamnd, {
157
231
  name: 'tRPC Upgrade CLI',
158
232
  version: `v${version}`,
159
233
  });
@@ -13,7 +13,6 @@ import type {
13
13
  } from 'jscodeshift';
14
14
 
15
15
  interface TransformOptions extends Options {
16
- trpcFile?: string;
17
16
  trpcImportName?: string;
18
17
  }
19
18
 
@@ -61,9 +60,9 @@ export default function transform(
61
60
  api: API,
62
61
  options: TransformOptions,
63
62
  ) {
64
- const { trpcFile, trpcImportName } = options;
65
- if (!trpcFile || !trpcImportName) {
66
- throw new Error('trpcFile and trpcImportName are required');
63
+ const { trpcImportName } = options;
64
+ if (!trpcImportName) {
65
+ throw new Error('trpcImportName is required');
67
66
  }
68
67
 
69
68
  const j = api.jscodeshift;
@@ -72,44 +71,27 @@ export default function transform(
72
71
 
73
72
  // Traverse all functions, and _do stuff_
74
73
  root.find(j.FunctionDeclaration).forEach((path) => {
75
- if (j(path).find(j.Identifier, { name: trpcImportName }).size() > 0) {
76
- updateTRPCImport(path);
77
- }
78
-
79
74
  replaceHooksWithOptions(path);
80
75
  removeSuspenseDestructuring(path);
81
76
  migrateUseUtils(path);
82
77
  });
83
78
  root.find(j.ArrowFunctionExpression).forEach((path) => {
84
- if (j(path).find(j.Identifier, { name: trpcImportName }).size() > 0) {
85
- updateTRPCImport(path);
86
- }
87
-
88
79
  replaceHooksWithOptions(path);
89
80
  removeSuspenseDestructuring(path);
90
81
  migrateUseUtils(path);
91
82
  });
92
83
 
84
+ if (dirtyFlag) {
85
+ updateTRPCImport();
86
+ }
87
+
93
88
  /**
94
89
  * === HELPER FUNCTIONS BELOW ===
95
90
  */
96
91
 
97
- function updateTRPCImport(
92
+ function ensureUseTRPCCall(
98
93
  path: ASTPath<FunctionDeclaration | ArrowFunctionExpression>,
99
94
  ) {
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
95
  const variableDeclaration = j.variableDeclaration('const', [
114
96
  j.variableDeclarator(
115
97
  j.identifier(trpcImportName!),
@@ -118,10 +100,21 @@ export default function transform(
118
100
  ]);
119
101
 
120
102
  if (j.FunctionDeclaration.check(path.node)) {
121
- const body = path.node.body.body;
122
- body.unshift(variableDeclaration);
103
+ path.node.body.body.unshift(variableDeclaration);
104
+ dirtyFlag = true;
123
105
  } else if (j.BlockStatement.check(path.node.body)) {
124
106
  path.node.body.body.unshift(variableDeclaration);
107
+ dirtyFlag = true;
108
+ }
109
+ }
110
+
111
+ function updateTRPCImport() {
112
+ const specifier = root.find(j.ImportSpecifier, {
113
+ imported: { name: trpcImportName },
114
+ });
115
+ if (specifier.size() > 0) {
116
+ specifier.replaceWith(j.importSpecifier(j.identifier('useTRPC')));
117
+ dirtyFlag = true;
125
118
  }
126
119
  }
127
120
 
@@ -148,11 +141,12 @@ export default function transform(
148
141
  }
149
142
 
150
143
  function replaceHooksWithOptions(
151
- path: ASTPath<FunctionDeclaration | ArrowFunctionExpression>,
144
+ fnPath: ASTPath<FunctionDeclaration | ArrowFunctionExpression>,
152
145
  ) {
153
146
  // REplace proxy-hooks with useX(options())
147
+ let hasInserted = false;
154
148
  for (const [hook, { fn, lib }] of Object.entries(hookToOptions)) {
155
- j(path)
149
+ j(fnPath)
156
150
  .find(j.CallExpression, {
157
151
  callee: {
158
152
  property: { name: hook },
@@ -171,6 +165,11 @@ export default function transform(
171
165
  // Rename the hook to the options function
172
166
  memberExpr.property.name = fn;
173
167
 
168
+ if (!hasInserted) {
169
+ ensureUseTRPCCall(fnPath);
170
+ hasInserted = true;
171
+ }
172
+
174
173
  // Wrap it in the hook call
175
174
  j(path).replaceWith(
176
175
  j.callExpression(j.identifier(hook), [path.node]),
@@ -237,7 +236,7 @@ export default function transform(
237
236
 
238
237
  // Replace util.PATH.proxyMethod() with trpc.PATH.queryFilter()
239
238
  const proxyMethod = memberExpr.property.name as ProxyMethod;
240
- memberExpr.object.object = j.identifier('trpc');
239
+ memberExpr.object.object = j.identifier(trpcImportName!);
241
240
  memberExpr.property = j.identifier('queryFilter');
242
241
 
243
242
  // Wrap it in queryClient.utilMethod()
@@ -285,8 +284,6 @@ export default function transform(
285
284
  return;
286
285
  }
287
286
 
288
- console.log(declarator.init.callee.name);
289
-
290
287
  const tuple = j.ArrayPattern.check(declarator?.id)
291
288
  ? declarator.id
292
289
  : null;
@@ -297,7 +294,7 @@ export default function transform(
297
294
  ? tuple.elements[1].name
298
295
  : null;
299
296
 
300
- if (declarator && queryName) {
297
+ if (queryName) {
301
298
  declarator.id = j.identifier(queryName);
302
299
  dirtyFlag = true;
303
300
 
@@ -311,6 +308,12 @@ export default function transform(
311
308
  ]),
312
309
  );
313
310
  }
311
+ } else if (dataName) {
312
+ // const [dataName] = ... => const { data: dataName } = ...
313
+ declarator.id = j.objectPattern([
314
+ j.property('init', j.identifier('data'), j.identifier(dataName)),
315
+ ]);
316
+ dirtyFlag = true;
314
317
  }
315
318
  });
316
319
  }
@@ -319,5 +322,3 @@ export default function transform(
319
322
  }
320
323
 
321
324
  export const parser = 'tsx';
322
-
323
- // https://go.codemod.com/ddX54TM
@@ -1,7 +1,6 @@
1
1
  import type { API, FileInfo, Options } from 'jscodeshift';
2
2
 
3
3
  interface TransformOptions extends Options {
4
- trpcFile?: string;
5
4
  trpcImportName?: string;
6
5
  }
7
6
 
@@ -10,32 +9,13 @@ export default function transform(
10
9
  api: API,
11
10
  options: TransformOptions,
12
11
  ) {
13
- const { trpcFile, trpcImportName, appRouterImportFile, appRouterImportName } =
14
- options;
12
+ const { trpcImportName } = options;
13
+ let routerName: string | undefined = undefined;
15
14
 
16
15
  const j = api.jscodeshift;
17
16
  const root = j(file.source);
18
17
  let dirtyFlag = false;
19
18
 
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
19
  // Find the variable declaration for `trpc`
40
20
  root.find(j.VariableDeclaration).forEach((path) => {
41
21
  const declaration = path.node.declarations[0];
@@ -48,6 +28,11 @@ export default function transform(
48
28
  j.Identifier.check(declaration.init.callee) &&
49
29
  declaration.init.callee.name === 'createTRPCReact'
50
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
+
51
36
  // Replace the `createTRPCReact` call with `createTRPCContext`
52
37
  declaration.init.callee.name = 'createTRPCContext';
53
38
 
@@ -89,7 +74,7 @@ export default function transform(
89
74
  });
90
75
  }
91
76
 
92
- // Replace trpc.createClient with createTRPCClient
77
+ // Replace trpc.createClient with createTRPCClient<TRouter>
93
78
  root
94
79
  .find(j.CallExpression, {
95
80
  callee: {
@@ -99,12 +84,13 @@ export default function transform(
99
84
  })
100
85
  .forEach((path) => {
101
86
  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
87
  dirtyFlag = true;
88
+
89
+ if (routerName) {
90
+ (path.node as any).typeParameters = j.tsTypeParameterInstantiation([
91
+ j.tsTypeReference(j.identifier(routerName)),
92
+ ]);
93
+ }
108
94
  });
109
95
 
110
96
  // Replace <trpc.Provider client={...} with <TRPCProvider trpcClient={...}
@@ -146,13 +132,11 @@ export default function transform(
146
132
 
147
133
  // Replace trpc import with TRPCProvider
148
134
  root
149
- .find(j.ImportDeclaration, {
150
- source: { value: trpcFile },
135
+ .find(j.ImportSpecifier, {
136
+ imported: { name: trpcImportName },
151
137
  })
152
138
  .forEach((path) => {
153
- path.node.specifiers = [
154
- j.importSpecifier(j.identifier('TRPCProvider')),
155
- ];
139
+ path.node.name = j.identifier('TRPCProvider');
156
140
  });
157
141
  }
158
142