Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eight-groups-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/upgrade': minor
---

Add support for @clerk/react
6 changes: 5 additions & 1 deletion packages/upgrade/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 <SDKWorkflow sdk={sdks[0]} />;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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";
`,
},
];
Original file line number Diff line number Diff line change
@@ -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());
});
});
2 changes: 1 addition & 1 deletion packages/upgrade/src/codemods/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
89 changes: 89 additions & 0 deletions packages/upgrade/src/codemods/transform-clerk-react-v6.cjs
Original file line number Diff line number Diff line change
@@ -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';
2 changes: 1 addition & 1 deletion packages/upgrade/src/components/Codemod.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export function Codemod(props) {
<Text>{glob.toString()}</Text>
) : (
<TextInput
defaultValue='**/*'
defaultValue='**/*.(js|jsx|ts|tsx|mjs|cjs)'
onSubmit={val => {
setGlob(val.split(/[ ,]/));
}}
Expand Down
Loading