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

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.js ADDED
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env node
2
+ import { parse } from '@bomb.sh/args';
3
+ import * as p from '@clack/prompts';
4
+ import { intro, log, multiselect, isCancel, outro } from '@clack/prompts';
5
+ import * as ts from 'typescript';
6
+ import * as Path from 'path';
7
+ import * as CP from 'node:child_process';
8
+ import * as Util from 'node:util';
9
+ import __node_cjsModule from 'node:module';
10
+
11
+ var version = "0.0.0-alpha.30";
12
+
13
+ function getProgram(args) {
14
+ const configFile = ts.findConfigFile(process.cwd(), (filepath)=>ts.sys.fileExists(filepath));
15
+ if (!configFile) {
16
+ p.log.error('No tsconfig found');
17
+ process.exit(1);
18
+ }
19
+ if (args.verbose) {
20
+ p.log.info(`Using tsconfig: ${configFile}`);
21
+ }
22
+ const { config } = ts.readConfigFile(configFile, (filepath)=>ts.sys.readFile(filepath));
23
+ const parsedConfig = ts.parseJsonConfigFileContent(config, ts.sys, process.cwd());
24
+ const program = ts.createProgram({
25
+ options: parsedConfig.options,
26
+ rootNames: parsedConfig.fileNames,
27
+ configFileParsingDiagnostics: parsedConfig.errors
28
+ });
29
+ return program;
30
+ }
31
+ function findSourceAndImportName(program) {
32
+ const files = program.getSourceFiles().filter((sourceFile)=>{
33
+ if (sourceFile.isDeclarationFile) return false;
34
+ let found = false;
35
+ ts.forEachChild(sourceFile, (node)=>{
36
+ if (!found && ts.isImportDeclaration(node)) {
37
+ const { moduleSpecifier } = node;
38
+ if (ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text.includes('@trpc/react-query')) {
39
+ found = true;
40
+ }
41
+ }
42
+ });
43
+ return found;
44
+ });
45
+ let importName = 'trpc';
46
+ files.forEach((sourceFile)=>{
47
+ ts.forEachChild(sourceFile, (node)=>{
48
+ if (ts.isVariableStatement(node) && node.modifiers?.some((mod)=>mod.getText(sourceFile) === 'export')) {
49
+ node.declarationList.declarations.forEach((declaration)=>{
50
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.getText(sourceFile) === 'createTRPCReact') {
51
+ importName = declaration.name.getText(sourceFile);
52
+ }
53
+ });
54
+ }
55
+ });
56
+ });
57
+ return {
58
+ files: files.map((d)=>d.fileName),
59
+ importName
60
+ };
61
+ }
62
+ function findTRPCImportReferences(program) {
63
+ const { files: filesImportingTRPC, importName } = findSourceAndImportName(program);
64
+ const trpcReferenceSpecifiers = new Map();
65
+ program.getSourceFiles().forEach((sourceFile)=>{
66
+ if (sourceFile.isDeclarationFile) return;
67
+ ts.forEachChild(sourceFile, (node)=>{
68
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
69
+ const resolved = ts.resolveModuleName(node.moduleSpecifier.text, sourceFile.fileName, program.getCompilerOptions(), ts.sys);
70
+ if (resolved.resolvedModule && filesImportingTRPC.includes(resolved.resolvedModule.resolvedFileName)) {
71
+ trpcReferenceSpecifiers.set(resolved.resolvedModule.resolvedFileName, node.moduleSpecifier.text);
72
+ }
73
+ }
74
+ });
75
+ });
76
+ const counts = {};
77
+ let currentMax = 0;
78
+ const mostUsed = {
79
+ file: ''
80
+ };
81
+ [
82
+ ...trpcReferenceSpecifiers.values()
83
+ ].forEach((specifier)=>{
84
+ counts[specifier] = (counts[specifier] || 0) + 1;
85
+ if (counts[specifier] > currentMax) {
86
+ currentMax = counts[specifier];
87
+ mostUsed.file = specifier;
88
+ }
89
+ });
90
+ return {
91
+ importName,
92
+ mostUsed,
93
+ all: Object.fromEntries(trpcReferenceSpecifiers.entries())
94
+ };
95
+ }
96
+
97
+ const execa = Util.promisify(CP.exec);
98
+
99
+ async function assertCleanGitTree() {
100
+ const { stdout } = await execa('git status');
101
+ if (!stdout.includes('nothing to commit')) {
102
+ p.cancel('Git tree is not clean, please commit your changes and try again, or run with `--force`');
103
+ process.exit(1);
104
+ }
105
+ }
106
+ async function filterIgnored(files) {
107
+ const { stdout } = await execa('git check-ignore **/*');
108
+ const ignores = stdout.split('\n');
109
+ if (process.env.VERBOSE) {
110
+ p.log.info(`cwd: ${process.cwd()}`);
111
+ p.log.info(`All files in program: ${files.map((file)=>file.fileName).join(', ')}`);
112
+ p.log.info(`Ignored files: ${ignores.join(', ')}`);
113
+ }
114
+ // Ignore "common files"
115
+ const filteredSourcePaths = files.filter((source)=>source.fileName.startsWith(Path.resolve()) && // only look ahead of current directory
116
+ !source.fileName.includes('/trpc/packages/') && // relative paths when running codemod locally
117
+ !source.fileName.includes('/node_modules/') && // always ignore node_modules
118
+ !ignores.includes(source.fileName)).map((source)=>source.fileName);
119
+ if (process.env.VERBOSE) {
120
+ p.log.info(`Filtered files: ${filteredSourcePaths.join(', ')}`);
121
+ }
122
+ return filteredSourcePaths;
123
+ }
124
+
125
+ function getPackageManager() {
126
+ const userAgent = process.env.npm_config_user_agent;
127
+ if (userAgent?.startsWith('pnpm')) return 'pnpm';
128
+ if (userAgent?.startsWith('yarn')) return 'yarn';
129
+ if (userAgent?.startsWith('bun')) return 'bun';
130
+ return 'npm';
131
+ }
132
+ async function installPackage(packageName) {
133
+ const packageManager = getPackageManager();
134
+ const installCmd = packageManager === 'yarn' ? 'add' : 'install';
135
+ const { stdout, stderr } = await execa(`${packageManager} ${installCmd} ${packageName}`);
136
+ if (stderr) {
137
+ p.log.error(stderr);
138
+ }
139
+ p.log.info(stdout);
140
+ }
141
+ async function uninstallPackage(packageName) {
142
+ const packageManager = getPackageManager();
143
+ const uninstallCmd = packageManager === 'yarn' ? 'remove' : 'uninstall';
144
+ const { stdout, stderr } = await execa(`${packageManager} ${uninstallCmd} ${packageName}`);
145
+ if (stderr) {
146
+ p.log.error(stderr);
147
+ }
148
+ p.log.info(stdout);
149
+ }
150
+
151
+ const require = __node_cjsModule.createRequire(import.meta.url);
152
+
153
+ const args = parse(process.argv.slice(2), {
154
+ default: {
155
+ force: false,
156
+ skipTanstackQuery: false,
157
+ verbose: false
158
+ },
159
+ alias: {
160
+ f: 'force',
161
+ h: 'help',
162
+ v: 'verbose',
163
+ q: 'skipTanstackQuery'
164
+ },
165
+ boolean: true
166
+ });
167
+ intro(`tRPC Upgrade CLI v${version}`);
168
+ if (args.help) {
169
+ log.info(`
170
+ Usage: upgrade [options]
171
+
172
+ Options:
173
+ -f, --force Skip git status check, use with caution
174
+ -q, --skipTanstackQuery Skip installing @trpc/tanstack-react-query package
175
+ -v, --verbose Enable verbose logging
176
+ -h, --help Show help
177
+ `.trim());
178
+ process.exit(0);
179
+ }
180
+ if (args.verbose) {
181
+ log.info(`Running upgrade with args: ${JSON.stringify(args, null, 2)}`);
182
+ }
183
+ if (!args.force) {
184
+ await assertCleanGitTree();
185
+ }
186
+ const transforms = await multiselect({
187
+ message: 'Select transforms to run',
188
+ options: [
189
+ {
190
+ value: require.resolve('@trpc/upgrade/transforms/hooksToOptions'),
191
+ label: 'Migrate Hooks to xxxOptions API'
192
+ },
193
+ {
194
+ value: require.resolve('@trpc/upgrade/transforms/provider'),
195
+ label: 'Migrate context provider setup'
196
+ }
197
+ ]
198
+ });
199
+ if (isCancel(transforms)) process.exit(0);
200
+ // Make sure provider transform runs first if it's selected
201
+ const sortedTransforms = transforms.sort((a)=>a.includes('provider') ? -1 : 1);
202
+ const program = getProgram(args);
203
+ const sourceFiles = program.getSourceFiles();
204
+ const possibleReferences = findTRPCImportReferences(program);
205
+ const trpcFile = possibleReferences.mostUsed.file;
206
+ const trpcImportName = possibleReferences.importName;
207
+ const commitedFiles = await filterIgnored(sourceFiles);
208
+ for (const transform of sortedTransforms){
209
+ log.info(`Running transform: ${transform}`);
210
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
211
+ const { run } = require('jscodeshift/src/Runner.js');
212
+ await run(transform, commitedFiles, {
213
+ ...args,
214
+ trpcFile,
215
+ trpcImportName
216
+ });
217
+ log.info(`Transform ${transform} completed`);
218
+ }
219
+ if (!args.skipTanstackQuery) {
220
+ log.info('Installing @trpc/tanstack-react-query');
221
+ await installPackage('@trpc/tanstack-react-query');
222
+ log.success('@trpc/tanstack-react-query installed');
223
+ log.info('Uninstalling @trpc/react-query');
224
+ await uninstallPackage('@trpc/react-query');
225
+ log.success('@trpc/react-query uninstalled');
226
+ }
227
+ outro('Upgrade complete! 🎉');
@@ -1,6 +1,33 @@
1
- Object.defineProperty(exports, '__esModule', { value: true });
1
+ /**
2
+ * Replaces the identifier for the root path key
3
+ * of a member expression
4
+ *
5
+ * For dot notation like `rootKey.x.y.z` the AST
6
+ * is constructed with the `rootKey` being nested deep
7
+ * inside a wrapper MemberExpression holding `rootKey.x`
8
+ * and so on
9
+ *
10
+ * This function helps replace the `rootKey` identifier with
11
+ * the provided identifier node
12
+ */ function replaceMemberExpressionRootIndentifier(j, expr, id) {
13
+ if (j.Identifier.check(expr.object)) {
14
+ expr.object = id;
15
+ return true;
16
+ }
17
+ return !j.MemberExpression.check(expr.object) ? false : replaceMemberExpressionRootIndentifier(j, expr.object, id);
18
+ }
19
+
20
+ /**
21
+ * Walks the path upwards to look for the closest parent
22
+ * of the mentioned type
23
+ */ function findParentOfType(path, type) {
24
+ if (!path.parent) {
25
+ return false;
26
+ }
27
+ return type.check(path.node) ? path : findParentOfType(path.parentPath, type);
28
+ }
2
29
 
