diff --git a/.changeset/quiet-mirrors-breathe.md b/.changeset/quiet-mirrors-breathe.md new file mode 100644 index 00000000000..8fd2faf9568 --- /dev/null +++ b/.changeset/quiet-mirrors-breathe.md @@ -0,0 +1,5 @@ +--- +'@clerk/upgrade': minor +--- + +Add a migration guide generator and improve scan output. diff --git a/packages/upgrade/package.json b/packages/upgrade/package.json index 5b9be885ede..fabf7f7f1e2 100644 --- a/packages/upgrade/package.json +++ b/packages/upgrade/package.json @@ -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", diff --git a/packages/upgrade/scripts/generate-guide.js b/packages/upgrade/scripts/generate-guide.js new file mode 100644 index 00000000000..b434fcdada9 --- /dev/null +++ b/packages/upgrade/scripts/generate-guide.js @@ -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= --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', ''), + }); + } + + 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); +}); diff --git a/packages/upgrade/src/__tests__/fixtures/nextjs-v6-scan-issues/package.json b/packages/upgrade/src/__tests__/fixtures/nextjs-v6-scan-issues/package.json new file mode 100644 index 00000000000..a456488eafa --- /dev/null +++ b/packages/upgrade/src/__tests__/fixtures/nextjs-v6-scan-issues/package.json @@ -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" + } +} diff --git a/packages/upgrade/src/__tests__/fixtures/nextjs-v6-scan-issues/pnpm-lock.yaml b/packages/upgrade/src/__tests__/fixtures/nextjs-v6-scan-issues/pnpm-lock.yaml new file mode 100644 index 00000000000..d57ee1b3a6c --- /dev/null +++ b/packages/upgrade/src/__tests__/fixtures/nextjs-v6-scan-issues/pnpm-lock.yaml @@ -0,0 +1,2 @@ +lockfileVersion: '6.0' + diff --git a/packages/upgrade/src/__tests__/fixtures/nextjs-v6-scan-issues/src/app.tsx b/packages/upgrade/src/__tests__/fixtures/nextjs-v6-scan-issues/src/app.tsx new file mode 100644 index 00000000000..f890201f541 --- /dev/null +++ b/packages/upgrade/src/__tests__/fixtures/nextjs-v6-scan-issues/src/app.tsx @@ -0,0 +1,30 @@ +import { ClerkProvider, SignIn, useAuth } from '@clerk/nextjs'; + +export default function App({ children }) { + return ( + + {children} + + ); +} + +export function SignInPage() { + return ( + + ); +} + +export function SamlCallback() { + const { isSignedIn } = useAuth(); + // Handle saml callback + return
SAML SSO Callback
; +} diff --git a/packages/upgrade/src/cli.js b/packages/upgrade/src/cli.js index 092f03d7df8..54eb7deef7f 100644 --- a/packages/upgrade/src/cli.js +++ b/packages/upgrade/src/cli.js @@ -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 }, }, }, ); @@ -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) { diff --git a/packages/upgrade/src/codemods/transform-align-experimental-unstable-prefixes.cjs b/packages/upgrade/src/codemods/transform-align-experimental-unstable-prefixes.cjs index 51cc43a612b..dccc639dfac 100644 --- a/packages/upgrade/src/codemods/transform-align-experimental-unstable-prefixes.cjs +++ b/packages/upgrade/src/codemods/transform-align-experimental-unstable-prefixes.cjs @@ -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; diff --git a/packages/upgrade/src/runner.js b/packages/upgrade/src/runner.js index 3bd07a0ebac..8d4134465eb 100644 --- a/packages/upgrade/src/runner.js +++ b/packages/upgrade/src/runner.js @@ -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);