Skip to content

Commit d4057db

Browse files
authored
feat(upgrade): improve generate-guide script for clerk-docs (#7454)
1 parent 38def4f commit d4057db

2 files changed

Lines changed: 163 additions & 25 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@clerk/upgrade": patch
3+
---
4+
5+
Improve `generate-guide` script to support generating guides for all SDKs at once and output MDX format compatible with clerk-docs

packages/upgrade/scripts/generate-guide.js

Lines changed: 158 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,25 +9,44 @@ import meow from 'meow';
99
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1010
const VERSIONS_DIR = path.join(__dirname, '../src/versions');
1111

12+
const SDK_DISPLAY_NAMES = {
13+
astro: 'Astro',
14+
'chrome-extension': 'Chrome Extension',
15+
expo: 'Expo',
16+
express: 'Express',
17+
fastify: 'Fastify',
18+
nextjs: 'Next.js',
19+
nuxt: 'Nuxt',
20+
react: 'React',
21+
'react-router': 'React Router',
22+
'tanstack-react-start': 'TanStack Start',
23+
vue: 'Vue',
24+
};
25+
1226
const cli = meow(
1327
`
1428
Usage
15-
$ pnpm run generate-guide --version=<version> --sdk=<sdk>
29+
$ pnpm run generate-guide --version=<version> [--sdk=<sdk>] [--output-dir=<dir>]
1630
1731
Options
18-
--version Version directory to use (e.g., core-3)
19-
--sdk SDK to generate guide for (e.g., nextjs, react, expo)
32+
--version Version directory to use (e.g., core-3)
33+
--sdk SDK to generate guide for (e.g., nextjs, react, expo)
34+
If omitted, generates guides for all SDKs
35+
--output-dir Directory to write generated files to
36+
If omitted, outputs to stdout (single SDK only)
2037
2138
Examples
2239
$ pnpm run generate-guide --version=core-3 --sdk=nextjs
2340
$ pnpm run generate-guide --version=core-3 --sdk=react > react-guide.md
41+
$ pnpm run generate-guide --version=core-3 --output-dir=./guides
2442
`,
2543
{
26-
importMeta: import.meta,
2744
flags: {
28-
version: { type: 'string', isRequired: true },
29-
sdk: { type: 'string', isRequired: true },
45+
outputDir: { type: 'string' },
46+
sdk: { type: 'string' },
47+
version: { isRequired: true, type: 'string' },
3048
},
49+
importMeta: import.meta,
3150
},
3251
);
3352

@@ -50,7 +69,10 @@ function loadChanges(version, sdk) {
5069
return [];
5170
}
5271

53-
const files = fs.readdirSync(changesDir).filter(f => f.endsWith('.md'));
72+
const files = fs
73+
.readdirSync(changesDir)
74+
.filter(f => f.endsWith('.md'))
75+
.sort();
5476
const changes = [];
5577

5678
for (const file of files) {
@@ -94,38 +116,91 @@ function groupByCategory(changes) {
94116

95117
function getCategoryHeading(category) {
96118
const headings = {
119+
'behavior-change': 'Behavior Change',
97120
breaking: 'Breaking Changes',
121+
deprecation: 'Deprecations',
98122
'deprecation-removal': 'Deprecation Removals',
123+
version: 'Version',
99124
warning: 'Warnings',
100125
};
101-
return headings[category] || category;
126+
if (headings[category]) {
127+
return headings[category];
128+
}
129+
130+
return category.replace(/[-_]+/g, ' ').replace(/\b\w/g, char => char.toUpperCase());
131+
}
132+
133+
function normalizeSdk(sdk) {
134+
return sdk.replace(/^@clerk\//, '');
135+
}
136+
137+
function getSdkDisplayName(sdk) {
138+
return SDK_DISPLAY_NAMES[sdk] || sdk;
139+
}
140+
141+
function indent(text, spaces) {
142+
const padding = ' '.repeat(spaces);
143+
return text
144+
.split('\n')
145+
.map(line => (line.trim() ? padding + line : line))
146+
.join('\n');
147+
}
148+
149+
function generateFrontmatter(sdk, versionName) {
150+
const displayName = getSdkDisplayName(sdk);
151+
return `---
152+
title: "Upgrading ${displayName} to ${versionName}"
153+
description: "Learn how to upgrade Clerk's ${displayName} SDK to the latest version."
154+
---
155+
156+
{/* 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. */}`;
157+
}
158+
159+
function renderAccordionCategory(lines, category, categoryChanges) {
160+
const sortedChanges = [...categoryChanges].sort((a, b) => a.title.localeCompare(b.title));
161+
const titles = sortedChanges.map(change => JSON.stringify(change.title));
162+
163+
lines.push(`## ${getCategoryHeading(category)}`);
164+
lines.push('');
165+
lines.push(`<Accordion titles={[${titles.join(', ')}]}>`);
166+
167+
for (const change of sortedChanges) {
168+
lines.push(' <AccordionPanel>');
169+
lines.push(indent(change.content, 4));
170+
lines.push(' </AccordionPanel>');
171+
}
172+
173+
lines.push('</Accordion>');
174+
lines.push('');
102175
}
103176

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

108-
lines.push(`# Upgrading @clerk/${sdk} to ${versionName}`);
181+
lines.push(generateFrontmatter(sdk, versionName));
109182
lines.push('');
110183

111-
if (versionConfig.docsUrl) {
112-
lines.push(`For the full migration guide, see: ${versionConfig.docsUrl}`);
113-
lines.push('');
114-
}
115-
116184
const grouped = groupByCategory(changes);
117-
const categoryOrder = ['breaking', 'deprecation-removal', 'warning'];
185+
const categoryOrder = ['breaking', 'deprecation-removal', 'deprecation', 'warning', 'version', 'behavior-change'];
186+
const seenCategories = new Set();
118187

119188
for (const category of categoryOrder) {
120189
const categoryChanges = grouped[category];
121190
if (!categoryChanges || categoryChanges.length === 0) {
122191
continue;
123192
}
124193

194+
seenCategories.add(category);
195+
if (category === 'breaking') {
196+
renderAccordionCategory(lines, category, categoryChanges);
197+
continue;
198+
}
199+
125200
lines.push(`## ${getCategoryHeading(category)}`);
126201
lines.push('');
127202

128-
for (const change of categoryChanges) {
203+
for (const change of [...categoryChanges].sort((a, b) => a.title.localeCompare(b.title))) {
129204
lines.push(`### ${change.title}`);
130205
lines.push('');
131206
lines.push(change.content);
@@ -134,15 +209,20 @@ function generateMarkdown(sdk, versionConfig, changes) {
134209
}
135210

136211
// Handle any categories not in the predefined order
137-
for (const [category, categoryChanges] of Object.entries(grouped)) {
138-
if (categoryOrder.includes(category)) {
212+
for (const [category, categoryChanges] of Object.entries(grouped).sort(([a], [b]) => a.localeCompare(b))) {
213+
if (seenCategories.has(category)) {
214+
continue;
215+
}
216+
217+
if (category === 'breaking') {
218+
renderAccordionCategory(lines, category, categoryChanges);
139219
continue;
140220
}
141221

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

145-
for (const change of categoryChanges) {
225+
for (const change of [...categoryChanges].sort((a, b) => a.title.localeCompare(b.title))) {
146226
lines.push(`### ${change.title}`);
147227
lines.push('');
148228
lines.push(change.content);
@@ -153,19 +233,72 @@ function generateMarkdown(sdk, versionConfig, changes) {
153233
return lines.join('\n');
154234
}
155235

236+
function generateGuideForSdk(sdk, version, versionConfig) {
237+
const changes = loadChanges(version, sdk);
238+
239+
if (changes.length === 0) {
240+
return null;
241+
}
242+
243+
return generateMarkdown(sdk, versionConfig, changes);
244+
}
245+
246+
function writeGuideToFile(outputDir, sdk, content) {
247+
if (!fs.existsSync(outputDir)) {
248+
fs.mkdirSync(outputDir, { recursive: true });
249+
}
250+
251+
const filePath = path.join(outputDir, `${sdk}.mdx`);
252+
fs.writeFileSync(filePath, content);
253+
return filePath;
254+
}
255+
156256
async function main() {
157-
const { version, sdk } = cli.flags;
257+
const { outputDir, sdk, version } = cli.flags;
158258

159259
const versionConfig = await loadVersionConfig(version);
160-
const changes = loadChanges(version, sdk);
161260

162-
if (changes.length === 0) {
163-
console.error(`No changes found for ${sdk} in ${version}`);
261+
// Determine which SDKs to generate
262+
const sdksToGenerate = sdk ? [normalizeSdk(sdk)] : Object.keys(versionConfig.sdkVersions || {});
263+
264+
if (sdksToGenerate.length === 0) {
265+
console.error(`No SDKs found in version config for ${version}`);
266+
process.exit(1);
267+
}
268+
269+
// If multiple SDKs and no output dir, require output dir
270+
if (sdksToGenerate.length > 1 && !outputDir) {
271+
console.error('--output-dir is required when generating multiple SDK guides');
272+
console.error(`SDKs to generate: ${sdksToGenerate.join(', ')}`);
164273
process.exit(1);
165274
}
166275

167-
const markdown = generateMarkdown(sdk, versionConfig, changes);
168-
console.log(markdown);
276+
const results = [];
277+
278+
for (const currentSdk of sdksToGenerate) {
279+
const markdown = generateGuideForSdk(currentSdk, version, versionConfig);
280+
281+
if (!markdown) {
282+
console.error(`No changes found for ${currentSdk} in ${version}, skipping...`);
283+
continue;
284+
}
285+
286+
if (outputDir) {
287+
const filePath = writeGuideToFile(outputDir, currentSdk, markdown);
288+
results.push({ sdk: currentSdk, filePath });
289+
} else {
290+
// Single SDK, output to stdout
291+
console.log(markdown);
292+
}
293+
}
294+
295+
if (outputDir && results.length > 0) {
296+
console.log(`\nGenerated ${results.length} guide(s):`);
297+
for (const { sdk: generatedSdk, filePath } of results) {
298+
const displayName = getSdkDisplayName(generatedSdk);
299+
console.log(` ${displayName}: ${filePath}`);
300+
}
301+
}
169302
}
170303

171304
main().catch(error => {

0 commit comments

Comments
 (0)