@clerk/upgrade 2.0.0-snapshot.v20251204175016 → 2.0.0-snapshot.v20251211120550

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 (55) hide show
  1. package/README.md +35 -5
  2. package/dist/__tests__/fixtures/expo-old-package/package-lock.json +5 -0
  3. package/dist/__tests__/fixtures/expo-old-package/package.json +10 -0
  4. package/dist/__tests__/fixtures/expo-old-package/src/App.tsx +14 -0
  5. package/dist/__tests__/fixtures/nextjs-v6/package.json +9 -0
  6. package/dist/__tests__/fixtures/nextjs-v6/pnpm-lock.yaml +2 -0
  7. package/dist/__tests__/fixtures/nextjs-v6/src/app.tsx +17 -0
  8. package/dist/__tests__/fixtures/nextjs-v7/package.json +9 -0
  9. package/dist/__tests__/fixtures/nextjs-v7/pnpm-lock.yaml +2 -0
  10. package/dist/__tests__/fixtures/nextjs-v7/src/app.tsx +16 -0
  11. package/dist/__tests__/fixtures/no-clerk/package.json +7 -0
  12. package/dist/__tests__/fixtures/react-v6/package.json +8 -0
  13. package/dist/__tests__/fixtures/react-v6/src/App.tsx +19 -0
  14. package/dist/__tests__/fixtures/react-v6/yarn.lock +2 -0
  15. package/dist/__tests__/helpers/create-fixture.js +56 -0
  16. package/dist/__tests__/integration/cli.test.js +275 -0
  17. package/dist/__tests__/integration/config.test.js +97 -0
  18. package/dist/__tests__/integration/detect-sdk.test.js +100 -0
  19. package/dist/__tests__/integration/runner.test.js +58 -0
  20. package/dist/cli.js +172 -44
  21. package/dist/codemods/__tests__/__fixtures__/transform-align-experimental-unstable-prefixes.fixtures.js +92 -0
  22. package/dist/codemods/__tests__/__fixtures__/transform-appearance-layout-to-options.fixtures.js +9 -0
  23. package/dist/codemods/__tests__/__fixtures__/transform-clerk-react-v6.fixtures.js +13 -0
  24. package/dist/codemods/__tests__/__fixtures__/transform-remove-deprecated-appearance-props.fixtures.js +63 -0
  25. package/dist/codemods/__tests__/__fixtures__/transform-themes-to-ui-themes.fixtures.js +41 -0
  26. package/dist/codemods/__tests__/transform-align-experimental-unstable-prefixes.test.js +15 -0
  27. package/dist/codemods/__tests__/transform-appearance-layout-to-options.test.js +15 -0
  28. package/dist/codemods/__tests__/transform-remove-deprecated-appearance-props.test.js +15 -0
  29. package/dist/codemods/__tests__/transform-themes-to-ui-themes.test.js +15 -0
  30. package/dist/codemods/index.js +67 -13
  31. package/dist/codemods/transform-align-experimental-unstable-prefixes.cjs +412 -0
  32. package/dist/codemods/transform-appearance-layout-to-options.cjs +65 -0
  33. package/dist/codemods/transform-clerk-react-v6.cjs +15 -7
  34. package/dist/codemods/transform-remove-deprecated-appearance-props.cjs +109 -0
  35. package/dist/codemods/transform-remove-deprecated-props.cjs +11 -32
  36. package/dist/codemods/transform-themes-to-ui-themes.cjs +65 -0
  37. package/dist/config.js +145 -0
  38. package/dist/render.js +170 -0
  39. package/dist/runner.js +98 -0
  40. package/dist/util/detect-sdk.js +125 -0
  41. package/dist/util/package-manager.js +94 -0
  42. package/dist/versions/core-3/changes/clerk-expo-package-rename.md +23 -0
  43. package/dist/versions/core-3/changes/clerk-react-package-rename.md +22 -0
  44. package/dist/versions/core-3/index.js +40 -0
  45. package/package.json +2 -8
  46. package/dist/app.js +0 -177
  47. package/dist/components/Codemod.js +0 -149
  48. package/dist/components/Command.js +0 -56
  49. package/dist/components/Header.js +0 -11
  50. package/dist/components/SDKWorkflow.js +0 -278
  51. package/dist/components/Scan.js +0 -180
  52. package/dist/components/UpgradeSDK.js +0 -116
  53. package/dist/util/expandable-list.js +0 -173
  54. package/dist/util/get-clerk-version.js +0 -22
  55. package/dist/util/guess-framework.js +0 -69
