Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,18 @@ const clerk = require("@clerk/clerk-react")
const clerk = require("@clerk/react")
`,
},
{
name: 'Handles directives with mixed legacy imports without double semicolons',
source: `"use client";

import { ClerkProvider, useSignIn, useSignUp } from "@clerk/nextjs";

export const dynamic = "force-dynamic";
`,
output: `"use client";
import { ClerkProvider } from "@clerk/nextjs";
import { useSignIn, useSignUp } from "@clerk/nextjs/legacy";

export const dynamic = "force-dynamic";`,
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
export const fixtures = [
{
name: 'ClerkProvider legacy redirect props',
source: `
import { ClerkProvider } from '@clerk/nextjs';

export function App({ children }) {
return (
<ClerkProvider
afterSignInUrl='/dashboard'
afterSignUpUrl='/welcome'
>
{children}
</ClerkProvider>
);
}
`,
output: `
import { ClerkProvider } from '@clerk/nextjs';

export function App({ children }) {
return (
<ClerkProvider
signInFallbackRedirectUrl='/dashboard'
signUpFallbackRedirectUrl='/welcome'
>
{children}
</ClerkProvider>
);
}
`,
},
{
name: 'SignIn legacy props',
source: `
import { SignIn as MySignIn } from '@clerk/nextjs';

export const Page = () => (
<MySignIn
afterSignInUrl='/home'
afterSignUpUrl='/after-sign-up'
fallbackRedirectUrl='/existing'
/>
);
`,
output: `
import { SignIn as MySignIn } from '@clerk/nextjs';

export const Page = () => (
<MySignIn
signUpFallbackRedirectUrl='/after-sign-up'
fallbackRedirectUrl='/existing' />
);
`,
},
{
name: 'SignUp legacy props',
source: `
import { SignUp } from '@clerk/react';

export function Example() {
return (
<SignUp afterSignUpUrl='/done' afterSignInUrl='/in' />
);
}
`,
output: `
import { SignUp } from '@clerk/react';

export function Example() {
return (<SignUp fallbackRedirectUrl='/done' signInFallbackRedirectUrl='/in' />);
}
`,
},
{
name: 'ClerkProvider redirectUrl only',
source: `
import { ClerkProvider } from '@clerk/react';

export const Provider = ({ children }) => (
<ClerkProvider redirectUrl='/legacy'>{children}</ClerkProvider>
);
`,
output: `
import { ClerkProvider } from '@clerk/react';

export const Provider = ({ children }) => (
<ClerkProvider signInFallbackRedirectUrl="/legacy" signUpFallbackRedirectUrl="/legacy">{children}</ClerkProvider>
);
`,
},
{
name: 'SignIn redirectUrl only',
source: `
import { SignIn } from '@clerk/nextjs';

export const Page = () => <SignIn redirectUrl='/legacy' />;
`,
output: `
import { SignIn } from '@clerk/nextjs';

export const Page = () => <SignIn fallbackRedirectUrl="/legacy" />;
`,
},
{
name: 'UserButton and organization props',
source: `
import { UserButton, OrganizationSwitcher, CreateOrganization } from '@clerk/react';

export const Actions = () => (
<>
<UserButton afterSignOutUrl='/bye' afterMultiSessionSingleSignOutUrl='/multi' />
<OrganizationSwitcher hideSlug afterSwitchOrganizationUrl='/org' />
<CreateOrganization hideSlug />
</>
);
`,
output: `
import { UserButton, OrganizationSwitcher, CreateOrganization } from '@clerk/react';

export const Actions = () => (
<>
<UserButton />
<OrganizationSwitcher afterSelectOrganizationUrl='/org' />
<CreateOrganization />
</>
);
`,
},
{
name: 'Object literals and destructuring',
source: `
const config = {
afterSignInUrl: '/one',
afterSignUpUrl: '/two',
activeSessions,
};

const { afterSignInUrl, afterSignUpUrl: custom, activeSessions: current } = config;
`,
output: `
const config = {
signInFallbackRedirectUrl: '/one',
signUpFallbackRedirectUrl: '/two',
signedInSessions: activeSessions,
};

const { signInFallbackRedirectUrl: afterSignInUrl, signUpFallbackRedirectUrl: custom, signedInSessions: current } = config;
`,
},
{
name: 'Member expressions and optional chaining',
source: `
const signInTarget = options.afterSignInUrl;
const signUpTarget = options?.afterSignUpUrl;
const fallback = options['afterSignInUrl'];
const hasSessions = client?.activeSessions?.length > 0 && client['activeSessions'];
`,
output: `
const signInTarget = options.signInFallbackRedirectUrl;
const signUpTarget = options?.signUpFallbackRedirectUrl;
const fallback = options["signInFallbackRedirectUrl"];
const hasSessions = client?.signedInSessions?.length > 0 && client["signedInSessions"];
`,
},
{
name: 'setActive beforeEmit callback',
source: `
await setActive({
session: '123',
beforeEmit: handleBeforeEmit,
});
`,
output: `
await setActive({
session: '123',
navigate: params => handleBeforeEmit(params.session),
});
`,
},
{
name: 'ClerkMiddlewareAuthObject type rename',
source: `
import type { ClerkMiddlewareAuthObject } from '@clerk/nextjs/server';

