@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 +3 -0
- package/dist/bin.js +227 -0
- package/dist/transforms/{hooksToOptions.cjs → hooksToOptions.js} +50 -10
- package/dist/transforms/{provider.cjs → provider.js} +1 -4
- package/package.json +9 -10
- package/src/bin/index.ts +99 -0
- package/src/lib/ast/modifiers.ts +28 -0
- package/src/lib/ast/scanners.ts +129 -0
- package/src/lib/ast/walkers.ts +17 -0
- package/src/lib/execa.ts +4 -0
- package/src/lib/git.ts +44 -0
- package/src/lib/pkgmgr.ts +34 -0
- package/src/transforms/hooksToOptions.ts +35 -6
- package/dist/cli.cjs +0 -78
- package/src/bin/cli.ts +0 -180
package/README.md
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
"version": "0.0.0-alpha.31",
|
|
4
4
|
"description": "Upgrade scripts for tRPC",
|
|
5
5
|
"author": "juliusmarminge",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"
|
|
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
|
-
"
|
|
17
|
+
"default": "./dist/transforms/hooksToOptions.js"
|
|
17
18
|
},
|
|
18
19
|
"./transforms/provider": {
|
|
19
|
-
"
|
|
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
|
-
"@
|
|
32
|
-
"@
|
|
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": "
|
|
42
|
-
"esbuild": "0.
|
|
40
|
+
"bunchee": "6.4.0",
|
|
41
|
+
"esbuild": "0.25.0",
|
|
43
42
|
"tsx": "^4.0.0"
|
|
44
43
|
},
|
|
45
44
|
"publishConfig": {
|
package/src/bin/index.ts
ADDED
|
@@ -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
|
+
}
|
package/src/lib/execa.ts
ADDED
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 =
|
|
209
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|