@trpc/upgrade 11.0.0-rc.800 → 11.0.0-rc.802

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/dist/bin.js ADDED
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+ import { basename, extname } from 'node:path';
3
+ import { parse } from '@bomb.sh/args';
4
+ import * as p from '@clack/prompts';
5
+ import { cancel, log, intro, multiselect, isCancel, outro } from '@clack/prompts';
6
+ import * as ts from 'typescript';
7
+ import { resolve } from 'path';
8
+ import { exec } from 'node:child_process';
9
+ import { promisify } from 'node:util';
10
+ import __node_cjsModule from 'node:module';
11
+
12
+ var version = "11.0.0-rc.802+1eccc1171";
13
+
14
+ function getProgram() {
15
+ const configFile = ts.findConfigFile(process.cwd(), (filepath)=>ts.sys.fileExists(filepath));
16
+ if (!configFile) {
17
+ p.log.error('No tsconfig found');
18
+ process.exit(1);
19
+ }
20
+ if (process.env.VERBOSE) {
21
+ p.log.info(`Using tsconfig: ${configFile}`);
22
+ }
23
+ const { config } = ts.readConfigFile(configFile, (filepath)=>ts.sys.readFile(filepath));
24
+ const parsedConfig = ts.parseJsonConfigFileContent(config, ts.sys, process.cwd());
25
+ const program = ts.createProgram({
26
+ options: parsedConfig.options,
27
+ rootNames: parsedConfig.fileNames,
28
+ configFileParsingDiagnostics: parsedConfig.errors
29
+ });
30
+ return program;
31
+ }
32
+ function findSourceAndImportName(program) {
33
+ const files = program.getSourceFiles().filter((sourceFile)=>{
34
+ if (sourceFile.isDeclarationFile) return false;
35
+ let found = false;
36
+ ts.forEachChild(sourceFile, (node)=>{
37
+ if (!found && ts.isImportDeclaration(node)) {
38
+ const { moduleSpecifier } = node;
39
+ if (ts.isStringLiteral(moduleSpecifier) && moduleSpecifier.text.includes('@trpc/react-query')) {
40
+ found = true;
41
+ }
42
+ }
43
+ });
44
+ return found;
45
+ });
46
+ let importName = 'trpc';
47
+ files.forEach((sourceFile)=>{
48
+ ts.forEachChild(sourceFile, (node)=>{
49
+ if (ts.isVariableStatement(node) && node.modifiers?.some((mod)=>mod.getText(sourceFile) === 'export')) {
50
+ node.declarationList.declarations.forEach((declaration)=>{
51
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer && ts.isCallExpression(declaration.initializer) && ts.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.getText(sourceFile) === 'createTRPCReact') {
52
+ importName = declaration.name.getText(sourceFile);
53
+ }
54
+ });
55
+ }
56
+ });
57
+ });
58
+ return {
59
+ files: files.map((d)=>d.fileName),
60
+ importName
61
+ };
62
+ }
63
+ function findTRPCImportReferences(program) {
64
+ const { files: filesImportingTRPC, importName } = findSourceAndImportName(program);
65
+ const trpcReferenceSpecifiers = new Map();
66
+ program.getSourceFiles().forEach((sourceFile)=>{
67
+ if (sourceFile.isDeclarationFile) return;
68
+ ts.forEachChild(sourceFile, (node)=>{
69
+ if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
70
+ const resolved = ts.resolveModuleName(node.moduleSpecifier.text, sourceFile.fileName, program.getCompilerOptions(), ts.sys);
71
+ if (resolved.resolvedModule && filesImportingTRPC.includes(resolved.resolvedModule.resolvedFileName)) {
72
+ trpcReferenceSpecifiers.set(resolved.resolvedModule.resolvedFileName, node.moduleSpecifier.text);
73
+ }
74
+ }
75
+ });
76
+ });
77
+ const counts = {};
78
+ let currentMax = 0;
79
+ const mostUsed = {
80
+ file: ''
81
+ };
82
+ [
83
+ ...trpcReferenceSpecifiers.values()
84
+ ].forEach((specifier)=>{
85
+ counts[specifier] = (counts[specifier] || 0) + 1;
86
+ if (counts[specifier] > currentMax) {
87
+ currentMax = counts[specifier];
88
+ mostUsed.file = specifier;
89
+ }
90
+ });
91
+ return {
92
+ importName,
93
+ mostUsed,
94
+ all: Object.fromEntries(trpcReferenceSpecifiers.entries())
95
+ };
96
+ }
97
+
98
+ const execa = promisify(exec);
99
+
100
+ async function assertCleanGitTree() {
101
+ const { stdout } = await execa('git status');
102
+ if (!stdout.includes('nothing to commit')) {
103
+ cancel('Git tree is not clean, please commit your changes and try again, or run with `--force`');
104
+ process.exit(1);
105
+ }
106
+ }
107
+ async function filterIgnored(files) {
108
+ const { stdout } = await execa('git check-ignore **/*');
109
+ const ignores = stdout.split('\n');
110
+ if (process.env.VERBOSE) {
111
+ log.info(`cwd: ${process.cwd()}`);
112
+ log.info(`All files in program: ${files.map((file)=>file.fileName).join(', ')}`);
113
+ log.info(`Ignored files: ${ignores.join(', ')}`);
114
+ }
115
+ // Ignore "common files"
116
+ const filteredSourcePaths = files.filter((source)=>source.fileName.startsWith(resolve()) && // only look ahead of current directory
117
+ !source.fileName.includes('/trpc/packages/') && // relative paths when running codemod locally
118
+ !source.fileName.includes('/node_modules/') && // always ignore node_modules
119
+ !ignores.includes(source.fileName)).map((source)=>source.fileName);
120
+ if (process.env.VERBOSE) {
121
+ log.info(`Filtered files: ${filteredSourcePaths.join(', ')}`);
122
+ }
123
+ return filteredSourcePaths;
124
+ }
125
+
126
+ function getPackageManager() {
127
+ const userAgent = process.env.npm_config_user_agent;
128
+ if (userAgent?.startsWith('pnpm')) return 'pnpm';
129
+ if (userAgent?.startsWith('yarn')) return 'yarn';
130
+ if (userAgent?.startsWith('bun')) return 'bun';
131
+ return 'npm';
132
+ }
133
+ async function installPackage(packageName) {
134
+ const packageManager = getPackageManager();
135
+ const installCmd = packageManager === 'yarn' ? 'add' : 'install';
136
+ const { stdout, stderr } = await execa(`${packageManager} ${installCmd} ${packageName}`);
137
+ if (stderr) {
138
+ log.error(stderr);
139
+ }
140
+ if (process.env.VERBOSE) {
141
+ log.info(stdout);
142
+ }
143
+ }
144
+ async function uninstallPackage(packageName) {
145
+ const packageManager = getPackageManager();
146
+ const uninstallCmd = packageManager === 'yarn' ? 'remove' : 'uninstall';
147
+ const { stdout, stderr } = await execa(`${packageManager} ${uninstallCmd} ${packageName}`);
148
+ if (stderr) {
149
+ log.error(stderr);
150
+ }
151
+ if (process.env.VERBOSE) {
152
+ log.info(stdout);
153
+ }
154
+ }
155
+
156
+ const require = __node_cjsModule.createRequire(import.meta.url);
157
+
158
+ const args = parse(process.argv.slice(2), {
159
+ default: {
160
+ force: false,
161
+ skipTanstackQuery: false,
162
+ verbose: false
163
+ },
164
+ alias: {
165
+ f: 'force',
166
+ h: 'help',
167
+ v: 'verbose',
168
+ q: 'skipTanstackQuery'
169
+ },
170
+ boolean: true
171
+ });
172
+ if (args.verbose) process.env.VERBOSE = '1';
173
+ intro(`tRPC Upgrade CLI v${version}`);
174
+ if (args.help) {
175
+ log.info(`
176
+ Usage: upgrade [options]
177
+
178
+ Options:
179
+ -f, --force Skip git status check, use with caution
180
+ -q, --skipTanstackQuery Skip installing @trpc/tanstack-react-query package
181
+ -v, --verbose Enable verbose logging
182
+ -h, --help Show help
183
+ `.trim());
184
+ process.exit(0);
185
+ }
186
+ if (args.verbose) {
187
+ log.info(`Running upgrade with args: ${JSON.stringify(args, null, 2)}`);
188
+ }
189
+ if (!args.force) {
190
+ await assertCleanGitTree();
191
+ }
192
+ const transforms = await multiselect({
193
+ message: 'Select transforms to run',
194
+ options: [
195
+ {
196
+ value: require.resolve('@trpc/upgrade/transforms/hooksToOptions'),
197
+ label: 'Migrate Hooks to xxxOptions API'
198
+ },
199
+ {
200
+ value: require.resolve('@trpc/upgrade/transforms/provider'),
201
+ label: 'Migrate context provider setup'
202
+ }
203
+ ]
204
+ });
205
+ if (isCancel(transforms)) process.exit(0);
206
+ // Make sure provider transform runs first if it's selected
207
+ const sortedTransforms = transforms.sort((a)=>a.includes('provider') ? -1 : 1);
208
+ const program = getProgram();
209
+ const sourceFiles = program.getSourceFiles();
210
+ const possibleReferences = findTRPCImportReferences(program);
211
+ const trpcFile = possibleReferences.mostUsed.file;
212
+ const trpcImportName = possibleReferences.importName;
213
+ const commitedFiles = await filterIgnored(sourceFiles);
214
+ for (const transform of sortedTransforms){
215
+ log.step(`Running transform: ${basename(transform, extname(transform))}`);
216
+ const { run } = await import('jscodeshift/src/Runner.js');
217
+ await run(transform, commitedFiles, {
218
+ ...args,
219
+ trpcFile,
220
+ trpcImportName
221
+ });
222
+ log.success(`Transform completed`);
223
+ }
224
+ if (!args.skipTanstackQuery) {
225
+ log.info('Installing @trpc/tanstack-react-query');
226
+ await installPackage('@trpc/tanstack-react-query');
227
+ log.success('@trpc/tanstack-react-query installed');
228
+ log.info('Uninstalling @trpc/react-query');
229
+ await uninstallPackage('@trpc/react-query');
230
+ log.success('@trpc/react-query uninstalled');
231
+ }
232
+ outro('Upgrade complete! 🎉');
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@trpc/upgrade",
3
- "version": "11.0.0-rc.800+892471d1b",
3
+ "version": "11.0.0-rc.802+1eccc1171",
4
4
  "description": "Upgrade scripts for tRPC",
