@@ -23,7 +23,7 @@ const repoRoot = path.resolve(__dirname, '..', '..');
2323function  parseArgs ( argv )  { 
2424  const  parser  =  yargs ( argv ) 
2525    . usage ( 
26-       'Usage: yarn generate-changelog [--codex|--claude] [--debug] [<pkg@version> ...]' 
26+       'Usage: yarn generate-changelog [--codex|--claude] [--debug] [--format <text|csv|json>] [ <pkg@version> ...]' 
2727    ) 
2828    . example ( 
2929@@ -50,6 +50,12 @@ function parseArgs(argv) {
5050      describe : 'Enable verbose debug logging.' , 
5151      default : false , 
5252    } ) 
53+     . option ( 'format' ,  { 
54+       type : 'string' , 
55+       describe : 'Output format for the generated changelog.' , 
56+       choices : [ 'text' ,  'csv' ,  'json' ] , 
57+       default : 'text' , 
58+     } ) 
5359    . help ( 'help' ) 
5460    . alias ( 'h' ,  'help' ) 
5561    . version ( false ) 
@@ -61,6 +67,7 @@ function parseArgs(argv) {
6167  const  args  =  parser . scriptName ( 'generate-changelog' ) . parse ( ) ; 
6268  const  packageSpecs  =  [ ] ; 
6369  const  debug  =  ! ! args . debug ; 
70+   const  format  =  args . format  ||  'text' ; 
6471  let  summarizer  =  null ; 
6572  if  ( args . codex  &&  args . claude )  { 
6673    throw  new  Error ( 'Choose either --codex or --claude, not both.' ) ; 
@@ -123,6 +130,7 @@ function parseArgs(argv) {
123130
124131  return  { 
125132    debug, 
133+     format, 
126134    summarizer, 
127135    packageSpecs, 
128136  } ; 
@@ -484,6 +492,22 @@ async function summarizePackageCommits({
484492
485493function  noopLogger ( )  { } 
486494
495+ function  escapeCsvValue ( value )  { 
496+   if  ( value  ==  null )  { 
497+     return  '' ; 
498+   } 
499+ 
500+   const  stringValue  =  String ( value ) . replace ( / \r ? \n | \r / g,  ' ' ) ; 
501+   if  ( stringValue . includes ( '"' )  ||  stringValue . includes ( ',' ) )  { 
502+     return  `"${ stringValue . replace ( / " / g,  '""' ) }  ; 
503+   } 
504+   return  stringValue ; 
505+ } 
506+ 
507+ function  toCsvRow ( values )  { 
508+   return  values . map ( escapeCsvValue ) . join ( ',' ) ; 
509+ } 
510+ 
487511async  function  runSummarizer ( command ,  prompt )  { 
488512  const  options  =  { cwd : repoRoot ,  maxBuffer : 5  *  1024  *  1024 } ; 
489513
@@ -634,7 +658,9 @@ async function fetchPullRequestMetadata(prNumber, {log}) {
634658} 
635659
636660async  function  main ( )  { 
637-   const  { packageSpecs,  summarizer,  debug}  =  parseArgs ( process . argv . slice ( 2 ) ) ; 
661+   const  { packageSpecs,  summarizer,  debug,  format}  =  parseArgs ( 
662+     process . argv . slice ( 2 ) 
663+   ) ; 
638664  const  log  =  debug 
639665    ? ( ...args )  =>  console . log ( '[generate-changelog]' ,  ...args ) 
640666    : noopLogger ; 
@@ -754,60 +780,214 @@ async function main() {
754780    log, 
755781  } ) ; 
756782
757-   const  outputLines  =  [ ] ; 
783+   const  noChangesMessage  =  'No changes since the last release.' ; 
784+   const  changelogEntries  =  [ ] ; 
758785  for  ( let  i  =  0 ;  i  <  packageSpecs . length ;  i ++ )  { 
759786    const  spec  =  packageSpecs [ i ] ; 
760-     outputLines . push ( `##  ${ spec . name } @ ${ spec . displayVersion  ||  spec . version } ` ) ; 
787+     const   versionText   =   spec . displayVersion  ||  spec . version ; 
761788    const  commitsForPackage  =  commitsByPackage . get ( spec . name )  ||  [ ] ; 
789+     const  entry  =  { 
790+       package : spec . name , 
791+       version : versionText , 
792+       hasChanges : commitsForPackage . length  >  0 , 
793+       commits : [ ] , 
794+       note : null , 
795+     } ; 
762796
763-     if  ( commitsForPackage . length   ===   0 )  { 
764-       outputLines . push ( '* No changes since the last release.' ) ; 
765-       outputLines . push ( '' ) ; 
797+     if  ( ! entry . hasChanges )  { 
798+       entry . note   =   noChangesMessage ; 
799+       changelogEntries . push ( entry ) ; 
766800      continue ; 
767801    } 
768802
769-     commitsForPackage . forEach ( commit  =>  { 
803+     const  summaryMap  =  summariesByPackage . get ( spec . name )  ||  new  Map ( ) ; 
804+     entry . commits  =  commitsForPackage . map ( commit  =>  { 
770805      if  ( commit . prNumber  &&  prMetadata . has ( commit . prNumber ) )  { 
771-         commit . authorLogin  =  prMetadata . get ( commit . prNumber ) . authorLogin ; 
806+         const  metadata  =  prMetadata . get ( commit . prNumber ) ; 
807+         if  ( metadata  &&  metadata . authorLogin )  { 
808+           commit . authorLogin  =  metadata . authorLogin ; 
809+         } 
772810      } 
773811
774-       const  prFragment  =  commit . prNumber 
775-         ? `[#${ commit . prNumber } ${ commit . prNumber }  
776-         : `commit ${ commit . sha . slice ( 0 ,  7 ) }  ; 
812+       let  summary  =  summaryMap . get ( commit . sha )  ||  commit . subject ; 
813+       if  ( commit . prNumber )  { 
814+         const  prPattern  =  new  RegExp ( `\\s*\\(#${ commit . prNumber }  ) ; 
815+         summary  =  summary . replace ( prPattern ,  '' ) . trim ( ) ; 
816+       } 
777817
778-       let  authorFragment  =  commit . authorLogin 
779-         ? `[@${ commit . authorLogin } ${ commit . authorLogin }  
780-         : commit . authorName  ||  'unknown author' ; 
818+       const  prNumber  =  commit . prNumber  ||  null ; 
819+       const  prUrl  =  prNumber 
820+         ? `https://github.com/facebook/react/pull/${ prNumber }  
821+         : null ; 
822+       const  commitSha  =  commit . sha ; 
823+       const  commitUrl  =  `https://github.com/facebook/react/commit/${ commitSha }  ; 
824+ 
825+       const  authorLogin  =  commit . authorLogin  ||  null ; 
826+       const  authorName  =  commit . authorName  ||  null ; 
827+       const  authorEmail  =  commit . authorEmail  ||  null ; 
828+ 
829+       let  authorUrl  =  null ; 
830+       let  authorDisplay  =  authorName  ||  'unknown author' ; 
831+ 
832+       if  ( authorLogin )  { 
833+         authorUrl  =  `https://github.com/${ authorLogin }  ; 
834+         authorDisplay  =  `[@${ authorLogin } ${ authorUrl }  ; 
835+       }  else  if  ( authorName  &&  authorName . startsWith ( '@' ) )  { 
836+         const  username  =  authorName . slice ( 1 ) ; 
837+         authorUrl  =  `https://github.com/${ username }  ; 
838+         authorDisplay  =  `[@${ username } ${ authorUrl }  ; 
839+       } 
781840
782-       if  ( 
783-         ! commit . authorLogin  && 
784-         commit . authorName  && 
785-         commit . authorName . startsWith ( '@' ) 
786-       )  { 
787-         const  username  =  commit . authorName . slice ( 1 ) ; 
788-         authorFragment  =  `[@${ username } ${ username }  ; 
841+       const  referenceDisplay  =  prNumber 
842+         ? `[#${ prNumber } ${ prUrl }  
843+         : `commit ${ commitSha . slice ( 0 ,  7 ) }  ; 
844+       const  referenceType  =  prNumber  ? 'pr'  : 'commit' ; 
845+       const  referenceId  =  prNumber  ? `#${ prNumber }   : commitSha . slice ( 0 ,  7 ) ; 
846+       const  referenceUrl  =  prNumber  ? prUrl  : commitUrl ; 
847+ 
848+       return  { 
849+         summary, 
850+         prNumber, 
851+         prUrl, 
852+         commitSha, 
853+         commitUrl, 
854+         authorLogin, 
855+         authorName, 
856+         authorEmail, 
857+         authorUrl, 
858+         authorDisplay, 
859+         referenceDisplay, 
860+         referenceType, 
861+         referenceId, 
862+         referenceUrl, 
863+       } ; 
864+     } ) ; 
865+ 
866+     changelogEntries . push ( entry ) ; 
867+   } 
868+ 
869+   log ( 'Generated changelog sections.' ) ; 
870+   if  ( format  ===  'text' )  { 
871+     const  outputLines  =  [ ] ; 
872+     for  ( let  i  =  0 ;  i  <  changelogEntries . length ;  i ++ )  { 
873+       const  entry  =  changelogEntries [ i ] ; 
874+       outputLines . push ( `## ${ entry . package } ${ entry . version }  ) ; 
875+       if  ( ! entry . hasChanges )  { 
876+         outputLines . push ( `* ${ entry . note }  ) ; 
877+         outputLines . push ( '' ) ; 
878+         continue ; 
789879      } 
790880
791-       const  summaryMap  =  summariesByPackage . get ( spec . name )  ||  new  Map ( ) ; 
792-       let  summary  =  summaryMap . get ( commit . sha )  ||  commit . subject ; 
881+       entry . commits . forEach ( commit  =>  { 
882+         outputLines . push ( 
883+           `* ${ commit . summary } ${ commit . referenceDisplay } ${ commit . authorDisplay }  
884+         ) ; 
885+       } ) ; 
886+       outputLines . push ( '' ) ; 
887+     } 
793888
794-       if  ( commit . prNumber )  { 
795-         const  prPattern  =  new  RegExp ( `\\s*\\(#${ commit . prNumber }  ) ; 
796-         summary  =  summary . replace ( prPattern ,  '' ) . trim ( ) ; 
889+     while  ( outputLines . length  &&  outputLines [ outputLines . length  -  1 ]  ===  '' )  { 
890+       outputLines . pop ( ) ; 
891+     } 
892+ 
893+     console . log ( outputLines . join ( '\n' ) ) ; 
894+     return ; 
895+   } 
896+ 
897+   if  ( format  ===  'csv' )  { 
898+     const  header  =  [ 
899+       'package' , 
900+       'version' , 
901+       'summary' , 
902+       'reference_type' , 
903+       'reference_id' , 
904+       'reference_url' , 
905+       'author_name' , 
906+       'author_login' , 
907+       'author_url' , 
908+       'author_email' , 
909+       'commit_sha' , 
910+       'commit_url' , 
911+     ] ; 
912+     const  rows  =  [ header ] ; 
913+     changelogEntries . forEach ( entry  =>  { 
914+       if  ( ! entry . hasChanges )  { 
915+         rows . push ( [ 
916+           entry . package , 
917+           entry . version , 
918+           entry . note , 
919+           '' , 
920+           '' , 
921+           '' , 
922+           '' , 
923+           '' , 
924+           '' , 
925+           '' , 
926+           '' , 
927+           '' , 
928+         ] ) ; 
929+         return ; 
797930      } 
798931
799-       outputLines . push ( `* ${ summary } ${ prFragment } ${ authorFragment }  ) ; 
932+       entry . commits . forEach ( commit  =>  { 
933+         const  authorName  = 
934+           commit . authorName  || 
935+           ( commit . authorLogin  ? `@${ commit . authorLogin }   : 'unknown author' ) ; 
936+         rows . push ( [ 
937+           entry . package , 
938+           entry . version , 
939+           commit . summary , 
940+           commit . referenceType , 
941+           commit . referenceId , 
942+           commit . referenceUrl , 
943+           authorName , 
944+           commit . authorLogin  ||  '' , 
945+           commit . authorUrl  ||  '' , 
946+           commit . authorEmail  ||  '' , 
947+           commit . commitSha , 
948+           commit . commitUrl , 
949+         ] ) ; 
950+       } ) ; 
800951    } ) ; 
801952
802-     outputLines . push ( '' ) ; 
953+     const  csvLines  =  rows . map ( toCsvRow ) ; 
954+     console . log ( csvLines . join ( '\n' ) ) ; 
955+     return ; 
803956  } 
804957
805-   while  ( outputLines . length  &&  outputLines [ outputLines . length  -  1 ]  ===  '' )  { 
806-     outputLines . pop ( ) ; 
958+   if  ( format  ===  'json' )  { 
959+     const  payload  =  changelogEntries . map ( entry  =>  ( { 
960+       package : entry . package , 
961+       version : entry . version , 
962+       hasChanges : entry . hasChanges , 
963+       note : entry . hasChanges  ? undefined  : entry . note , 
964+       commits : entry . commits . map ( commit  =>  ( { 
965+         summary : commit . summary , 
966+         prNumber : commit . prNumber , 
967+         prUrl : commit . prUrl , 
968+         commitSha : commit . commitSha , 
969+         commitUrl : commit . commitUrl , 
970+         author : { 
971+           login : commit . authorLogin , 
972+           name : commit . authorName , 
973+           email : commit . authorEmail , 
974+           url : commit . authorUrl , 
975+           display : commit . authorDisplay , 
976+         } , 
977+         reference : { 
978+           type : commit . referenceType , 
979+           id : commit . referenceId , 
980+           url : commit . referenceUrl , 
981+           label : commit . referenceDisplay , 
982+         } , 
983+       } ) ) , 
984+     } ) ) ; 
985+ 
986+     console . log ( JSON . stringify ( payload ,  null ,  2 ) ) ; 
987+     return ; 
807988  } 
808989
809-   log ( 'Generated changelog sections.' ) ; 
810-   console . log ( outputLines . join ( '\n' ) ) ; 
990+   throw  new  Error ( `Unsupported format: ${ format }  ) ; 
811991} 
812992
813993main ( ) . catch ( error  =>  { 
0 commit comments