Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
bac9b4e
Adds codemod to transform clerk/themes to clerk/ui/themes
brkalow Dec 4, 2025
1392ce5
Adds codemod to transform experimental and unstable prefixes
brkalow Dec 4, 2025
2205f80
Adds codemod to transform appearance.layout -> appearance.options
brkalow Dec 4, 2025
e6b18d3
Adds codemod to transform appearance prop changes
brkalow Dec 5, 2025
a2f1e49
refactor upgrade CLI to not use ink
brkalow Dec 5, 2025
4e06115
Tweaks to codemod output
brkalow Dec 5, 2025
6640fb0
adjust complete output
brkalow Dec 5, 2025
edaf412
add change files that line up with current changesets
brkalow Dec 5, 2025
fb05d75
updates lockfile
brkalow Dec 5, 2025
f3750b4
Adds changeset
brkalow Dec 5, 2025
ec24828
don't hardcode release id
brkalow Dec 5, 2025
5a34413
remove unused file
brkalow Dec 5, 2025
7820fed
format
brkalow Dec 5, 2025
6ef9538
Ajdust output and fix tests. Undo fixture changes
brkalow Dec 6, 2025
5520100
undo codemod changes
brkalow Dec 6, 2025
e00579e
cleanup codemod output
brkalow Dec 6, 2025
698157f
fix lint
brkalow Dec 6, 2025
68be931
Merge branch 'vincent-and-the-doctor' into brk.feat/upgrade-cli-core-3
brkalow Dec 8, 2025
a06f038
use chalk
brkalow Dec 8, 2025
bfa2d05
Merge branch 'brk.feat/upgrade-cli-core-3' into brk.feat/core-3-changes
brkalow Dec 8, 2025
936f024
Adds script to generate a migration guide, and update output of the s…
brkalow Dec 8, 2025
0bd9cd4
Merge branch 'main' into brk.feat/generate-migration-guide
jacekradko Dec 12, 2025
794d6b0
changeset
jacekradko Dec 12, 2025
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/quiet-mirrors-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@clerk/upgrade': minor
---

Add a migration guide generator and improve scan output.
1 change: 1 addition & 0 deletions packages/upgrade/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dev": "babel --keep-file-extension --out-dir=dist --watch src --copy-files",
"format": "node ../../scripts/format-package.mjs",
"format:check": "node ../../scripts/format-package.mjs --check",
"generate-guide": "node scripts/generate-guide.js",
"lint": "eslint src/",
"lint:publint": "publint",
"test": "vitest run",
Expand Down
174 changes: 174 additions & 0 deletions packages/upgrade/scripts/generate-guide.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url';

import matter from 'gray-matter';
import meow from 'meow';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const VERSIONS_DIR = path.join(__dirname, '../src/versions');

const cli = meow(
`
Usage
$ pnpm run generate-guide --version=<version> --sdk=<sdk>

Options
--version Version directory to use (e.g., core-3)
--sdk SDK to generate guide for (e.g., nextjs, react, expo)

Examples
$ pnpm run generate-guide --version=core-3 --sdk=nextjs
$ pnpm run generate-guide --version=core-3 --sdk=react > react-guide.md
`,
{
importMeta: import.meta,
flags: {
version: { type: 'string', isRequired: true },
sdk: { type: 'string', isRequired: true },
},
},
);

async function loadVersionConfig(version) {
const configPath = path.join(VERSIONS_DIR, version, 'index.js');

if (!fs.existsSync(configPath)) {
throw new Error(`Version config not found: ${configPath}`);
}

const moduleUrl = pathToFileURL(configPath).href;
const mod = await import(moduleUrl);
return mod.default ?? mod;
}