3
- /* eslint-disable @typescript-eslint/no-non-null-assertion */ /* eslint-disable no-console */ const hookToOptions = {
30
+ const hookToOptions = {
4
31
  useQuery: {
5
32
  lib: '@tanstack/react-query',
6
33
  fn: 'queryOptions'
@@ -148,30 +175,44 @@ function transform(file, api, options) {
148
175
  }
149
176
  }
150
177
  }).forEach((path)=>{
151
- if (j.VariableDeclarator.check(path.parentPath.node) && j.Identifier.check(path.parentPath.node.id)) {
178
+ const isTRPCContextUtil = j.MemberExpression.check(path.value.callee) && j.Identifier.check(path.value.callee.object) && path.value.callee.object.name == trpcImportName;
179
+ if (isTRPCContextUtil && j.VariableDeclarator.check(path.parentPath.node) && j.Identifier.check(path.parentPath.node.id)) {
152
180
  const oldIdentifier = path.parentPath.node.id;
153
181
  // Find all the references to `utils` and replace with `queryClient[helperMap](trpc.PATH.queryFilter())`
154
182
  root.find(j.Identifier, {
155
183
  name: oldIdentifier.name
156
184
  }).forEach((path)=>{
157
185
  if (j.MemberExpression.check(path.parent?.parent?.node)) {
158
- const callExprPath = path.parent.parent.parent;
186
+ const callExprPath = findParentOfType(path.parentPath, j.CallExpression);
187
+ if (!callExprPath) {
188
+ console.warn(`Failed to walk up the tree to find utilMethod call expression, on file: ${file.path}`, callExprPath, {
189
+ start: path.node.loc?.start,
190
+ end: path.node.loc?.end
191
+ });
192
+ return;
193
+ }
159
194
  const callExpr = callExprPath.node;
160
195
  const memberExpr = callExpr.callee;
161
196
  if (!j.CallExpression.check(callExpr) || !j.MemberExpression.check(memberExpr)) {
162
- console.warn('Failed to walk up the tree to find utilMethod call expression', callExpr);
197
+ console.warn(`Failed to walk up the tree to find utilMethod with a \`trpc.PATH.<call>\`, on file: ${file.path}`, callExpr, {
198
+ start: path.node.loc?.start,
199
+ end: path.node.loc?.end
200
+ });
163
201
  return;
164
202
  }
165
- if (!(j.MemberExpression.check(memberExpr.object) && j.Identifier.check(memberExpr.object.object) && j.Identifier.check(memberExpr.property) && memberExpr.property.name in utilMap)) {
203
+ if (!(j.MemberExpression.check(memberExpr.object) && j.Identifier.check(memberExpr.property) && memberExpr.property.name in utilMap)) {
166
204
  console.warn('Failed to identify utilMethod from proxy call expression', memberExpr);
167
205
  return;
168
206
  }
169
207
  // Replace util.PATH.proxyMethod() with trpc.PATH.queryFilter()
170
208
  const proxyMethod = memberExpr.property.name;
171
- memberExpr.object.object = j.identifier(trpcImportName);
209
+ const replacedPath = replaceMemberExpressionRootIndentifier(j, memberExpr, j.identifier(trpcImportName));
210
+ if (!replacedPath) {
211
+ console.warn('Failed to wrap proxy call expression', memberExpr);
212
+ }
172
213
  memberExpr.property = j.identifier('queryFilter');
173
214
  // Wrap it in queryClient.utilMethod()
174
- j(callExprPath).replaceWith(j.memberExpression(j.identifier('queryClient'), j.callExpression(j.identifier(utilMap[proxyMethod]), [
215
+ callExprPath.replace(j.memberExpression(j.identifier('queryClient'), j.callExpression(j.identifier(utilMap[proxyMethod]), [
175
216
  callExpr
176
217
  ])));
177
218
  }
@@ -218,5 +259,4 @@ function transform(file, api, options) {
218
259
  }
219
260
  const parser = 'tsx';
220
261
 
221
- exports.default = transform;
222
- exports.parser = parser;
262
+ export { transform as default, parser };
@@ -1,5 +1,3 @@
1
- Object.defineProperty(exports, '__esModule', { value: true });
2
-
3
1
  function transform(file, api, options) {
4
2
  const { trpcImportName } = options;
5
3
  let routerName = undefined;
@@ -116,5 +114,4 @@ function transform(file, api, options) {
116
114
  }
117
115
  const parser = 'tsx';
118
116
 
119
- exports.default = transform;
120
- exports.parser = parser;
117
+ export { transform as default, parser };
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@trpc/upgrade",
3
- "version": "0.0.0-alpha.3",
3
+ "version": "0.0.0-alpha.31",
4
4
  "description": "Upgrade scripts for tRPC",
5
5
  "author": "juliusmarminge",
6
6
  "license": "MIT",
7
- "bin": "./dist/cli.cjs",
7
+ "type": "module",
8
+ "bin": "./dist/bin.js",
8
9
  "homepage": "https://trpc.io",
9
10
  "repository": {
10
11
  "type": "git",
@@ -13,10 +14,10 @@
13
14
  },
14
15
  "exports": {
15
16
  "./transforms/hooksToOptions": {
16
- "require": "./dist/transforms/hooksToOptions.cjs"
17
+ "default": "./dist/transforms/hooksToOptions.js"
17
18
  },
18
19
  "./transforms/provider": {
19
- "require": "./dist/transforms/provider.cjs"
20
+ "default": "./dist/transforms/provider.js"
20
21
  }
21
22
  },
22
23
  "files": [
@@ -28,18 +29,16 @@
28
29
  "!**/__tests__"
29
30
  ],
30
31
  "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",
32
+ "@bomb.sh/args": "0.3.0",
33
+ "@clack/prompts": "0.10.0",
35
34
  "jscodeshift": "17.1.1",
36
35
  "typescript": "^5.6.2"
37
36
  },
38
37
  "devDependencies": {
39
38
  "@types/jscodeshift": "0.12.0",
40
39
  "@types/node": "^22.9.0",
41
- "bunchee": "5.6.1",
42
- "esbuild": "0.19.2",
40
+ "bunchee": "6.4.0",
41
+ "esbuild": "0.25.0",
43
42
  "tsx": "^4.0.0"
44
43
  },
45
44
  "publishConfig": {
@@ -0,0 +1,99 @@
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
+ const args = parse(process.argv.slice(2), {
10
+ default: {
11
+ force: false,
12
+ skipTanstackQuery: false,
13
+ verbose: false,
14
+ },
15
+ alias: {
16
+ f: 'force',
17
+ h: 'help',
18
+ v: 'verbose',
19
+ q: 'skipTanstackQuery',
20
+ },
21
+ boolean: true,
22
+ });
23
+
24
+ intro(`tRPC Upgrade CLI v${version}`);
25
+
26
+ if (args.help) {
27
+ log.info(
28
+ `
29
+ Usage: upgrade [options]
30
+
31
+ Options:
32
+ -f, --force Skip git status check, use with caution
33
+ -q, --skipTanstackQuery Skip installing @trpc/tanstack-react-query package
34
+ -v, --verbose Enable verbose logging
35
+ -h, --help Show help
36
+ `.trim(),
37
+ );
38
+ process.exit(0);
39
+ }
40
+
41
+ if (args.verbose) {
42
+ log.info(`Running upgrade with args: ${JSON.stringify(args, null, 2)}`);
43
+ }
44
+
45
+ if (!args.force) {
46
+ await assertCleanGitTree();
47
+ }
48
+
49
+ const transforms = await multiselect({
50
+ message: 'Select transforms to run',
51
+ options: [
52
+ {
53
+ value: require.resolve('@trpc/upgrade/transforms/hooksToOptions'),
54
+ label: 'Migrate Hooks to xxxOptions API',
55
+ },
56
+ {
57
+ value: require.resolve('@trpc/upgrade/transforms/provider'),
58
+ label: 'Migrate context provider setup',
59
+ },
60
+ ],
61
+ });
62
+ if (isCancel(transforms)) process.exit(0);
63
+
64
+ // Make sure provider transform runs first if it's selected
65
+ const sortedTransforms = transforms.sort((a) =>
66
+ a.includes('provider') ? -1 : 1,
67
+ );
68
+
69
+ const program = getProgram(args);
70
+ const sourceFiles = program.getSourceFiles();
71
+ const possibleReferences = findTRPCImportReferences(program);
72
+ const trpcFile = possibleReferences.mostUsed.file;
73
+ const trpcImportName = possibleReferences.importName;
74
+
75
+ const commitedFiles = await filterIgnored(sourceFiles);
76
+
77
+ for (const transform of sortedTransforms) {
78
+ log.info(`Running transform: ${transform}`);
79
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
80
+ const { run } = require('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! 🎉');
@@ -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);