5
5
  "author": "juliusmarminge",
6
6
  "license": "MIT",
7
- "bin": "./dist/bin.cjs",
7
+ "type": "module",
8
+ "bin": "./dist/bin.js",
8
9
  "homepage": "https://trpc.io",
9
10
  "repository": {
10
11
  "type": "git",
@@ -32,10 +33,8 @@
32
33
  "!**/__tests__"
33
34
  ],
34
35
  "dependencies": {
35
- "@effect/cli": "0.56.2",
36
- "@effect/platform": "0.77.2",
37
- "@effect/platform-node": "0.73.2",
38
- "effect": "3.13.2",
36
+ "@bomb.sh/args": "0.3.0",
37
+ "@clack/prompts": "0.10.0",
39
38
  "jscodeshift": "17.1.1",
40
39
  "typescript": "^5.6.2"
41
40
  },
@@ -52,5 +51,5 @@
52
51
  "funding": [
53
52
  "https://trpc.io/sponsor"
54
53
  ],
55
- "gitHead": "892471d1bd6914ca2df548bdda2e703e3c73a5e7"
54
+ "gitHead": "1eccc1171fba8b1bdb4d8acd83ab37d1e0a98b22"
56
55
  }
package/src/bin/index.ts CHANGED
@@ -1,247 +1,100 @@
1
- /* eslint-disable @typescript-eslint/unbound-method */
2
- import path from 'node: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
- Logger,
11
- LogLevel,
12
- Match,
13
- Order,
14
- pipe,
15
- Predicate,
16
- Stream,
17
- String,
18
- } from 'effect';
19
- import type { SourceFile } from 'typescript';
20
- import {
21
- createProgram,
22
- findConfigFile,
23
- parseJsonConfigFileContent,
24
- readConfigFile,
25
- sys,
26
- } from 'typescript';
1
+ #!/usr/bin/env node
2
+ import { basename, extname } from 'node:path';
3
+ import { parse } from '@bomb.sh/args';
4
+ import { intro, isCancel, log, multiselect, outro } from '@clack/prompts';
27
5
  import { version } from '../../package.json';