function loadChanges(version, sdk) {
const changesDir = path.join(VERSIONS_DIR, version, 'changes');

if (!fs.existsSync(changesDir)) {
return [];
}

const files = fs.readdirSync(changesDir).filter(f => f.endsWith('.md'));
const changes = [];

for (const file of files) {
const filePath = path.join(changesDir, file);
const content = fs.readFileSync(filePath, 'utf8');
const parsed = matter(content);
const fm = parsed.data;

const packages = fm.packages || ['*'];
const appliesToSdk = packages.includes('*') || packages.includes(sdk);

if (!appliesToSdk) {
continue;
}

changes.push({
title: fm.title,
packages,
category: fm.category || 'breaking',
content: parsed.content.trim(),
slug: file.replace('.md', ''),
});
}
Comment on lines +56 to +76
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate required frontmatter fields.

The code doesn't validate that fm.title exists before using it. If a markdown file is missing the title field in its frontmatter, the generated output will contain "### undefined" (see line 129 where change.title is used).

Apply this fix to validate required fields:

   for (const file of files) {
     const filePath = path.join(changesDir, file);
     const content = fs.readFileSync(filePath, 'utf8');
     const parsed = matter(content);
     const fm = parsed.data;
+
+    if (!fm.title) {
+      console.warn(`Warning: ${file} is missing a title field, skipping`);
+      continue;
+    }
 
     const packages = fm.packages || ['*'];
     const appliesToSdk = packages.includes('*') || packages.includes(sdk);
🤖 Prompt for AI Agents
In packages/upgrade/scripts/generate-guide.js around lines 56 to 76, the code
reads frontmatter but doesn't validate required fields like title; add a check
after reading fm to ensure fm.title exists and is non-empty — if missing, either
log a clear error/warning with the file path and skip that file (continue) or
throw an Error to fail the build; update changes.push to only run when the
required fields are present so generated guides never contain "undefined"
titles.


return changes;
}

function groupByCategory(changes) {
const groups = {};

for (const change of changes) {
const category = change.category;
if (!groups[category]) {
groups[category] = [];
}
groups[category].push(change);
}

return groups;
}

function getCategoryHeading(category) {
const headings = {
breaking: 'Breaking Changes',
'deprecation-removal': 'Deprecation Removals',
warning: 'Warnings',
};
return headings[category] || category;
}

function generateMarkdown(sdk, versionConfig, changes) {
const lines = [];
const versionName = versionConfig.name || versionConfig.id;

lines.push(`# Upgrading @clerk/${sdk} to ${versionName}`);
lines.push('');

if (versionConfig.docsUrl) {
lines.push(`For the full migration guide, see: ${versionConfig.docsUrl}`);
lines.push('');
}

const grouped = groupByCategory(changes);
const categoryOrder = ['breaking', 'deprecation-removal', 'warning'];

for (const category of categoryOrder) {
const categoryChanges = grouped[category];
if (!categoryChanges || categoryChanges.length === 0) {
continue;
}

lines.push(`## ${getCategoryHeading(category)}`);
lines.push('');

for (const change of categoryChanges) {
lines.push(`### ${change.title}`);
lines.push('');
lines.push(change.content);
lines.push('');
}
}

// Handle any categories not in the predefined order
for (const [category, categoryChanges] of Object.entries(grouped)) {
if (categoryOrder.includes(category)) {
continue;
}

lines.push(`## ${getCategoryHeading(category)}`);
lines.push('');

for (const change of categoryChanges) {
lines.push(`### ${change.title}`);
lines.push('');
lines.push(change.content);
lines.push('');
}
}

return lines.join('\n');
}

async function main() {
const { version, sdk } = cli.flags;

const versionConfig = await loadVersionConfig(version);
const changes = loadChanges(version, sdk);

if (changes.length === 0) {
console.error(`No changes found for ${sdk} in ${version}`);
process.exit(1);
}

const markdown = generateMarkdown(sdk, versionConfig, changes);
console.log(markdown);
}

main().catch(error => {
console.error(error.message);
process.exit(1);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "test-nextjs-v6-scan-issues",
"version": "1.0.0",
"dependencies": {
"@clerk/nextjs": "^6.0.0",
"next": "^14.0.0",
"react": "^18.0.0"
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { ClerkProvider, SignIn, useAuth } from '@clerk/nextjs';

export default function App({ children }) {
return (
<ClerkProvider
appearance={{
layout: {
socialButtonsPlacement: 'bottom',
},
}}
>
{children}
</ClerkProvider>
);
}

export function SignInPage() {
return (
<SignIn
afterSignInUrl='/dashboard'
afterSignUpUrl='/onboarding'
/>
);
}

export function SamlCallback() {
const { isSignedIn } = useAuth();
// Handle saml callback
return <div>SAML SSO Callback</div>;
}
10 changes: 5 additions & 5 deletions packages/upgrade/src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ const cli = meow(
{
importMeta: import.meta,
flags: {
sdk: { type: 'string' },
dir: { type: 'string', default: process.cwd() },
dryRun: { type: 'boolean', default: false },
glob: { type: 'string', default: '**/*.(js|jsx|ts|tsx|mjs|cjs)' },
ignore: { type: 'string', isMultiple: true },
skipUpgrade: { type: 'boolean', default: false },
release: { type: 'string' },
dryRun: { type: 'boolean', default: false },
sdk: { type: 'string' },
skipCodemods: { type: 'boolean', default: false },
skipUpgrade: { type: 'boolean', default: false },
},
},
);
Expand All @@ -70,12 +70,12 @@ async function main() {

const options = {
dir: cli.flags.dir,
dryRun: cli.flags.dryRun,
glob: cli.flags.glob,
ignore: cli.flags.ignore,
skipUpgrade: cli.flags.skipUpgrade,
release: cli.flags.release,
dryRun: cli.flags.dryRun,
skipCodemods: cli.flags.skipCodemods,
skipUpgrade: cli.flags.skipUpgrade,
};

if (options.dryRun) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,17 +36,6 @@ const CHROME_CLIENT_NAMES = new Set(['__unstable__createClerkClient', 'createCle
const CHROME_BACKGROUND_SOURCE = '@clerk/chrome-extension/background';
const CHROME_LEGACY_SOURCE = '@clerk/chrome-extension';

/**
* Transforms experimental and unstable prefixed identifiers to their stable or internal equivalents.
* Also moves theme-related imports to @clerk/ui/themes/experimental and Chrome extension imports
* to @clerk/chrome-extension/background. Removes deprecated billing-related props.
*
* @param {Object} file - The file object containing the source code
* @param {string} file.source - The source code to transform
* @param {Object} api - The jscodeshift API
* @param {Function} api.jscodeshift - The jscodeshift function
* @returns {string|undefined} The transformed source code, or undefined if no changes were made
*/
module.exports = function transformAlignExperimentalUnstablePrefixes({ source }, { jscodeshift: j }) {
const root = j(source);
let dirty = false;
Expand Down
4 changes: 2 additions & 2 deletions packages/upgrade/src/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ export async function runCodemods(config, sdk, options) {
return;
}

const glob = typeof options.glob === 'string' ? options.glob.split(/[ ,]/).filter(Boolean) : options.glob;
const patterns = typeof options.glob === 'string' ? options.glob.split(/[ ,]/).filter(Boolean) : options.glob;

for (const transform of codemods) {
const spinner = createSpinner(`Running codemod: ${transform}`);

try {
const result = await runCodemod(transform, glob, options);
const result = await runCodemod(transform, patterns, options);
spinner.success(`Codemod applied: ${chalk.dim(transform)}`);
renderCodemodResults(transform, result);

Expand Down
Loading