@clerk/upgrade 2.0.0-snapshot.v20251204143242 → 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.
Files changed (54) hide show
  1. package/dist/__tests__/fixtures/expo-old-package/package-lock.json +5 -0
  2. package/dist/__tests__/fixtures/expo-old-package/package.json +10 -0
  3. package/dist/__tests__/fixtures/expo-old-package/src/App.tsx +14 -0
  4. package/dist/__tests__/fixtures/nextjs-v6/package.json +9 -0
  5. package/dist/__tests__/fixtures/nextjs-v6/pnpm-lock.yaml +2 -0
  6. package/dist/__tests__/fixtures/nextjs-v6/src/app.tsx +17 -0
  7. package/dist/__tests__/fixtures/nextjs-v7/package.json +9 -0
  8. package/dist/__tests__/fixtures/nextjs-v7/pnpm-lock.yaml +2 -0
  9. package/dist/__tests__/fixtures/nextjs-v7/src/app.tsx +16 -0
  10. package/dist/__tests__/fixtures/no-clerk/package.json +7 -0
  11. package/dist/__tests__/fixtures/react-v6/package.json +8 -0
  12. package/dist/__tests__/fixtures/react-v6/src/App.tsx +19 -0
  13. package/dist/__tests__/fixtures/react-v6/yarn.lock +2 -0
  14. package/dist/__tests__/helpers/create-fixture.js +56 -0
  15. package/dist/__tests__/integration/cli.test.js +230 -0
  16. package/dist/__tests__/integration/config.test.js +76 -0
  17. package/dist/__tests__/integration/detect-sdk.test.js +100 -0
  18. package/dist/__tests__/integration/runner.test.js +79 -0
  19. package/dist/cli.js +159 -45
  20. package/dist/codemods/__tests__/__fixtures__/transform-align-experimental-unstable-prefixes.fixtures.js +68 -0
  21. package/dist/codemods/__tests__/__fixtures__/transform-appearance-layout-to-options.fixtures.js +9 -0
  22. package/dist/codemods/__tests__/__fixtures__/transform-clerk-react-v6.fixtures.js +13 -0
  23. package/dist/codemods/__tests__/__fixtures__/transform-remove-deprecated-appearance-props.fixtures.js +63 -0
  24. package/dist/codemods/__tests__/__fixtures__/transform-themes-to-ui-themes.fixtures.js +41 -0
  25. package/dist/codemods/__tests__/transform-align-experimental-unstable-prefixes.test.js +15 -0
  26. package/dist/codemods/__tests__/transform-appearance-layout-to-options.test.js +15 -0
  27. package/dist/codemods/__tests__/transform-remove-deprecated-appearance-props.test.js +15 -0
  28. package/dist/codemods/__tests__/transform-themes-to-ui-themes.test.js +15 -0
  29. package/dist/codemods/index.js +67 -13
  30. package/dist/codemods/transform-align-experimental-unstable-prefixes.cjs +400 -0
  31. package/dist/codemods/transform-appearance-layout-to-options.cjs +65 -0
  32. package/dist/codemods/transform-clerk-react-v6.cjs +15 -7
  33. package/dist/codemods/transform-remove-deprecated-appearance-props.cjs +109 -0
  34. package/dist/codemods/transform-remove-deprecated-props.cjs +12 -12
  35. package/dist/codemods/transform-themes-to-ui-themes.cjs +65 -0
  36. package/dist/config.js +122 -0
  37. package/dist/render.js +164 -0
  38. package/dist/runner.js +98 -0
  39. package/dist/util/detect-sdk.js +125 -0
  40. package/dist/util/package-manager.js +94 -0
  41. package/dist/versions/core-3/changes/clerk-expo-package-rename.md +23 -0
  42. package/dist/versions/core-3/changes/clerk-react-package-rename.md +23 -0
  43. package/dist/versions/core-3/index.js +40 -0
  44. package/package.json +2 -8
  45. package/dist/app.js +0 -177
  46. package/dist/components/Codemod.js +0 -97
  47. package/dist/components/Command.js +0 -56
  48. package/dist/components/Header.js +0 -11
  49. package/dist/components/SDKWorkflow.js +0 -278
  50. package/dist/components/Scan.js +0 -180
  51. package/dist/components/UpgradeSDK.js +0 -116
  52. package/dist/util/expandable-list.js +0 -173
  53. package/dist/util/get-clerk-version.js +0 -22
  54. package/dist/util/guess-framework.js +0 -69