28
- import { findTRPCImportReferences } from '../lib/ast/scanners';
29
-
30
- const MakeCommand = (command: string, ...args: string[]) => {
31
- return Command.make(command, ...args).pipe(
32
- Command.workingDirectory(process.cwd()),
33
- Command.runInShell(true),
34
- );
35
- };
36
-
37
- const assertCleanGitTree = Command.string(MakeCommand('git', 'status'))
38
- .pipe()
39
- .pipe(
40
- Effect.filterOrFail(
41
- String.includes('nothing to commit'),
42
- () =>
43
- 'Git tree is not clean, please commit your changes and try again, or run with `--force`',
44
- ),
45
- );
46
- const getPackageManager = () =>
47
- Match.value(process.env.npm_config_user_agent ?? 'npm').pipe(
48
- Match.when(String.startsWith('pnpm'), () => 'pnpm'),
49
- Match.when(String.startsWith('yarn'), () => 'yarn'),
50
- Match.when(String.startsWith('bun'), () => 'bun'),
51
- Match.orElse(() => 'npm'),
52
- );
53
-
54
- const installPackage = (packageName: string) => {
55
- const packageManager = getPackageManager();
56
- return Command.streamLines(
57
- MakeCommand(packageManager, 'install', packageName),
58
- ).pipe(Stream.mapEffect(Console.log), Stream.runDrain);
59
- };
60
-
61
- const uninstallPackage = (packageName: string) => {
62
- const packageManager = getPackageManager();
63
- const uninstallCmd = packageManager === 'yarn' ? 'remove' : 'uninstall';
64
- return Command.streamLines(
65
- MakeCommand(packageManager, uninstallCmd, packageName),
66
- ).pipe(Stream.mapEffect(Console.log), Stream.runDrain);
67
- };
68
-
69
- const filterIgnored = (files: readonly SourceFile[]) =>
70
- Effect.gen(function* () {
71
- const ignores = yield* Command.string(
72
- MakeCommand('git', 'check-ignore', '**/*').pipe(Command.runInShell(true)),
73
- ).pipe(
74
- Effect.tap((_) => Effect.logDebug('Ignored files output:', _)),
75
- Effect.map((_) => _.split('\n')),
76
- );
77
-
78
- yield* Effect.logDebug('cwd:', process.cwd());
79
-
80
- yield* Effect.logDebug(
81
- 'All files in program:',
82
- files.map((_) => _.fileName),
83
- );
84
- yield* Effect.logDebug('Ignored files:', ignores);
85
-
86
- // Ignore "common files"
87
- const filteredSourcePaths = files
88
- .filter(
89
- (source) =>
90
- source.fileName.startsWith(path.resolve()) && // only look ahead of current directory
91
- !source.fileName.includes('/trpc/packages/') && // relative paths when running codemod locally
92
- !source.fileName.includes('/node_modules/') && // always ignore node_modules
93
- !ignores.includes(source.fileName), // ignored files
94
- )
95
- .map((source) => source.fileName);
96
-
97
- yield* Effect.logDebug('Filtered files:', filteredSourcePaths);
98
-
99
- return filteredSourcePaths;
100
- });
101
-
102
- const TSProgram = Effect.succeed(
103
- findConfigFile(process.cwd(), sys.fileExists),
104
- ).pipe(
105
- Effect.filterOrFail(Predicate.isNotNullable, () => 'No tsconfig found'),
106
- Effect.tap((_) => Effect.logDebug('Using tsconfig', _)),
107
- Effect.map((_) => readConfigFile(_, sys.readFile)),
108
- Effect.map((_) => parseJsonConfigFileContent(_.config, sys, process.cwd())),
109
- Effect.map((_) =>
110
- createProgram({
111
- options: _.options,
112
- rootNames: _.fileNames,
113
- configFileParsingDiagnostics: _.errors,
114
- }),
115
- ),
116
- );
6
+ import { findTRPCImportReferences, getProgram } from '../lib/ast/scanners';
7
+ import { assertCleanGitTree, filterIgnored } from '../lib/git';
8
+ import { installPackage, uninstallPackage } from '../lib/pkgmgr';
9
+
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
+ if (args.verbose) process.env.VERBOSE = '1';
117
25
 
