From fc1ae4eac0d6d76ae634f450d3a5e43df9bcdee0 Mon Sep 17 00:00:00 2001 From: Dylan Staley <88163+dstaley@users.noreply.github.com> Date: Tue, 7 Oct 2025 18:52:24 -0500 Subject: [PATCH] feat(upgrade): Add support for @clerk/react --- .changeset/eight-groups-poke.md | 5 + packages/upgrade/src/app.js | 6 +- .../transform-clerk-react-v6.fixtures.js | 66 +++++ .../transform-clerk-react-v6.test.js | 13 + packages/upgrade/src/codemods/index.js | 2 +- .../src/codemods/transform-clerk-react-v6.cjs | 89 ++++++ packages/upgrade/src/components/Codemod.js | 2 +- .../upgrade/src/components/SDKWorkflow.js | 271 ++++++++++++------ packages/upgrade/src/components/UpgradeSDK.js | 24 +- 9 files changed, 378 insertions(+), 100 deletions(-) create mode 100644 .changeset/eight-groups-poke.md create mode 100644 packages/upgrade/src/codemods/__tests__/__fixtures__/transform-clerk-react-v6.fixtures.js create mode 100644 packages/upgrade/src/codemods/__tests__/transform-clerk-react-v6.test.js create mode 100644 packages/upgrade/src/codemods/transform-clerk-react-v6.cjs diff --git a/.changeset/eight-groups-poke.md b/.changeset/eight-groups-poke.md new file mode 100644 index 00000000000..eab224cd900 --- /dev/null +++ b/.changeset/eight-groups-poke.md @@ -0,0 +1,5 @@ +--- +'@clerk/upgrade': minor +--- + +Add support for @clerk/react diff --git a/packages/upgrade/src/app.js b/packages/upgrade/src/app.js index 7799a795735..92f69d593ca 100644 --- a/packages/upgrade/src/app.js +++ b/packages/upgrade/src/app.js @@ -59,7 +59,11 @@ export default function App(props) { }, [fromVersion]); // Handle the individual SDK upgrade - if (!fromVersion && !toVersion && sdks[0] === 'nextjs') { + if ( + !fromVersion && + !toVersion && + ['nextjs', 'clerk-react', 'clerk-expo', 'react-router', 'tanstack-react-start'].includes(sdks[0]) + ) { return ; } diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-clerk-react-v6.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-clerk-react-v6.fixtures.js new file mode 100644 index 00000000000..d9157d44798 --- /dev/null +++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-clerk-react-v6.fixtures.js @@ -0,0 +1,66 @@ +export const fixtures = [ + { + name: 'Basic import transform', + source: ` +import { ClerkProvider } from "@clerk/clerk-react" + `, + output: ` +import { ClerkProvider } from "@clerk/react" +`, + }, + { + name: 'Basic legacy import', + source: ` +import { useSignIn, useSignUp } from "@clerk/clerk-react" + `, + output: ` +import { useSignIn, useSignUp } from "@clerk/react/legacy" +`, + }, + { + name: 'Basic legacy import (@clerk/clerk-expo)', + source: ` +import { useSignIn, useSignUp } from "@clerk/clerk-expo" + `, + output: ` +import { useSignIn, useSignUp } from "@clerk/expo/legacy" +`, + }, + { + name: 'Basic legacy import (@clerk/nextjs)', + source: ` +import { useSignIn, useSignUp } from "@clerk/nextjs" + `, + output: ` +import { useSignIn, useSignUp } from "@clerk/nextjs/legacy" +`, + }, + { + name: 'Basic legacy import (@clerk/react-router)', + source: ` +import { useSignIn, useSignUp } from "@clerk/react-router" + `, + output: ` +import { useSignIn, useSignUp } from "@clerk/react-router/legacy" +`, + }, + { + name: 'Basic legacy import (@clerk/tanstack-react-start)', + source: ` +import { useSignIn, useSignUp } from "@clerk/tanstack-react-start" + `, + output: ` +import { useSignIn, useSignUp } from "@clerk/tanstack-react-start/legacy" +`, + }, + { + name: 'Mixed legacy imports', + source: ` +import { ClerkProvider, useSignIn, useSignUp } from "@clerk/clerk-react" + `, + output: ` +import { ClerkProvider } from "@clerk/react"; +import { useSignIn, useSignUp } from "@clerk/react/legacy"; +`, + }, +]; diff --git a/packages/upgrade/src/codemods/__tests__/transform-clerk-react-v6.test.js b/packages/upgrade/src/codemods/__tests__/transform-clerk-react-v6.test.js new file mode 100644 index 00000000000..4e82415a9c4 --- /dev/null +++ b/packages/upgrade/src/codemods/__tests__/transform-clerk-react-v6.test.js @@ -0,0 +1,13 @@ +import { applyTransform } from 'jscodeshift/dist/testUtils'; +import { describe, expect, it } from 'vitest'; + +import transformer from '../transform-clerk-react-v6.cjs'; +import { fixtures } from './__fixtures__/transform-clerk-react-v6.fixtures'; + +describe('transform-clerk-react-v6', () => { + it.each(fixtures)(`$name`, ({ source, output }) => { + const result = applyTransform(transformer, {}, { source }); + + expect(result).toEqual(output.trim()); + }); +}); diff --git a/packages/upgrade/src/codemods/index.js b/packages/upgrade/src/codemods/index.js index 5e7245c3d3e..a2b426f00ac 100644 --- a/packages/upgrade/src/codemods/index.js +++ b/packages/upgrade/src/codemods/index.js @@ -38,6 +38,6 @@ export async function runCodemod(transform = 'transform-async-request', glob, op dry: false, ...options, // we must silence stdout to prevent output from interfering with ink CLI - silent: true, + // silent: true, }); } diff --git a/packages/upgrade/src/codemods/transform-clerk-react-v6.cjs b/packages/upgrade/src/codemods/transform-clerk-react-v6.cjs new file mode 100644 index 00000000000..a2b0db9490c --- /dev/null +++ b/packages/upgrade/src/codemods/transform-clerk-react-v6.cjs @@ -0,0 +1,89 @@ +const PACKAGES = [ + ['@clerk/clerk-react', '@clerk/react'], + ['@clerk/clerk-expo', '@clerk/expo'], + ['@clerk/nextjs', '@clerk/nextjs'], + ['@clerk/react-router', '@clerk/react-router'], + ['@clerk/tanstack-react-start', '@clerk/tanstack-react-start'], +]; + +/** + * Transforms imports of `@clerk/clerk-react` to `@clerk/react`, in addition to updating imports of `useSignIn` and + * `useSignUp` to import from the `/legacy` subpath. + * + * @param {import('jscodeshift').FileInfo} FileInfo - The parameters object + * @param {import('jscodeshift').API} api - The API object provided by jscodeshift + * @param {Object} _options - Additional options (unused) + * @returns {string|undefined} - The transformed source code if modifications were made, otherwise undefined + */ +module.exports = function transformClerkReactV6({ source }, { jscodeshift: j }) { + const root = j(source); + let dirtyFlag = false; + + PACKAGES.forEach(([sourcePackage, targetPackage]) => { + // Transform imports from '@clerk/clerk-react' + root.find(j.ImportDeclaration, { source: { value: sourcePackage } }).forEach(path => { + const node = path.node; + const specifiers = node.specifiers || []; + const importKind = node.importKind; // preserve type-only imports + + // If there are no specifiers (side-effect import), simply retarget to '@clerk/react' + if (specifiers.length === 0) { + node.source.value = targetPackage; + dirtyFlag = true; + return; + } + + /** Split specifiers into legacy and non-legacy groups */ + const legacySpecifiers = []; + const nonLegacySpecifiers = []; + + for (const spec of specifiers) { + if ( + j.ImportSpecifier.check(spec) && + (spec.imported.name === 'useSignIn' || spec.imported.name === 'useSignUp') + ) { + legacySpecifiers.push(spec); + } else { + nonLegacySpecifiers.push(spec); + } + } + + if (legacySpecifiers.length > 0 && nonLegacySpecifiers.length > 0) { + // Mixed import: keep non-legacy on '@clerk/react', emit a new import for legacy hooks + node.specifiers = nonLegacySpecifiers; + node.source = j.literal(targetPackage); + if (importKind) { + node.importKind = importKind; + } + const legacyImportDecl = j.importDeclaration(legacySpecifiers, j.literal(`${targetPackage}/legacy`)); + if (importKind) { + legacyImportDecl.importKind = importKind; + } + j(path).insertAfter(legacyImportDecl); + dirtyFlag = true; + return; + } + + if (legacySpecifiers.length > 0) { + // Only legacy hooks present + node.source.value = `${targetPackage}/legacy`; + if (importKind) { + node.importKind = importKind; + } + dirtyFlag = true; + return; + } + + // Only non-legacy imports present + node.source.value = targetPackage; + if (importKind) { + node.importKind = importKind; + } + dirtyFlag = true; + }); + }); + + return dirtyFlag ? root.toSource() : undefined; +}; + +module.exports.parser = 'tsx'; diff --git a/packages/upgrade/src/components/Codemod.js b/packages/upgrade/src/components/Codemod.js index 9498dd800f4..99fd107d073 100644 --- a/packages/upgrade/src/components/Codemod.js +++ b/packages/upgrade/src/components/Codemod.js @@ -49,7 +49,7 @@ export function Codemod(props) { {glob.toString()} ) : ( { setGlob(val.split(/[ ,]/)); }} diff --git a/packages/upgrade/src/components/SDKWorkflow.js b/packages/upgrade/src/components/SDKWorkflow.js index f0dd802e640..06acc6076a6 100644 --- a/packages/upgrade/src/components/SDKWorkflow.js +++ b/packages/upgrade/src/components/SDKWorkflow.js @@ -9,6 +9,26 @@ import { Command } from './Command.js'; import { Header } from './Header.js'; import { UpgradeSDK } from './UpgradeSDK.js'; +function versionNeedsUpgrade(sdk, version) { + if (sdk === 'clerk-react' && version === 5) { + return true; + } + + if (sdk === 'clerk-expo' && version === 2) { + return true; + } + + if (sdk === 'react-router' && version === 2) { + return true; + } + + if (sdk === 'tanstack-react-start' && version === 0) { + return true; + } + + return false; +} + /** * SDKWorkflow component handles the upgrade process for a given SDK. * It checks the current version of the SDK and provides the necessary steps @@ -29,111 +49,180 @@ export function SDKWorkflow(props) { const [version] = useState(getClerkSdkVersion(sdk)); - if (sdk !== 'nextjs') { + if (!['nextjs', 'clerk-react', 'clerk-expo', 'react-router', 'tanstack-react-start'].includes(sdk)) { return ( - The SDK upgrade functionality is only available for @clerk/nextjs at the moment. + The SDK upgrade functionality is not available for @clerk/{sdk} at the moment. ); } - // Right now, we only have one codemod for the `@clerk/nextjs` async request transformation - return ( - <> -
- - Clerk SDK used: @clerk/{sdk} - - - Migrating from version: {version} - - {runCodemod ? ( + if (sdk === 'nextjs') { + // Right now, we only have one codemod for the `@clerk/nextjs` async request transformation + return ( + <> +
+ + Clerk SDK used: @clerk/{sdk} + - Executing codemod: yes + Migrating from version: {version} - ) : null} - - {version === 5 && ( - <> - - {upgradeComplete ? ( - + Executing codemod: yes + + ) : null} + + {version === 5 && ( + <> + - ) : null} - - )} - {version === 6 && ( - <> - {runCodemod ? ( - + ) : null} + + )} + {version === 6 && ( + <> + - ) : ( - <> - - Looks like you are already on the latest version of @clerk/{sdk}. Would you like to - run the associated codemod? - - { + if (value === 'yes') { + setRunCodemod(true); + } else { + setDone(true); + } + }} + /> + )} - /> - - )} - - ); + + )} + {done && ( + <> + + Done upgrading @clerk/nextjs + + } + onError={() => null} + onSuccess={() => ( + + + We have detected that your application might be using the useAuth hook from{' '} + @clerk/nextjs. + + + + If usages of this hook are server-side rendered, you might need to add the dynamic{' '} + prop to your application's root ClerkProvider. + + + + You can find more information about this change in the Clerk documentation at{' '} + + https://clerk.com/docs/references/nextjs/rendering-modes + + . + + + )} + /> + + )} + + ); + } + + if (['clerk-react', 'clerk-expo', 'react-router', 'tanstack-react-start'].includes(sdk)) { + const replacePackage = sdk === 'clerk-react' || sdk === 'clerk-expo'; + return ( + <> +
+ + Clerk SDK used: @clerk/{sdk} + + + Migrating from version: {version} + + {runCodemod ? ( + + Executing codemod: yes + + ) : null} + + {versionNeedsUpgrade(sdk, version) && ( + <> + + {upgradeComplete ? ( + + ) : null} + + )} + {done && ( + <> + + {replacePackage ? ( + <> + Done upgrading to @clerk/{sdk.replace('clerk-', '')} + + ) : ( + <> + Done upgrading @clerk/{sdk} + + )} + + + )} + + ); + } } diff --git a/packages/upgrade/src/components/UpgradeSDK.js b/packages/upgrade/src/components/UpgradeSDK.js index 1fc3f3113a7..c29b8edeb04 100644 --- a/packages/upgrade/src/components/UpgradeSDK.js +++ b/packages/upgrade/src/components/UpgradeSDK.js @@ -15,14 +15,25 @@ function detectPackageManager() { return undefined; } -function upgradeCommand(sdk, packageManager) { +/** + * + * @param {string} sdk + * @param {string} packageManager + * @param {boolean} replacePackage + * @returns + */ +function upgradeCommand(sdk, packageManager, replacePackage = false) { + let packageName = `@clerk/${sdk}`; + if (replacePackage) { + packageName = packageName.replace('clerk-', ''); + } switch (packageManager) { case 'yarn': - return `yarn add @clerk/${sdk}@latest`; + return `yarn add ${packageName}@latest`; case 'pnpm': - return `pnpm add @clerk/${sdk}@latest`; + return `pnpm add ${packageName}@latest`; default: - return `npm install @clerk/${sdk}@latest`; + return `npm install ${packageName}@latest`; } } @@ -33,12 +44,13 @@ function upgradeCommand(sdk, packageManager) { * @param {Object} props * @param {Function} props.callback - The callback function to be called after the command execution. * @param {string} props.sdk - The SDK for which the upgrade command is run. + * @param {boolean} props.replacePackage - Whether to replace legacy `clerk-` packages with their new versions. * @returns {JSX.Element} The rendered component. * * @example * */ -export function UpgradeSDK({ callback, sdk }) { +export function UpgradeSDK({ callback, sdk, replacePackage = false }) { const [command, setCommand] = useState(); const [error, setError] = useState(); const [packageManager, setPackageManager] = useState(detectPackageManager()); @@ -52,7 +64,7 @@ export function UpgradeSDK({ callback, sdk }) { if (previous) { return previous; } - return upgradeCommand(sdk, packageManager); + return upgradeCommand(sdk, packageManager, replacePackage); }); if (!command) { return;