Skip to content

Commit 950141c

Browse files
authored
fix RAC ExampleSwitcher rendering in markdown docs (#9333)
1 parent 5de83cd commit 950141c

File tree

1 file changed

+250
-42
lines changed

1 file changed

+250
-42
lines changed

packages/dev/s2-docs/scripts/generateMarkdownDocs.mjs

Lines changed: 250 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import * as babel from '@babel/parser';
44
import {fileURLToPath} from 'url';
55
import fs from 'fs';
6-
import {getBaseUrl} from '../src/pageUtils.ts';
76
import glob from 'fast-glob';
87
import path from 'path';
98
import {Project} from 'ts-morph';
@@ -13,6 +12,33 @@ import remarkStringify from 'remark-stringify';
1312
import {unified} from 'unified';
1413
import {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+
1642
const __dirname = path.dirname(fileURLToPath(import.meta.url));
1743
const REPO_ROOT = path.resolve(__dirname, '../../../../');
1844
const 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(/type=["']([^"']+)["']/);
1033+
if (typeMatch) {
1034+
result.type = typeMatch[1];
1035+
}
1036+
1037+
// Extract files={[...]}
1038+
const filesMatch = meta.match(/files=\{(\[[^\]]+\])\}/);
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*(begin|end)/i.test(l))
1091+
.map(l => l.replace(/\/\*\s*PROPS\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*(begin|end)/i.test(l))
1168+
.map(l => l.replace(/\/\*\s*PROPS\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

Comments
 (0)