@@ -0,0 +1,109 @@
1
+ const VARIABLE_RENAMES = {
2
+ colorText: 'colorForeground',
3
+ colorTextSecondary: 'colorMutedForeground',
4
+ colorInputText: 'colorInputForeground',
5
+ colorInputBackground: 'colorInput',
6
+ colorTextOnPrimaryBackground: 'colorPrimaryForeground',
7
+ spacingUnit: 'spacing'
8
+ };
9
+ const isStringLiteral = node => node && node.type === 'Literal' && typeof node.value === 'string' || node && node.type === 'StringLiteral' && typeof node.value === 'string';
10
+ const getKeyName = key => {
11
+ if (!key) {
12
+ return null;
13
+ }
14
+ if (key.type === 'Identifier') {
15
+ return key.name;
16
+ }
17
+ if (isStringLiteral(key)) {
18
+ return key.value;
19
+ }
20
+ return null;
21
+ };
22
+ module.exports = function transformRemoveDeprecatedAppearanceProps({
23
+ source
24
+ }, {
25
+ jscodeshift: j
26
+ }) {
27
+ const root = j(source);
28
+ let dirty = false;
29
+ const renamePropertyKey = (prop, newName) => {
30
+ if (j.Identifier.check(prop.key)) {
31
+ prop.key.name = newName;
32
+ } else if (isStringLiteral(prop.key)) {
33
+ prop.key.value = newName;
34
+ } else {
35
+ prop.key = j.identifier(newName);
36
+ prop.computed = false;
37
+ }
38
+ dirty = true;
39
+ };
40
+ const maybeRenameBaseTheme = prop => {
41
+ if (!prop || prop.computed) {
42
+ return;
43
+ }
44
+ if (getKeyName(prop.key) === 'baseTheme') {
45
+ renamePropertyKey(prop, 'theme');
46
+ }
47
+ };
48
+ const maybeRenameVariableKey = prop => {
49
+ if (!prop || prop.computed) {
50
+ return;
51
+ }
52
+ const keyName = getKeyName(prop.key);
53
+ const newName = VARIABLE_RENAMES[keyName];
54
+ if (newName) {
55
+ renamePropertyKey(prop, newName);
56
+ }
57
+ };
58
+ const transformAppearanceObject = objExpr => {
59
+ const props = objExpr.properties || [];
60
+ props.forEach(prop => {
61
+ if (!prop || !prop.key) {
62
+ return;
63
+ }
64
+ maybeRenameBaseTheme(prop);
65
+ if (getKeyName(prop.key) === 'variables' && j.ObjectExpression.check(prop.value)) {
66
+ (prop.value.properties || []).forEach(maybeRenameVariableKey);
67
+ }
68
+ });
69
+ };
70
+ const findObjectForIdentifier = identifier => {
71
+ if (!identifier || !j.Identifier.check(identifier)) {
72
+ return null;
73
+ }
74
+ const name = identifier.name;
75
+ const decl = root.find(j.VariableDeclarator, {
76
+ id: {
77
+ type: 'Identifier',
78
+ name
79
+ }
80
+ }).filter(p => j.ObjectExpression.check(p.node.init)).at(0);
81
+ if (decl.size() === 0) {
82
+ return null;
83
+ }
84
+ return decl.get().node.init;
85
+ };
86
+ root.find(j.JSXAttribute, {
87
+ name: {
88
+ name: 'appearance'
89
+ }
90
+ }).forEach(path => {
91
+ const {
92
+ value
93
+ } = path.node;
94
+ if (!value || !j.JSXExpressionContainer.check(value)) {
95
+ return;
96
+ }
97
+ const expr = value.expression;
98
+ if (j.ObjectExpression.check(expr)) {
99
+ transformAppearanceObject(expr);
100
+ } else if (j.Identifier.check(expr)) {
101
+ const obj = findObjectForIdentifier(expr);
102
+ if (obj) {
103
+ transformAppearanceObject(obj);
104
+ }
105
+ }
106
+ });
107
+ return dirty ? root.toSource() : undefined;
108
+ };
109
+ module.exports.parser = 'tsx';
@@ -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
- }, options = {}) {
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
- if (stats) {
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
- removedCount += 1;
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, filePath)) {
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, filePath) {
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
- if (stats) {
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
- const calleeExpression = clone(originalExpression);
403
- const callExpression = j.callExpression(calleeExpression, [j.memberExpression(paramIdentifier, j.identifier('session'))]);
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,145 @@
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, release) {
8
+ const versionDirs = fs.readdirSync(VERSIONS_DIR, {
9
+ withFileTypes: true
10
+ }).filter(d => d.isDirectory()).map(d => d.name);
11
+
12
+ // If a specific release is requested, load it directly
13
+ if (release) {
14
+ if (!versionDirs.includes(release)) {
15
+ return null;
16
+ }
17
+ const configPath = path.join(VERSIONS_DIR, release, 'index.js');
18
+ if (!fs.existsSync(configPath)) {
19
+ return null;
20
+ }
21
+ const moduleUrl = pathToFileURL(configPath).href;
22
+ const mod = await import(moduleUrl);
23
+ const config = mod.default ?? mod;
24
+ const changes = loadChanges(release, sdk);
25
+ const versionStatus = getVersionStatus(config, sdk, currentVersion);
26
+ return {
27
+ ...config,
28
+ changes,
29
+ versionStatus,
30
+ needsUpgrade: versionStatus === 'needs-upgrade',
31
+ alreadyUpgraded: versionStatus === 'already-upgraded'
32
+ };
33
+ }
34
+ let applicableConfig = null;
35
+ for (const versionDir of versionDirs) {
36
+ const configPath = path.join(VERSIONS_DIR, versionDir, 'index.js');
37
+ if (!fs.existsSync(configPath)) {
38
+ continue;
39
+ }
40
+ const moduleUrl = pathToFileURL(configPath).href;
41
+ const mod = await import(moduleUrl);
42
+ const config = mod.default ?? mod;
43
+ if (!config.sdkVersions) {
44
+ continue;
45
+ }
46
+ const versionStatus = getVersionStatus(config, sdk, currentVersion);
47
+ if (versionStatus === 'unsupported' || versionStatus === 'unknown') {
48
+ continue;
49
+ }
50
+ if (versionStatus === 'needs-upgrade') {
51
+ const changes = loadChanges(versionDir, sdk);
52
+ return {
53
+ ...config,
54
+ changes,
55
+ versionStatus,
56
+ needsUpgrade: true,
57
+ alreadyUpgraded: false
58
+ };
59
+ }
60
+ if (versionStatus === 'already-upgraded' && !applicableConfig) {
61
+ applicableConfig = {
62
+ config,
63
+ versionDir
64
+ };
65
+ }
66
+ }
67
+ if (applicableConfig) {
68
+ const changes = loadChanges(applicableConfig.versionDir, sdk);
69
+ return {
70
+ ...applicableConfig.config,
71
+ changes,
72
+ versionStatus: 'already-upgraded',
73
+ needsUpgrade: false,
74
+ alreadyUpgraded: true
75
+ };
76
+ }
77
+ return null;
78
+ }
79
+ function getVersionStatus(config, sdk, currentVersion) {
80
+ if (!config?.sdkVersions) {
81
+ return 'unknown';
82
+ }
83
+ const range = config.sdkVersions[sdk];
84
+ if (!range) {
85
+ return 'unknown';
86
+ }
87
+ if (typeof currentVersion !== 'number') {
88
+ return 'unknown';
89
+ }
90
+ if (typeof range.from === 'number' && currentVersion < range.from) {
91
+ return 'unsupported';
92
+ }
93
+ if (typeof range.to === 'number' && currentVersion >= range.to) {
94
+ return 'already-upgraded';
95
+ }
96
+ return 'needs-upgrade';
97
+ }
98
+ export function getTargetPackageName(sdk) {
99
+ if (sdk === 'clerk-react' || sdk === 'react') {
100
+ return '@clerk/react';
101
+ }
102
+ if (sdk === 'clerk-expo' || sdk === 'expo') {
103
+ return '@clerk/expo';
104
+ }
105
+ return `@clerk/${sdk}`;
106
+ }
107
+ export function getOldPackageName(sdk) {
108
+ if (sdk === 'clerk-react' || sdk === 'react') {
109
+ return '@clerk/clerk-react';
110
+ }
111
+ if (sdk === 'clerk-expo' || sdk === 'expo') {
112
+ return '@clerk/clerk-expo';
113
+ }
114
+ return null;
115
+ }
116
+ function loadChanges(versionDir, sdk) {
117
+ const changesDir = path.join(VERSIONS_DIR, versionDir, 'changes');
118
+ if (!fs.existsSync(changesDir)) {
119
+ return [];
120
+ }
121
+ const files = fs.readdirSync(changesDir).filter(f => f.endsWith('.md'));
122
+ const changes = [];
123
+ for (const file of files) {
124
+ const filePath = path.join(changesDir, file);
125
+ const content = fs.readFileSync(filePath, 'utf8');
126
+ const parsed = matter(content);
127
+ const fm = parsed.data;
128
+ const packages = fm.packages || ['*'];
129
+ const appliesToSdk = packages.includes('*') || packages.includes(sdk);
130
+ if (!appliesToSdk) {
131
+ continue;
132
+ }
133
+ 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;
134
+ changes.push({
135
+ title: fm.title,
136
+ matcher,
137
+ packages,
138
+ category: fm.category || 'breaking',
139
+ warning: fm.warning || fm.category === 'warning',
140
+ docsAnchor: fm.docsAnchor || file.replace('.md', ''),
141
+ content: parsed.content
142
+ });
143
+ }
144
+ return changes;
145
+ }
package/dist/render.js ADDED
@@ -0,0 +1,170 @@
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, defaultYes = false) {
47
+ const rl = readline.createInterface({
48
+ input: process.stdin,
49
+ output: process.stdout
50
+ });
51
+ return new Promise(resolve => {
52
+ const prompt = defaultYes ? `${message} (Y/n): ` : `${message} (y/N): `;
53
+ rl.question(prompt, answer => {
54
+ rl.close();
55
+ const normalized = answer.trim().toLowerCase();
56
+ if (!normalized) {
57
+ resolve(defaultYes);
58
+ return;
59
+ }
60
+ resolve(normalized === 'y' || normalized === 'yes');
61
+ });
62
+ });
63
+ }
64
+ export async function promptSelect(message, options) {
65
+ const rl = readline.createInterface({
66
+ input: process.stdin,
67
+ output: process.stdout
68
+ });
69
+ console.log(message);
70
+ options.forEach((opt, i) => {
71
+ console.log(` ${i + 1}) ${opt.label}`);
72
+ });
73
+ return new Promise(resolve => {
74
+ rl.question('Enter number: ', answer => {
75
+ rl.close();
76
+ const index = parseInt(answer, 10) - 1;
77
+ if (index >= 0 && index < options.length) {
78
+ resolve(options[index].value);
79
+ } else {
80
+ resolve(null);
81
+ }
82
+ });
83
+ });
84
+ }
85
+ export async function promptText(message, defaultValue = '') {
86
+ const rl = readline.createInterface({
87
+ input: process.stdin,
88
+ output: process.stdout
89
+ });
90
+ const prompt = defaultValue ? `${message} [${defaultValue}]: ` : `${message}: `;
91
+ return new Promise(resolve => {
92
+ rl.question(prompt, answer => {
93
+ rl.close();
94
+ resolve(answer || defaultValue);
95
+ });
96
+ });
97
+ }
98
+ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
99
+ export function createSpinner(label) {
100
+ let frameIndex = 0;
101
+ let interval = null;
102
+ let currentLabel = label;
103
+ const start = () => {
104
+ interval = setInterval(() => {
105
+ process.stdout.write(`\r${spinnerFrames[frameIndex]} ${currentLabel}`);
106
+ frameIndex = (frameIndex + 1) % spinnerFrames.length;
107
+ }, 80);
108
+ };
109
+ start();
110
+ return {
111
+ update(newLabel) {
112
+ currentLabel = newLabel;
113
+ },
114
+ stop() {
115
+ if (interval) {
116
+ clearInterval(interval);
117
+ interval = null;
118
+ process.stdout.write('\r\x1b[K');
119
+ }
120
+ },
121
+ success(message) {
122
+ if (interval) {
123
+ clearInterval(interval);
124
+ interval = null;
125
+ }
126
+ process.stdout.write(`\r\x1b[K${chalk.green('✓')} ${message}\n`);
127
+ },
128
+ error(message) {
129
+ if (interval) {
130
+ clearInterval(interval);
131
+ interval = null;
132
+ }
133
+ process.stdout.write(`\r\x1b[K${chalk.red('✗')} ${message}\n`);
134
+ }
135
+ };
136
+ }
137
+ export function renderCodemodResults(transform, result) {
138
+ console.log(` ${result.ok ?? 0} file(s) modified, ${chalk.red(` ${result.error ?? 0} errors`)}`);
139
+ console.log('');
140
+ }
141
+ export function renderScanResults(results, docsUrl) {
142
+ if (results.length === 0) {
143
+ console.log(chalk.green('✓ No breaking changes detected!'));
144
+ console.log('');
145
+ return;
146
+ }
147
+ console.log(chalk.yellow.bold(`Found ${results.length} potential issue(s) to review:`));
148
+ console.log('');
149
+ for (const item of results) {
150
+ console.log(chalk.bold(item.title));
151
+ if (item.warning) {
152
+ console.log(chalk.yellow('(warning - may not require action)'));
153
+ }
154
+ console.log(chalk.gray(`Found ${item.instances.length} instance(s):`));
155
+ for (const inst of item.instances) {
156
+ console.log(chalk.gray(` ${inst.file}:${inst.position.line}:${inst.position.column}`));
157
+ }
158
+ const link = docsUrl && item.docsAnchor ? `${docsUrl}#${item.docsAnchor}` : null;
159
+ if (link) {
160
+ console.log(chalk.blue(`→ View in migration guide: ${link}`));
161
+ }
162
+ console.log('');
163
+ }
164
+ }
165
+ export function renderComplete(sdk, docsUrl) {
166
+ console.log(chalk.green.bold(`✅ Upgrade complete for @clerk/${sdk}`));
167
+ console.log('');
168
+ console.log(`Review the changes above and test your application before deployment.`);
169
+ console.log(chalk.gray(`For more information, see the migration guide: ${docsUrl}`));
170
+ }
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
+ }