@clerk/upgrade 2.0.0-snapshot.v20251204175016 → 2.0.0-snapshot.v20251208202852
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/__tests__/fixtures/expo-old-package/package-lock.json +5 -0
- package/dist/__tests__/fixtures/expo-old-package/package.json +10 -0
- package/dist/__tests__/fixtures/expo-old-package/src/App.tsx +14 -0
- package/dist/__tests__/fixtures/nextjs-v6/package.json +9 -0
- package/dist/__tests__/fixtures/nextjs-v6/pnpm-lock.yaml +2 -0
- package/dist/__tests__/fixtures/nextjs-v6/src/app.tsx +17 -0
- package/dist/__tests__/fixtures/nextjs-v7/package.json +9 -0
- package/dist/__tests__/fixtures/nextjs-v7/pnpm-lock.yaml +2 -0
- package/dist/__tests__/fixtures/nextjs-v7/src/app.tsx +16 -0
- package/dist/__tests__/fixtures/no-clerk/package.json +7 -0
- package/dist/__tests__/fixtures/react-v6/package.json +8 -0
- package/dist/__tests__/fixtures/react-v6/src/App.tsx +19 -0
- package/dist/__tests__/fixtures/react-v6/yarn.lock +2 -0
- package/dist/__tests__/helpers/create-fixture.js +56 -0
- package/dist/__tests__/integration/cli.test.js +230 -0
- package/dist/__tests__/integration/config.test.js +76 -0
- package/dist/__tests__/integration/detect-sdk.test.js +100 -0
- package/dist/__tests__/integration/runner.test.js +79 -0
- package/dist/cli.js +159 -45
- package/dist/codemods/__tests__/__fixtures__/transform-align-experimental-unstable-prefixes.fixtures.js +68 -0
- package/dist/codemods/__tests__/__fixtures__/transform-appearance-layout-to-options.fixtures.js +9 -0
- package/dist/codemods/__tests__/__fixtures__/transform-clerk-react-v6.fixtures.js +13 -0
- package/dist/codemods/__tests__/__fixtures__/transform-remove-deprecated-appearance-props.fixtures.js +63 -0
- package/dist/codemods/__tests__/__fixtures__/transform-themes-to-ui-themes.fixtures.js +41 -0
- package/dist/codemods/__tests__/transform-align-experimental-unstable-prefixes.test.js +15 -0
- package/dist/codemods/__tests__/transform-appearance-layout-to-options.test.js +15 -0
- package/dist/codemods/__tests__/transform-remove-deprecated-appearance-props.test.js +15 -0
- package/dist/codemods/__tests__/transform-themes-to-ui-themes.test.js +15 -0
- package/dist/codemods/index.js +67 -13
- package/dist/codemods/transform-align-experimental-unstable-prefixes.cjs +400 -0
- package/dist/codemods/transform-appearance-layout-to-options.cjs +65 -0
- package/dist/codemods/transform-clerk-react-v6.cjs +15 -7
- package/dist/codemods/transform-remove-deprecated-appearance-props.cjs +109 -0
- package/dist/codemods/transform-remove-deprecated-props.cjs +11 -32
- package/dist/codemods/transform-themes-to-ui-themes.cjs +65 -0
- package/dist/config.js +122 -0
- package/dist/render.js +164 -0
- package/dist/runner.js +98 -0
- package/dist/util/detect-sdk.js +125 -0
- package/dist/util/package-manager.js +94 -0
- package/dist/versions/core-3/changes/clerk-expo-package-rename.md +23 -0
- package/dist/versions/core-3/changes/clerk-react-package-rename.md +23 -0
- package/dist/versions/core-3/index.js +40 -0
- package/package.json +2 -8
- package/dist/app.js +0 -177
- package/dist/components/Codemod.js +0 -149
- package/dist/components/Command.js +0 -56
- package/dist/components/Header.js +0 -11
- package/dist/components/SDKWorkflow.js +0 -278
- package/dist/components/Scan.js +0 -180
- package/dist/components/UpgradeSDK.js +0 -116
- package/dist/util/expandable-list.js +0 -173
- package/dist/util/get-clerk-version.js +0 -22
- package/dist/util/guess-framework.js +0 -69
|
@@ -20,14 +20,13 @@ const COMPONENT_REDIRECT_ATTR = new Map([['ClerkProvider', {
|
|
|
20
20
|
const COMPONENTS_WITH_USER_BUTTON_REMOVALS = new Map([['UserButton', ['afterSignOutUrl', 'afterMultiSessionSingleSignOutUrl']]]);
|
|
21
21
|
const ORGANIZATION_SWITCHER_RENAMES = new Map([['afterSwitchOrganizationUrl', 'afterSelectOrganizationUrl']]);
|
|
22
22
|
module.exports = function transformDeprecatedProps({
|
|
23
|
-
source
|
|
24
|
-
path: filePath
|
|
23
|
+
source
|
|
25
24
|
}, {
|
|
26
|
-
jscodeshift: j
|
|
27
|
-
|
|
25
|
+
jscodeshift: j,
|
|
26
|
+
stats
|
|
27
|
+
}) {
|
|
28
28
|
const root = j(source);
|
|
29
29
|
let dirty = false;
|
|
30
|
-
const stats = options.clerkUpgradeStats;
|
|
31
30
|
const {
|
|
32
31
|
namedImports,
|
|
33
32
|
namespaceImports
|
|
@@ -41,29 +40,15 @@ module.exports = function transformDeprecatedProps({
|
|
|
41
40
|
if (COMPONENTS_WITH_HIDE_SLUG.has(canonicalName)) {
|
|
42
41
|
if (removeJsxAttribute(j, jsxNode, 'hideSlug')) {
|
|
43
42
|
dirty = true;
|
|
44
|
-
|
|
45
|
-
stats.hideSlugRemoved = (stats.hideSlugRemoved || 0) + 1;
|
|
46
|
-
stats.hideSlugFiles = stats.hideSlugFiles || [];
|
|
47
|
-
if (!stats.hideSlugFiles.includes(filePath)) {
|
|
48
|
-
stats.hideSlugFiles.push(filePath);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
43
|
+
stats('hideSlugRemoved');
|
|
51
44
|
}
|
|
52
45
|
}
|
|
53
46
|
if (COMPONENTS_WITH_USER_BUTTON_REMOVALS.has(canonicalName)) {
|
|
54
47
|
const propsToRemove = COMPONENTS_WITH_USER_BUTTON_REMOVALS.get(canonicalName);
|
|
55
|
-
let removedCount = 0;
|
|
56
48
|
for (const attrName of propsToRemove) {
|
|
57
49
|
if (removeJsxAttribute(j, jsxNode, attrName)) {
|
|
58
50
|
dirty = true;
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (removedCount > 0 && stats) {
|
|
63
|
-
stats.userbuttonAfterSignOutPropsRemoved = (stats.userbuttonAfterSignOutPropsRemoved || 0) + removedCount;
|
|
64
|
-
stats.userbuttonFilesAffected = stats.userbuttonFilesAffected || [];
|
|
65
|
-
if (!stats.userbuttonFilesAffected.includes(filePath)) {
|
|
66
|
-
stats.userbuttonFilesAffected.push(filePath);
|
|
51
|
+
stats('userbuttonAfterSignOutPropsRemoved');
|
|
67
52
|
}
|
|
68
53
|
}
|
|
69
54
|
}
|
|
@@ -115,7 +100,7 @@ module.exports = function transformDeprecatedProps({
|
|
|
115
100
|
if (renameObjectProperties(root, j, 'activeSessions', 'signedInSessions')) {
|
|
116
101
|
dirty = true;
|
|
117
102
|
}
|
|
118
|
-
if (transformSetActiveBeforeEmit(root, j, stats
|
|
103
|
+
if (transformSetActiveBeforeEmit(root, j, stats)) {
|
|
119
104
|
dirty = true;
|
|
120
105
|
}
|
|
121
106
|
if (renameTypeReferences(root, j, 'ClerkMiddlewareAuthObject', 'ClerkMiddlewareSessionAuthObject')) {
|
|
@@ -338,7 +323,7 @@ function renameTSPropertySignatures(root, j, oldName, newName) {
|
|
|
338
323
|
});
|
|
339
324
|
return changed;
|
|
340
325
|
}
|
|
341
|
-
function transformSetActiveBeforeEmit(root, j, stats
|
|
326
|
+
function transformSetActiveBeforeEmit(root, j, stats) {
|
|
342
327
|
let changed = false;
|
|
343
328
|
root.find(j.CallExpression).filter(path => isSetActiveCall(path.node.callee)).forEach(path => {
|
|
344
329
|
const [args0] = path.node.arguments;
|
|
@@ -362,13 +347,7 @@ function transformSetActiveBeforeEmit(root, j, stats, filePath) {
|
|
|
362
347
|
const navigateProp = j.objectProperty(j.identifier('navigate'), buildNavigateArrowFunction(j, originalValue));
|
|
363
348
|
args0.properties.splice(beforeEmitIndex, 1, navigateProp);
|
|
364
349
|
changed = true;
|
|
365
|
-
|
|
366
|
-
stats.beforeEmitTransformed = (stats.beforeEmitTransformed || 0) + 1;
|
|
367
|
-
stats.beforeEmitFiles = stats.beforeEmitFiles || [];
|
|
368
|
-
if (!stats.beforeEmitFiles.includes(filePath)) {
|
|
369
|
-
stats.beforeEmitFiles.push(filePath);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
350
|
+
stats('beforeEmitTransformed');
|
|
372
351
|
});
|
|
373
352
|
return changed;
|
|
374
353
|
}
|
|
@@ -399,8 +378,8 @@ function getPropertyValueExpression(valueNode) {
|
|
|
399
378
|
}
|
|
400
379
|
function buildNavigateArrowFunction(j, originalExpression) {
|
|
401
380
|
const paramIdentifier = j.identifier('params');
|
|
402
|
-
|
|
403
|
-
const callExpression = j.callExpression(
|
|
381
|
+
// No need to clone - we're moving the expression from beforeEmit to navigate
|
|
382
|
+
const callExpression = j.callExpression(originalExpression, [j.memberExpression(paramIdentifier, j.identifier('session'))]);
|
|
404
383
|
return j.arrowFunctionExpression([paramIdentifier], callExpression);
|
|
405
384
|
}
|
|
406
385
|
function renameTypeReferences(root, j, oldName, newName) {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const LEGACY_PACKAGE = '@clerk/themes';
|
|
2
|
+
const TARGET_PACKAGE = '@clerk/ui/themes';
|
|
3
|
+
const isStringLiteral = (j, node) => j.Literal.check(node) && typeof node.value === 'string' || j.StringLiteral && j.StringLiteral.check(node);
|
|
4
|
+
const getReplacement = value => {
|
|
5
|
+
if (typeof value !== 'string' || !value.startsWith(LEGACY_PACKAGE)) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
return `${TARGET_PACKAGE}${value.slice(LEGACY_PACKAGE.length)}`;
|
|
9
|
+
};
|
|
10
|
+
module.exports = function transformThemesToUiThemes({
|
|
11
|
+
source
|
|
12
|
+
}, {
|
|
13
|
+
jscodeshift: j
|
|
14
|
+
}) {
|
|
15
|
+
const root = j(source);
|
|
16
|
+
let dirty = false;
|
|
17
|
+
const replaceSourceLiteral = literal => {
|
|
18
|
+
if (!isStringLiteral(j, literal)) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const nextValue = getReplacement(literal.value);
|
|
22
|
+
if (nextValue && nextValue !== literal.value) {
|
|
23
|
+
literal.value = nextValue;
|
|
24
|
+
dirty = true;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
root.find(j.ImportDeclaration).forEach(path => {
|
|
28
|
+
replaceSourceLiteral(path.node.source);
|
|
29
|
+
});
|
|
30
|
+
root.find(j.ExportNamedDeclaration).forEach(path => {
|
|
31
|
+
if (path.node.source) {
|
|
32
|
+
replaceSourceLiteral(path.node.source);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
root.find(j.ExportAllDeclaration).forEach(path => {
|
|
36
|
+
replaceSourceLiteral(path.node.source);
|
|
37
|
+
});
|
|
38
|
+
root.find(j.CallExpression, {
|
|
39
|
+
callee: {
|
|
40
|
+
name: 'require'
|
|
41
|
+
}
|
|
42
|
+
}).forEach(path => {
|
|
43
|
+
const [arg] = path.node.arguments || [];
|
|
44
|
+
if (arg) {
|
|
45
|
+
replaceSourceLiteral(arg);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
if (j.ImportExpression) {
|
|
49
|
+
root.find(j.ImportExpression).forEach(path => {
|
|
50
|
+
replaceSourceLiteral(path.node.source);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
root.find(j.CallExpression, {
|
|
54
|
+
callee: {
|
|
55
|
+
type: 'Import'
|
|
56
|
+
}
|
|
57
|
+
}).forEach(path => {
|
|
58
|
+
const [arg] = path.node.arguments || [];
|
|
59
|
+
if (arg) {
|
|
60
|
+
replaceSourceLiteral(arg);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
return dirty ? root.toSource() : undefined;
|
|
64
|
+
};
|
|
65
|
+
module.exports.parser = 'tsx';
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
4
|
+
import matter from 'gray-matter';
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const VERSIONS_DIR = path.join(__dirname, 'versions');
|
|
7
|
+
export async function loadConfig(sdk, currentVersion) {
|
|
8
|
+
const versionDirs = fs.readdirSync(VERSIONS_DIR, {
|
|
9
|
+
withFileTypes: true
|
|
10
|
+
}).filter(d => d.isDirectory()).map(d => d.name);
|
|
11
|
+
let applicableConfig = null;
|
|
12
|
+
for (const versionDir of versionDirs) {
|
|
13
|
+
const configPath = path.join(VERSIONS_DIR, versionDir, 'index.js');
|
|
14
|
+
if (!fs.existsSync(configPath)) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const moduleUrl = pathToFileURL(configPath).href;
|
|
18
|
+
const mod = await import(moduleUrl);
|
|
19
|
+
const config = mod.default ?? mod;
|
|
20
|
+
if (!config.sdkVersions) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const versionStatus = getVersionStatus(config, sdk, currentVersion);
|
|
24
|
+
if (versionStatus === 'unsupported' || versionStatus === 'unknown') {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (versionStatus === 'needs-upgrade') {
|
|
28
|
+
const changes = loadChanges(versionDir, sdk);
|
|
29
|
+
return {
|
|
30
|
+
...config,
|
|
31
|
+
changes,
|
|
32
|
+
versionStatus,
|
|
33
|
+
needsUpgrade: true,
|
|
34
|
+
alreadyUpgraded: false
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (versionStatus === 'already-upgraded' && !applicableConfig) {
|
|
38
|
+
applicableConfig = {
|
|
39
|
+
config,
|
|
40
|
+
versionDir
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (applicableConfig) {
|
|
45
|
+
const changes = loadChanges(applicableConfig.versionDir, sdk);
|
|
46
|
+
return {
|
|
47
|
+
...applicableConfig.config,
|
|
48
|
+
changes,
|
|
49
|
+
versionStatus: 'already-upgraded',
|
|
50
|
+
needsUpgrade: false,
|
|
51
|
+
alreadyUpgraded: true
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function getVersionStatus(config, sdk, currentVersion) {
|
|
57
|
+
if (!config?.sdkVersions) {
|
|
58
|
+
return 'unknown';
|
|
59
|
+
}
|
|
60
|
+
const range = config.sdkVersions[sdk];
|
|
61
|
+
if (!range) {
|
|
62
|
+
return 'unknown';
|
|
63
|
+
}
|
|
64
|
+
if (typeof currentVersion !== 'number') {
|
|
65
|
+
return 'unknown';
|
|
66
|
+
}
|
|
67
|
+
if (typeof range.from === 'number' && currentVersion < range.from) {
|
|
68
|
+
return 'unsupported';
|
|
69
|
+
}
|
|
70
|
+
if (typeof range.to === 'number' && currentVersion >= range.to) {
|
|
71
|
+
return 'already-upgraded';
|
|
72
|
+
}
|
|
73
|
+
return 'needs-upgrade';
|
|
74
|
+
}
|
|
75
|
+
export function getTargetPackageName(sdk) {
|
|
76
|
+
if (sdk === 'clerk-react' || sdk === 'react') {
|
|
77
|
+
return '@clerk/react';
|
|
78
|
+
}
|
|
79
|
+
if (sdk === 'clerk-expo' || sdk === 'expo') {
|
|
80
|
+
return '@clerk/expo';
|
|
81
|
+
}
|
|
82
|
+
return `@clerk/${sdk}`;
|
|
83
|
+
}
|
|
84
|
+
export function getOldPackageName(sdk) {
|
|
85
|
+
if (sdk === 'clerk-react' || sdk === 'react') {
|
|
86
|
+
return '@clerk/clerk-react';
|
|
87
|
+
}
|
|
88
|
+
if (sdk === 'clerk-expo' || sdk === 'expo') {
|
|
89
|
+
return '@clerk/clerk-expo';
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function loadChanges(versionDir, sdk) {
|
|
94
|
+
const changesDir = path.join(VERSIONS_DIR, versionDir, 'changes');
|
|
95
|
+
if (!fs.existsSync(changesDir)) {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
const files = fs.readdirSync(changesDir).filter(f => f.endsWith('.md'));
|
|
99
|
+
const changes = [];
|
|
100
|
+
for (const file of files) {
|
|
101
|
+
const filePath = path.join(changesDir, file);
|
|
102
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
103
|
+
const parsed = matter(content);
|
|
104
|
+
const fm = parsed.data;
|
|
105
|
+
const packages = fm.packages || ['*'];
|
|
106
|
+
const appliesToSdk = packages.includes('*') || packages.includes(sdk);
|
|
107
|
+
if (!appliesToSdk) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const matcher = fm.matcher ? Array.isArray(fm.matcher) ? fm.matcher.map(m => new RegExp(m, `g${fm.matcherFlags || ''}`)) : new RegExp(fm.matcher, `g${fm.matcherFlags || ''}`) : null;
|
|
111
|
+
changes.push({
|
|
112
|
+
title: fm.title,
|
|
113
|
+
matcher,
|
|
114
|
+
packages,
|
|
115
|
+
category: fm.category || 'breaking',
|
|
116
|
+
warning: fm.warning || fm.category === 'warning',
|
|
117
|
+
docsAnchor: fm.docsAnchor || file.replace('.md', ''),
|
|
118
|
+
content: parsed.content
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
return changes;
|
|
122
|
+
}
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as readline from 'node:readline';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
export function renderHeader() {
|
|
4
|
+
console.log('');
|
|
5
|
+
console.log(chalk.magenta.bold('>> Clerk Upgrade CLI <<'));
|
|
6
|
+
console.log('');
|
|
7
|
+
}
|
|
8
|
+
export function renderText(message, color) {
|
|
9
|
+
const colorFn = chalk[color] || (s => s);
|
|
10
|
+
console.log(colorFn(message));
|
|
11
|
+
}
|
|
12
|
+
export function renderSuccess(message) {
|
|
13
|
+
console.log(chalk.green(`✅ ${message}`));
|
|
14
|
+
}
|
|
15
|
+
export function renderError(message) {
|
|
16
|
+
console.error(chalk.red(`⛔ ${message}`));
|
|
17
|
+
}
|
|
18
|
+
export function renderWarning(message) {
|
|
19
|
+
console.log(chalk.yellow(`⚠️ ${message}`));
|
|
20
|
+
}
|
|
21
|
+
export function renderNewline() {
|
|
22
|
+
console.log('');
|
|
23
|
+
}
|
|
24
|
+
export function renderConfig({
|
|
25
|
+
sdk,
|
|
26
|
+
currentVersion,
|
|
27
|
+
fromVersion,
|
|
28
|
+
toVersion,
|
|
29
|
+
versionName,
|
|
30
|
+
dir,
|
|
31
|
+
packageManager
|
|
32
|
+
}) {
|
|
33
|
+
console.log(`🔧 ${chalk.bold('Upgrade config')}`);
|
|
34
|
+
const versionSuffix = currentVersion ? ` ${chalk.gray(`(v${currentVersion})`)}` : '';
|
|
35
|
+
console.log(`Clerk SDK: ${chalk.green(`@clerk/${sdk}`)}${versionSuffix}`);
|
|
36
|
+
if (fromVersion && toVersion) {
|
|
37
|
+
const versionLabel = versionName ? ` ${chalk.gray(`(${versionName})`)}` : '';
|
|
38
|
+
console.log(`Upgrade: ${chalk.green(`v${fromVersion}`)} → ${chalk.green(`v${toVersion}`)}${versionLabel}`);
|
|
39
|
+
}
|
|
40
|
+
console.log(`Directory: ${chalk.green(dir)}`);
|
|
41
|
+
if (packageManager) {
|
|
42
|
+
console.log(`Package manager: ${chalk.green(packageManager)}`);
|
|
43
|
+
}
|
|
44
|
+
console.log('');
|
|
45
|
+
}
|
|
46
|
+
export async function promptConfirm(message) {
|
|
47
|
+
const rl = readline.createInterface({
|
|
48
|
+
input: process.stdin,
|
|
49
|
+
output: process.stdout
|
|
50
|
+
});
|
|
51
|
+
return new Promise(resolve => {
|
|
52
|
+
rl.question(`${message} (y/n): `, answer => {
|
|
53
|
+
rl.close();
|
|
54
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
export async function promptSelect(message, options) {
|
|
59
|
+
const rl = readline.createInterface({
|
|
60
|
+
input: process.stdin,
|
|
61
|
+
output: process.stdout
|
|
62
|
+
});
|
|
63
|
+
console.log(message);
|
|
64
|
+
options.forEach((opt, i) => {
|
|
65
|
+
console.log(` ${i + 1}) ${opt.label}`);
|
|
66
|
+
});
|
|
67
|
+
return new Promise(resolve => {
|
|
68
|
+
rl.question('Enter number: ', answer => {
|
|
69
|
+
rl.close();
|
|
70
|
+
const index = parseInt(answer, 10) - 1;
|
|
71
|
+
if (index >= 0 && index < options.length) {
|
|
72
|
+
resolve(options[index].value);
|
|
73
|
+
} else {
|
|
74
|
+
resolve(null);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
export async function promptText(message, defaultValue = '') {
|
|
80
|
+
const rl = readline.createInterface({
|
|
81
|
+
input: process.stdin,
|
|
82
|
+
output: process.stdout
|
|
83
|
+
});
|
|
84
|
+
const prompt = defaultValue ? `${message} [${defaultValue}]: ` : `${message}: `;
|
|
85
|
+
return new Promise(resolve => {
|
|
86
|
+
rl.question(prompt, answer => {
|
|
87
|
+
rl.close();
|
|
88
|
+
resolve(answer || defaultValue);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
93
|
+
export function createSpinner(label) {
|
|
94
|
+
let frameIndex = 0;
|
|
95
|
+
let interval = null;
|
|
96
|
+
let currentLabel = label;
|
|
97
|
+
const start = () => {
|
|
98
|
+
interval = setInterval(() => {
|
|
99
|
+
process.stdout.write(`\r${spinnerFrames[frameIndex]} ${currentLabel}`);
|
|
100
|
+
frameIndex = (frameIndex + 1) % spinnerFrames.length;
|
|
101
|
+
}, 80);
|
|
102
|
+
};
|
|
103
|
+
start();
|
|
104
|
+
return {
|
|
105
|
+
update(newLabel) {
|
|
106
|
+
currentLabel = newLabel;
|
|
107
|
+
},
|
|
108
|
+
stop() {
|
|
109
|
+
if (interval) {
|
|
110
|
+
clearInterval(interval);
|
|
111
|
+
interval = null;
|
|
112
|
+
process.stdout.write('\r\x1b[K');
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
success(message) {
|
|
116
|
+
if (interval) {
|
|
117
|
+
clearInterval(interval);
|
|
118
|
+
interval = null;
|
|
119
|
+
}
|
|
120
|
+
process.stdout.write(`\r\x1b[K${chalk.green('✓')} ${message}\n`);
|
|
121
|
+
},
|
|
122
|
+
error(message) {
|
|
123
|
+
if (interval) {
|
|
124
|
+
clearInterval(interval);
|
|
125
|
+
interval = null;
|
|
126
|
+
}
|
|
127
|
+
process.stdout.write(`\r\x1b[K${chalk.red('✗')} ${message}\n`);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function renderCodemodResults(transform, result) {
|
|
132
|
+
console.log(` ${result.ok ?? 0} file(s) modified, ${chalk.red(` ${result.error ?? 0} errors`)}`);
|
|
133
|
+
console.log('');
|
|
134
|
+
}
|
|
135
|
+
export function renderScanResults(results, docsUrl) {
|
|
136
|
+
if (results.length === 0) {
|
|
137
|
+
console.log(chalk.green('✓ No breaking changes detected!'));
|
|
138
|
+
console.log('');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
console.log(chalk.yellow.bold(`Found ${results.length} potential issue(s) to review:`));
|
|
142
|
+
console.log('');
|
|
143
|
+
for (const item of results) {
|
|
144
|
+
console.log(chalk.bold(item.title));
|
|
145
|
+
if (item.warning) {
|
|
146
|
+
console.log(chalk.yellow('(warning - may not require action)'));
|
|
147
|
+
}
|
|
148
|
+
console.log(chalk.gray(`Found ${item.instances.length} instance(s):`));
|
|
149
|
+
for (const inst of item.instances) {
|
|
150
|
+
console.log(chalk.gray(` ${inst.file}:${inst.position.line}:${inst.position.column}`));
|
|
151
|
+
}
|
|
152
|
+
const link = docsUrl && item.docsAnchor ? `${docsUrl}#${item.docsAnchor}` : null;
|
|
153
|
+
if (link) {
|
|
154
|
+
console.log(chalk.blue(`→ View in migration guide: ${link}`));
|
|
155
|
+
}
|
|
156
|
+
console.log('');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
export function renderComplete(sdk, docsUrl) {
|
|
160
|
+
console.log(chalk.green.bold(`✅ Upgrade complete for @clerk/${sdk}`));
|
|
161
|
+
console.log('');
|
|
162
|
+
console.log(`Review the changes above and test your application before deployment.`);
|
|
163
|
+
console.log(chalk.gray(`For more information, see the migration guide: ${docsUrl}`));
|
|
164
|
+
}
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { convertPathToPattern, globby } from 'globby';
|
|
5
|
+
import indexToPosition from 'index-to-position';
|
|
6
|
+
import { getCodemodConfig, runCodemod } from './codemods/index.js';
|
|
7
|
+
import { createSpinner, renderCodemodResults } from './render.js';
|
|
8
|
+
const GLOBBY_IGNORE = ['node_modules/**', '**/node_modules/**', '.git/**', 'dist/**', '**/dist/**', 'build/**', '**/build/**', '.next/**', '**/.next/**', 'package.json', '**/package.json', 'package-lock.json', '**/package-lock.json', 'yarn.lock', '**/yarn.lock', 'pnpm-lock.yaml', '**/pnpm-lock.yaml', '**/*.(png|webp|svg|gif|jpg|jpeg)+', '**/*.(mp4|mkv|wmv|m4v|mov|avi|flv|webm|flac|mka|m4a|aac|ogg)+'];
|
|
9
|
+
export async function runCodemods(config, sdk, options) {
|
|
10
|
+
const codemods = config.codemods || [];
|
|
11
|
+
if (codemods.length === 0) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const glob = typeof options.glob === 'string' ? options.glob.split(/[ ,]/).filter(Boolean) : options.glob;
|
|
15
|
+
for (const transform of codemods) {
|
|
16
|
+
const spinner = createSpinner(`Running codemod: ${transform}`);
|
|
17
|
+
try {
|
|
18
|
+
const result = await runCodemod(transform, glob, options);
|
|
19
|
+
spinner.success(`Codemod applied: ${chalk.dim(transform)}`);
|
|
20
|
+
renderCodemodResults(transform, result);
|
|
21
|
+
const codemodConfig = getCodemodConfig(transform);
|
|
22
|
+
if (codemodConfig?.renderSummary && result.stats) {
|
|
23
|
+
codemodConfig.renderSummary(result.stats);
|
|
24
|
+
}
|
|
25
|
+
} catch (error) {
|
|
26
|
+
spinner.error(`Codemod failed: ${transform}`);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export async function runScans(config, sdk, options) {
|
|
32
|
+
const matchers = loadMatchers(config, sdk);
|
|
33
|
+
if (matchers.length === 0) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const spinner = createSpinner('Scanning files for breaking changes...');
|
|
37
|
+
try {
|
|
38
|
+
const pattern = convertPathToPattern(path.resolve(options.dir));
|
|
39
|
+
const files = await globby(pattern, {
|
|
40
|
+
ignore: [...GLOBBY_IGNORE, ...(options.ignore || [])]
|
|
41
|
+
});
|
|
42
|
+
const results = {};
|
|
43
|
+
for (let idx = 0; idx < files.length; idx++) {
|
|
44
|
+
const file = files[idx];
|
|
45
|
+
spinner.update(`Scanning ${path.basename(file)} (${idx + 1}/${files.length})`);
|
|
46
|
+
const content = await fs.readFile(file, 'utf8');
|
|
47
|
+
for (const matcher of matchers) {
|
|
48
|
+
const matches = findMatches(content, matcher.matcher);
|
|
49
|
+
if (matches.length === 0) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!results[matcher.title]) {
|
|
53
|
+
results[matcher.title] = {
|
|
54
|
+
instances: [],
|
|
55
|
+
...matcher
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
for (const match of matches) {
|
|
59
|
+
const position = indexToPosition(content, match.index, {
|
|
60
|
+
oneBased: true
|
|
61
|
+
});
|
|
62
|
+
const fileRelative = path.relative(process.cwd(), file);
|
|
63
|
+
const isDuplicate = results[matcher.title].instances.some(i => i.position.line === position.line && i.position.column === position.column && i.file === fileRelative);
|
|
64
|
+
if (!isDuplicate) {
|
|
65
|
+
results[matcher.title].instances.push({
|
|
66
|
+
sdk,
|
|
67
|
+
position,
|
|
68
|
+
file: fileRelative
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
spinner.success(`Scanned ${files.length} files`);
|
|
75
|
+
return Object.values(results);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
spinner.error('Scan failed');
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function loadMatchers(config, sdk) {
|
|
82
|
+
if (!config.changes) {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
return config.changes.filter(change => {
|
|
86
|
+
if (!change.matcher) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
const packages = change.packages || ['*'];
|
|
90
|
+
return packages.includes('*') || packages.includes(sdk);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function findMatches(content, matcher) {
|
|
94
|
+
if (Array.isArray(matcher)) {
|
|
95
|
+
return matcher.flatMap(m => Array.from(content.matchAll(m)));
|
|
96
|
+
}
|
|
97
|
+
return Array.from(content.matchAll(matcher));
|
|
98
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { readPackageSync } from 'read-pkg';
|
|
4
|
+
import semverRegex from 'semver-regex';
|
|
5
|
+
import { getOldPackageName } from '../config.js';
|
|
6
|
+
const SUPPORTED_SDKS = [{
|
|
7
|
+
label: '@clerk/nextjs',
|
|
8
|
+
value: 'nextjs'
|
|
9
|
+
}, {
|
|
10
|
+
label: '@clerk/react',
|
|
11
|
+
value: 'react'
|
|
12
|
+
}, {
|
|
13
|
+
label: '@clerk/clerk-react',
|
|
14
|
+
value: 'react'
|
|
15
|
+
}, {
|
|
16
|
+
label: '@clerk/expo',
|
|
17
|
+
value: 'expo'
|
|
18
|
+
}, {
|
|
19
|
+
label: '@clerk/clerk-expo',
|
|
20
|
+
value: 'expo'
|
|
21
|
+
}, {
|
|
22
|
+
label: '@clerk/react-router',
|
|
23
|
+
value: 'react-router'
|
|
24
|
+
}, {
|
|
25
|
+
label: '@clerk/tanstack-react-start',
|
|
26
|
+
value: 'tanstack-react-start'
|
|
27
|
+
}, {
|
|
28
|
+
label: '@clerk/astro',
|
|
29
|
+
value: 'astro'
|
|
30
|
+
}, {
|
|
31
|
+
label: '@clerk/nuxt',
|
|
32
|
+
value: 'nuxt'
|
|
33
|
+
}, {
|
|
34
|
+
label: '@clerk/vue',
|
|
35
|
+
value: 'vue'
|
|
36
|
+
}, {
|
|
37
|
+
label: '@clerk/express',
|
|
38
|
+
value: 'express'
|
|
39
|
+
}, {
|
|
40
|
+
label: '@clerk/fastify',
|
|
41
|
+
value: 'fastify'
|
|
42
|
+
}, {
|
|
43
|
+
label: '@clerk/clerk-js',
|
|
44
|
+
value: 'js'
|
|
45
|
+
}, {
|
|
46
|
+
label: '@clerk/backend',
|
|
47
|
+
value: 'backend'
|
|
48
|
+
}];
|
|
49
|
+
export function getSupportedSdks() {
|
|
50
|
+
return SUPPORTED_SDKS;
|
|
51
|
+
}
|
|
52
|
+
export function detectSdk(dir) {
|
|
53
|
+
let pkg;
|
|
54
|
+
try {
|
|
55
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
56
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const deps = pkg.dependencies ? Object.keys(pkg.dependencies) : [];
|
|
61
|
+
const devDeps = pkg.devDependencies ? Object.keys(pkg.devDependencies) : [];
|
|
62
|
+
const allDeps = [...deps, ...devDeps];
|
|
63
|
+
for (const {
|
|
64
|
+
label,
|
|
65
|
+
value
|
|
66
|
+
} of SUPPORTED_SDKS) {
|
|
67
|
+
if (allDeps.includes(label)) {
|
|
68
|
+
return value;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
export function getSdkVersion(sdk, dir) {
|
|
74
|
+
let pkg;
|
|
75
|
+
try {
|
|
76
|
+
pkg = readPackageSync({
|
|
77
|
+
cwd: dir
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const pkgName = sdk.startsWith('@clerk/') ? sdk : `@clerk/${sdk}`;
|
|
83
|
+
const oldPkgName = getOldPackageName(sdk);
|
|
84
|
+
const version = pkg.dependencies?.[pkgName] || pkg.devDependencies?.[pkgName] || oldPkgName && pkg.dependencies?.[oldPkgName] || oldPkgName && pkg.devDependencies?.[oldPkgName];
|
|
85
|
+
if (!version) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return getMajorVersion(version);
|
|
89
|
+
}
|
|
90
|
+
export function getMajorVersion(semver) {
|
|
91
|
+
const cleaned = semver.replace(/^[\^~]/, '');
|
|
92
|
+
const match = cleaned.match(semverRegex());
|
|
93
|
+
if (match) {
|
|
94
|
+
const [major] = match[0].split('.');
|
|
95
|
+
return parseInt(major, 10);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
export function getInstalledClerkPackages(dir) {
|
|
100
|
+
let pkg;
|
|
101
|
+
try {
|
|
102
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
103
|
+
pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
104
|
+
} catch {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
const deps = pkg.dependencies ? Object.entries(pkg.dependencies) : [];
|
|
108
|
+
const devDeps = pkg.devDependencies ? Object.entries(pkg.devDependencies) : [];
|
|
109
|
+
return Object.fromEntries([...deps, ...devDeps].filter(([name]) => name.startsWith('@clerk/')).map(([name, version]) => [name, getMajorVersion(version)]));
|
|
110
|
+
}
|
|
111
|
+
export function normalizeSdkName(sdk) {
|
|
112
|
+
if (!sdk) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
if (sdk.startsWith('@clerk/')) {
|
|
116
|
+
return sdk.replace('@clerk/', '');
|
|
117
|
+
}
|
|
118
|
+
if (sdk === 'clerk-react') {
|
|
119
|
+
return 'react';
|
|
120
|
+
}
|
|
121
|
+
if (sdk === 'clerk-expo') {
|
|
122
|
+
return 'expo';
|
|
123
|
+
}
|
|
124
|
+
return sdk;
|
|
125
|
+
}
|