@@ -9,25 +9,44 @@ import meow from 'meow';
99const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
1010const 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+
1226const 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
95117function 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 ( / ^ @ c l e r k \/ / , '' ) ;
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
104177function 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+
156256async 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
171304main ( ) . catch ( error => {
0 commit comments