import fs from 'node:fs'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); import type { PackageInfo } from './publish'; const workspacePath = (relativePath: string) => resolve(__dirname, '../../', relativePath); // Pre-compiled regex patterns for performance const INDENT_REGEX = /^(\s*)/; const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---\n/; const FRONTMATTER_REPLACE_REGEX = /^---\n[\s\S]*?\n---\n/; const TITLE_REGEX = /^title:\s*(.+)$/m; const DESCRIPTION_REGEX = /^description:\s*(.+)$/m; const COMMON_SDK_CONFIG_REGEX = //g; const WEB_SDK_CONFIG_REGEX = //g; const CODE_BLOCK_REGEX = /```[\s\S]*?```/g; const IMPORT_STATEMENT_REGEX = /^import\s+.*$/gm; const TABS_ITEMS_REGEX = //; const TAB_VALUE_REGEX = /([\s\S]*?)<\/Tab>/g; const TABS_WRAPPER_REGEX = /]*>([\s\S]*?)<\/Tabs>/g; const SELF_CLOSING_JSX_REGEX = /<[A-Z][a-zA-Z]*[^>]*\/>/g; const JSX_COMPONENT_REGEX = /<([A-Z][a-zA-Z]*)[^>]*>([\s\S]*?)<\/\1>/g; const REMAINING_JSX_REGEX = /<\/?[A-Z][a-zA-Z]*[^>]*>/g; const QUOTE_REGEX = /['"]/g; const dedentContent = (text: string): string => { const lines = text.split('\n'); if (lines.length === 0) { return text; } // Find the minimum indentation (excluding empty lines) // We'll dedent code blocks too, so include them in the calculation let minIndent = Number.POSITIVE_INFINITY; for (const line of lines) { const trimmed = line.trim(); // Skip empty lines if (trimmed.length === 0) { continue; } const indent = line.match(INDENT_REGEX)?.[1]?.length ?? 0; if (indent < minIndent) { minIndent = indent; } } // If no indentation found, return as-is if (minIndent === Number.POSITIVE_INFINITY || minIndent === 0) { return text; } // Remove the common indentation from all lines return lines .map((line) => { // For lines shorter than minIndent, just return them as-is (preserves empty lines) if (line.length < minIndent) { return line; } // Remove the common indentation const dedented = line.slice(minIndent); // If the line was all whitespace, return empty string to preserve the line return line.trim().length === 0 ? '' : dedented; }) .join('\n'); }; const transformMdxToReadme = ( mdxContent: string, packageName: string ): string => { let content = mdxContent; // Load MDX component content files const commonSdkConfigPath = workspacePath( 'apps/public/src/components/common-sdk-config.mdx' ); const webSdkConfigPath = workspacePath( 'apps/public/src/components/web-sdk-config.mdx' ); let commonSdkConfigContent = ''; let webSdkConfigContent = ''; try { if (fs.existsSync(commonSdkConfigPath)) { commonSdkConfigContent = fs.readFileSync(commonSdkConfigPath, 'utf-8'); } } catch { // Ignore if file doesn't exist } try { if (fs.existsSync(webSdkConfigPath)) { webSdkConfigContent = fs.readFileSync(webSdkConfigPath, 'utf-8'); } } catch { // Ignore if file doesn't exist } // Extract title from frontmatter const frontmatterMatch = content.match(FRONTMATTER_REGEX); let title = packageName; let description = ''; if (frontmatterMatch?.[1]) { const frontmatter = frontmatterMatch[1]; const titleMatch = frontmatter.match(TITLE_REGEX); const descMatch = frontmatter.match(DESCRIPTION_REGEX); if (titleMatch?.[1]) { title = titleMatch[1].trim(); } if (descMatch?.[1]) { description = descMatch[1].trim(); } // Remove frontmatter content = content.replace(FRONTMATTER_REPLACE_REGEX, ''); } // Replace MDX component references with their actual content // This must happen before code block protection so component content is also protected if (commonSdkConfigContent) { content = content.replace( COMMON_SDK_CONFIG_REGEX, `\n${commonSdkConfigContent}\n` ); } if (webSdkConfigContent) { content = content.replace( WEB_SDK_CONFIG_REGEX, `\n${webSdkConfigContent}\n` ); } // Protect code blocks from transformation // Extract code blocks before any transformations to preserve their content const codeBlockPlaceholders: string[] = []; // Extract and replace code blocks with placeholders content = content.replace(CODE_BLOCK_REGEX, (match) => { const placeholder = `__CODE_BLOCK_${codeBlockPlaceholders.length}__`; codeBlockPlaceholders.push(match); return placeholder; }); // Remove import statements (outside code blocks) content = content.replace(IMPORT_STATEMENT_REGEX, ''); // Handle Tabs component specially - convert to markdown sections // Extract tabs items from items prop const tabsItemsMatch = content.match(TABS_ITEMS_REGEX); const tabsItems = tabsItemsMatch?.[1] ? tabsItemsMatch[1] .replace(QUOTE_REGEX, '') .split(',') .map((item) => item.trim()) : []; // Replace Tabs/Tab structure with markdown sections if (tabsItems.length > 0) { // Match each Tab and convert to a markdown section content = content.replace(TAB_VALUE_REGEX, (_match, value, tabContent) => { const dedented = dedentContent(tabContent).trim(); return `\n#### ${value}\n\n${dedented}\n\n`; }); // Remove the Tabs wrapper content = content.replace(TABS_WRAPPER_REGEX, '$1'); } else { // Fallback: if no items prop, just convert tabs to sections content = content.replace(TAB_VALUE_REGEX, (_match, value, tabContent) => { const dedented = dedentContent(tabContent).trim(); return `\n#### ${value}\n\n${dedented}\n\n`; }); content = content.replace(TABS_WRAPPER_REGEX, '$1'); } // Remove self-closing JSX components (like , ) content = content.replace(SELF_CLOSING_JSX_REGEX, ''); // Remove JSX component tags but preserve content between opening/closing tags // Handle nested components by recursively removing outer tags // This regex matches opening tag, captures content (including nested tags), and closing tag let previousContent = ''; while (content !== previousContent) { previousContent = content; // Match JSX components with their content - handles one level of nesting content = content.replace( JSX_COMPONENT_REGEX, (_match, _tagName, innerContent) => { return dedentContent(innerContent).trim(); } ); } // Remove any remaining JSX tags (self-closing or unmatched) content = content.replace(REMAINING_JSX_REGEX, ''); // Restore code blocks codeBlockPlaceholders.forEach((codeBlock, index) => { content = content.replace(`__CODE_BLOCK_${index}__`, codeBlock); }); // Convert internal links (starting with /) to absolute URLs content = content.replace( /\[([^\]]+)\]\((\/[^)]+)\)/g, '[$1](https://openpanel.dev$2)' ); // Clean up extra blank lines content = content.replace(/\n{3,}/g, '\n\n').trim(); // Build the README header const docUrl = `https://openpanel.dev/docs/sdks/${packageName.replace('@openpanel/', '')}`; let readme = `# ${title}\n\n`; if (description) { readme += `${description}\n\n`; } readme += `> 📖 **Full documentation:** [${docUrl}](${docUrl})\n\n`; readme += '---\n\n'; readme += content; return readme; }; export const generateReadme = ( packages: Record, dependents: string[] ): string[] => { const generatedReadmes: string[] = []; for (const dep of dependents) { const pkg = packages[dep]; const docPath = pkg?.config?.docPath; if (!docPath) { console.log( `📝 Skipping README generation for ${dep} (no docPath configured)` ); continue; } const packagePath = workspacePath(pkg.localPath); const readmePath = join(packagePath, 'README.md'); console.log(`📝 Generating README for ${dep}`); const mdxContent = fs.readFileSync(workspacePath(docPath), 'utf-8'); const readmeContent = transformMdxToReadme(mdxContent, pkg.name); fs.writeFileSync(readmePath, readmeContent, 'utf-8'); generatedReadmes.push(readmePath); } return generatedReadmes; };