type Handler = (auth: ClerkMiddlewareAuthObject) => void;
`,
output: `
import type { ClerkMiddlewareSessionAuthObject } from '@clerk/nextjs/server';

type Handler = (auth: ClerkMiddlewareSessionAuthObject) => void;
`,
},
{
name: 'Namespace import support',
source: `
import * as Clerk from '@clerk/nextjs';

export const Provider = ({ children }) => (
<Clerk.ClerkProvider redirectUrl='/deep' />
);
`,
output: `
import * as Clerk from '@clerk/nextjs';

export const Provider = ({ children }) => (
<Clerk.ClerkProvider signInFallbackRedirectUrl="/deep" signUpFallbackRedirectUrl="/deep" />
);
`,
},
];
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-remove-deprecated-props.cjs';
import { fixtures } from './__fixtures__/transform-remove-deprecated-props.fixtures';

describe('transform-remove-deprecated-props', () => {
it.each(fixtures)('$name', ({ source, output }) => {
const result = applyTransform(transformer, {}, { source });

expect(result).toEqual(output.trim());
});
});
61 changes: 37 additions & 24 deletions packages/upgrade/src/codemods/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,51 @@ import { run } from 'jscodeshift/src/Runner.js';

const __dirname = dirname(fileURLToPath(import.meta.url));

export async function runCodemod(transform = 'transform-async-request', glob, options) {
const GLOBBY_IGNORE = [
'**/*.md',
'node_modules/**',
'**/node_modules/**',
'.git/**',
'**/*.json',
'package.json',
'**/package.json',
'package-lock.json',
'**/package-lock.json',
'yarn.lock',
'**/yarn.lock',
'pnpm-lock.yaml',
'**/pnpm-lock.yaml',
'yalc.lock',
'**/*.(ico|png|webp|svg|gif|jpg|jpeg)+',
'**/*.(mp4|mkv|wmv|m4v|mov|avi|flv|webm|flac|mka|m4a|aac|ogg)+',
'**/*.(css|scss|sass|less|styl)+',
];

export async function runCodemod(transform = 'transform-async-request', glob, options = {}) {
if (!transform) {
throw new Error('No transform provided');
}
const resolvedPath = resolve(__dirname, `${transform}.cjs`);

const paths = await globby(glob, {
ignore: [
'**/*.md',
'node_modules/**',
'**/node_modules/**',
'.git/**',
'**/*.json',
'package.json',
'**/package.json',
'package-lock.json',
'**/package-lock.json',
'yarn.lock',
'**/yarn.lock',
'pnpm-lock.yaml',
'**/pnpm-lock.yaml',
'yalc.lock',
'**/*.(ico|png|webp|svg|gif|jpg|jpeg)+', // common image files
'**/*.(mp4|mkv|wmv|m4v|mov|avi|flv|webm|flac|mka|m4a|aac|ogg)+', // common video files] }).then(files => {
'**/*.(css|scss|sass|less|styl)+', // common style files
],
const paths = await globby(glob, { ignore: GLOBBY_IGNORE });

// First pass: dry run to collect stats (jscodeshift only reports stats in dry mode)
const dryResult = await run(resolvedPath, paths ?? [], {
...options,
dry: true,
silent: true,
});

return await run(resolvedPath, paths ?? [], {
dry: false,
// Second pass: apply the changes
const result = await run(resolvedPath, paths ?? [], {
...options,
// we must silence stdout to prevent output from interfering with ink CLI
dry: false,
silent: true,
});

// Merge stats from dry run into final result
return {
...result,
stats: dryResult.stats,
};
}
25 changes: 18 additions & 7 deletions packages/upgrade/src/codemods/transform-clerk-react-v6.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,20 @@ module.exports = function transformClerkReactV6({ source }, { jscodeshift: j })

if (legacySpecifiers.length > 0 && nonLegacySpecifiers.length > 0) {
// Mixed import: keep non-legacy on targetPackage, emit a new import for legacy hooks
node.specifiers = nonLegacySpecifiers;
node.source = j.literal(targetPackage);
// Use replaceWith to avoid formatting issues with insertAfter
const mainImport = j.importDeclaration(nonLegacySpecifiers, j.stringLiteral(targetPackage));
if (importKind) {
node.importKind = importKind;
mainImport.importKind = importKind;
}
const legacyImportDecl = j.importDeclaration(legacySpecifiers, j.literal(`${targetPackage}/legacy`));
// Preserve leading comments/whitespace from original import
mainImport.comments = node.comments;

const legacyImport = j.importDeclaration(legacySpecifiers, j.stringLiteral(`${targetPackage}/legacy`));
if (importKind) {
legacyImportDecl.importKind = importKind;
legacyImport.importKind = importKind;
}
j(path).insertAfter(legacyImportDecl);

j(path).replaceWith([mainImport, legacyImport]);
dirtyFlag = true;
return;
}
Expand Down Expand Up @@ -137,7 +141,14 @@ module.exports = function transformClerkReactV6({ source }, { jscodeshift: j })
});
});

return dirtyFlag ? root.toSource() : undefined;
if (!dirtyFlag) {
return undefined;
}

let result = root.toSource();
// Fix double semicolons that can occur when recast reprints directive prologues (e.g., "use client";)
result = result.replace(/^(['"`][^'"`]+['"`]);;/gm, '$1;');
return result;
};

module.exports.parser = 'tsx';
Loading
Loading