118
- // FIXME :: hacky
119
- const transformPath = (path: string) =>
120
- process.env.DEV ? path : path.replace('../', './').replace('.ts', '.cjs');
26
+ intro(`tRPC Upgrade CLI v${version}`);
121
27
 
122
- const force = Options.boolean('force').pipe(
123
- Options.withAlias('f'),
124
- Options.withDefault(false),
125
- Options.withDescription('Skip git status check, use with caution'),
126
- );
28
+ if (args.help) {
29
+ log.info(
30
+ `
31
+ Usage: upgrade [options]
127
32
 
128
- /**
129
- * TODO: Instead of default values these should be detected automatically from the TS program
130
- */
131
- const trpcFile = Options.text('trpcFile').pipe(
132
- Options.withAlias('f'),
133
- Options.withDefault('~/trpc'),
134
- Options.withDescription('Path to the trpc import file'),
135
- );
33
+ Options:
34
+ -f, --force Skip git status check, use with caution
35
+ -q, --skipTanstackQuery Skip installing @trpc/tanstack-react-query package
36
+ -v, --verbose Enable verbose logging
37
+ -h, --help Show help
38
+ `.trim(),
39
+ );
40
+ process.exit(0);
41
+ }
42
+
43
+ if (args.verbose) {
44
+ log.info(`Running upgrade with args: ${JSON.stringify(args, null, 2)}`);
45
+ }
46
+
47
+ if (!args.force) {
48
+ await assertCleanGitTree();
49
+ }
50
+
51
+ const transforms = await multiselect({
52
+ message: 'Select transforms to run',
53
+ options: [
54
+ {
55
+ value: require.resolve('@trpc/upgrade/transforms/hooksToOptions'),
56
+ label: 'Migrate Hooks to xxxOptions API',
57
+ },
58
+ {
59
+ value: require.resolve('@trpc/upgrade/transforms/provider'),
60
+ label: 'Migrate context provider setup',
61
+ },
62
+ ],
63
+ });
64
+ if (isCancel(transforms)) process.exit(0);
136
65
 
