33import * as babel from '@babel/parser' ;
44import { fileURLToPath } from 'url' ;
55import fs from 'fs' ;
6- import { getBaseUrl } from '../src/pageUtils.ts' ;
76import glob from 'fast-glob' ;
87import path from 'path' ;
98import { Project } from 'ts-morph' ;
@@ -13,6 +12,33 @@ import remarkStringify from 'remark-stringify';
1312import { unified } from 'unified' ;
1413import { visit } from 'unist-util-visit' ;
1514
15+ const BASE_URL = {
16+ dev : {
17+ 'react-aria' : 'http://localhost:1234' ,
18+ 's2' : 'http://localhost:4321'
19+ } ,
20+ stage : {
21+ 'react-aria' : 'https://d5iwopk28bdhl.cloudfront.net' ,
22+ 's2' : 'https://d1pzu54gtk2aed.cloudfront.net'
23+ } ,
24+ prod : {
25+ 'react-aria' : 'https://react-aria.adobe.com' ,
26+ 's2' : 'https://react-spectrum.adobe.com'
27+ }
28+ } ;
29+
30+ function getBaseUrl ( library ) {
31+ let env = process . env . DOCS_ENV ;
32+ let base = env
33+ ? BASE_URL [ env ] [ library ]
34+ : `http://localhost:1234/${ library } ` ;
35+ let publicUrl = process . env . PUBLIC_URL ;
36+ if ( publicUrl ) {
37+ base += publicUrl . replace ( / \/ $ / , '' ) ;
38+ }
39+ return base ;
40+ }
41+
1642const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
1743const REPO_ROOT = path . resolve ( __dirname , '../../../../' ) ;
1844const S2_SRC_ROOT = path . join ( REPO_ROOT , 'packages/@react-spectrum/s2/src' ) ;
@@ -938,61 +964,243 @@ function remarkDocsComponentsToMarkdown() {
938964 exampleTitles = Array . isArray ( parsed ) ? parsed : [ ] ;
939965 }
940966
941- // Fallback default titles when none were provided.
942- if ( exampleTitles . length === 0 ) {
943- exampleTitles = [ 'Vanilla CSS' , 'Tailwind' ] ;
944- }
945-
946- // Children may include whitespace/text nodes – filter to VisualExample elements.
947967 const visualChildren = ( node . children || [ ] ) . filter ( c => c . type === 'mdxJsxFlowElement' && c . name === 'VisualExample' ) ;
968+ const codeChildren = ( node . children || [ ] ) . filter ( c => c . type === 'code' ) ;
948969
949970 // Build replacement markdown nodes.
950971 const newNodes = [ ] ;
951972
952- visualChildren . forEach ( ( vChild , i ) => {
953- const title = exampleTitles [ i ] || `Example ${ i + 1 } ` ;
954-
955- // ## {title} example
956- newNodes . push ( {
957- type : 'heading' ,
958- depth : 2 ,
959- children : [ { type : 'text' , value : `${ title } example` } ]
960- } ) ;
961-
962- // Extract files attribute from VisualExample
963- const filesAttr = vChild . attributes ?. find ( a => a . name === 'files' ) ;
964- let fileList = [ ] ;
965- if ( filesAttr ) {
966- if ( filesAttr . value ?. type === 'mdxJsxAttributeValueExpression' ) {
967- const parsed = parseExpression ( filesAttr . value . value , file ) ;
968- fileList = Array . isArray ( parsed ) ? parsed : [ ] ;
969- } else if ( Array . isArray ( filesAttr . value ) ) {
970- fileList = filesAttr . value ;
971- }
973+ if ( visualChildren . length > 0 ) {
974+ if ( exampleTitles . length === 0 ) {
975+ exampleTitles = [ 'Vanilla CSS' , 'Tailwind' ] ;
972976 }
973977
974- fileList . forEach ( fp => {
975- const absPath = path . join ( REPO_ROOT , fp ) ;
976- if ( ! fs . existsSync ( absPath ) ) { return ; }
977- const contents = fs . readFileSync ( absPath , 'utf8' ) ;
978- const ext = path . extname ( fp ) . slice ( 1 ) ;
978+ visualChildren . forEach ( ( vChild , i ) => {
979+ const title = exampleTitles [ i ] || `Example ${ i + 1 } ` ;
979980
980- // ### {filename}
981+ // ## {title} example
981982 newNodes . push ( {
982983 type : 'heading' ,
983- depth : 3 ,
984- children : [ { type : 'text' , value : path . basename ( fp ) } ]
984+ depth : 2 ,
985+ children : [ { type : 'text' , value : ` ${ title } example` } ]
985986 } ) ;
986987
987- // ```{lang}\n{contents}\n```
988- newNodes . push ( {
989- type : 'code' ,
990- lang : ext || undefined ,
991- meta : '' ,
992- value : contents
988+ // Extract files attribute from VisualExample
989+ const filesAttr = vChild . attributes ?. find ( a => a . name === 'files' ) ;
990+ let fileList = [ ] ;
991+ if ( filesAttr ) {
992+ if ( filesAttr . value ?. type === 'mdxJsxAttributeValueExpression' ) {
993+ const parsed = parseExpression ( filesAttr . value . value , file ) ;
994+ fileList = Array . isArray ( parsed ) ? parsed : [ ] ;
995+ } else if ( Array . isArray ( filesAttr . value ) ) {
996+ fileList = filesAttr . value ;
997+ }
998+ }
999+
1000+ fileList . forEach ( fp => {
1001+ const absPath = path . join ( REPO_ROOT , fp ) ;
1002+ if ( ! fs . existsSync ( absPath ) ) { return ; }
1003+ const contents = fs . readFileSync ( absPath , 'utf8' ) ;
1004+ const ext = path . extname ( fp ) . slice ( 1 ) ;
1005+
1006+ // ### {filename}
1007+ newNodes . push ( {
1008+ type : 'heading' ,
1009+ depth : 3 ,
1010+ children : [ { type : 'text' , value : path . basename ( fp ) } ]
1011+ } ) ;
1012+
1013+ // ```{lang}\n{contents}\n```
1014+ newNodes . push ( {
1015+ type : 'code' ,
1016+ lang : ext || undefined ,
1017+ meta : '' ,
1018+ value : contents
1019+ } ) ;
9931020 } ) ;
9941021 } ) ;
995- } ) ;
1022+ }
1023+
1024+ // Handle code block children (type="vanilla"|"tailwind" and files=[...])
1025+ if ( codeChildren . length > 0 ) {
1026+ // Parse metadata from code blocks to extract type and files
1027+ const parseCodeMeta = ( meta ) => {
1028+ if ( ! meta ) { return { } ; }
1029+ const result = { } ;
1030+
1031+ // Extract type
1032+ const typeMatch = meta . match ( / t y p e = [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / ) ;
1033+ if ( typeMatch ) {
1034+ result . type = typeMatch [ 1 ] ;
1035+ }
1036+
1037+ // Extract files={[...]}
1038+ const filesMatch = meta . match ( / f i l e s = \{ ( \[ [ ^ \] ] + \] ) \} / ) ;
1039+ if ( filesMatch ) {
1040+ try {
1041+ result . files = JSON . parse ( filesMatch [ 1 ] ) ;
1042+ } catch {
1043+ const parsed = parseExpression ( filesMatch [ 1 ] , file ) ;
1044+ if ( Array . isArray ( parsed ) ) {
1045+ result . files = parsed ;
1046+ }
1047+ }
1048+ }
1049+
1050+ return result ;
1051+ } ;
1052+
1053+ const typeToTitle = {
1054+ 'vanilla' : 'Vanilla CSS' ,
1055+ 'tailwind' : 'Tailwind'
1056+ } ;
1057+
1058+ // Check if this is a "component" type ExampleSwitcher (each code block gets its own example title)
1059+ const typeAttr = node . attributes ?. find ( a => a . name === 'type' ) ;
1060+ let switcherType = null ;
1061+ if ( typeAttr ) {
1062+ if ( typeAttr . value ?. type === 'mdxJsxAttributeValueExpression' ) {
1063+ switcherType = typeAttr . value . value . replace ( / [ ' " ` ] / g, '' ) . trim ( ) ;
1064+ } else if ( typeof typeAttr . value === 'string' ) {
1065+ switcherType = typeAttr . value . trim ( ) ;
1066+ }
1067+ }
1068+
1069+ if ( switcherType === 'component' && exampleTitles . length > 0 ) {
1070+ // Each code block gets its own heading from the examples array
1071+ codeChildren . forEach ( ( codeChild , i ) => {
1072+ const title = exampleTitles [ i ] || `Example ${ i + 1 } ` ;
1073+ const meta = parseCodeMeta ( codeChild . meta ) ;
1074+
1075+ // ## {title} example
1076+ newNodes . push ( {
1077+ type : 'heading' ,
1078+ depth : 2 ,
1079+ children : [ { type : 'text' , value : `${ title } example` } ]
1080+ } ) ;
1081+
1082+ // Clean up the code value
1083+ let codeValue = codeChild . value ;
1084+ if ( codeValue . startsWith ( '"use client";\n' ) ) {
1085+ codeValue = codeValue . slice ( 14 ) ;
1086+ }
1087+ // Remove docs rendering-specific comments
1088+ codeValue = codeValue
1089+ . split ( '\n' )
1090+ . filter ( l => ! / ^ \s * \/ \/ \/ - \s * ( b e g i n | e n d ) / i. test ( l ) )
1091+ . map ( l => l . replace ( / \/ \* \s * P R O P S \s * \* \/ / gi, '' ) )
1092+ . join ( '\n' ) ;
1093+
1094+ newNodes . push ( {
1095+ type : 'code' ,
1096+ lang : codeChild . lang || 'tsx' ,
1097+ meta : '' ,
1098+ value : codeValue
1099+ } ) ;
1100+
1101+ // Add referenced files for this specific example
1102+ if ( meta . files && Array . isArray ( meta . files ) ) {
1103+ meta . files . forEach ( fp => {
1104+ const absPath = path . join ( REPO_ROOT , fp ) ;
1105+ if ( ! fs . existsSync ( absPath ) ) { return ; }
1106+ const contents = fs . readFileSync ( absPath , 'utf8' ) ;
1107+ const ext = path . extname ( fp ) . slice ( 1 ) ;
1108+
1109+ // ### {filename}
1110+ newNodes . push ( {
1111+ type : 'heading' ,
1112+ depth : 3 ,
1113+ children : [ { type : 'text' , value : path . basename ( fp ) } ]
1114+ } ) ;
1115+
1116+ // ```{lang}\n{contents}\n```
1117+ newNodes . push ( {
1118+ type : 'code' ,
1119+ lang : ext || undefined ,
1120+ meta : '' ,
1121+ value : contents
1122+ } ) ;
1123+ } ) ;
1124+ }
1125+ } ) ;
1126+ } else {
1127+ // Group code blocks by type (vanilla, tailwind, etc.)
1128+ const codeBlocksByType = new Map ( ) ;
1129+ codeChildren . forEach ( ( codeChild ) => {
1130+ const meta = parseCodeMeta ( codeChild . meta ) ;
1131+ const type = meta . type || 'vanilla' ;
1132+ if ( ! codeBlocksByType . has ( type ) ) {
1133+ codeBlocksByType . set ( type , [ ] ) ;
1134+ }
1135+ codeBlocksByType . get ( type ) . push ( { code : codeChild , meta} ) ;
1136+ } ) ;
1137+
1138+ // Process each type group
1139+ for ( const [ type , codeBlocks ] of codeBlocksByType ) {
1140+ const title = typeToTitle [ type ] || type . charAt ( 0 ) . toUpperCase ( ) + type . slice ( 1 ) ;
1141+
1142+ // ## {title} example
1143+ newNodes . push ( {
1144+ type : 'heading' ,
1145+ depth : 2 ,
1146+ children : [ { type : 'text' , value : `${ title } example` } ]
1147+ } ) ;
1148+
1149+ // Collect all unique files from all code blocks of this type
1150+ const allFiles = new Set ( ) ;
1151+ codeBlocks . forEach ( ( { meta} ) => {
1152+ if ( meta . files && Array . isArray ( meta . files ) ) {
1153+ meta . files . forEach ( f => allFiles . add ( f ) ) ;
1154+ }
1155+ } ) ;
1156+
1157+ // Add the inline example code first
1158+ codeBlocks . forEach ( ( { code} ) => {
1159+ // Clean up the code value
1160+ let codeValue = code . value ;
1161+ if ( codeValue . startsWith ( '"use client";\n' ) ) {
1162+ codeValue = codeValue . slice ( 14 ) ;
1163+ }
1164+ // Remove docs rendering-specific comments
1165+ codeValue = codeValue
1166+ . split ( '\n' )
1167+ . filter ( l => ! / ^ \s * \/ \/ \/ - \s * ( b e g i n | e n d ) / i. test ( l ) )
1168+ . map ( l => l . replace ( / \/ \* \s * P R O P S \s * \* \/ / gi, '' ) )
1169+ . join ( '\n' ) ;
1170+
1171+ newNodes . push ( {
1172+ type : 'code' ,
1173+ lang : code . lang || 'tsx' ,
1174+ meta : '' ,
1175+ value : codeValue
1176+ } ) ;
1177+ } ) ;
1178+
1179+ // Add referenced files
1180+ allFiles . forEach ( fp => {
1181+ const absPath = path . join ( REPO_ROOT , fp ) ;
1182+ if ( ! fs . existsSync ( absPath ) ) { return ; }
1183+ const contents = fs . readFileSync ( absPath , 'utf8' ) ;
1184+ const ext = path . extname ( fp ) . slice ( 1 ) ;
1185+
1186+ // ### {filename}
1187+ newNodes . push ( {
1188+ type : 'heading' ,
1189+ depth : 3 ,
1190+ children : [ { type : 'text' , value : path . basename ( fp ) } ]
1191+ } ) ;
1192+
1193+ // ```{lang}\n{contents}\n```
1194+ newNodes . push ( {
1195+ type : 'code' ,
1196+ lang : ext || undefined ,
1197+ meta : '' ,
1198+ value : contents
1199+ } ) ;
1200+ } ) ;
1201+ }
1202+ }
1203+ }
9961204
9971205 // Replace ExampleSwitcher node with generated markdown.
9981206 parent . children . splice ( index , 1 , ...newNodes ) ;
0 commit comments