diff --git a/packages/upgrade/scripts/generate-guide.js b/packages/upgrade/scripts/generate-guide.js index b434fcdada9..efbd1d0a02f 100644 --- a/packages/upgrade/scripts/generate-guide.js +++ b/packages/upgrade/scripts/generate-guide.js @@ -9,25 +9,44 @@ import meow from 'meow'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const VERSIONS_DIR = path.join(__dirname, '../src/versions'); +const SDK_DISPLAY_NAMES = { + astro: 'Astro', + 'chrome-extension': 'Chrome Extension', + expo: 'Expo', + express: 'Express', + fastify: 'Fastify', + nextjs: 'Next.js', + nuxt: 'Nuxt', + react: 'React', + 'react-router': 'React Router', + 'tanstack-react-start': 'TanStack Start', + vue: 'Vue', +}; + const cli = meow( ` Usage - $ pnpm run generate-guide --version= --sdk= + $ pnpm run generate-guide --version= [--sdk=] [--output-dir=] Options - --version Version directory to use (e.g., core-3) - --sdk SDK to generate guide for (e.g., nextjs, react, expo) + --version Version directory to use (e.g., core-3) + --sdk SDK to generate guide for (e.g., nextjs, react, expo) + If omitted, generates guides for all SDKs + --output-dir Directory to write generated files to + If omitted, outputs to stdout (single SDK only) Examples $ pnpm run generate-guide --version=core-3 --sdk=nextjs $ pnpm run generate-guide --version=core-3 --sdk=react > react-guide.md + $ pnpm run generate-guide --version=core-3 --output-dir=./guides `, { - importMeta: import.meta, flags: { - version: { type: 'string', isRequired: true }, - sdk: { type: 'string', isRequired: true }, + outputDir: { type: 'string' }, + sdk: { type: 'string' }, + version: { isRequired: true, type: 'string' }, }, + importMeta: import.meta, }, ); @@ -50,7 +69,10 @@ function loadChanges(version, sdk) { return []; } - const files = fs.readdirSync(changesDir).filter(f => f.endsWith('.md')); + const files = fs + .readdirSync(changesDir) + .filter(f => f.endsWith('.md')) + .sort(); const changes = []; for (const file of files) { @@ -94,27 +116,74 @@ function groupByCategory(changes) { function getCategoryHeading(category) { const headings = { + 'behavior-change': 'Behavior Change', breaking: 'Breaking Changes', + deprecation: 'Deprecations', 'deprecation-removal': 'Deprecation Removals', + version: 'Version', warning: 'Warnings', }; - return headings[category] || category; + if (headings[category]) { + return headings[category]; + } + + return category.replace(/[-_]+/g, ' ').replace(/\b\w/g, char => char.toUpperCase()); +} + +function normalizeSdk(sdk) { + return sdk.replace(/^@clerk\//, ''); +} + +function getSdkDisplayName(sdk) { + return SDK_DISPLAY_NAMES[sdk] || sdk; +} + +function indent(text, spaces) { + const padding = ' '.repeat(spaces); + return text + .split('\n') + .map(line => (line.trim() ? padding + line : line)) + .join('\n'); +} + +function generateFrontmatter(sdk, versionName) { + const displayName = getSdkDisplayName(sdk); + return `--- +title: "Upgrading ${displayName} to ${versionName}" +description: "Learn how to upgrade Clerk's ${displayName} SDK to the latest version." +--- + +{/* WARNING: This is a generated file and should not be edited directly. To update its contents, see the "upgrade" package in the clerk/javascript repo. */}`; +} + +function renderAccordionCategory(lines, category, categoryChanges) { + const sortedChanges = [...categoryChanges].sort((a, b) => a.title.localeCompare(b.title)); + const titles = sortedChanges.map(change => JSON.stringify(change.title)); + + lines.push(`## ${getCategoryHeading(category)}`); + lines.push(''); + lines.push(``); + + for (const change of sortedChanges) { + lines.push(' '); + lines.push(indent(change.content, 4)); + lines.push(' '); + } + + lines.push(''); + lines.push(''); } function generateMarkdown(sdk, versionConfig, changes) { const lines = []; const versionName = versionConfig.name || versionConfig.id; - lines.push(`# Upgrading @clerk/${sdk} to ${versionName}`); + lines.push(generateFrontmatter(sdk, 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']; + const categoryOrder = ['breaking', 'deprecation-removal', 'deprecation', 'warning', 'version', 'behavior-change']; + const seenCategories = new Set(); for (const category of categoryOrder) { const categoryChanges = grouped[category]; @@ -122,10 +191,16 @@ function generateMarkdown(sdk, versionConfig, changes) { continue; } + seenCategories.add(category); + if (category === 'breaking') { + renderAccordionCategory(lines, category, categoryChanges); + continue; + } + lines.push(`## ${getCategoryHeading(category)}`); lines.push(''); - for (const change of categoryChanges) { + for (const change of [...categoryChanges].sort((a, b) => a.title.localeCompare(b.title))) { lines.push(`### ${change.title}`); lines.push(''); lines.push(change.content); @@ -134,15 +209,20 @@ function generateMarkdown(sdk, versionConfig, changes) { } // Handle any categories not in the predefined order - for (const [category, categoryChanges] of Object.entries(grouped)) { - if (categoryOrder.includes(category)) { + for (const [category, categoryChanges] of Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b))) { + if (seenCategories.has(category)) { + continue; + } + + if (category === 'breaking') { + renderAccordionCategory(lines, category, categoryChanges); continue; } lines.push(`## ${getCategoryHeading(category)}`); lines.push(''); - for (const change of categoryChanges) { + for (const change of [...categoryChanges].sort((a, b) => a.title.localeCompare(b.title))) { lines.push(`### ${change.title}`); lines.push(''); lines.push(change.content); @@ -153,19 +233,72 @@ function generateMarkdown(sdk, versionConfig, changes) { return lines.join('\n'); } +function generateGuideForSdk(sdk, version, versionConfig) { + const changes = loadChanges(version, sdk); + + if (changes.length === 0) { + return null; + } + + return generateMarkdown(sdk, versionConfig, changes); +} + +function writeGuideToFile(outputDir, sdk, content) { + if (!fs.existsSync(outputDir)) { + fs.mkdirSync(outputDir, { recursive: true }); + } + + const filePath = path.join(outputDir, `${sdk}.mdx`); + fs.writeFileSync(filePath, content); + return filePath; +} + async function main() { - const { version, sdk } = cli.flags; + const { outputDir, sdk, version } = 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}`); + // Determine which SDKs to generate + const sdksToGenerate = sdk ? [normalizeSdk(sdk)] : Object.keys(versionConfig.sdkVersions || {}); + + if (sdksToGenerate.length === 0) { + console.error(`No SDKs found in version config for ${version}`); + process.exit(1); + } + + // If multiple SDKs and no output dir, require output dir + if (sdksToGenerate.length > 1 && !outputDir) { + console.error('--output-dir is required when generating multiple SDK guides'); + console.error(`SDKs to generate: ${sdksToGenerate.join(', ')}`); process.exit(1); } - const markdown = generateMarkdown(sdk, versionConfig, changes); - console.log(markdown); + const results = []; + + for (const currentSdk of sdksToGenerate) { + const markdown = generateGuideForSdk(currentSdk, version, versionConfig); + + if (!markdown) { + console.error(`No changes found for ${currentSdk} in ${version}, skipping...`); + continue; + } + + if (outputDir) { + const filePath = writeGuideToFile(outputDir, currentSdk, markdown); + results.push({ sdk: currentSdk, filePath }); + } else { + // Single SDK, output to stdout + console.log(markdown); + } + } + + if (outputDir && results.length > 0) { + console.log(`\nGenerated ${results.length} guide(s):`); + for (const { sdk: generatedSdk, filePath } of results) { + const displayName = getSdkDisplayName(generatedSdk); + console.log(` ${displayName}: ${filePath}`); + } + } } main().catch(error => {