-
Notifications
You must be signed in to change notification settings - Fork 437
Expand file tree
/
Copy pathcli.js
More file actions
358 lines (306 loc) · 11.3 KB
/
cli.js
File metadata and controls
358 lines (306 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
#!/usr/bin/env node
import meow from 'meow';
import { getAvailableReleases, getOldPackageName, getTargetPackageName, loadConfig } from './config.js';
import {
createSpinner,
promptConfirm,
promptSelect,
renderComplete,
renderConfig,
renderError,
renderHeader,
renderNewline,
renderScanResults,
renderSuccess,
renderText,
renderWarning,
} from './render.js';
import { runCodemods, runScans } from './runner.js';
import {
detectSdk,
findWorkspaceRoot,
getSdkVersion,
getSdkVersionFromWorkspaces,
getSupportedSdks,
normalizeSdkName,
} from './util/detect-sdk.js';
import {
detectPackageManager,
getInstallCommand,
getPackageManagerDisplayName,
hasPackage,
removePackage,
upgradePackage,
} from './util/package-manager.js';
const isInteractive = process.stdin.isTTY;
const cli = meow(
`
Usage
$ npx @clerk/upgrade
Options
--sdk Name of the SDK you're upgrading (e.g., nextjs, react)
--dir Directory to scan (defaults to current directory)
--glob Glob pattern for files to transform (defaults to **/*.{js,jsx,ts,tsx,mjs,cjs})
--ignore Directories/files to ignore (can be used multiple times)
--skip-upgrade Skip the upgrade step
--release Name of the release you're upgrading to (e.g. core-3)
--canary Upgrade to the latest canary version instead of the stable release
--dry-run Show what would be done without making changes
Examples
$ npx @clerk/upgrade
$ npx @clerk/upgrade --sdk=nextjs
$ npx @clerk/upgrade --dir=./src --ignore=**/test/**
$ npx @clerk/upgrade --canary
$ npx @clerk/upgrade --dry-run
Non-interactive mode (CI):
When running in CI or piped environments, --sdk is required if it cannot be auto-detected.
If your version cannot be resolved (e.g. catalog: protocol), also provide --release.
Example:
$ npx @clerk/upgrade --sdk=nextjs --release=core-3 --dir=./packages/web
`,
{
importMeta: import.meta,
flags: {
dir: { type: 'string', default: process.cwd() },
dryRun: { type: 'boolean', default: false },
glob: { type: 'string', default: '**/*.(js|jsx|ts|tsx|mjs|cjs)' },
ignore: { type: 'string', isMultiple: true },
release: { type: 'string' },
sdk: { type: 'string' },
canary: { type: 'boolean', default: false },
skipCodemods: { type: 'boolean', default: false },
skipUpgrade: { type: 'boolean', default: false },
},
},
);
async function main() {
renderHeader();
const options = {
canary: cli.flags.canary,
dir: cli.flags.dir,
dryRun: cli.flags.dryRun,
glob: cli.flags.glob,
ignore: cli.flags.ignore,
release: cli.flags.release,
skipCodemods: cli.flags.skipCodemods,
skipUpgrade: cli.flags.skipUpgrade,
};
if (options.dryRun) {
renderWarning(' Upgrade running in dry run mode - no changes will be made');
renderNewline();
}
// Step 1: Detect or prompt for SDK
let sdk = normalizeSdkName(cli.flags.sdk);
if (!sdk) {
sdk = detectSdk(options.dir);
}
if (!sdk) {
const isWorkspace = !!findWorkspaceRoot(options.dir);
if (!isInteractive) {
renderError('Could not detect Clerk SDK. Please provide --sdk flag in non-interactive mode.');
if (isWorkspace) {
renderText('');
renderText('It looks like you are in a monorepo. Try pointing to a specific workspace package:');
renderText(' npx @clerk/upgrade --dir=./apps/web');
renderText('');
renderText('Or specify the SDK directly:');
renderText(' npx @clerk/upgrade --sdk=nextjs');
} else {
renderText(
'Supported SDKs: ' +
getSupportedSdks()
.map(s => s.value)
.join(', '),
);
renderText('');
renderText('Example: npx @clerk/upgrade --sdk=nextjs');
}
process.exit(1);
}
const sdkOptions = getSupportedSdks().map(s => ({
label: s.label,
value: s.value,
}));
sdk = await promptSelect('Could not detect Clerk SDK. Please select which SDK you are upgrading:', sdkOptions);
}
if (!sdk) {
renderError('No SDK selected. Exiting.');
process.exit(1);
}
// Step 2: Get current version and detect package manager
const currentVersion = getSdkVersion(sdk, options.dir) ?? getSdkVersionFromWorkspaces(sdk, options.dir);
const packageManager = detectPackageManager(options.dir);
// Step 3: If version couldn't be detected and no release specified, prompt user
let release = options.release;
if (currentVersion === null && !release) {
const availableReleases = getAvailableReleases();
if (availableReleases.length === 0) {
renderError('No upgrade configurations found.');
process.exit(1);
}
const isWorkspace = !!findWorkspaceRoot(options.dir);
renderWarning(
`Could not detect your @clerk/${sdk} version (you may be using workspace:, catalog:, or a non-standard version specifier).`,
);
renderNewline();
if (!isInteractive) {
if (isWorkspace) {
renderText('It looks like you are in a monorepo. Try pointing to a specific workspace package:');
renderText(` npx @clerk/upgrade --dir=./apps/web`);
renderText('');
renderText('Or specify the release directly:');
renderText(` npx @clerk/upgrade --sdk=${sdk} --release=${availableReleases[0]}`);
} else {
renderError('Could not detect version. Please provide --release flag in non-interactive mode.');
renderText('Available releases: ' + availableReleases.join(', '));
renderText('');
renderText(`Example: npx @clerk/upgrade --sdk=${sdk} --release=${availableReleases[0]}`);
}
process.exit(1);
}
const releaseOptions = availableReleases.map(r => ({
label: r.replace('-', ' ').replace(/\b\w/g, c => c.toUpperCase()),
value: r,
}));
release = await promptSelect('Which upgrade would you like to perform?', releaseOptions);
if (!release) {
renderError('No release selected. Exiting.');
process.exit(1);
}
renderNewline();
}
// Step 4: Load version config
const config = await loadConfig(sdk, currentVersion, release);
if (!config) {
const isWorkspace = !!findWorkspaceRoot(options.dir);
renderError(`No upgrade path found for @clerk/${sdk}.`);
if (isWorkspace) {
renderText('');
renderText('It looks like you are in a monorepo. Try pointing to a specific workspace package:');
renderText(' npx @clerk/upgrade --dir=./apps/web');
renderText('');
renderText('Or specify the SDK and release directly:');
renderText(` npx @clerk/upgrade --sdk=nextjs --release=${getAvailableReleases()[0] || 'core-3'}`);
} else {
renderText('Your version may be too old for this upgrade tool.');
}
process.exit(1);
}
// Step 5: Display configuration
renderConfig({
sdk,
currentVersion,
fromVersion: config.sdkVersions?.[sdk]?.from,
toVersion: config.sdkVersions?.[sdk]?.to,
versionName: config.name,
dir: options.dir,
packageManager: getPackageManagerDisplayName(packageManager),
});
if (isInteractive && !(await promptConfirm('Ready to upgrade?', true))) {
renderError('Upgrade cancelled. Exiting...');
process.exit(0);
}
console.log('');
// Step 6: Handle upgrade status
if (options.skipUpgrade) {
renderText('Skipping package upgrade (--skip-upgrade flag)', 'yellow');
renderNewline();
} else if (config.alreadyUpgraded && !options.canary) {
renderSuccess(`You're already on the latest major version of @clerk/${sdk}`);
} else if (config.needsUpgrade || options.canary) {
await performUpgrade(sdk, packageManager, config, options);
}
// Step 6b: Handle package replacements
if (config.packageReplacements?.length > 0 && !options.skipUpgrade) {
await performPackageReplacements(packageManager, config, options);
}
// Step 7: Run codemods
if (config.codemods?.length > 0) {
renderText(`Running ${config.codemods.length} codemod(s)...`, 'blue');
await runCodemods(config, sdk, options);
renderSuccess('All codemods applied');
renderNewline();
}
// Step 8: Run scans
if (config.changes?.length > 0) {
renderText('Scanning for additional breaking changes...', 'blue');
const results = await runScans(config, sdk, options);
renderScanResults(results, config.docsUrl);
}
// Step 9: Done
renderComplete(sdk, config.docsUrl);
}
async function performUpgrade(sdk, packageManager, config, options) {
const targetPackage = getTargetPackageName(sdk);
const oldPackage = getOldPackageName(sdk);
const targetVersion = options.canary ? 'canary' : config.sdkVersions?.[sdk]?.to;
if (options.dryRun) {
renderText(`[dry run] Would upgrade ${targetPackage} to version ${targetVersion}`, 'yellow');
if (oldPackage) {
renderText(`[dry run] Would remove old package ${oldPackage}`, 'yellow');
}
renderNewline();
return;
}
// Remove old package if this is a rename (clerk-react -> react, clerk-expo -> expo)
if (oldPackage) {
const removeSpinner = createSpinner(`Removing ${oldPackage}...`);
try {
await removePackage(packageManager, oldPackage, options.dir);
removeSpinner.success(`Removed ${oldPackage}`);
} catch {
removeSpinner.error(`Failed to remove ${oldPackage}`);
}
}
// Upgrade to the new version
const spinner = createSpinner(`Upgrading ${targetPackage} to version ${targetVersion}...`);
try {
await upgradePackage(packageManager, targetPackage, targetVersion, options.dir);
spinner.success(`Upgraded ${targetPackage} to version ${targetVersion}`);
} catch (error) {
spinner.error(`Failed to upgrade ${targetPackage}`);
renderError(error.message);
process.exit(1);
}
}
async function performPackageReplacements(packageManager, config, options) {
const replacements = config.packageReplacements;
if (!replacements?.length) {
return;
}
for (const { from, to } of replacements) {
if (!hasPackage(from, options.dir)) {
continue;
}
const targetVersion = options.canary ? 'canary' : 'latest';
if (options.dryRun) {
renderText(`[dry run] Would replace ${from} with ${to}@${targetVersion}`, 'yellow');
continue;
}
const removeSpinner = createSpinner(`Removing ${from}...`);
try {
await removePackage(packageManager, from, options.dir);
removeSpinner.success(`Removed ${from}`);
} catch (error) {
removeSpinner.error(`Failed to remove ${from}`);
renderError(error.message);
renderWarning(`You may need to manually remove ${from} and install ${to}`);
continue;
}
const installSpinner = createSpinner(`Installing ${to}@${targetVersion}...`);
try {
await upgradePackage(packageManager, to, targetVersion, options.dir);
installSpinner.success(`Installed ${to}@${targetVersion}`);
} catch (error) {
installSpinner.error(`Failed to install ${to}`);
renderError(error.message);
const [cmd, args] = getInstallCommand(packageManager, to, targetVersion, options.dir);
renderWarning(`${from} was removed but ${to} could not be installed. Please run: ${cmd} ${args.join(' ')}`);
throw new Error(`Package replacement failed: ${from} -> ${to}`);
}
}
}
main().catch(error => {
renderError(error.message);
process.exit(1);
});