137
- const trpcImportName = Options.text('trpcImportName').pipe(
138
- Options.withAlias('i'),
139
- Options.withDefault('trpc'),
140
- Options.withDescription('Name of the trpc import'),
66
+ // Make sure provider transform runs first if it's selected
67
+ const sortedTransforms = transforms.sort((a) =>
68
+ a.includes('provider') ? -1 : 1,
141
69
  );
142
70
 
143
- const skipTanstackQuery = Options.boolean('skipTanstackQuery').pipe(
144
- Options.withAlias('q'),
145
- Options.withDefault(false),
146
- Options.withDescription('Skip installing @trpc/tanstack-react-query package'),
147
- );
71
+ const program = getProgram();
72
+ const sourceFiles = program.getSourceFiles();
73
+ const possibleReferences = findTRPCImportReferences(program);
74
+ const trpcFile = possibleReferences.mostUsed.file;
75
+ const trpcImportName = possibleReferences.importName;
148
76
 
149
- const verbose = Options.boolean('verbose').pipe(
150
- Options.withAlias('v'),
151
- Options.withDefault(false),
152
- Options.withDescription('Enable verbose logging'),
153
- );
77
+ const commitedFiles = await filterIgnored(sourceFiles);
154
78
 
155
- const rootComamnd = CLICommand.make(
156
- 'upgrade',
157
- {
158
- force,
79
+ for (const transform of sortedTransforms) {
80
+ log.step(`Running transform: ${basename(transform, extname(transform))}`);
81
+ const { run } = await import('jscodeshift/src/Runner.js');
82
+ await run(transform, commitedFiles, {
83
+ ...args,
159
84
  trpcFile,
160
85
  trpcImportName,
161
- skipTanstackQuery,
162
- verbose,
163
- },
164
- (args) =>
165
- Effect.gen(function* () {
166
- if (args.verbose) {
167
- yield* Effect.log('Running upgrade with args:', args);
168
- }
169
- if (!args.force) {
170
- yield* assertCleanGitTree;
171
- }
172
- const transforms = yield* pipe(
173
- Prompt.multiSelect({
174
- message: 'Select transforms to run',
175
- choices: [
176
- {
177
- title: 'Migrate Hooks to xxxOptions API',
178
- value: require.resolve(
179
- transformPath('../transforms/hooksToOptions.ts'),
180
- ),
181
- },
182
- {
183
- title: 'Migrate context provider setup',
184
- value: require.resolve(
185
- transformPath('../transforms/provider.ts'),
186
- ),
187
- },
188
- ],
189
- }),
190
- Effect.flatMap((selected) => {
191
- if (selected.length === 0) {
192
- return Effect.fail(
193
- new Error('Please select at least one transform to run'),
194
- );
195
- }
196
- return Effect.succeed(selected);
197
- }),
198
- Effect.map(
199
- // Make sure provider transform runs first if it's selected
200
- Array.sortWith((a) => !a.includes('provider.ts'), Order.boolean),
201
- ),
202
- );
203
-
204
- const program = yield* TSProgram;
205
- const sourceFiles = program.getSourceFiles();
206
-
207
- const possibleReferences = findTRPCImportReferences(program);
208
- const trpcFile = possibleReferences.mostUsed.file;
209
- const trpcImportName = possibleReferences.importName;
210
-
211
- const commitedFiles = yield* filterIgnored(sourceFiles);
212
- yield* Effect.forEach(transforms, (transform) => {
213
- return pipe(
214
- Effect.log('Running transform', transform),
215
- Effect.flatMap(() =>
216
- Effect.tryPromise(async () =>
217
- import('jscodeshift/src/Runner.js').then(({ run }) =>
218
- run(transform, commitedFiles, {
219
- ...args,
220
- trpcFile: trpcFile,
221
- trpcImportName: trpcImportName,
222
- }),
223
- ),
224
- ),
225
- ),
226
- Effect.map((_) => Effect.log('Transform result', _)),
227
- );
228
- });
229
-
230
- if (!args.skipTanstackQuery) {
231
- yield* Effect.log('Installing @trpc/tanstack-react-query');
232
- yield* installPackage('@trpc/tanstack-react-query');
86
+ });
87
+ log.success(`Transform completed`);
88
+ }
233
89
 
234
- yield* Effect.log('Uninstalling @trpc/react-query');
235
- yield* uninstallPackage('@trpc/react-query');
236
- }
237
- }).pipe(
238
- Logger.withMinimumLogLevel(args.verbose ? LogLevel.Debug : LogLevel.Info),
239
- ),
240
- );
90
+ if (!args.skipTanstackQuery) {
91
+ log.info('Installing @trpc/tanstack-react-query');
92
+ await installPackage('@trpc/tanstack-react-query');
93
+ log.success('@trpc/tanstack-react-query installed');
241
94
 
242
- const cli = CLICommand.run(rootComamnd, {
243
- name: 'tRPC Upgrade CLI',
244
- version: `v${version}`,
245
- });
95
+ log.info('Uninstalling @trpc/react-query');
96
+ await uninstallPackage('@trpc/react-query');
97
+ log.success('@trpc/react-query uninstalled');
98
+ }
246
99
 
247
- cli(process.argv).pipe(Effect.provide(NodeContext.layer), NodeRuntime.runMain);
100
+ outro('Upgrade complete! 🎉');
@@ -1,25 +1,45 @@
1
- import {
2
- forEachChild,
3
- isCallExpression,
4
- isIdentifier,
5
- isImportDeclaration,
6
- isStringLiteral,
7
- isVariableDeclaration,
8
- isVariableStatement,
9
- resolveModuleName,
10
- sys,
11
- type Program,
12
- } from 'typescript';
1
+ import * as p from '@clack/prompts';
2
+ import * as ts from 'typescript';
13
3
 
14
- export function findSourceAndImportName(program: Program) {
4
+ export function getProgram() {
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 (process.env.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) {
15
35
  const files = program.getSourceFiles().filter((sourceFile) => {
16
36
  if (sourceFile.isDeclarationFile) return false;
17
37
  let found = false;
18
- forEachChild(sourceFile, (node) => {
19
- if (!found && isImportDeclaration(node)) {
38
+ ts.forEachChild(sourceFile, (node) => {
39
+ if (!found && ts.isImportDeclaration(node)) {
20
40
  const { moduleSpecifier } = node;
21
41
  if (
22
- isStringLiteral(moduleSpecifier) &&
42
+ ts.isStringLiteral(moduleSpecifier) &&
23
43
  moduleSpecifier.text.includes('@trpc/react-query')
24
44
  ) {
25
45
  found = true;
@@ -31,17 +51,17 @@ export function findSourceAndImportName(program: Program) {
31
51
 
32
52
  let importName = 'trpc';
33
53
  files.forEach((sourceFile) => {
34
- forEachChild(sourceFile, (node) => {
54
+ ts.forEachChild(sourceFile, (node) => {
35
55
  if (
36
- isVariableStatement(node) &&
56
+ ts.isVariableStatement(node) &&
37
57
  node.modifiers?.some((mod) => mod.getText(sourceFile) === 'export')
38
58
  ) {
39
59
  node.declarationList.declarations.forEach((declaration) => {
40
60
  if (
41
- isVariableDeclaration(declaration) &&
61
+ ts.isVariableDeclaration(declaration) &&
42
62
  declaration.initializer &&
43
- isCallExpression(declaration.initializer) &&
44
- isIdentifier(declaration.initializer.expression) &&
63
+ ts.isCallExpression(declaration.initializer) &&
64
+ ts.isIdentifier(declaration.initializer.expression) &&
45
65
  declaration.initializer.expression.getText(sourceFile) ===
46
66
  'createTRPCReact'
47
67
  ) {
@@ -58,20 +78,23 @@ export function findSourceAndImportName(program: Program) {
58
78
  };
59
79
  }
60
80
 
61
- export function findTRPCImportReferences(program: Program) {
81
+ export function findTRPCImportReferences(program: ts.Program) {
62
82
  const { files: filesImportingTRPC, importName } =
63
83
  findSourceAndImportName(program);
64
84
  const trpcReferenceSpecifiers = new Map<string, string>();
65
85
 
66
86
  program.getSourceFiles().forEach((sourceFile) => {
67
87
  if (sourceFile.isDeclarationFile) return;
68
- forEachChild(sourceFile, (node) => {
69
- if (isImportDeclaration(node) && isStringLiteral(node.moduleSpecifier)) {
70
- const resolved = resolveModuleName(
88
+ ts.forEachChild(sourceFile, (node) => {
89
+ if (
90
+ ts.isImportDeclaration(node) &&
91
+ ts.isStringLiteral(node.moduleSpecifier)
92
+ ) {
93
+ const resolved = ts.resolveModuleName(
71
94
  node.moduleSpecifier.text,
72
95
  sourceFile.fileName,
73
96
  program.getCompilerOptions(),
74
- sys,
97
+ ts.sys,
75
98
  );
76
99
  if (
77
100
  resolved.resolvedModule &&
@@ -0,0 +1,4 @@
1
+ import { exec } from 'node:child_process';
2
+ import { promisify } from 'node:util';
3
+
4
+ export const execa = promisify(exec);
package/src/lib/git.ts ADDED
@@ -0,0 +1,44 @@
1
+ import { resolve } from 'path';
2
+ import { cancel, log } 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
+ 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
+ log.info(`cwd: ${process.cwd()}`);
22
+ log.info(
23
+ `All files in program: ${files.map((file) => file.fileName).join(', ')}`,
24
+ );
25
+ log.info(`Ignored files: ${ignores.join(', ')}`);
26
+ }
27
+
28
+ // Ignore "common files"
29
+ const filteredSourcePaths = files
30
+ .filter(
31
+ (source) =>
32
+ source.fileName.startsWith(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
+ log.info(`Filtered files: ${filteredSourcePaths.join(', ')}`);
41
+ }
42
+
43
+ return filteredSourcePaths;
44
+ }
@@ -0,0 +1,38 @@
1
+ import { log } 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
+ log.error(stderr);
20
+ }
21
+ if (process.env.VERBOSE) {
22
+ log.info(stdout);
23
+ }
24
+ }
25
+
26
+ export async function uninstallPackage(packageName: string) {
27
+ const packageManager = getPackageManager();
28
+ const uninstallCmd = packageManager === 'yarn' ? 'remove' : 'uninstall';
29
+ const { stdout, stderr } = await execa(
30
+ `${packageManager} ${uninstallCmd} ${packageName}`,
31
+ );
32
+ if (stderr) {
33
+ log.error(stderr);
34
+ }
35
+ if (process.env.VERBOSE) {
36
+ log.info(stdout);
37
+ }
38
+ }
package/dist/bin.cjs DELETED
@@ -1,178 +0,0 @@
1
- #!/usr/bin/env node
2
- var path = require('node: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 = "11.0.0-rc.800+892471d1b";
14
-
15
- function findSourceAndImportName(program) {
16
- const files = program.getSourceFiles().filter((sourceFile)=>{
17
- if (sourceFile.isDeclarationFile) return false;
18
- let found = false;
19
- typescript.forEachChild(sourceFile, (node)=>{
20
- if (!found && typescript.isImportDeclaration(node)) {
21
- const { moduleSpecifier } = node;
22
- if (typescript.isStringLiteral(moduleSpecifier) && moduleSpecifier.text.includes('@trpc/react-query')) {
23
- found = true;
24
- }
25
- }
26
- });
27
- return found;
28
- });
29
- let importName = 'trpc';
30
- files.forEach((sourceFile)=>{
31
- typescript.forEachChild(sourceFile, (node)=>{
32
- if (typescript.isVariableStatement(node) && node.modifiers?.some((mod)=>mod.getText(sourceFile) === 'export')) {
33
- node.declarationList.declarations.forEach((declaration)=>{
34
- if (typescript.isVariableDeclaration(declaration) && declaration.initializer && typescript.isCallExpression(declaration.initializer) && typescript.isIdentifier(declaration.initializer.expression) && declaration.initializer.expression.getText(sourceFile) === 'createTRPCReact') {
35
- importName = declaration.name.getText(sourceFile);
36
- }
37
- });
38
- }
39
- });
40
- });
41
- return {
42
- files: files.map((d)=>d.fileName),
43
- importName
44
- };
45
- }
46
- function findTRPCImportReferences(program) {
47
- const { files: filesImportingTRPC, importName } = findSourceAndImportName(program);
48
- const trpcReferenceSpecifiers = new Map();
49
- program.getSourceFiles().forEach((sourceFile)=>{
50
- if (sourceFile.isDeclarationFile) return;
51
- typescript.forEachChild(sourceFile, (node)=>{
52
- if (typescript.isImportDeclaration(node) && typescript.isStringLiteral(node.moduleSpecifier)) {
53
- const resolved = typescript.resolveModuleName(node.moduleSpecifier.text, sourceFile.fileName, program.getCompilerOptions(), typescript.sys);
54
- if (resolved.resolvedModule && filesImportingTRPC.includes(resolved.resolvedModule.resolvedFileName)) {
55
- trpcReferenceSpecifiers.set(resolved.resolvedModule.resolvedFileName, node.moduleSpecifier.text);
56
- }
57
- }
58
- });
59
- });
60
- const counts = {};
61
- let currentMax = 0;
62
- const mostUsed = {
63
- file: ''
64
- };
65
- [
66
- ...trpcReferenceSpecifiers.values()
67
- ].forEach((specifier)=>{
68
- counts[specifier] = (counts[specifier] || 0) + 1;
69
- if (counts[specifier] > currentMax) {
70
- currentMax = counts[specifier];
71
- mostUsed.file = specifier;
72
- }
73
- });
74
- return {
75
- importName,
76
- mostUsed,
77
- all: Object.fromEntries(trpcReferenceSpecifiers.entries())
78
- };
79
- }
80
-
81
- const MakeCommand = (command, ...args)=>{
82
- return platform.Command.make(command, ...args).pipe(platform.Command.workingDirectory(process.cwd()), platform.Command.runInShell(true));
83
- };
84
- 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`'));
85
- 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'));
86
- const installPackage = (packageName)=>{
87
- const packageManager = getPackageManager();
88
- return platform.Command.streamLines(MakeCommand(packageManager, 'install', packageName)).pipe(effect.Stream.mapEffect(effect.Console.log), effect.Stream.runDrain);
89
- };
90
- const uninstallPackage = (packageName)=>{
91
- const packageManager = getPackageManager();
92
- const uninstallCmd = packageManager === 'yarn' ? 'remove' : 'uninstall';
93
- return platform.Command.streamLines(MakeCommand(packageManager, uninstallCmd, packageName)).pipe(effect.Stream.mapEffect(effect.Console.log), effect.Stream.runDrain);
94
- };
95
- const filterIgnored = (files)=>effect.Effect.gen(function*() {
96
- const ignores = yield* platform.Command.string(MakeCommand('git', 'check-ignore', '**/*').pipe(platform.Command.runInShell(true))).pipe(effect.Effect.tap((_)=>effect.Effect.logDebug('Ignored files output:', _)), effect.Effect.map((_)=>_.split('\n')));
97
- yield* effect.Effect.logDebug('cwd:', process.cwd());
98
- yield* effect.Effect.logDebug('All files in program:', files.map((_)=>_.fileName));
99
- yield* effect.Effect.logDebug('Ignored files:', ignores);
100
- // Ignore "common files"
101
- const filteredSourcePaths = files.filter((source)=>source.fileName.startsWith(path__default.default.resolve()) && // only look ahead of current directory
102
- !source.fileName.includes('/trpc/packages/') && // relative paths when running codemod locally
103
- !source.fileName.includes('/node_modules/') && // always ignore node_modules
104
- !ignores.includes(source.fileName)).map((source)=>source.fileName);
105
- yield* effect.Effect.logDebug('Filtered files:', filteredSourcePaths);
106
- return filteredSourcePaths;
107
- });
108
- 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({
109
- options: _.options,
110
- rootNames: _.fileNames,
111
- configFileParsingDiagnostics: _.errors
112
- })));
113
- // FIXME :: hacky
114
- const transformPath = (path)=>process.env.DEV ? path : path.replace('../', './').replace('.ts', '.cjs');
115
- 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'));
116
- /**
117
- * TODO: Instead of default values these should be detected automatically from the TS program
118
- */ 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'));
119
- 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'));
120
- 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'));
121
- 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'));
122
- const rootComamnd = cli$1.Command.make('upgrade', {
123
- force,
124
- trpcFile,
125
- trpcImportName,
126
- skipTanstackQuery,
127
- verbose
128
- }, (args)=>effect.Effect.gen(function*() {
129
- if (args.verbose) {
130
- yield* effect.Effect.log('Running upgrade with args:', args);
131
- }
132
- if (!args.force) {
133
- yield* assertCleanGitTree;
134
- }
135
- const transforms = yield* effect.pipe(cli$1.Prompt.multiSelect({
136
- message: 'Select transforms to run',
137
- choices: [
138
- {
139
- title: 'Migrate Hooks to xxxOptions API',
140
- value: require.resolve(transformPath('../transforms/hooksToOptions.ts'))
141
- },
142
- {
143
- title: 'Migrate context provider setup',
144
- value: require.resolve(transformPath('../transforms/provider.ts'))
145
- }
146
- ]
147
- }), effect.Effect.flatMap((selected)=>{
148
- if (selected.length === 0) {
149
- return effect.Effect.fail(new Error('Please select at least one transform to run'));
150
- }
151
- return effect.Effect.succeed(selected);
152
- }), effect.Effect.map(// Make sure provider transform runs first if it's selected
153
- effect.Array.sortWith((a)=>!a.includes('provider.ts'), effect.Order.boolean)));
154
- const program = yield* TSProgram;
155
- const sourceFiles = program.getSourceFiles();
156
- const possibleReferences = findTRPCImportReferences(program);
157
- const trpcFile = possibleReferences.mostUsed.file;
158
- const trpcImportName = possibleReferences.importName;
159
- const commitedFiles = yield* filterIgnored(sourceFiles);
160
- yield* effect.Effect.forEach(transforms, (transform)=>{
161
- 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, {
162
- ...args,
163
- trpcFile: trpcFile,
164
- trpcImportName: trpcImportName
165
- })))), effect.Effect.map((_)=>effect.Effect.log('Transform result', _)));
166
- });
167
- if (!args.skipTanstackQuery) {
168
- yield* effect.Effect.log('Installing @trpc/tanstack-react-query');
169
- yield* installPackage('@trpc/tanstack-react-query');
170
- yield* effect.Effect.log('Uninstalling @trpc/react-query');
171
- yield* uninstallPackage('@trpc/react-query');
172
- }
173
- }).pipe(effect.Logger.withMinimumLogLevel(args.verbose ? effect.LogLevel.Debug : effect.LogLevel.Info)));
174
- const cli = cli$1.Command.run(rootComamnd, {
175
- name: 'tRPC Upgrade CLI',
176
- version: `v${version}`
177
- });
178
- cli(process.argv).pipe(effect.Effect.provide(platformNode.NodeContext.layer), platformNode.NodeRuntime.runMain);