From 794bf7f78f2a31e164be1747974a9c00dd6bc8ce Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 24 Nov 2025 10:59:46 -0600 Subject: [PATCH 01/11] feat(upgrade): prop removal code mods --- ...nsform-remove-deprecated-props.fixtures.js | 212 ++++++++ .../transform-remove-deprecated-props.test.js | 14 + .../transform-remove-deprecated-props.cjs | 502 ++++++++++++++++++ 3 files changed, 728 insertions(+) create mode 100644 packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js create mode 100644 packages/upgrade/src/codemods/__tests__/transform-remove-deprecated-props.test.js create mode 100644 packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js new file mode 100644 index 00000000000..b4fc4ed4f78 --- /dev/null +++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js @@ -0,0 +1,212 @@ +export const fixtures = [ + { + name: 'ClerkProvider legacy redirect props', + source: ` +import { ClerkProvider } from '@clerk/nextjs'; + +export function App({ children }) { + return ( + + {children} + + ); +} + `, + output: ` +import { ClerkProvider } from '@clerk/nextjs'; + +export function App({ children }) { + return ( + + {children} + + ); +} + `, + }, + { + name: 'SignIn legacy props', + source: ` +import { SignIn as MySignIn } from '@clerk/nextjs'; + +export const Page = () => ( + +); + `, + output: ` +import { SignIn as MySignIn } from '@clerk/nextjs'; + +export const Page = () => ( + +); + `, + }, + { + name: 'SignUp legacy props', + source: ` +import { SignUp } from '@clerk/react'; + +export function Example() { + return ( + + ); +} + `, + output: ` +import { SignUp } from '@clerk/react'; + +export function Example() { + return (); +} + `, + }, + { + name: 'ClerkProvider redirectUrl only', + source: ` +import { ClerkProvider } from '@clerk/react'; + +export const Provider = ({ children }) => ( + {children} +); + `, + output: ` +import { ClerkProvider } from '@clerk/react'; + +export const Provider = ({ children }) => ( + {children} +); + `, + }, + { + name: 'SignIn redirectUrl only', + source: ` +import { SignIn } from '@clerk/nextjs'; + +export const Page = () => ; + `, + output: ` +import { SignIn } from '@clerk/nextjs'; + +export const Page = () => ; + `, + }, + { + name: 'UserButton and organization props', + source: ` +import { UserButton, OrganizationSwitcher, CreateOrganization } from '@clerk/react'; + +export const Actions = () => ( + <> + + + + +); + `, + output: ` +import { UserButton, OrganizationSwitcher, CreateOrganization } from '@clerk/react'; + +export const Actions = () => ( + <> + + + + +); + `, + }, + { + 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 }) => ( + +); + `, + output: ` +import * as Clerk from '@clerk/nextjs'; + +export const Provider = ({ children }) => ( + +); + `, + }, +]; + diff --git a/packages/upgrade/src/codemods/__tests__/transform-remove-deprecated-props.test.js b/packages/upgrade/src/codemods/__tests__/transform-remove-deprecated-props.test.js new file mode 100644 index 00000000000..b524469ffc1 --- /dev/null +++ b/packages/upgrade/src/codemods/__tests__/transform-remove-deprecated-props.test.js @@ -0,0 +1,14 @@ +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()); + }); +}); + diff --git a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs new file mode 100644 index 00000000000..ec2e0609f84 --- /dev/null +++ b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs @@ -0,0 +1,502 @@ +const CLERK_PACKAGE_PREFIX = '@clerk/'; +const COMPONENTS_WITH_HIDE_SLUG = new Set(['CreateOrganization', 'OrganizationSwitcher', 'OrganizationList']); +const COMPONENT_RENAMES = new Map([ + ['ClerkProvider', { afterSignInUrl: 'signInFallbackRedirectUrl', afterSignUpUrl: 'signUpFallbackRedirectUrl' }], + ['SignIn', { afterSignInUrl: 'fallbackRedirectUrl', afterSignUpUrl: 'signUpFallbackRedirectUrl' }], + ['SignUp', { afterSignInUrl: 'signInFallbackRedirectUrl', afterSignUpUrl: 'fallbackRedirectUrl' }], +]); +const COMPONENT_REDIRECT_ATTR = new Map([ + ['ClerkProvider', { targetAttrs: ['signInFallbackRedirectUrl', 'signUpFallbackRedirectUrl'] }], + ['SignIn', { targetAttrs: ['fallbackRedirectUrl'] }], + ['SignUp', { targetAttrs: ['fallbackRedirectUrl'] }], +]); +const COMPONENTS_WITH_USER_BUTTON_REMOVALS = new Map([ + ['UserButton', ['afterSignOutUrl', 'afterMultiSessionSingleSignOutUrl']], +]); +const ORGANIZATION_SWITCHER_RENAMES = new Map([['afterSwitchOrganizationUrl', 'afterSelectOrganizationUrl']]); + +module.exports = function transformDeprecatedProps({ source }, { jscodeshift: j }) { + const root = j(source); + let dirty = false; + + const { namedImports, namespaceImports } = collectClerkImports(root, j); + + root.find(j.JSXOpeningElement).forEach(path => { + const canonicalName = getCanonicalComponentName(path.node.name, namedImports, namespaceImports); + if (!canonicalName) { + return; + } + + const jsxNode = path.node; + + if (COMPONENTS_WITH_HIDE_SLUG.has(canonicalName)) { + if (removeJsxAttribute(j, jsxNode, 'hideSlug')) { + dirty = true; + } + } + + if (COMPONENTS_WITH_USER_BUTTON_REMOVALS.has(canonicalName)) { + for (const attrName of COMPONENTS_WITH_USER_BUTTON_REMOVALS.get(canonicalName)) { + if (removeJsxAttribute(j, jsxNode, attrName)) { + dirty = true; + } + } + } + + if (COMPONENT_RENAMES.has(canonicalName)) { + const renameMap = COMPONENT_RENAMES.get(canonicalName); + for (const [oldName, newName] of Object.entries(renameMap)) { + if (renameJsxAttribute(j, jsxNode, oldName, newName)) { + dirty = true; + } + } + } + + if (COMPONENT_REDIRECT_ATTR.has(canonicalName)) { + if (handleRedirectAttribute(j, jsxNode, canonicalName)) { + dirty = true; + } + } + + if (canonicalName === 'OrganizationSwitcher') { + for (const [oldName, newName] of ORGANIZATION_SWITCHER_RENAMES) { + if (renameJsxAttribute(j, jsxNode, oldName, newName)) { + dirty = true; + } + } + } + }); + + if (renameObjectProperties(root, j, 'afterSignInUrl', 'signInFallbackRedirectUrl')) { + dirty = true; + } + if (renameObjectProperties(root, j, 'afterSignUpUrl', 'signUpFallbackRedirectUrl')) { + dirty = true; + } + + if (renameMemberExpressions(root, j, 'afterSignInUrl', 'signInFallbackRedirectUrl')) { + dirty = true; + } + if (renameMemberExpressions(root, j, 'afterSignUpUrl', 'signUpFallbackRedirectUrl')) { + dirty = true; + } + + if (renameTSPropertySignatures(root, j, 'afterSignInUrl', 'signInFallbackRedirectUrl')) { + dirty = true; + } + if (renameTSPropertySignatures(root, j, 'afterSignUpUrl', 'signUpFallbackRedirectUrl')) { + dirty = true; + } + if (renameTSPropertySignatures(root, j, 'activeSessions', 'signedInSessions')) { + dirty = true; + } + + if (renameMemberExpressions(root, j, 'activeSessions', 'signedInSessions')) { + dirty = true; + } + if (renameObjectProperties(root, j, 'activeSessions', 'signedInSessions')) { + dirty = true; + } + + if (transformSetActiveBeforeEmit(root, j)) { + dirty = true; + } + + if (renameTypeReferences(root, j, 'ClerkMiddlewareAuthObject', 'ClerkMiddlewareSessionAuthObject')) { + dirty = true; + } + + return dirty ? root.toSource() : undefined; +}; + +module.exports.parser = 'tsx'; + +function collectClerkImports(root, j) { + const namedImports = new Map(); + const namespaceImports = new Set(); + + root.find(j.ImportDeclaration).forEach(path => { + const sourceVal = path.node.source.value; + if (typeof sourceVal !== 'string' || !sourceVal.startsWith(CLERK_PACKAGE_PREFIX)) { + return; + } + + for (const specifier of path.node.specifiers || []) { + if (j.ImportSpecifier.check(specifier)) { + const localName = specifier.local ? specifier.local.name : specifier.imported.name; + namedImports.set(localName, specifier.imported.name); + } else if (j.ImportNamespaceSpecifier.check(specifier) || j.ImportDefaultSpecifier.check(specifier)) { + namespaceImports.add(specifier.local.name); + } + } + }); + + return { namedImports, namespaceImports }; +} + +function getCanonicalComponentName(nameNode, namedImports, namespaceImports) { + if (!nameNode) { + return null; + } + + if (nameNode.type === 'JSXIdentifier') { + return namedImports.get(nameNode.name) || nameNode.name; + } + + if (nameNode.type === 'JSXMemberExpression') { + const identifierName = getNamespaceMemberName(nameNode, namespaceImports); + if (identifierName) { + return identifierName; + } + } + + return null; +} + +function getNamespaceMemberName(memberNode, namespaceImports) { + if (memberNode.object.type === 'JSXIdentifier') { + return namespaceImports.has(memberNode.object.name) ? memberNode.property.name : null; + } + + if (memberNode.object.type === 'JSXMemberExpression') { + const resolved = getNamespaceMemberName(memberNode.object, namespaceImports); + return resolved ? memberNode.property.name : null; + } + + return null; +} + +function renameJsxAttribute(j, jsxNode, oldName, newName) { + if (!jsxNode.attributes) { + return false; + } + const attrIndex = jsxNode.attributes.findIndex(attr => isJsxAttrNamed(attr, oldName)); + if (attrIndex === -1) { + return false; + } + + const targetExists = jsxNode.attributes.some(attr => isJsxAttrNamed(attr, newName)); + if (targetExists) { + jsxNode.attributes.splice(attrIndex, 1); + return true; + } + + const attribute = jsxNode.attributes[attrIndex]; + attribute.name.name = newName; + return true; +} + +function removeJsxAttribute(j, jsxNode, name) { + if (!jsxNode.attributes) { + return false; + } + const initialLength = jsxNode.attributes.length; + jsxNode.attributes = jsxNode.attributes.filter(attr => !isJsxAttrNamed(attr, name)); + return jsxNode.attributes.length !== initialLength; +} + +function isJsxAttrNamed(attribute, name) { + return attribute && attribute.type === 'JSXAttribute' && attribute.name && attribute.name.name === name; +} + +function handleRedirectAttribute(j, jsxNode, canonicalName) { + if (!jsxNode.attributes) { + return false; + } + + const data = COMPONENT_REDIRECT_ATTR.get(canonicalName); + const attrIndex = jsxNode.attributes.findIndex(attr => isJsxAttrNamed(attr, 'redirectUrl')); + if (attrIndex === -1) { + return false; + } + + const redirectAttr = jsxNode.attributes[attrIndex]; + + const insertions = []; + for (const targetName of data.targetAttrs) { + if (!jsxNode.attributes.some(attr => isJsxAttrNamed(attr, targetName))) { + insertions.push(createJsxAttributeWithClonedValue(j, targetName, redirectAttr.value)); + } + } + + jsxNode.attributes.splice(attrIndex, 1, ...insertions); + return true; +} + +function createJsxAttributeWithClonedValue(j, name, value) { + let clonedValue = null; + if (value) { + clonedValue = clone(value); + } + return j.jsxAttribute(j.jsxIdentifier(name), clonedValue); +} + +function clone(node) { + return node ? JSON.parse(JSON.stringify(node)) : node; +} + +function renameObjectProperties(root, j, oldName, newName) { + let changed = false; + + root.find(j.ObjectProperty).forEach(path => { + if (!isPropertyKeyNamed(path.node.key, oldName)) { + return; + } + + const parent = path.parentPath && path.parentPath.node; + const isPattern = parent && parent.type === 'ObjectPattern'; + const originalLocalName = getLocalIdentifierName(path.node); + + if (path.node.shorthand) { + path.node.shorthand = false; + const identifierName = originalLocalName || oldName; + path.node.value = j.identifier(identifierName); + } + + if (path.node.key.type === 'Identifier') { + path.node.key.name = newName; + } else if (path.node.key.type === 'StringLiteral') { + path.node.key.value = newName; + } else if (path.node.key.type === 'Literal') { + path.node.key.value = newName; + } + + changed = true; + }); + + return changed; +} + +function getLocalIdentifierName(propertyNode) { + if (!propertyNode) { + return null; + } + + if (propertyNode.value && propertyNode.value.type === 'Identifier') { + return propertyNode.value.name; + } + + if (propertyNode.shorthand && propertyNode.key && propertyNode.key.type === 'Identifier') { + return propertyNode.key.name; + } + + return null; +} + +function isPropertyKeyNamed(keyNode, name) { + if (!keyNode) { + return false; + } + if (keyNode.type === 'Identifier') { + return keyNode.name === name; + } + if (keyNode.type === 'StringLiteral' || keyNode.type === 'Literal') { + return keyNode.value === name; + } + return false; +} + +function renameMemberExpressions(root, j, oldName, newName) { + let changed = false; + + root + .find(j.MemberExpression, { + property: { type: 'Identifier', name: oldName }, + computed: false, + }) + .forEach(path => { + path.node.property.name = newName; + changed = true; + }); + + root + .find(j.MemberExpression, { + computed: true, + }) + .forEach(path => { + if (path.node.property.type === 'Literal' && path.node.property.value === oldName) { + path.node.property.value = newName; + changed = true; + } + if (path.node.property.type === 'StringLiteral' && path.node.property.value === oldName) { + path.node.property.value = newName; + changed = true; + } + }); + + root + .find(j.OptionalMemberExpression, { + property: { type: 'Identifier', name: oldName }, + computed: false, + }) + .forEach(path => { + path.node.property.name = newName; + changed = true; + }); + + root + .find(j.OptionalMemberExpression, { + computed: true, + }) + .forEach(path => { + if (path.node.property.type === 'Literal' && path.node.property.value === oldName) { + path.node.property.value = newName; + changed = true; + } + if (path.node.property.type === 'StringLiteral' && path.node.property.value === oldName) { + path.node.property.value = newName; + changed = true; + } + }); + + return changed; +} + +function renameTSPropertySignatures(root, j, oldName, newName) { + let changed = false; + + root.find(j.TSPropertySignature).forEach(path => { + if (!isPropertyKeyNamed(path.node.key, oldName)) { + return; + } + + if (path.node.key.type === 'Identifier') { + path.node.key.name = newName; + } else if (path.node.key.type === 'StringLiteral') { + path.node.key.value = newName; + } + + changed = true; + }); + + return changed; +} + +function transformSetActiveBeforeEmit(root, j) { + let changed = false; + + root + .find(j.CallExpression) + .filter(path => isSetActiveCall(path.node.callee, j)) + .forEach(path => { + const [args0] = path.node.arguments; + if (!args0 || args0.type !== 'ObjectExpression') { + return; + } + const beforeEmitIndex = args0.properties.findIndex(prop => isPropertyNamed(prop, 'beforeEmit')); + if (beforeEmitIndex === -1) { + return; + } + const beforeEmitProp = args0.properties[beforeEmitIndex]; + if (!beforeEmitProp || beforeEmitProp.type !== 'ObjectProperty') { + return; + } + const originalValue = getPropertyValueExpression(beforeEmitProp.value, j); + if (!originalValue) { + args0.properties.splice(beforeEmitIndex, 1); + changed = true; + return; + } + + const navigateProp = j.objectProperty(j.identifier('navigate'), buildNavigateArrowFunction(j, originalValue)); + args0.properties.splice(beforeEmitIndex, 1, navigateProp); + changed = true; + }); + + return changed; +} + +function isSetActiveCall(callee, j) { + if (!callee) { + return false; + } + if (callee.type === 'Identifier') { + return callee.name === 'setActive'; + } + if (callee.type === 'MemberExpression' || callee.type === 'OptionalMemberExpression') { + const property = callee.property; + return property && property.type === 'Identifier' && property.name === 'setActive'; + } + return false; +} + +function isPropertyNamed(prop, name) { + return prop && prop.type === 'ObjectProperty' && isPropertyKeyNamed(prop.key, name); +} + +function getPropertyValueExpression(valueNode, j) { + if (!valueNode) { + return null; + } + if (valueNode.type === 'JSXExpressionContainer') { + return valueNode.expression; + } + return valueNode; +} + +function buildNavigateArrowFunction(j, originalExpression) { + const paramIdentifier = j.identifier('params'); + const calleeExpression = clone(originalExpression); + const callExpression = j.callExpression(calleeExpression, [ + j.memberExpression(paramIdentifier, j.identifier('session')), + ]); + return j.arrowFunctionExpression([paramIdentifier], callExpression); +} + +function renameTypeReferences(root, j, oldName, newName) { + let changed = false; + + root.find(j.ImportSpecifier).forEach(path => { + const imported = path.node.imported; + if (imported && imported.type === 'Identifier' && imported.name === oldName) { + imported.name = newName; + if (path.node.local && path.node.local.name === oldName) { + path.node.local.name = newName; + } + changed = true; + } + }); + + root.find(j.TSTypeReference).forEach(path => { + if (renameEntityName(path.node.typeName, oldName, newName)) { + changed = true; + } + }); + + root.find(j.TSExpressionWithTypeArguments).forEach(path => { + if (renameEntityName(path.node.expression, oldName, newName)) { + changed = true; + } + }); + + root.find(j.TSQualifiedName).forEach(path => { + if (path.node.right.type === 'Identifier' && path.node.right.name === oldName) { + path.node.right.name = newName; + changed = true; + } + }); + + return changed; +} + +function renameEntityName(entity, oldName, newName) { + if (!entity) { + return false; + } + + if (entity.type === 'Identifier' && entity.name === oldName) { + entity.name = newName; + return true; + } + + if (entity.type === 'TSQualifiedName') { + if (entity.right.type === 'Identifier' && entity.right.name === oldName) { + entity.right.name = newName; + return true; + } + return renameEntityName(entity.left, oldName, newName); + } + + return false; +} + From fb1d5dc2722005abeb3d9eb1270f7cca84ad2d2c Mon Sep 17 00:00:00 2001 From: Jacek Date: Mon, 1 Dec 2025 13:42:31 -0600 Subject: [PATCH 02/11] wip --- .../upgrade/src/components/SDKWorkflow.js | 398 +++++++++++------- 1 file changed, 253 insertions(+), 145 deletions(-) diff --git a/packages/upgrade/src/components/SDKWorkflow.js b/packages/upgrade/src/components/SDKWorkflow.js index 06acc6076a6..fbfd496c59b 100644 --- a/packages/upgrade/src/components/SDKWorkflow.js +++ b/packages/upgrade/src/components/SDKWorkflow.js @@ -9,6 +9,12 @@ import { Command } from './Command.js'; import { Header } from './Header.js'; import { UpgradeSDK } from './UpgradeSDK.js'; +const CODEMODS = { + ASYNC_REQUEST: 'transform-async-request', + CLERK_REACT_V6: 'transform-clerk-react-v6', + REMOVE_DEPRECATED_PROPS: 'transform-remove-deprecated-props', +}; + function versionNeedsUpgrade(sdk, version) { if (sdk === 'clerk-react' && version === 5) { return true; @@ -58,171 +64,273 @@ export function SDKWorkflow(props) { } if (sdk === 'nextjs') { - // Right now, we only have one codemod for the `@clerk/nextjs` async request transformation return ( - <> -
- - Clerk SDK used: @clerk/{sdk} - + + ); + } + + if (['clerk-react', 'clerk-expo', 'react-router', 'tanstack-react-start'].includes(sdk)) { + return ( + + ); + } +} + +function NextjsWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgradeComplete, upgradeComplete, version }) { + const [v6CodemodComplete, setV6CodemodComplete] = useState(false); + + return ( + <> +
+ + Clerk SDK used: @clerk/{sdk} + + + Migrating from version: {version} + + {runCodemod ? ( - Migrating from version: {version} + Executing codemod: yes - {runCodemod ? ( - - Executing codemod: yes - - ) : null} - - {version === 5 && ( - <> - + {version === 5 && ( + <> + + {upgradeComplete ? ( + - {upgradeComplete ? ( - - ) : null} - - )} - {version === 6 && ( - <> - - {upgradeComplete ? ( + ) : null} + + )} + {version === 6 && ( + <> + + {upgradeComplete ? ( + + ) : null} + {v6CodemodComplete ? ( + + ) : null} + + )} + {version === 7 && ( + <> + {runCodemod ? ( + <> - ) : null} - - )} - {version === 7 && ( - <> - {runCodemod ? ( - + ) : null} + + ) : ( + <> + + Looks like you are already on the latest version of @clerk/{sdk}. Would you like to + run the associated codemods? + + { - if (value === 'yes') { - setRunCodemod(true); - } else { - setDone(true); - } - }} - /> - + + + 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 + + . + + )} - - )} - {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} - +function ReactSdkWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgradeComplete, upgradeComplete, version }) { + const [v6CodemodComplete, setV6CodemodComplete] = useState(false); + const replacePackage = sdk === 'clerk-react' || sdk === 'clerk-expo'; + const needsUpgrade = versionNeedsUpgrade(sdk, version); + + return ( + <> +
+ + Clerk SDK used: @clerk/{sdk} + + + Migrating from version: {version} + + {runCodemod ? ( - Migrating from version: {version} + Executing codemod: yes - {runCodemod ? ( - - Executing codemod: yes - - ) : null} - - {versionNeedsUpgrade(sdk, version) && ( - <> - + {needsUpgrade && ( + <> + + {upgradeComplete ? ( + + ) : null} + {v6CodemodComplete ? ( + - {upgradeComplete ? ( + ) : null} + + )} + {!needsUpgrade && ( + <> + {runCodemod ? ( + <> - ) : null} - - )} - {done && ( - <> - - {replacePackage ? ( - <> - Done upgrading to @clerk/{sdk.replace('clerk-', '')} - - ) : ( - <> - Done upgrading @clerk/{sdk} - - )} - - - )} - - ); - } + {v6CodemodComplete ? ( + + ) : null} + + ) : ( + <> + + Looks like you are already on the latest version of @clerk/{sdk}. Would you like to + run the associated codemods? + + { + const numeric = typeof value === 'number' ? value : Number(value); + setVersion(Number.isNaN(numeric) ? 7 : numeric); + setVersionConfirmed(true); + }} + /> + + ); + } + if (sdk === 'nextjs') { return ( @@ -123,6 +160,7 @@ function NextjsWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgr callback={setV6CodemodComplete} sdk={sdk} transform={CODEMODS.ASYNC_REQUEST} + onGlobResolved={setGlob} /> ) : null} {v6CodemodComplete ? ( @@ -130,6 +168,7 @@ function NextjsWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgr callback={setDone} sdk={sdk} transform={CODEMODS.REMOVE_DEPRECATED_PROPS} + glob={glob} /> ) : null} @@ -145,6 +184,7 @@ function NextjsWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgr callback={setV6CodemodComplete} sdk={sdk} transform={CODEMODS.CLERK_REACT_V6} + onGlobResolved={setGlob} /> ) : null} {v6CodemodComplete ? ( @@ -152,6 +192,7 @@ function NextjsWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgr callback={setDone} sdk={sdk} transform={CODEMODS.REMOVE_DEPRECATED_PROPS} + glob={glob} /> ) : null} @@ -164,12 +205,14 @@ function NextjsWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgr callback={setV6CodemodComplete} sdk={sdk} transform={CODEMODS.CLERK_REACT_V6} + onGlobResolved={setGlob} /> {v6CodemodComplete ? ( ) : null} @@ -207,27 +250,7 @@ function NextjsWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgr } message={} 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 - - . - - - )} + onSuccess={NextjsUseAuthWarning} /> )} @@ -235,8 +258,33 @@ function NextjsWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgr ); } +function NextjsUseAuthWarning() { + return ( + + + 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 + + . + + + ); +} + function ReactSdkWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgradeComplete, upgradeComplete, version }) { const [v6CodemodComplete, setV6CodemodComplete] = useState(false); + const [glob, setGlob] = useState(); const replacePackage = sdk === 'clerk-react' || sdk === 'clerk-expo'; const needsUpgrade = versionNeedsUpgrade(sdk, version); @@ -267,6 +315,7 @@ function ReactSdkWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUp callback={setV6CodemodComplete} sdk={sdk} transform={CODEMODS.CLERK_REACT_V6} + onGlobResolved={setGlob} /> ) : null} {v6CodemodComplete ? ( @@ -274,6 +323,7 @@ function ReactSdkWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUp callback={setDone} sdk={sdk} transform={CODEMODS.REMOVE_DEPRECATED_PROPS} + glob={glob} /> ) : null} @@ -286,12 +336,14 @@ function ReactSdkWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUp callback={setV6CodemodComplete} sdk={sdk} transform={CODEMODS.CLERK_REACT_V6} + onGlobResolved={setGlob} /> {v6CodemodComplete ? ( ) : null} diff --git a/packages/upgrade/src/components/Scan.js b/packages/upgrade/src/components/Scan.js index 75f513af27e..1a6cdbd8f48 100644 --- a/packages/upgrade/src/components/Scan.js +++ b/packages/upgrade/src/components/Scan.js @@ -30,7 +30,7 @@ export function Scan(props) { // { sdkName: [{ title: 'x', matcher: /x/, slug: 'x', ... }] } useEffect(() => { setStatus(`Loading data for ${toVersion} migration`); - import(`../versions/${toVersion}/index.js`).then(version => { + void import(`../versions/${toVersion}/index.js`).then(version => { setMatchers( sdks.reduce((m, sdk) => { m[sdk] = version.default[sdk]; @@ -47,7 +47,7 @@ export function Scan(props) { setStatus('Collecting files to scan'); const pattern = convertPathToPattern(path.resolve(dir)); - globby(pattern, { + void globby(pattern, { ignore: [ 'node_modules/**', '**/node_modules/**', @@ -78,7 +78,7 @@ export function Scan(props) { } const allResults = {}; - Promise.all( + void Promise.all( // first we read all the files files.map(async (file, idx) => { const content = await fs.readFile(file, 'utf8'); @@ -142,8 +142,8 @@ export function Scan(props) { }), ) .then(() => { - const newResults = [...results, ...Object.keys(allResults).map(k => allResults[k])]; - setResults(newResults); + const aggregatedResults = Object.keys(allResults).map(k => allResults[k]); + setResults(prevResults => [...prevResults, ...aggregatedResults]); // Anonymously track how many instances of each breaking change item were encountered. // This only tracks the name of the breaking change found, and how many instances of it @@ -151,14 +151,14 @@ export function Scan(props) { // It is used internally to help us understand what the most common sticking points are // for our users so we can appropriate prioritize support/guidance/docs around them. if (!disableTelemetry) { - fetch('https://api.segment.io/v1/batch', { + void fetch('https://api.segment.io/v1/batch', { method: 'POST', headers: { Authorization: `Basic ${Buffer.from('5TkC1SM87VX2JRJcIGBBmL7sHLRWaIvc:').toString('base64')}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ - batch: newResults.map(item => { + batch: aggregatedResults.map(item => { return { type: 'track', userId: 'clerk-upgrade-tool', @@ -189,7 +189,7 @@ export function Scan(props) { .catch(err => { console.error(err); }); - }, [matchers, files, noWarnings, disableTelemetry]); + }, [matchers, files, noWarnings, disableTelemetry, fromVersion, toVersion, uuid]); return complete ? ( <> diff --git a/packages/upgrade/src/components/UpgradeSDK.js b/packages/upgrade/src/components/UpgradeSDK.js index c29b8edeb04..ae09e220dd4 100644 --- a/packages/upgrade/src/components/UpgradeSDK.js +++ b/packages/upgrade/src/components/UpgradeSDK.js @@ -80,7 +80,7 @@ export function UpgradeSDK({ callback, sdk, replacePackage = false }) { .finally(() => { callback(true); }); - }, [command, packageManager, sdk]); + }, [callback, command, packageManager, replacePackage, sdk]); return ( <> diff --git a/packages/upgrade/src/guide-generators/core-2/backend/index.js b/packages/upgrade/src/guide-generators/core-2/backend/index.js index 7c8fb025632..55d56e6a1ac 100644 --- a/packages/upgrade/src/guide-generators/core-2/backend/index.js +++ b/packages/upgrade/src/guide-generators/core-2/backend/index.js @@ -47,4 +47,4 @@ async function generate() { ]); } -generate().then(writeToFile(cwd)); +void generate().then(writeToFile(cwd)); diff --git a/packages/upgrade/src/guide-generators/core-2/chrome-extension/index.js b/packages/upgrade/src/guide-generators/core-2/chrome-extension/index.js index 1de0d8420ba..b999b13a66b 100644 --- a/packages/upgrade/src/guide-generators/core-2/chrome-extension/index.js +++ b/packages/upgrade/src/guide-generators/core-2/chrome-extension/index.js @@ -43,4 +43,4 @@ async function generate() { ]); } -generate().then(writeToFile(cwd)); +void generate().then(writeToFile(cwd)); diff --git a/packages/upgrade/src/guide-generators/core-2/expo/index.js b/packages/upgrade/src/guide-generators/core-2/expo/index.js index e84dd32c26f..0599cc115dd 100644 --- a/packages/upgrade/src/guide-generators/core-2/expo/index.js +++ b/packages/upgrade/src/guide-generators/core-2/expo/index.js @@ -42,4 +42,4 @@ async function generate() { ]); } -generate().then(writeToFile(cwd)); +void generate().then(writeToFile(cwd)); diff --git a/packages/upgrade/src/guide-generators/core-2/fastify/index.js b/packages/upgrade/src/guide-generators/core-2/fastify/index.js index 4fcd6907c84..568a5344386 100644 --- a/packages/upgrade/src/guide-generators/core-2/fastify/index.js +++ b/packages/upgrade/src/guide-generators/core-2/fastify/index.js @@ -38,4 +38,4 @@ async function generate() { ]); } -generate().then(writeToFile(cwd)); +void generate().then(writeToFile(cwd)); diff --git a/packages/upgrade/src/guide-generators/core-2/js/index.js b/packages/upgrade/src/guide-generators/core-2/js/index.js index b03eee8502e..ba009f4e654 100644 --- a/packages/upgrade/src/guide-generators/core-2/js/index.js +++ b/packages/upgrade/src/guide-generators/core-2/js/index.js @@ -48,4 +48,4 @@ async function generate() { ]); } -generate().then(writeToFile(cwd)); +void generate().then(writeToFile(cwd)); diff --git a/packages/upgrade/src/guide-generators/core-2/nextjs/index.js b/packages/upgrade/src/guide-generators/core-2/nextjs/index.js index 23c52e2b5de..39a16047cfb 100644 --- a/packages/upgrade/src/guide-generators/core-2/nextjs/index.js +++ b/packages/upgrade/src/guide-generators/core-2/nextjs/index.js @@ -47,4 +47,4 @@ async function generate() { ]); } -generate().then(writeToFile(cwd)); +void generate().then(writeToFile(cwd)); diff --git a/packages/upgrade/src/guide-generators/core-2/node/index.js b/packages/upgrade/src/guide-generators/core-2/node/index.js index 63bf1cc7d10..495d4bdd27d 100644 --- a/packages/upgrade/src/guide-generators/core-2/node/index.js +++ b/packages/upgrade/src/guide-generators/core-2/node/index.js @@ -41,4 +41,4 @@ async function generate() { ]); } -generate().then(writeToFile(cwd)); +void generate().then(writeToFile(cwd)); diff --git a/packages/upgrade/src/guide-generators/core-2/overview/index.js b/packages/upgrade/src/guide-generators/core-2/overview/index.js index 9655397f444..c2be30679d5 100644 --- a/packages/upgrade/src/guide-generators/core-2/overview/index.js +++ b/packages/upgrade/src/guide-generators/core-2/overview/index.js @@ -3,7 +3,7 @@ import { assembleContent, frontmatter, markdown, writeToFile } from '../../text- const cwd = 'core-2/overview'; async function generate() { - return assembleContent({ data: {}, cwd }, [ + return await assembleContent({ data: {}, cwd }, [ frontmatter({ title: `Upgrading to Clerk Core 2`, description: `Learn how to upgrade to the latest version of Clerk's SDKs`, @@ -12,4 +12,4 @@ async function generate() { ]); } -generate().then(writeToFile(cwd)); +void generate().then(writeToFile(cwd)); diff --git a/packages/upgrade/src/guide-generators/core-2/react/index.js b/packages/upgrade/src/guide-generators/core-2/react/index.js index baec27cae58..5e6490a177b 100644 --- a/packages/upgrade/src/guide-generators/core-2/react/index.js +++ b/packages/upgrade/src/guide-generators/core-2/react/index.js @@ -45,4 +45,4 @@ async function generate() { ]); } -generate().then(writeToFile(cwd)); +void generate().then(writeToFile(cwd)); diff --git a/packages/upgrade/src/guide-generators/core-2/remix/index.js b/packages/upgrade/src/guide-generators/core-2/remix/index.js index fd478336ece..7071557a295 100644 --- a/packages/upgrade/src/guide-generators/core-2/remix/index.js +++ b/packages/upgrade/src/guide-generators/core-2/remix/index.js @@ -45,4 +45,4 @@ async function generate() { ]); } -generate().then(writeToFile(cwd)); +void generate().then(writeToFile(cwd)); diff --git a/packages/upgrade/src/guide-generators/core-2/retheme/index.js b/packages/upgrade/src/guide-generators/core-2/retheme/index.js index 3db473d47ca..91a7898d860 100644 --- a/packages/upgrade/src/guide-generators/core-2/retheme/index.js +++ b/packages/upgrade/src/guide-generators/core-2/retheme/index.js @@ -35,4 +35,4 @@ async function generate() { ]); } -generate().then(writeToFile(cwd)); +void generate().then(writeToFile(cwd)); diff --git a/packages/upgrade/src/util/generate-changelog.js b/packages/upgrade/src/util/generate-changelog.js index 606b4c0803c..3fd6e6c4821 100644 --- a/packages/upgrade/src/util/generate-changelog.js +++ b/packages/upgrade/src/util/generate-changelog.js @@ -31,7 +31,7 @@ ${entry.content} return output; } -generate().then(console.log); +void generate().then(console.log); function getSdkName(val) { return SDKS.find(sdk => val === sdk.value).label; From df0c069b588834f6cf9dfa94d72562865422f332 Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 3 Dec 2025 14:26:56 -0600 Subject: [PATCH 04/11] format --- ...nsform-remove-deprecated-props.fixtures.js | 1 - .../transform-remove-deprecated-props.test.js | 1 - .../transform-remove-deprecated-props.cjs | 1 - .../upgrade/src/components/SDKWorkflow.js | 26 ++++++++++++++++--- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js index b4fc4ed4f78..3eaab4af5af 100644 --- a/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js +++ b/packages/upgrade/src/codemods/__tests__/__fixtures__/transform-remove-deprecated-props.fixtures.js @@ -209,4 +209,3 @@ export const Provider = ({ children }) => ( `, }, ]; - diff --git a/packages/upgrade/src/codemods/__tests__/transform-remove-deprecated-props.test.js b/packages/upgrade/src/codemods/__tests__/transform-remove-deprecated-props.test.js index b524469ffc1..957ace57590 100644 --- a/packages/upgrade/src/codemods/__tests__/transform-remove-deprecated-props.test.js +++ b/packages/upgrade/src/codemods/__tests__/transform-remove-deprecated-props.test.js @@ -11,4 +11,3 @@ describe('transform-remove-deprecated-props', () => { expect(result).toEqual(output.trim()); }); }); - diff --git a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs index d442ab35de8..ef28df1d09e 100644 --- a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs +++ b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs @@ -497,4 +497,3 @@ function renameEntityName(entity, oldName, newName) { return false; } - diff --git a/packages/upgrade/src/components/SDKWorkflow.js b/packages/upgrade/src/components/SDKWorkflow.js index afea254463f..ecd2a491c71 100644 --- a/packages/upgrade/src/components/SDKWorkflow.js +++ b/packages/upgrade/src/components/SDKWorkflow.js @@ -130,7 +130,16 @@ export function SDKWorkflow(props) { } } -function NextjsWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgradeComplete, upgradeComplete, version }) { +function NextjsWorkflow({ + done, + runCodemod, + sdk, + setDone, + setRunCodemod, + setUpgradeComplete, + upgradeComplete, + version, +}) { const [v6CodemodComplete, setV6CodemodComplete] = useState(false); const [glob, setGlob] = useState(); @@ -267,8 +276,8 @@ function NextjsUseAuthWarning() { - If usages of this hook are server-side rendered, you might need to add the dynamic{' '} - prop to your application's root ClerkProvider. + If usages of this hook are server-side rendered, you might need to add the dynamic prop to + your application's root ClerkProvider. @@ -282,7 +291,16 @@ function NextjsUseAuthWarning() { ); } -function ReactSdkWorkflow({ done, runCodemod, sdk, setDone, setRunCodemod, setUpgradeComplete, upgradeComplete, version }) { +function ReactSdkWorkflow({ + done, + runCodemod, + sdk, + setDone, + setRunCodemod, + setUpgradeComplete, + upgradeComplete, + version, +}) { const [v6CodemodComplete, setV6CodemodComplete] = useState(false); const [glob, setGlob] = useState(); const replacePackage = sdk === 'clerk-react' || sdk === 'clerk-expo'; From 1a91b6c6427cdd26a448adf6abc8b0fc508efbdb Mon Sep 17 00:00:00 2001 From: Jacek Date: Wed, 3 Dec 2025 20:31:06 -0600 Subject: [PATCH 05/11] collect transform stats --- packages/upgrade/src/codemods/index.js | 13 +++++++++++-- .../transform-remove-deprecated-props.cjs | 8 +++++++- packages/upgrade/src/components/Codemod.js | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/upgrade/src/codemods/index.js b/packages/upgrade/src/codemods/index.js index 5e7245c3d3e..c9c31941117 100644 --- a/packages/upgrade/src/codemods/index.js +++ b/packages/upgrade/src/codemods/index.js @@ -6,7 +6,7 @@ import { run } from 'jscodeshift/src/Runner.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); -export async function runCodemod(transform = 'transform-async-request', glob, options) { +export async function runCodemod(transform = 'transform-async-request', glob, options = {}) { if (!transform) { throw new Error('No transform provided'); } @@ -34,10 +34,19 @@ export async function runCodemod(transform = 'transform-async-request', glob, op ], }); - return await run(resolvedPath, paths ?? [], { + const clerkUpgradeStats = options.clerkUpgradeStats ?? {}; + + const result = await run(resolvedPath, paths ?? [], { dry: false, ...options, + // expose a mutable stats bag so individual transforms can record structured information + clerkUpgradeStats, // we must silence stdout to prevent output from interfering with ink CLI silent: true, }); + + return { + ...result, + clerkUpgradeStats, + }; } diff --git a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs index ef28df1d09e..6fb95272935 100644 --- a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs +++ b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs @@ -15,7 +15,7 @@ const COMPONENTS_WITH_USER_BUTTON_REMOVALS = new Map([ ]); const ORGANIZATION_SWITCHER_RENAMES = new Map([['afterSwitchOrganizationUrl', 'afterSelectOrganizationUrl']]); -module.exports = function transformDeprecatedProps({ source }, { jscodeshift: j }) { +module.exports = function transformDeprecatedProps({ source }, { jscodeshift: j }, options = {}) { const root = j(source); let dirty = false; @@ -36,11 +36,17 @@ module.exports = function transformDeprecatedProps({ source }, { jscodeshift: j } if (COMPONENTS_WITH_USER_BUTTON_REMOVALS.has(canonicalName)) { + let removedCount = 0; for (const attrName of COMPONENTS_WITH_USER_BUTTON_REMOVALS.get(canonicalName)) { if (removeJsxAttribute(j, jsxNode, attrName)) { dirty = true; + removedCount += 1; } } + if (removedCount > 0 && options.clerkUpgradeStats) { + options.clerkUpgradeStats.userbuttonAfterSignOutPropsRemoved = + (options.clerkUpgradeStats.userbuttonAfterSignOutPropsRemoved || 0) + removedCount; + } } if (COMPONENT_RENAMES.has(canonicalName)) { diff --git a/packages/upgrade/src/components/Codemod.js b/packages/upgrade/src/components/Codemod.js index 6824080a158..949d4fd3353 100644 --- a/packages/upgrade/src/components/Codemod.js +++ b/packages/upgrade/src/components/Codemod.js @@ -86,6 +86,24 @@ export function Codemod(props) { {result.skip ?? 0} skipped {result.nochange ?? 0} unmodified {result.timeElapsed && Time elapsed: {result.timeElapsed}} + + {transform === 'transform-remove-deprecated-props' && + result.clerkUpgradeStats?.userbuttonAfterSignOutPropsRemoved > 0 && ( + <> + + + Found and removed {result.clerkUpgradeStats.userbuttonAfterSignOutPropsRemoved} usage(s) of + UserButton sign-out redirect props (afterSignOutUrl /{' '} + afterMultiSessionSingleSignOutUrl). + + + In Core 3, these props have been removed. Configure sign-out redirects globally via + ClerkProvider afterSignOutUrl (or the corresponding environment variable) or use + SignOutButton redirectUrl for one-off flows. + + + )} + )} From 3288fbe0694fafb764df558a0d28632be80836d7 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 4 Dec 2025 11:38:41 -0600 Subject: [PATCH 06/11] wip --- .../transform-remove-deprecated-props.cjs | 34 ++++-- packages/upgrade/src/components/Codemod.js | 105 +++++++++++++++--- 2 files changed, 116 insertions(+), 23 deletions(-) diff --git a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs index 6fb95272935..e514849af83 100644 --- a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs +++ b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs @@ -15,9 +15,10 @@ const COMPONENTS_WITH_USER_BUTTON_REMOVALS = new Map([ ]); const ORGANIZATION_SWITCHER_RENAMES = new Map([['afterSwitchOrganizationUrl', 'afterSelectOrganizationUrl']]); -module.exports = function transformDeprecatedProps({ source }, { jscodeshift: j }, options = {}) { +module.exports = function transformDeprecatedProps({ source, path: filePath }, { jscodeshift: j }, options = {}) { const root = j(source); let dirty = false; + const stats = options.clerkUpgradeStats; const { namedImports, namespaceImports } = collectClerkImports(root, j); @@ -32,20 +33,31 @@ module.exports = function transformDeprecatedProps({ source }, { jscodeshift: j if (COMPONENTS_WITH_HIDE_SLUG.has(canonicalName)) { if (removeJsxAttribute(j, jsxNode, 'hideSlug')) { dirty = true; + if (stats) { + stats.hideSlugRemoved = (stats.hideSlugRemoved || 0) + 1; + stats.hideSlugFiles = stats.hideSlugFiles || []; + if (!stats.hideSlugFiles.includes(filePath)) { + stats.hideSlugFiles.push(filePath); + } + } } } if (COMPONENTS_WITH_USER_BUTTON_REMOVALS.has(canonicalName)) { + const propsToRemove = COMPONENTS_WITH_USER_BUTTON_REMOVALS.get(canonicalName); let removedCount = 0; - for (const attrName of COMPONENTS_WITH_USER_BUTTON_REMOVALS.get(canonicalName)) { + for (const attrName of propsToRemove) { if (removeJsxAttribute(j, jsxNode, attrName)) { dirty = true; removedCount += 1; } } - if (removedCount > 0 && options.clerkUpgradeStats) { - options.clerkUpgradeStats.userbuttonAfterSignOutPropsRemoved = - (options.clerkUpgradeStats.userbuttonAfterSignOutPropsRemoved || 0) + removedCount; + if (removedCount > 0 && stats) { + stats.userbuttonAfterSignOutPropsRemoved = (stats.userbuttonAfterSignOutPropsRemoved || 0) + removedCount; + stats.userbuttonFilesAffected = stats.userbuttonFilesAffected || []; + if (!stats.userbuttonFilesAffected.includes(filePath)) { + stats.userbuttonFilesAffected.push(filePath); + } } } @@ -104,7 +116,7 @@ module.exports = function transformDeprecatedProps({ source }, { jscodeshift: j dirty = true; } - if (transformSetActiveBeforeEmit(root, j)) { + if (transformSetActiveBeforeEmit(root, j, stats, filePath)) { dirty = true; } @@ -376,7 +388,7 @@ function renameTSPropertySignatures(root, j, oldName, newName) { return changed; } -function transformSetActiveBeforeEmit(root, j) { +function transformSetActiveBeforeEmit(root, j, stats, filePath) { let changed = false; root @@ -405,6 +417,14 @@ function transformSetActiveBeforeEmit(root, j) { const navigateProp = j.objectProperty(j.identifier('navigate'), buildNavigateArrowFunction(j, originalValue)); args0.properties.splice(beforeEmitIndex, 1, navigateProp); changed = true; + + if (stats) { + stats.beforeEmitTransformed = (stats.beforeEmitTransformed || 0) + 1; + stats.beforeEmitFiles = stats.beforeEmitFiles || []; + if (!stats.beforeEmitFiles.includes(filePath)) { + stats.beforeEmitFiles.push(filePath); + } + } }); return changed; diff --git a/packages/upgrade/src/components/Codemod.js b/packages/upgrade/src/components/Codemod.js index 949d4fd3353..8100f47ccfd 100644 --- a/packages/upgrade/src/components/Codemod.js +++ b/packages/upgrade/src/components/Codemod.js @@ -87,22 +87,9 @@ export function Codemod(props) { {result.nochange ?? 0} unmodified {result.timeElapsed && Time elapsed: {result.timeElapsed}} - {transform === 'transform-remove-deprecated-props' && - result.clerkUpgradeStats?.userbuttonAfterSignOutPropsRemoved > 0 && ( - <> - - - Found and removed {result.clerkUpgradeStats.userbuttonAfterSignOutPropsRemoved} usage(s) of - UserButton sign-out redirect props (afterSignOutUrl /{' '} - afterMultiSessionSingleSignOutUrl). - - - In Core 3, these props have been removed. Configure sign-out redirects globally via - ClerkProvider afterSignOutUrl (or the corresponding environment variable) or use - SignOutButton redirectUrl for one-off flows. - - - )} + {transform === 'transform-remove-deprecated-props' && ( + + )} @@ -111,3 +98,89 @@ export function Codemod(props) { ); } + +function ManualInterventionSummary({ stats }) { + if (!stats) { + return null; + } + + const hasUserButtonChanges = stats.userbuttonAfterSignOutPropsRemoved > 0; + const hasHideSlugChanges = stats.hideSlugRemoved > 0; + const hasBeforeEmitChanges = stats.beforeEmitTransformed > 0; + + if (!hasUserButtonChanges && !hasHideSlugChanges && !hasBeforeEmitChanges) { + return null; + } + + return ( + <> + + + ⚠️ Manual intervention may be required: + + + {hasUserButtonChanges && ( + <> + + + • Removed {stats.userbuttonAfterSignOutPropsRemoved} UserButton sign-out redirect prop(s) + + + {' '}Configure redirects via ClerkProvider afterSignOutUrl or{' '} + SignOutButton redirectUrl + + + + )} + + {hasHideSlugChanges && ( + <> + + + • Removed {stats.hideSlugRemoved} hideSlug prop(s) + + {' '}This prop has been removed. Slug visibility is now controlled differently. + + + )} + + {hasBeforeEmitChanges && ( + <> + + + • Transformed {stats.beforeEmitTransformed} setActive beforeEmit to{' '} + navigate + + + {' '}Callback signature changed: now receives params object instead of{' '} + session directly. Review the transformed code. + + + + )} + + ); +} + +function FileList({ files }) { + if (!files?.length) { + return null; + } + + return ( + <> + {' '}Files: + {files.map((file, index) => ( + + {' '}- {file} + + ))} + + ); +} From dd15573a578793883c0d5978aa4437e61f970d9d Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 4 Dec 2025 13:12:55 -0600 Subject: [PATCH 07/11] wip --- packages/upgrade/src/codemods/index.js | 58 +++++++------- .../transform-remove-deprecated-props.cjs | 34 ++------ packages/upgrade/src/components/Codemod.js | 78 +++++++++---------- 3 files changed, 73 insertions(+), 97 deletions(-) diff --git a/packages/upgrade/src/codemods/index.js b/packages/upgrade/src/codemods/index.js index c9c31941117..bb9c2cbc624 100644 --- a/packages/upgrade/src/codemods/index.js +++ b/packages/upgrade/src/codemods/index.js @@ -6,47 +6,51 @@ import { run } from 'jscodeshift/src/Runner.js'; const __dirname = dirname(fileURLToPath(import.meta.url)); +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 }); - const clerkUpgradeStats = options.clerkUpgradeStats ?? {}; + // 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, + }); + // Second pass: apply the changes const result = await run(resolvedPath, paths ?? [], { - dry: false, ...options, - // expose a mutable stats bag so individual transforms can record structured information - clerkUpgradeStats, - // 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, - clerkUpgradeStats, + stats: dryResult.stats, }; } diff --git a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs index e514849af83..73fd5d3bb82 100644 --- a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs +++ b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs @@ -15,10 +15,9 @@ const COMPONENTS_WITH_USER_BUTTON_REMOVALS = new Map([ ]); const ORGANIZATION_SWITCHER_RENAMES = new Map([['afterSwitchOrganizationUrl', 'afterSelectOrganizationUrl']]); -module.exports = function transformDeprecatedProps({ source, path: filePath }, { jscodeshift: j }, options = {}) { +module.exports = function transformDeprecatedProps({ source }, { jscodeshift: j, stats }) { const root = j(source); let dirty = false; - const stats = options.clerkUpgradeStats; const { namedImports, namespaceImports } = collectClerkImports(root, j); @@ -33,30 +32,16 @@ module.exports = function transformDeprecatedProps({ source, path: filePath }, { if (COMPONENTS_WITH_HIDE_SLUG.has(canonicalName)) { if (removeJsxAttribute(j, jsxNode, 'hideSlug')) { dirty = true; - if (stats) { - stats.hideSlugRemoved = (stats.hideSlugRemoved || 0) + 1; - stats.hideSlugFiles = stats.hideSlugFiles || []; - if (!stats.hideSlugFiles.includes(filePath)) { - stats.hideSlugFiles.push(filePath); - } - } + stats('hideSlugRemoved'); } } if (COMPONENTS_WITH_USER_BUTTON_REMOVALS.has(canonicalName)) { const propsToRemove = COMPONENTS_WITH_USER_BUTTON_REMOVALS.get(canonicalName); - let removedCount = 0; for (const attrName of propsToRemove) { if (removeJsxAttribute(j, jsxNode, attrName)) { dirty = true; - removedCount += 1; - } - } - if (removedCount > 0 && stats) { - stats.userbuttonAfterSignOutPropsRemoved = (stats.userbuttonAfterSignOutPropsRemoved || 0) + removedCount; - stats.userbuttonFilesAffected = stats.userbuttonFilesAffected || []; - if (!stats.userbuttonFilesAffected.includes(filePath)) { - stats.userbuttonFilesAffected.push(filePath); + stats('userbuttonAfterSignOutPropsRemoved'); } } } @@ -116,7 +101,7 @@ module.exports = function transformDeprecatedProps({ source, path: filePath }, { dirty = true; } - if (transformSetActiveBeforeEmit(root, j, stats, filePath)) { + if (transformSetActiveBeforeEmit(root, j, stats)) { dirty = true; } @@ -388,7 +373,7 @@ function renameTSPropertySignatures(root, j, oldName, newName) { return changed; } -function transformSetActiveBeforeEmit(root, j, stats, filePath) { +function transformSetActiveBeforeEmit(root, j, stats) { let changed = false; root @@ -417,14 +402,7 @@ function transformSetActiveBeforeEmit(root, j, stats, filePath) { const navigateProp = j.objectProperty(j.identifier('navigate'), buildNavigateArrowFunction(j, originalValue)); args0.properties.splice(beforeEmitIndex, 1, navigateProp); changed = true; - - if (stats) { - stats.beforeEmitTransformed = (stats.beforeEmitTransformed || 0) + 1; - stats.beforeEmitFiles = stats.beforeEmitFiles || []; - if (!stats.beforeEmitFiles.includes(filePath)) { - stats.beforeEmitFiles.push(filePath); - } - } + stats('beforeEmitTransformed'); }); return changed; diff --git a/packages/upgrade/src/components/Codemod.js b/packages/upgrade/src/components/Codemod.js index 8100f47ccfd..36372c43e84 100644 --- a/packages/upgrade/src/components/Codemod.js +++ b/packages/upgrade/src/components/Codemod.js @@ -87,9 +87,7 @@ export function Codemod(props) { {result.nochange ?? 0} unmodified {result.timeElapsed && Time elapsed: {result.timeElapsed}} - {transform === 'transform-remove-deprecated-props' && ( - - )} + {transform === 'transform-remove-deprecated-props' && } @@ -104,11 +102,11 @@ function ManualInterventionSummary({ stats }) { return null; } - const hasUserButtonChanges = stats.userbuttonAfterSignOutPropsRemoved > 0; - const hasHideSlugChanges = stats.hideSlugRemoved > 0; - const hasBeforeEmitChanges = stats.beforeEmitTransformed > 0; + const userButtonCount = stats.userbuttonAfterSignOutPropsRemoved || 0; + const hideSlugCount = stats.hideSlugRemoved || 0; + const beforeEmitCount = stats.beforeEmitTransformed || 0; - if (!hasUserButtonChanges && !hasHideSlugChanges && !hasBeforeEmitChanges) { + if (!userButtonCount && !hideSlugCount && !beforeEmitCount) { return null; } @@ -122,65 +120,61 @@ function ManualInterventionSummary({ stats }) { ⚠️ Manual intervention may be required: - {hasUserButtonChanges && ( + {userButtonCount > 0 && ( <> - • Removed {stats.userbuttonAfterSignOutPropsRemoved} UserButton sign-out redirect prop(s) + • Removed {userButtonCount} UserButton sign-out redirect prop(s) ( + afterSignOutUrl, afterMultiSessionSingleSignOutUrl) + + + {' '}These props have been removed from UserButton. To configure sign-out redirects: + + + {' '}- Global default: Add afterSignOutUrl prop to{' '} + {''} - {' '}Configure redirects via ClerkProvider afterSignOutUrl or{' '} - SignOutButton redirectUrl + {' '}- Per-button control: Use {''} + + + {' '}- Programmatic: Call {'clerk.signOut({ redirectUrl: "/your-path" })'} - )} - {hasHideSlugChanges && ( + {hideSlugCount > 0 && ( <> - • Removed {stats.hideSlugRemoved} hideSlug prop(s) + • Removed {hideSlugCount} hideSlug prop(s) from organization components - {' '}This prop has been removed. Slug visibility is now controlled differently. - + + {' '}The hideSlug prop has been removed from CreateOrganization, + + {' '}OrganizationSwitcher, and OrganizationList components. + {' '}Organization slugs are now managed through the Clerk Dashboard settings. )} - {hasBeforeEmitChanges && ( + {beforeEmitCount > 0 && ( <> - • Transformed {stats.beforeEmitTransformed} setActive beforeEmit to{' '} - navigate + • Transformed {beforeEmitCount} setActive({'{ beforeEmit }'}) to{' '} + setActive({'{ navigate }'}) + + + {' '}The callback now receives an object with session property: + {' '}Before: beforeEmit: (session) => doSomething(session) - {' '}Callback signature changed: now receives params object instead of{' '} - session directly. Review the transformed code. + {' '}After: navigate: ({'{ session }'}) => doSomething(session) - + {' '}The codemod wrapped your callback to extract session from the params object. + {' '}Consider refactoring to use destructuring directly for cleaner code. )} ); } - -function FileList({ files }) { - if (!files?.length) { - return null; - } - - return ( - <> - {' '}Files: - {files.map((file, index) => ( - - {' '}- {file} - - ))} - - ); -} From 68a52697bd74bb59d7e924aad30d800d2e67f9e6 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 4 Dec 2025 13:59:00 -0600 Subject: [PATCH 08/11] wip --- .../src/codemods/transform-remove-deprecated-props.cjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs index 73fd5d3bb82..30909cdcf9f 100644 --- a/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs +++ b/packages/upgrade/src/codemods/transform-remove-deprecated-props.cjs @@ -438,8 +438,8 @@ function getPropertyValueExpression(valueNode) { function buildNavigateArrowFunction(j, originalExpression) { const paramIdentifier = j.identifier('params'); - const calleeExpression = clone(originalExpression); - const callExpression = j.callExpression(calleeExpression, [ + // No need to clone - we're moving the expression from beforeEmit to navigate + const callExpression = j.callExpression(originalExpression, [ j.memberExpression(paramIdentifier, j.identifier('session')), ]); return j.arrowFunctionExpression([paramIdentifier], callExpression); From e0b39ecb13e9cc05e3a78f0e9256d02b5f58cbd0 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 4 Dec 2025 14:18:04 -0600 Subject: [PATCH 09/11] wip --- packages/upgrade/src/components/Codemod.js | 72 +++++++++++----------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/packages/upgrade/src/components/Codemod.js b/packages/upgrade/src/components/Codemod.js index 36372c43e84..c952fe551b8 100644 --- a/packages/upgrade/src/components/Codemod.js +++ b/packages/upgrade/src/components/Codemod.js @@ -1,5 +1,5 @@ import { Spinner, StatusMessage, TextInput } from '@inkjs/ui'; -import { Newline, Text } from 'ink'; +import { Box, Newline, Text } from 'ink'; import React, { useEffect, useState } from 'react'; import { runCodemod } from '../codemods/index.js'; @@ -111,8 +111,10 @@ function ManualInterventionSummary({ stats }) { } return ( - <> - + {userButtonCount > 0 && ( - <> - + - • Removed {userButtonCount} UserButton sign-out redirect prop(s) ( - afterSignOutUrl, afterMultiSessionSingleSignOutUrl) - - - {' '}These props have been removed from UserButton. To configure sign-out redirects: + • Removed {userButtonCount} UserButton sign-out redirect prop(s) + To configure sign-out redirects: - {' '}- Global default: Add afterSignOutUrl prop to{' '} - {''} + {' '}- Global: Add afterSignOutUrl to {''} - {' '}- Per-button control: Use {''} + {' '}- Per-button: Use {''} - {' '}- Programmatic: Call {'clerk.signOut({ redirectUrl: "/your-path" })'} + {' '}- Programmatic: {'clerk.signOut({ redirectUrl: "..." })'} - + )} {hideSlugCount > 0 && ( - <> - + - • Removed {hideSlugCount} hideSlug prop(s) from organization components + • Removed {hideSlugCount} hideSlug prop(s) - {' '}The hideSlug prop has been removed from CreateOrganization, + {' '}Removed from CreateOrganization, OrganizationSwitcher, and OrganizationList. - {' '}OrganizationSwitcher, and OrganizationList components. - {' '}Organization slugs are now managed through the Clerk Dashboard settings. - + Slugs are now managed in the Clerk Dashboard. + )} {beforeEmitCount > 0 && ( - <> - + - • Transformed {beforeEmitCount} setActive({'{ beforeEmit }'}) to{' '} - setActive({'{ navigate }'}) - - - {' '}The callback now receives an object with session property: + • Transformed {beforeEmitCount} {'setActive({ beforeEmit })'} →{' '} + {'setActive({ navigate })'} - {' '}Before: beforeEmit: (session) => doSomething(session) - - {' '}After: navigate: ({'{ session }'}) => doSomething(session) - - {' '}The codemod wrapped your callback to extract session from the params object. - {' '}Consider refactoring to use destructuring directly for cleaner code. - + The callback now receives an object with session property: + Before: beforeEmit: (session) => ... + After: navigate: (params) => ...(params.session) + Consider refactoring to use destructuring for cleaner code. + )} - + ); } From 9cac081bc7cc7a3a09d1e5b6f69deaa9c1e36ef2 Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 4 Dec 2025 14:19:34 -0600 Subject: [PATCH 10/11] wip --- packages/upgrade/src/components/Codemod.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/upgrade/src/components/Codemod.js b/packages/upgrade/src/components/Codemod.js index c952fe551b8..958c1389a29 100644 --- a/packages/upgrade/src/components/Codemod.js +++ b/packages/upgrade/src/components/Codemod.js @@ -119,7 +119,7 @@ function ManualInterventionSummary({ stats }) { bold color='yellow' > - ⚠️ Manual intervention may be required: + Manual intervention may be required: {userButtonCount > 0 && ( From 30db72192221287452ea3533d3f94296b77e09ad Mon Sep 17 00:00:00 2001 From: Jacek Date: Thu, 4 Dec 2025 14:29:35 -0600 Subject: [PATCH 11/11] fix double ;; issue --- .../transform-clerk-react-v6.fixtures.js | 14 +++++++++++ .../src/codemods/transform-clerk-react-v6.cjs | 25 +++++++++++++------ packages/upgrade/src/components/Codemod.js | 13 +++++----- 3 files changed, 39 insertions(+), 13 deletions(-) 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 index ea7dc985ea0..6b2e99fbb8f 100644 --- 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 @@ -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";`, + }, ]; diff --git a/packages/upgrade/src/codemods/transform-clerk-react-v6.cjs b/packages/upgrade/src/codemods/transform-clerk-react-v6.cjs index 63ac3b1802f..9a1d54b975b 100644 --- a/packages/upgrade/src/codemods/transform-clerk-react-v6.cjs +++ b/packages/upgrade/src/codemods/transform-clerk-react-v6.cjs @@ -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; } @@ -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'; diff --git a/packages/upgrade/src/components/Codemod.js b/packages/upgrade/src/components/Codemod.js index 958c1389a29..bb0068dd437 100644 --- a/packages/upgrade/src/components/Codemod.js +++ b/packages/upgrade/src/components/Codemod.js @@ -132,13 +132,16 @@ function ManualInterventionSummary({ stats }) { To configure sign-out redirects: - {' '}- Global: Add afterSignOutUrl to {''} + {' '} + - Global: Add afterSignOutUrl to {''} - {' '}- Per-button: Use {''} + {' '} + - Per-button: Use {''} - {' '}- Programmatic: {'clerk.signOut({ redirectUrl: "..." })'} + {' '} + - Programmatic: {'clerk.signOut({ redirectUrl: "..." })'} )} @@ -151,9 +154,7 @@ function ManualInterventionSummary({ stats }) { • Removed {hideSlugCount} hideSlug prop(s) - - {' '}Removed from CreateOrganization, OrganizationSwitcher, and OrganizationList. - + Removed from CreateOrganization, OrganizationSwitcher, and OrganizationList. Slugs are now managed in the Clerk Dashboard. )}