@trpc/upgrade 0.0.0-alpha.3 → 0.0.0-alpha.30

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/bin.cjs ADDED
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ var args = require('@bomb.sh/args');
3
+ var p = require('@clack/prompts');
4
+ var ts = require('typescript');
5
+ var Path = require('path');
6
+ var CP = require('node:child_process');
7
+ var Util = require('node:util');
8
+
9
+ function _interopNamespace(e) {
10
+ if (e && e.__esModule) return e;
11
+ var n = Object.create(null);
12
+ if (e) {
13
+ Object.keys(e).forEach(function (k) {
14
+ if (k !== 'default') {
15
+ var d = Object.getOwnPropertyDescriptor(e, k);
16
+ Object.defineProperty(n, k, d.get ? d : {
17
+ enumerable: true,
18
+ get: function () { return e[k]; }
19
+ });
20
+ }
21
+ });
22
+ }
23
+ n.default = e;
24
+ return n;
25
+ }
26
+
27
+ var p__namespace = /*#__PURE__*/_interopNamespace(p);
28
+ var ts__namespace = /*#__PURE__*/_interopNamespace(ts);
29
+ var Path__namespace = /*#__PURE__*/_interopNamespace(Path);
30
+ var CP__namespace = /*#__PURE__*/_interopNamespace(CP);
31
+ var Util__namespace = /*#__PURE__*/_interopNamespace(Util);
32
+
33
+ var version = "0.0.0-alpha.29";
34
+
35
+ function getProgram(args) {
36
+ const configFile = ts__namespace.findConfigFile(process.cwd(), (filepath)=>ts__namespace.sys.fileExists(filepath));
37
+ if (!configFile) {
38
+ p__namespace.log.error('No tsconfig found');
39
+ process.exit(1);
40
+ }
41
+ if (args.verbose) {
42
+ p__namespace.log.info(`Using tsconfig: ${configFile}`);
43
+ }
44
+ const { config } = ts__namespace.readConfigFile(configFile, (filepath)=>ts__namespace.sys.readFile(filepath));
45
+ const parsedConfig = ts__namespace.parseJsonConfigFileContent(config, ts__namespace.sys, process.cwd());
46
+ const program = ts__namespace.createProgram({
47
+ options: parsedConfig.options,
48
+ rootNames: parsedConfig.fileNames,
49
+ configFileParsingDiagnostics: parsedConfig.errors
50
+ });
51
+ return program;
52
+ }
53
+ function findSourceAndImportName(program) {
54
+ const files = program.getSourceFiles().filter((sourceFile)=>{
55
+ if (sourceFile.isDeclarationFile) return false;
56
+ let found = false;
57
+ ts__namespace.forEachChild(sourceFile, (node)=>{
58
+ if (!found && ts__namespace.isImportDeclaration(node)) {
59
+ const { moduleSpecifier } = node;
60
+ if (ts__namespace.isStringLiteral(moduleSpecifier) && moduleSpecifier.text.includes('@trpc/react-query')) {
61
+ found = true;
62
+ }
63
+ }
64
+ });
65
+ return found;
66
+ });
67
+ let importName = 'trpc';
68
+ files.forEach((sourceFile)=>{
69
+ ts__namespace.forEachChild(sourceFile, (node)=>{
70
+ if (ts__namespace.isVariableStatement(node) && node.modifiers?.some((mod)=>mod.getText(sourceFile) === 'export')) {
71
+ node.declarationList.declarations.forEach((declaration)=>{
72
+ if (ts__namespace.isVariableDeclaration(declaration) && declaration.initializer && ts__namespace.isCallExpression(declaration.initializer) && ts__namespace.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.getText(sourceFile) === 'createTRPCReact') {
73
+ importName = declaration.name.getText(sourceFile);
74
+ }
75
+ });
76
+ }
77
+ });
78
+ });
79
+ return {
80
+ files: files.map((d)=>d.fileName),
81
+ importName
82
+ };
83
+ }
84
+ function findTRPCImportReferences(program) {
85
+ const { files: filesImportingTRPC, importName } = findSourceAndImportName(program);
86
+ const trpcReferenceSpecifiers = new Map();
87
+ program.getSourceFiles().forEach((sourceFile)=>{
88
+ if (sourceFile.isDeclarationFile) return;
89
+ ts__namespace.forEachChild(sourceFile, (node)=>{
90
+ if (ts__namespace.isImportDeclaration(node) && ts__namespace.isStringLiteral(node.moduleSpecifier)) {
91
+ const resolved = ts__namespace.resolveModuleName(node.moduleSpecifier.text, sourceFile.fileName, program.getCompilerOptions(), ts__namespace.sys);
92
+ if (resolved.resolvedModule && filesImportingTRPC.includes(resolved.resolvedModule.resolvedFileName)) {
93
+ trpcReferenceSpecifiers.set(resolved.resolvedModule.resolvedFileName, node.moduleSpecifier.text);
94
+ }
95
+ }
96
+ });
97
+ });
98
+ const counts = {};
99
+ let currentMax = 0;
100
+ const mostUsed = {
101
+ file: ''
102
+ };
103
+ [
104
+ ...trpcReferenceSpecifiers.values()
105
+ ].forEach((specifier)=>{
106
+ counts[specifier] = (counts[specifier] || 0) + 1;
107
+ if (counts[specifier] > currentMax) {
108
+ currentMax = counts[specifier];
109
+ mostUsed.file = specifier;
110
+ }
111
+ });
112
+ return {
113
+ importName,
114
+ mostUsed,
115
+ all: Object.fromEntries(trpcReferenceSpecifiers.entries())
116
+ };
117
+ }
118
+
119
+ const execa = Util__namespace.promisify(CP__namespace.exec);
120
+
121
+ async function assertCleanGitTree() {
122
+ const { stdout } = await execa('git status');
123
+ if (!stdout.includes('nothing to commit')) {
124
+ p__namespace.cancel('Git tree is not clean, please commit your changes and try again, or run with `--force`');
125
+ process.exit(1);
126
+ }
127
+ }
128
+ async function filterIgnored(files) {
129
+ const { stdout } = await execa('git check-ignore **/*');
130
+ const ignores = stdout.split('\n');
131
+ if (process.env.VERBOSE) {
132
+ p__namespace.log.info(`cwd: ${process.cwd()}`);
133
+ p__namespace.log.info(`All files in program: ${files.map((file)=>file.fileName).join(', ')}`);
134
+ p__namespace.log.info(`Ignored files: ${ignores.join(', ')}`);
135
+ }
136
+ // Ignore "common files"
137
+ const filteredSourcePaths = files.filter((source)=>source.fileName.startsWith(Path__namespace.resolve()) && // only look ahead of current directory
138
+ !source.fileName.includes('/trpc/packages/') && // relative paths when running codemod locally
139
+ !source.fileName.includes('/node_modules/') && // always ignore node_modules
140
+ !ignores.includes(source.fileName)).map((source)=>source.fileName);
141
+ if (process.env.VERBOSE) {
142
+ p__namespace.log.info(`Filtered files: ${filteredSourcePaths.join(', ')}`);
143
+ }
144
+ return filteredSourcePaths;
145
+ }
146
+
147
+ function getPackageManager() {
148
+ const userAgent = process.env.npm_config_user_agent;
149
+ if (userAgent?.startsWith('pnpm')) return 'pnpm';
150
+ if (userAgent?.startsWith('yarn')) return 'yarn';
151
+ if (userAgent?.startsWith('bun')) return 'bun';
152
+ return 'npm';
153
+ }
154
+ async function installPackage(packageName) {
155
+ const packageManager = getPackageManager();
156
+ const installCmd = packageManager === 'yarn' ? 'add' : 'install';
157
+ const { stdout, stderr } = await execa(`${packageManager} ${installCmd} ${packageName}`);
158
+ if (stderr) {
159
+ p__namespace.log.error(stderr);
160
+ }
161
+ p__namespace.log.info(stdout);
162
+ }
163
+ async function uninstallPackage(packageName) {
164
+ const packageManager = getPackageManager();
165
+ const uninstallCmd = packageManager === 'yarn' ? 'remove' : 'uninstall';
166
+ const { stdout, stderr } = await execa(`${packageManager} ${uninstallCmd} ${packageName}`);
167
+ if (stderr) {
168
+ p__namespace.log.error(stderr);
169
+ }
170
+ p__namespace.log.info(stdout);
171
+ }
172
+
173
+ void (async ()=>{
174
+ const args$1 = args.parse(process.argv.slice(2), {
175
+ default: {
176
+ force: false,
177
+ skipTanstackQuery: false,
178
+ verbose: false
179
+ },
180
+ alias: {
181
+ f: 'force',
182
+ h: 'help',
183
+ v: 'verbose',
184
+ q: 'skipTanstackQuery'
185
+ },
186
+ boolean: true
187
+ });
188
+ p.intro(`tRPC Upgrade CLI v${version}`);
189
+ if (args$1.help) {
190
+ p.log.info(`
191
+ Usage: upgrade [options]
192
+
193
+ Options:
194
+ -f, --force Skip git status check, use with caution
195
+ -q, --skipTanstackQuery Skip installing @trpc/tanstack-react-query package
196
+ -v, --verbose Enable verbose logging
197
+ -h, --help Show help
198
+ `.trim());
199
+ process.exit(0);
200
+ }
201
+ if (args$1.verbose) {
202
+ p.log.info(`Running upgrade with args: ${JSON.stringify(args$1, null, 2)}`);
203
+ }
204
+ if (!args$1.force) {
205
+ await assertCleanGitTree();
206
+ }
207
+ const transforms = await p.multiselect({
208
+ message: 'Select transforms to run',
209
+ options: [
210
+ {
211
+ value: require.resolve('@trpc/upgrade/transforms/hooksToOptions'),
212
+ label: 'Migrate Hooks to xxxOptions API'
213
+ },
214
+ {
215
+ value: require.resolve('@trpc/upgrade/transforms/provider'),
216
+ label: 'Migrate context provider setup'
217
+ }
218
+ ]
219
+ });
220
+ if (p.isCancel(transforms)) process.exit(0);
221
+ // Make sure provider transform runs first if it's selected
222
+ const sortedTransforms = transforms.sort((a)=>a.includes('provider') ? -1 : 1);
223
+ const program = getProgram(args$1);
224
+ const sourceFiles = program.getSourceFiles();
225
+ const possibleReferences = findTRPCImportReferences(program);
226
+ const trpcFile = possibleReferences.mostUsed.file;
227
+ const trpcImportName = possibleReferences.importName;
228
+ const commitedFiles = await filterIgnored(sourceFiles);
229
+ for (const transform of sortedTransforms){
230
+ p.log.info(`Running transform: ${transform}`);
231
+ const { run } = await import('jscodeshift/src/Runner.js');
232
+ await run(transform, commitedFiles, {
233
+ ...args$1,
234
+ trpcFile,
235
+ trpcImportName
236
+ });
237
+ p.log.info(`Transform ${transform} completed`);
238
+ }
239
+ if (!args$1.skipTanstackQuery) {
240
+ p.log.info('Installing @trpc/tanstack-react-query');
241
+ await installPackage('@trpc/tanstack-react-query');
242
+ p.log.success('@trpc/tanstack-react-query installed');
243
+ p.log.info('Uninstalling @trpc/react-query');
244
+ await uninstallPackage('@trpc/react-query');
245
+ p.log.success('@trpc/react-query uninstalled');
246
+ }
247
+ p.outro('Upgrade complete! 🎉');
248
+ })();
@@ -1,6 +1,35 @@
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
+ return !j.MemberExpression.check(expr.object) ? false : replaceMemberExpressionRootIndentifier(j, expr.object, id);
20
+ }
21
+
22
+ /**
23
+ * Walks the path upwards to look for the closest parent
24
+ * of the mentioned type
25
+ */ function findParentOfType(path, type) {
26
+ if (!path.parent) {
27
+ return false;
28
+ }
29
+ return type.check(path.node) ? path : findParentOfType(path.parentPath, type);
30
+ }
31
+
32
+ const hookToOptions = {
4
33
  useQuery: {
5
34
  lib: '@tanstack/react-query',
6
35
  fn: 'queryOptions'
@@ -148,30 +177,44 @@ function transform(file, api, options) {
148
177
  }
149
178
  }
150
179
  }).forEach((path)=>{
151
- if (j.VariableDeclarator.check(path.parentPath.node) && j.Identifier.check(path.parentPath.node.id)) {
180
+ const isTRPCContextUtil = j.MemberExpression.check(path.value.callee) && j.Identifier.check(path.value.callee.object) && path.value.callee.object.name == trpcImportName;
181
+ if (isTRPCContextUtil && j.VariableDeclarator.check(path.parentPath.node) && j.Identifier.check(path.parentPath.node.id)) {
152
182
  const oldIdentifier = path.parentPath.node.id;
153
183
  // Find all the references to `utils` and replace with `queryClient[helperMap](trpc.PATH.queryFilter())`
154
184
  root.find(j.Identifier, {
155
185
  name: oldIdentifier.name
156
186
  }).forEach((path)=>{
157
187
  if (j.MemberExpression.check(path.parent?.parent?.node)) {
158
- const callExprPath = path.parent.parent.parent;
188
+ const callExprPath = findParentOfType(path.parentPath, j.CallExpression);
189
+ if (!callExprPath) {
190
+ console.warn(`Failed to walk up the tree to find utilMethod call expression, on file: ${file.path}`, callExprPath, {
191
+ start: path.node.loc?.start,
192
+ end: path.node.loc?.end
193
+ });
194
+ return;
195
+ }
159
196
  const callExpr = callExprPath.node;
160
197
  const memberExpr = callExpr.callee;
161
198
  if (!j.CallExpression.check(callExpr) || !j.MemberExpression.check(memberExpr)) {
162
- console.warn('Failed to walk up the tree to find utilMethod call expression', callExpr);
199
+ console.warn(`Failed to walk up the tree to find utilMethod with a \`trpc.PATH.<call>\`, on file: ${file.path}`, callExpr, {
200
+ start: path.node.loc?.start,
201
+ end: path.node.loc?.end
202
+ });
163
203
  return;
164
204
  }
165
- if (!(j.MemberExpression.check(memberExpr.object) && j.Identifier.check(memberExpr.object.object) && j.Identifier.check(memberExpr.property) && memberExpr.property.name in utilMap)) {
205
+ if (!(j.MemberExpression.check(memberExpr.object) && j.Identifier.check(memberExpr.property) && memberExpr.property.name in utilMap)) {
166
206
  console.warn('Failed to identify utilMethod from proxy call expression', memberExpr);
167
207
  return;
168
208
  }
169
209
  // Replace util.PATH.proxyMethod() with trpc.PATH.queryFilter()
170
210
  const proxyMethod = memberExpr.property.name;
171
- memberExpr.object.object = j.identifier(trpcImportName);
211
+ const replacedPath = replaceMemberExpressionRootIndentifier(j, memberExpr, j.identifier(trpcImportName));
212
+ if (!replacedPath) {
213
+ console.warn('Failed to wrap proxy call expression', memberExpr);
214
+ }
172
215
  memberExpr.property = j.identifier('queryFilter');
173
216
  // Wrap it in queryClient.utilMethod()
174
- j(callExprPath).replaceWith(j.memberExpression(j.identifier('queryClient'), j.callExpression(j.identifier(utilMap[proxyMethod]), [
217
+ callExprPath.replace(j.memberExpression(j.identifier('queryClient'), j.callExpression(j.identifier(utilMap[proxyMethod]), [
175
218
  callExpr
176
219
  ])));
177
220
  }
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@trpc/upgrade",
3
- "version": "0.0.0-alpha.3",
3
+ "version": "0.0.0-alpha.30",
4
4
  "description": "Upgrade scripts for tRPC",
5
5
  "author": "juliusmarminge",
6
6
  "license": "MIT",
7
- "bin": "./dist/cli.cjs",
7
+ "bin": "./dist/bin.cjs",
8
8
  "homepage": "https://trpc.io",
9
9
  "repository": {
10
10
  "type": "git",
@@ -13,10 +13,10 @@
13
13
  },
14
14
  "exports": {
15
15
  "./transforms/hooksToOptions": {
16
- "require": "./dist/transforms/hooksToOptions.cjs"
16
+ "default": "./dist/transforms/hooksToOptions.cjs"
17
17
  },
18
18
  "./transforms/provider": {
19
- "require": "./dist/transforms/provider.cjs"
19
+ "default": "./dist/transforms/provider.cjs"
20
20
  }
21
21
  },
22
22
  "files": [
@@ -28,18 +28,16 @@
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
+ "@bomb.sh/args": "0.3.0",
32
+ "@clack/prompts": "0.10.0",
35
33
  "jscodeshift": "17.1.1",
36
34
  "typescript": "^5.6.2"
37
35
  },
38
36
  "devDependencies": {
39
37
  "@types/jscodeshift": "0.12.0",
40
38
  "@types/node": "^22.9.0",
41
- "bunchee": "5.6.1",
42
- "esbuild": "0.19.2",
39
+ "bunchee": "6.4.0",
40
+ "esbuild": "0.25.0",
43
41
  "tsx": "^4.0.0"
44
42
  },
45
43
  "publishConfig": {
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import { parse } from '@bomb.sh/args';
3
+ import { intro, isCancel, log, multiselect, outro } from '@clack/prompts';
4
+ import { version } from '../../package.json';
5
+ import { findTRPCImportReferences, getProgram } from '../lib/ast/scanners';
6
+ import { assertCleanGitTree, filterIgnored } from '../lib/git';
7
+ import { installPackage, uninstallPackage } from '../lib/pkgmgr';
8
+
9
+ void (async () => {
10
+ const args = parse(process.argv.slice(2), {
11
+ default: {
12
+ force: false,
13
+ skipTanstackQuery: false,
14
+ verbose: false,
15
+ },
16
+ alias: {
17
+ f: 'force',
18
+ h: 'help',
19
+ v: 'verbose',
20
+ q: 'skipTanstackQuery',
21
+ },
22
+ boolean: true,
23
+ });
24
+
25
+ intro(`tRPC Upgrade CLI v${version}`);
26
+
27
+ if (args.help) {
28
+ log.info(
29
+ `
30
+ Usage: upgrade [options]
31
+
32
+ Options:
33
+ -f, --force Skip git status check, use with caution
34
+ -q, --skipTanstackQuery Skip installing @trpc/tanstack-react-query package
35
+ -v, --verbose Enable verbose logging
36
+ -h, --help Show help
37
+ `.trim(),
38
+ );
39
+ process.exit(0);
40
+ }
41
+
42
+ if (args.verbose) {
43
+ log.info(`Running upgrade with args: ${JSON.stringify(args, null, 2)}`);
44
+ }
45
+
46
+ if (!args.force) {
47
+ await assertCleanGitTree();
48
+ }
49
+
50
+ const transforms = await multiselect({
51
+ message: 'Select transforms to run',
52
+ options: [
53
+ {
54
+ value: require.resolve('@trpc/upgrade/transforms/hooksToOptions'),
55
+ label: 'Migrate Hooks to xxxOptions API',
56
+ },
57
+ {
58
+ value: require.resolve('@trpc/upgrade/transforms/provider'),
59
+ label: 'Migrate context provider setup',
60
+ },
61
+ ],
62
+ });
63
+ if (isCancel(transforms)) process.exit(0);
64
+
65
+ // Make sure provider transform runs first if it's selected
66
+ const sortedTransforms = transforms.sort((a) =>
67
+ a.includes('provider') ? -1 : 1,
68
+ );
69
+
70
+ const program = getProgram(args);
71
+ const sourceFiles = program.getSourceFiles();
72
+ const possibleReferences = findTRPCImportReferences(program);
73
+ const trpcFile = possibleReferences.mostUsed.file;
74
+ const trpcImportName = possibleReferences.importName;
75
+
76
+ const commitedFiles = await filterIgnored(sourceFiles);
77
+
78
+ for (const transform of sortedTransforms) {
79
+ log.info(`Running transform: ${transform}`);
80
+ const { run } = await import('jscodeshift/src/Runner.js');
81
+ await run(transform, commitedFiles, {
82
+ ...args,
83
+ trpcFile,
84
+ trpcImportName,
85
+ });
86
+ log.info(`Transform ${transform} completed`);
87
+ }
88
+
89
+ if (!args.skipTanstackQuery) {
90
+ log.info('Installing @trpc/tanstack-react-query');
91
+ await installPackage('@trpc/tanstack-react-query');
92
+ log.success('@trpc/tanstack-react-query installed');
93
+
94
+ log.info('Uninstalling @trpc/react-query');
95
+ await uninstallPackage('@trpc/react-query');
96
+ log.success('@trpc/react-query uninstalled');
97
+ }
98
+
99
+ outro('Upgrade complete! 🎉');
100
+ })();
@@ -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
+ ): boolean {
20
+ if (j.Identifier.check(expr.object)) {
21
+ expr.object = id;
22
+ return true;
23
+ }
24
+
25
+ return !j.MemberExpression.check(expr.object)
26
+ ? false
27
+ : replaceMemberExpressionRootIndentifier(j, expr.object, id);
28
+ }
@@ -0,0 +1,129 @@
1
+ import * as p from '@clack/prompts';
2
+ import * as ts from 'typescript';
3
+
4
+ export function getProgram(args: Record<string, unknown>) {
5
+ const configFile = ts.findConfigFile(process.cwd(), (filepath) =>
6
+ ts.sys.fileExists(filepath),
7
+ );
8
+ if (!configFile) {
9
+ p.log.error('No tsconfig found');
10
+ process.exit(1);
11
+ }
12
+
13
+ if (args.verbose) {
14
+ p.log.info(`Using tsconfig: ${configFile}`);
15
+ }
16
+
17
+ const { config } = ts.readConfigFile(configFile, (filepath) =>
18
+ ts.sys.readFile(filepath),
19
+ );
20
+ const parsedConfig = ts.parseJsonConfigFileContent(
21
+ config,
22
+ ts.sys,
23
+ process.cwd(),
24
+ );
25
+ const program = ts.createProgram({
26
+ options: parsedConfig.options,
27
+ rootNames: parsedConfig.fileNames,
28
+ configFileParsingDiagnostics: parsedConfig.errors,
29
+ });
30
+
31
+ return program;
32
+ }
33
+
34
+ export function findSourceAndImportName(program: ts.Program) {
35
+ const files = program.getSourceFiles().filter((sourceFile) => {
36
+ if (sourceFile.isDeclarationFile) return false;
37
+ let found = false;
38
+ ts.forEachChild(sourceFile, (node) => {
39
+ if (!found && ts.isImportDeclaration(node)) {
40
+ const { moduleSpecifier } = node;
41
+ if (
42
+ ts.isStringLiteral(moduleSpecifier) &&
43
+ moduleSpecifier.text.includes('@trpc/react-query')
44
+ ) {
45
+ found = true;
46
+ }
47
+ }
48
+ });
49
+ return found;
50
+ });
51
+
52
+ let importName = 'trpc';
53
+ files.forEach((sourceFile) => {
54
+ ts.forEachChild(sourceFile, (node) => {
55
+ if (
56
+ ts.isVariableStatement(node) &&
57
+ node.modifiers?.some((mod) => mod.getText(sourceFile) === 'export')
58
+ ) {
59
+ node.declarationList.declarations.forEach((declaration) => {
60
+ if (
61
+ ts.isVariableDeclaration(declaration) &&
62
+ declaration.initializer &&
63
+ ts.isCallExpression(declaration.initializer) &&
64
+ ts.isIdentifier(declaration.initializer.expression) &&
65
+ declaration.initializer.expression.getText(sourceFile) ===
66
+ 'createTRPCReact'
67
+ ) {
68
+ importName = declaration.name.getText(sourceFile);
69
+ }
70
+ });
71
+ }
72
+ });
73
+ });
74
+
75
+ return {
76
+ files: files.map((d) => d.fileName),
77
+ importName,
78
+ };
79
+ }
80
+
81
+ export function findTRPCImportReferences(program: ts.Program) {
82
+ const { files: filesImportingTRPC, importName } =
83
+ findSourceAndImportName(program);
84
+ const trpcReferenceSpecifiers = new Map<string, string>();
85
+
86
+ program.getSourceFiles().forEach((sourceFile) => {
87
+ if (sourceFile.isDeclarationFile) return;
88
+ ts.forEachChild(sourceFile, (node) => {
89
+ if (
90
+ ts.isImportDeclaration(node) &&
91
+ ts.isStringLiteral(node.moduleSpecifier)
92
+ ) {
93
+ const resolved = ts.resolveModuleName(
94
+ node.moduleSpecifier.text,
95
+ sourceFile.fileName,
96
+ program.getCompilerOptions(),
97
+ ts.sys,
98
+ );
99
+ if (
100
+ resolved.resolvedModule &&
101
+ filesImportingTRPC.includes(resolved.resolvedModule.resolvedFileName)
102
+ ) {
103
+ trpcReferenceSpecifiers.set(
104
+ resolved.resolvedModule.resolvedFileName,
105
+ node.moduleSpecifier.text,
106
+ );
107
+ }
108
+ }
109
+ });
110
+ });
111
+
112
+ const counts: Record<string, number> = {};
113
+ let currentMax = 0;
114
+ const mostUsed = { file: '' };
115
+
116
+ [...trpcReferenceSpecifiers.values()].forEach((specifier) => {
117
+ counts[specifier] = (counts[specifier] || 0) + 1;
118
+ if (counts[specifier] > currentMax) {
119
+ currentMax = counts[specifier];
120
+ mostUsed.file = specifier;
121
+ }
122
+ });
123
+
124
+ return {
125
+ importName,
126
+ mostUsed,
127
+ all: Object.fromEntries(trpcReferenceSpecifiers.entries()),
128
+ };
129
+ }
@@ -0,0 +1,17 @@
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<TPath> | false {
11
+ if (!path.parent) {
12
+ return false;
13
+ }
14
+ return type.check(path.node)
15
+ ? (path as ASTPath<TPath>)
16
+ : findParentOfType(path.parentPath, type);
17
+ }
@@ -0,0 +1,4 @@
1
+ import * as CP from 'node:child_process';
2
+ import * as Util from 'node:util';
3
+
4
+ export const execa = Util.promisify(CP.exec);
package/src/lib/git.ts ADDED
@@ -0,0 +1,44 @@
1
+ import * as Path from 'path';
2
+ import * as p from '@clack/prompts';
3
+ import type { SourceFile } from 'typescript';
4
+ import { execa } from './execa';
5
+
6
+ export async function assertCleanGitTree() {
7
+ const { stdout } = await execa('git status');
8
+ if (!stdout.includes('nothing to commit')) {
9
+ p.cancel(
10
+ 'Git tree is not clean, please commit your changes and try again, or run with `--force`',
11
+ );
12
+ process.exit(1);
13
+ }
14
+ }
15
+
16
+ export async function filterIgnored(files: readonly SourceFile[]) {
17
+ const { stdout } = await execa('git check-ignore **/*');
18
+ const ignores = stdout.split('\n');
19
+
20
+ if (process.env.VERBOSE) {
21
+ p.log.info(`cwd: ${process.cwd()}`);
22
+ p.log.info(
23
+ `All files in program: ${files.map((file) => file.fileName).join(', ')}`,
24
+ );
25
+ p.log.info(`Ignored files: ${ignores.join(', ')}`);
26
+ }
27
+
28
+ // Ignore "common files"
29
+ const filteredSourcePaths = files
30
+ .filter(
31
+ (source) =>
32
+ source.fileName.startsWith(Path.resolve()) && // only look ahead of current directory
33
+ !source.fileName.includes('/trpc/packages/') && // relative paths when running codemod locally
34
+ !source.fileName.includes('/node_modules/') && // always ignore node_modules
35
+ !ignores.includes(source.fileName), // ignored files
36
+ )
37
+ .map((source) => source.fileName);
38
+
39
+ if (process.env.VERBOSE) {
40
+ p.log.info(`Filtered files: ${filteredSourcePaths.join(', ')}`);
41
+ }
42
+
43
+ return filteredSourcePaths;
44
+ }
@@ -0,0 +1,34 @@
1
+ import * as p from '@clack/prompts';
2
+ import { execa } from './execa';
3
+
4
+ function getPackageManager() {
5
+ const userAgent = process.env.npm_config_user_agent;
6
+ if (userAgent?.startsWith('pnpm')) return 'pnpm';
7
+ if (userAgent?.startsWith('yarn')) return 'yarn';
8
+ if (userAgent?.startsWith('bun')) return 'bun';
9
+ return 'npm';
10
+ }
11
+
12
+ export async function installPackage(packageName: string) {
13
+ const packageManager = getPackageManager();
14
+ const installCmd = packageManager === 'yarn' ? 'add' : 'install';
15
+ const { stdout, stderr } = await execa(
16
+ `${packageManager} ${installCmd} ${packageName}`,
17
+ );
18
+ if (stderr) {
19
+ p.log.error(stderr);
20
+ }
21
+ p.log.info(stdout);
22
+ }
23
+
24
+ export async function uninstallPackage(packageName: string) {
25
+ const packageManager = getPackageManager();
26
+ const uninstallCmd = packageManager === 'yarn' ? 'remove' : 'uninstall';
27
+ const { stdout, stderr } = await execa(
28
+ `${packageManager} ${uninstallCmd} ${packageName}`,
29
+ );
30
+ if (stderr) {
31
+ p.log.error(stderr);
32
+ }
33
+ p.log.info(stdout);
34
+ }
@@ -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;
@@ -194,7 +196,13 @@ export default function transform(
194
196
  },
195
197
  })
196
198
  .forEach((path) => {
199
+ const isTRPCContextUtil =
200
+ j.MemberExpression.check(path.value.callee) &&
201
+ j.Identifier.check(path.value.callee.object) &&
202
+ path.value.callee.object.name == trpcImportName;
203
+
197
204
  if (
205
+ isTRPCContextUtil &&
198
206
  j.VariableDeclarator.check(path.parentPath.node) &&
199
207
  j.Identifier.check(path.parentPath.node.id)
200
208
  ) {
@@ -205,16 +213,28 @@ export default function transform(
205
213
  .find(j.Identifier, { name: oldIdentifier.name })
206
214
  .forEach((path) => {
207
215
  if (j.MemberExpression.check(path.parent?.parent?.node)) {
208
- const callExprPath = path.parent.parent.parent;
209
- const callExpr = callExprPath.node as CallExpression;
216
+ const callExprPath = findParentOfType<CallExpression>(
217
+ path.parentPath,
218
+ j.CallExpression,
219
+ );
220
+ if (!callExprPath) {
221
+ console.warn(
222
+ `Failed to walk up the tree to find utilMethod call expression, on file: ${file.path}`,
223
+ callExprPath,
224
+ { start: path.node.loc?.start, end: path.node.loc?.end },
225
+ );
226
+ return;
227
+ }
228
+ const callExpr = callExprPath.node;
210
229
  const memberExpr = callExpr.callee as MemberExpression;
211
230
  if (
212
231
  !j.CallExpression.check(callExpr) ||
213
232
  !j.MemberExpression.check(memberExpr)
214
233
  ) {
215
234
  console.warn(
216
- 'Failed to walk up the tree to find utilMethod call expression',
235
+ `Failed to walk up the tree to find utilMethod with a \`trpc.PATH.<call>\`, on file: ${file.path}`,
217
236
  callExpr,
237
+ { start: path.node.loc?.start, end: path.node.loc?.end },
218
238
  );
219
239
  return;
220
240
  }
@@ -222,7 +242,6 @@ export default function transform(
222
242
  if (
223
243
  !(
224
244
  j.MemberExpression.check(memberExpr.object) &&
225
- j.Identifier.check(memberExpr.object.object) &&
226
245
  j.Identifier.check(memberExpr.property) &&
227
246
  memberExpr.property.name in utilMap
228
247
  )
@@ -236,11 +255,21 @@ export default function transform(
236
255
 
237
256
  // Replace util.PATH.proxyMethod() with trpc.PATH.queryFilter()
238
257
  const proxyMethod = memberExpr.property.name as ProxyMethod;
239
- memberExpr.object.object = j.identifier(trpcImportName!);
258
+ const replacedPath = replaceMemberExpressionRootIndentifier(
259
+ j,
260
+ memberExpr,
261
+ j.identifier(trpcImportName),
262
+ );
263
+ if (!replacedPath) {
264
+ console.warn(
265
+ 'Failed to wrap proxy call expression',
266
+ memberExpr,
267
+ );
268
+ }
240
269
  memberExpr.property = j.identifier('queryFilter');
241
270
 
242
271
  // Wrap it in queryClient.utilMethod()
243
- j(callExprPath).replaceWith(
272
+ callExprPath.replace(
244
273
  j.memberExpression(
245
274
  j.identifier('queryClient'),
246
275
  j.callExpression(j.identifier(utilMap[proxyMethod]), [
package/dist/cli.cjs DELETED
@@ -1,78 +0,0 @@
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 typescript = require('typescript');
8
-
9
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
-
11
- var path__default = /*#__PURE__*/_interopDefault(path);
12
-
13
- var version = "0.0.0-alpha.2";
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`'));
16
- 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);
19
- };
20
- 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);
24
- // Ignore "common files"
25
- const filteredSourcePaths = files.filter((source)=>source.fileName.startsWith(path__default.default.resolve()) && // only look ahead of current directory
26
- !source.fileName.includes('/trpc/packages/') && // relative paths when running codemod locally
27
- !ignores.includes(source.fileName)).map((source)=>source.fileName);
28
- yield* effect.Effect.log('Filtered files:', filteredSourcePaths);
29
- return filteredSourcePaths;
30
- });
31
- 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
- options: _.options,
33
- rootNames: _.fileNames,
34
- configFileParsingDiagnostics: _.errors
35
- })));
36
- // FIXME :: hacky
37
- const transformPath = (path)=>process.env.DEV ? path : path.replace('../', './').replace('.ts', '.cjs');
38
- 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'));
39
- /**
40
- * TODO: Instead of default values these should be detected automatically from the TS program
41
- */ 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
- 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'));
43
- const rootComamnd = cli$1.Command.make('upgrade', {
44
- force,
45
- trpcFile,
46
- trpcImportName
47
- }, (args)=>effect.Effect.gen(function*() {
48
- if (!args.force) {
49
- yield* assertCleanGitTree;
50
- }
51
- const transforms = yield* effect.Effect.map(cli$1.Prompt.multiSelect({
52
- message: 'Select transforms to run',
53
- choices: [
54
- {
55
- title: 'Migrate Hooks to xxxOptions API',
56
- value: require.resolve(transformPath('../transforms/hooksToOptions.ts'))
57
- },
58
- {
59
- title: 'Migrate context provider setup',
60
- value: require.resolve(transformPath('../transforms/provider.ts'))
61
- }
62
- ]
63
- }), // Make sure provider transform runs first if it's selected
64
- effect.Array.sortWith((a)=>!a.includes('provider.ts'), effect.Order.boolean));
65
- const program = yield* TSProgram;
66
- const sourceFiles = program.getSourceFiles();
67
- const commitedFiles = yield* filterIgnored(sourceFiles);
68
- 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, args)))), effect.Effect.map((_)=>effect.Effect.log('Transform result', _)));
70
- });
71
- yield* effect.Effect.log('Installing @trpc/tanstack-react-query');
72
- yield* installPackage('@trpc/tanstack-react-query');
73
- }));
74
- const cli = cli$1.Command.run(rootComamnd, {
75
- name: 'tRPC Upgrade CLI',
76
- version: `v${version}`
77
- });
78
- cli(process.argv).pipe(effect.Effect.provide(platformNode.NodeContext.layer), platformNode.NodeRuntime.runMain);
package/src/bin/cli.ts DELETED
@@ -1,180 +0,0 @@
1
- /* eslint-disable @typescript-eslint/unbound-method */
2
- import path from 'path';
3
- import { Command as CLICommand, Options, Prompt } from '@effect/cli';
4
- import { Command } from '@effect/platform';
5
- import { NodeContext, NodeRuntime } from '@effect/platform-node';
6
- import {
7
- Array,
8
- Console,
9
- Effect,
10
- Match,
11
- Order,
12
- pipe,
13
- Predicate,
14
- Stream,
15
- String,
16
- } from 'effect';
17
- import type { SourceFile } from 'typescript';
18
- import {
19
- createProgram,
20
- findConfigFile,
21
- parseJsonConfigFileContent,
22
- readConfigFile,
23
- sys,
24
- } from 'typescript';
25
- import { version } from '../../package.json';
26
-
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
- );
34
-
35
- const installPackage = (packageName: string) => {
36
- const packageManager = Match.value(
37
- process.env.npm_config_user_agent ?? 'npm',
38
- ).pipe(
39
- Match.when(String.startsWith('pnpm'), () => 'pnpm'),
40
- Match.when(String.startsWith('yarn'), () => 'yarn'),
41
- Match.when(String.startsWith('bun'), () => 'bun'),
42
- Match.orElse(() => 'npm'),
43
- );
44
- return Command.streamLines(
45
- Command.make(packageManager, 'install', packageName),
46
- ).pipe(Stream.mapEffect(Console.log), Stream.runDrain);
47
- };
48
-
49
- const filterIgnored = (files: readonly SourceFile[]) =>
50
- Effect.gen(function* () {
51
- const ignores = yield* Command.string(
52
- Command.make('git', 'check-ignore', '**/*'),
53
- ).pipe(Effect.map((_) => _.split('\n')));
54
-
55
- yield* Effect.log(
56
- 'All files in program:',
57
- files.map((_) => _.fileName),
58
- );
59
- yield* Effect.log('Ignored files:', ignores);
60
-
61
- // Ignore "common files"
62
- const filteredSourcePaths = files
63
- .filter(
64
- (source) =>
65
- source.fileName.startsWith(path.resolve()) && // only look ahead of current directory
66
- !source.fileName.includes('/trpc/packages/') && // relative paths when running codemod locally
67
- !ignores.includes(source.fileName), // ignored files
68
- )
69
- .map((source) => source.fileName);
70
-
71
- yield* Effect.log('Filtered files:', filteredSourcePaths);
72
-
73
- return filteredSourcePaths;
74
- });
75
-
76
- const TSProgram = Effect.succeed(
77
- findConfigFile(process.cwd(), sys.fileExists),
78
- ).pipe(
79
- Effect.filterOrFail(Predicate.isNotNullable, () => 'No tsconfig found'),
80
- Effect.tap((_) => Effect.logDebug('Using tsconfig', _)),
81
- Effect.map((_) => readConfigFile(_, sys.readFile)),
82
- Effect.map((_) => parseJsonConfigFileContent(_.config, sys, process.cwd())),
83
- Effect.map((_) =>
84
- createProgram({
85
- options: _.options,
86
- rootNames: _.fileNames,
87
- configFileParsingDiagnostics: _.errors,
88
- }),
89
- ),
90
- );
91
-
92
- // FIXME :: hacky
93
- const transformPath = (path: string) =>
94
- process.env.DEV ? path : path.replace('../', './').replace('.ts', '.cjs');
95
-
96
- const force = Options.boolean('force').pipe(
97
- Options.withAlias('f'),
98
- Options.withDefault(false),
99
- Options.withDescription('Skip git status check, use with caution'),
100
- );
101
-
102
- /**
103
- * TODO: Instead of default values these should be detected automatically from the TS program
104
- */
105
- const trpcFile = Options.text('trpcFile').pipe(
106
- Options.withAlias('f'),
107
- Options.withDefault('~/trpc'),
108
- Options.withDescription('Path to the trpc import file'),
109
- );
110
-
111
- const trpcImportName = Options.text('trpcImportName').pipe(
112
- Options.withAlias('i'),
113
- Options.withDefault('trpc'),
114
- Options.withDescription('Name of the trpc import'),
115
- );
116
-
117
- const rootComamnd = CLICommand.make(
118
- 'upgrade',
119
- {
120
- force,
121
- trpcFile,
122
- trpcImportName,
123
- },
124
- (args) =>
125
- Effect.gen(function* () {
126
- if (!args.force) {
127
- yield* assertCleanGitTree;
128
- }
129
-
130
- const transforms = yield* Effect.map(
131
- Prompt.multiSelect({
132
- message: 'Select transforms to run',
133
- choices: [
134
- {
135
- title: 'Migrate Hooks to xxxOptions API',
136
- value: require.resolve(
137
- transformPath('../transforms/hooksToOptions.ts'),
138
- ),
139
- },
140
- {
141
- title: 'Migrate context provider setup',
142
- value: require.resolve(
143
- transformPath('../transforms/provider.ts'),
144
- ),
145
- },
146
- ],
147
- }),
148
- // Make sure provider transform runs first if it's selected
149
- Array.sortWith((a) => !a.includes('provider.ts'), Order.boolean),
150
- );
151
-
152
- const program = yield* TSProgram;
153
- const sourceFiles = program.getSourceFiles();
154
-
155
- const commitedFiles = yield* filterIgnored(sourceFiles);
156
- yield* Effect.forEach(transforms, (transform) => {
157
- return pipe(
158
- Effect.log('Running transform', transform),
159
- Effect.flatMap(() =>
160
- Effect.tryPromise(async () =>
161
- import('jscodeshift/src/Runner.js').then(({ run }) =>
162
- run(transform, commitedFiles, args),
163
- ),
164
- ),
165
- ),
166
- Effect.map((_) => Effect.log('Transform result', _)),
167
- );
168
- });
169
-
170
- yield* Effect.log('Installing @trpc/tanstack-react-query');
171
- yield* installPackage('@trpc/tanstack-react-query');
172
- }),
173
- );
174
-
175
- const cli = CLICommand.run(rootComamnd, {
176
- name: 'tRPC Upgrade CLI',
177
- version: `v${version}`,
178
- });
179
-
180
- cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);