@@ -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
+ }
@@ -0,0 +1,94 @@
1
+ import { spawn } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ export function detectPackageManager(dir) {
5
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) {
6
+ return 'pnpm';
7
+ }
8
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) {
9
+ return 'yarn';
10
+ }
11
+ if (fs.existsSync(path.join(dir, 'bun.lockb')) || fs.existsSync(path.join(dir, 'bun.lock'))) {
12
+ return 'bun';
13
+ }
14
+ if (fs.existsSync(path.join(dir, 'package-lock.json'))) {
15
+ return 'npm';
16
+ }
17
+ return 'npm';
18
+ }
19
+ export function getInstallCommand(packageManager, packageName, version = 'latest') {
20
+ const pkg = version === 'latest' ? packageName : `${packageName}@${version}`;
21
+ switch (packageManager) {
22
+ case 'pnpm':
23
+ return ['pnpm', ['add', pkg]];
24
+ case 'yarn':
25
+ return ['yarn', ['add', pkg]];
26
+ case 'bun':
27
+ return ['bun', ['add', pkg]];
28
+ case 'npm':
29
+ default:
30
+ return ['npm', ['install', pkg]];
31
+ }
32
+ }
33
+ export function getUninstallCommand(packageManager, packageName) {
34
+ switch (packageManager) {
35
+ case 'pnpm':
36
+ return ['pnpm', ['remove', packageName]];
37
+ case 'yarn':
38
+ return ['yarn', ['remove', packageName]];
39
+ case 'bun':
40
+ return ['bun', ['remove', packageName]];
41
+ case 'npm':
42
+ default:
43
+ return ['npm', ['uninstall', packageName]];
44
+ }
45
+ }
46
+ export async function runPackageManagerCommand(command, args, cwd) {
47
+ return new Promise((resolve, reject) => {
48
+ const child = spawn(command, args, {
49
+ cwd,
50
+ stdio: 'pipe',
51
+ shell: process.platform === 'win32'
52
+ });
53
+ let stdout = '';
54
+ let stderr = '';
55
+ child.stdout?.on('data', data => {
56
+ stdout += data.toString();
57
+ });
58
+ child.stderr?.on('data', data => {
59
+ stderr += data.toString();
60
+ });
61
+ child.on('close', code => {
62
+ if (code === 0) {
63
+ resolve({
64
+ stdout,
65
+ stderr
66
+ });
67
+ } else {
68
+ reject(new Error(`Command failed with code ${code}: ${stderr || stdout}`));
69
+ }
70
+ });
71
+ child.on('error', reject);
72
+ });
73
+ }
74
+ export async function upgradePackage(packageManager, packageName, version, cwd) {
75
+ const [cmd, args] = getInstallCommand(packageManager, packageName, version);
76
+ return runPackageManagerCommand(cmd, args, cwd);
77
+ }
78
+ export async function removePackage(packageManager, packageName, cwd) {
79
+ const [cmd, args] = getUninstallCommand(packageManager, packageName);
80
+ return runPackageManagerCommand(cmd, args, cwd);
81
+ }
82
+ export function getPackageManagerDisplayName(packageManager) {
83
+ switch (packageManager) {
84
+ case 'pnpm':
85
+ return 'pnpm';
86
+ case 'yarn':
87
+ return 'Yarn';
88
+ case 'bun':
89
+ return 'Bun';
90
+ case 'npm':
91
+ default:
92
+ return 'npm';
93
+ }
94
+ }
@@ -0,0 +1,23 @@
1
+ ---
2
+ title: '`@clerk/clerk-expo` renamed to `@clerk/expo`'
3
+ packages: ['expo']
4
+ matcher: '@clerk/clerk-expo'
5
+ category: 'breaking'
6
+ docsAnchor: 'clerk-expo-rename'
7
+ ---
8
+
9
+ The `@clerk/clerk-expo` package has been renamed to `@clerk/expo`.
10
+
11
+ Update your imports:
12
+
13
+ ```diff
14
+ - import { ClerkProvider, useUser } from '@clerk/clerk-expo';
15
+ + import { ClerkProvider, useUser } from '@clerk/expo';
16
+ ```
17
+
18
+ And update your package.json:
19
+
20
+ ```diff
21
+ - "@clerk/clerk-expo": "^2.0.0",
22
+ + "@clerk/expo": "^3.0.0",
23
+ ```