mirror of
https://github.com/stack-auth/stack.git
synced 2026-07-20 21:29:36 +08:00
New docs (#698)
Co-authored-by: Konsti Wohlwend <[email protected]> Co-authored-by: Madison Kennedy <[email protected]> Co-authored-by: BilalG1 <[email protected]>
This commit is contained in:
co-authored by
Konsti Wohlwend
Madison Kennedy
BilalG1
parent
8ed08ed6b3
commit
4e467c4026
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Get __dirname equivalent in ES modules
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
/**
|
||||
* Recursively remove all files and directories within a directory
|
||||
* but keep the directory itself
|
||||
*/
|
||||
function clearDirectory(dirPath) {
|
||||
if (!fs.existsSync(dirPath)) {
|
||||
console.log(`Directory ${dirPath} does not exist.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const items = fs.readdirSync(dirPath);
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(dirPath, item);
|
||||
const stat = fs.statSync(itemPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
// Recursively remove directory and all its contents
|
||||
fs.rmSync(itemPath, { recursive: true, force: true });
|
||||
console.log(`Removed directory: ${itemPath}`);
|
||||
} else {
|
||||
// Remove file
|
||||
fs.unlinkSync(itemPath);
|
||||
console.log(`Removed file: ${itemPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const docsPath = path.join(__dirname, '..', 'content', 'docs');
|
||||
const apiDocsPath = path.join(__dirname, '..', 'content', 'api');
|
||||
|
||||
console.log('🧹 Clearing all files and directories in content/docs, and content/api');
|
||||
console.log(`Target directory: ${docsPath}`);
|
||||
console.log(`Target directory: ${apiDocsPath}`);
|
||||
|
||||
try {
|
||||
clearDirectory(docsPath);
|
||||
clearDirectory(apiDocsPath);
|
||||
console.log('✅ Successfully cleared content/docs directory!');
|
||||
} catch (error) {
|
||||
console.error('❌ Error clearing content/docs directory:', error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the script
|
||||
main();
|
||||
@@ -0,0 +1,340 @@
|
||||
import fs from 'fs';
|
||||
import { glob } from 'glob';
|
||||
import yaml from 'js-yaml';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// Get __dirname equivalent in ESM
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
// Configure paths
|
||||
const TEMPLATE_DIR = path.resolve(__dirname, '../templates');
|
||||
const OUTPUT_BASE_DIR = path.resolve(__dirname, '../content/docs');
|
||||
const CONFIG_FILE = path.resolve(__dirname, '../docs-platform.yml');
|
||||
const PLATFORMS = ['next', 'react', 'js', 'python'];
|
||||
|
||||
// Platform groups mapping
|
||||
const PLATFORM_GROUPS = {
|
||||
'react-like': ['next', 'react'], // Platforms that use React components
|
||||
'js-like': ['next', 'react', 'js'] // Platforms that use JavaScript SDK (includes React-based platforms)
|
||||
};
|
||||
|
||||
// Load platform configuration
|
||||
let platformConfig = {};
|
||||
try {
|
||||
const configContent = fs.readFileSync(CONFIG_FILE, 'utf8');
|
||||
platformConfig = yaml.load(configContent);
|
||||
console.log('Loaded platform configuration from docs-platform.yml');
|
||||
} catch (error) {
|
||||
console.error('Failed to load platform configuration:', error.message);
|
||||
console.log('Falling back to include all files for all platforms');
|
||||
}
|
||||
|
||||
// Platform folder naming - now using root folders
|
||||
function getFolderName(platform) {
|
||||
return platform; // Use direct platform names instead of pages-{platform}
|
||||
}
|
||||
|
||||
// Platform display names
|
||||
function getPlatformDisplayName(platform) {
|
||||
const platformNames = {
|
||||
'next': 'Next.js',
|
||||
'react': 'React',
|
||||
'js': 'JavaScript',
|
||||
'python': 'Python'
|
||||
};
|
||||
return platformNames[platform] || platform;
|
||||
}
|
||||
|
||||
// Platform-specific content markers - Updated regex to handle both syntaxes (with and without colon)
|
||||
const PLATFORM_START_MARKER = /{\s*\/\*\s*IF_PLATFORM:?\s*([\w-]+)\s*\*\/\s*}/;
|
||||
const PLATFORM_ELSE_MARKER = /{\s*\/\*\s*ELSE_IF_PLATFORM:?\s+([\w-]+)\s*\*\/\s*}/;
|
||||
const PLATFORM_END_MARKER = /{\s*\/\*\s*END_PLATFORM\s*\*\/\s*}/;
|
||||
|
||||
/**
|
||||
* Check if a platform or platform group includes the target platform
|
||||
*/
|
||||
function isPlatformMatch(platformSpec, targetPlatform) {
|
||||
// Direct platform match
|
||||
if (platformSpec === targetPlatform) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Platform group match
|
||||
if (PLATFORM_GROUPS[platformSpec]) {
|
||||
return PLATFORM_GROUPS[platformSpec].includes(targetPlatform);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file should be included for a specific platform
|
||||
*/
|
||||
function shouldIncludeFileForPlatform(platform, filePath) {
|
||||
// If no configuration loaded, include everything
|
||||
if (!platformConfig.pages) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Find the page configuration for this file
|
||||
const pageConfig = platformConfig.pages.find(page => page.path === filePath);
|
||||
|
||||
// If no specific configuration found, exclude by default
|
||||
if (!pageConfig) {
|
||||
console.log(`No configuration found for ${filePath}, excluding by default`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the platform is in the allowed list
|
||||
return pageConfig.platforms.includes(platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a template file for a specific platform
|
||||
*/
|
||||
function processTemplateForPlatform(content, targetPlatform) {
|
||||
const lines = content.split('\n');
|
||||
let result = [];
|
||||
let currentPlatformSpec = null;
|
||||
let isIncluding = true;
|
||||
let platformSection = false;
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Check for platform start
|
||||
const startMatch = line.match(PLATFORM_START_MARKER);
|
||||
if (startMatch) {
|
||||
platformSection = true;
|
||||
currentPlatformSpec = startMatch[1];
|
||||
isIncluding = isPlatformMatch(currentPlatformSpec, targetPlatform);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for platform else
|
||||
const elseMatch = line.match(PLATFORM_ELSE_MARKER);
|
||||
if (elseMatch && platformSection) {
|
||||
currentPlatformSpec = elseMatch[1];
|
||||
isIncluding = isPlatformMatch(currentPlatformSpec, targetPlatform);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for platform end
|
||||
const endMatch = line.match(PLATFORM_END_MARKER);
|
||||
if (endMatch && platformSection) {
|
||||
platformSection = false;
|
||||
isIncluding = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Include the line if we're supposed to
|
||||
if (isIncluding) {
|
||||
result.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
return result.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate meta.json files for Fumadocs navigation
|
||||
*/
|
||||
function generateMetaFiles() {
|
||||
// Process meta.json files for each platform from templates
|
||||
for (const platform of PLATFORMS) {
|
||||
const folderName = getFolderName(platform);
|
||||
const platformDisplayName = getPlatformDisplayName(platform);
|
||||
|
||||
// Find all meta.json files in the template directory
|
||||
const metaFiles = glob.sync('**/meta.json', { cwd: TEMPLATE_DIR });
|
||||
|
||||
for (const metaFile of metaFiles) {
|
||||
const srcPath = path.join(TEMPLATE_DIR, metaFile);
|
||||
const destPath = path.join(OUTPUT_BASE_DIR, folderName, metaFile);
|
||||
|
||||
// If this is a nested meta.json (not root), check if the folder should exist for this platform
|
||||
if (metaFile !== 'meta.json') {
|
||||
const folderPath = path.dirname(metaFile);
|
||||
|
||||
// Check if any pages in this folder are included for this platform
|
||||
const hasContentInFolder = platformConfig.pages && platformConfig.pages.some(configPage =>
|
||||
configPage.path.startsWith(`${folderPath}/`) &&
|
||||
configPage.platforms.includes(platform)
|
||||
);
|
||||
|
||||
if (!hasContentInFolder) {
|
||||
console.log(`Skipped meta.json for ${folderPath} (no content for ${platform})`);
|
||||
continue; // Skip this meta.json file
|
||||
}
|
||||
}
|
||||
|
||||
// Read and parse the template meta.json
|
||||
const templateContent = fs.readFileSync(srcPath, 'utf8');
|
||||
const metaData = JSON.parse(templateContent);
|
||||
|
||||
// If this is the root meta.json, customize it for the platform
|
||||
if (metaFile === 'meta.json') {
|
||||
metaData.title = platformDisplayName;
|
||||
metaData.description = `Stack Auth for ${platformDisplayName} applications`;
|
||||
metaData.root = true;
|
||||
|
||||
// Filter pages based on platform configuration
|
||||
if (platformConfig.pages && metaData.pages) {
|
||||
const cleanedPages = [];
|
||||
let currentSectionPages = [];
|
||||
let currentSectionHeader = null;
|
||||
|
||||
for (let i = 0; i < metaData.pages.length; i++) {
|
||||
const page = metaData.pages[i];
|
||||
|
||||
// If this is a section divider
|
||||
if (typeof page === 'string' && page.startsWith('---')) {
|
||||
// Process the previous section first (or pages before first section)
|
||||
if (currentSectionPages.length > 0) {
|
||||
if (currentSectionHeader !== null) {
|
||||
// Add section header if we had one
|
||||
cleanedPages.push(currentSectionHeader);
|
||||
}
|
||||
cleanedPages.push(...currentSectionPages);
|
||||
}
|
||||
|
||||
// Start new section
|
||||
currentSectionHeader = page;
|
||||
currentSectionPages = [];
|
||||
}
|
||||
// If this is a folder reference (like "...customization")
|
||||
else if (typeof page === 'string' && page.startsWith('...')) {
|
||||
// Only include folder references if they have content for this platform
|
||||
const folderName = page.substring(3); // Remove "..."
|
||||
const hasContentInFolder = platformConfig.pages.some(configPage =>
|
||||
configPage.path.startsWith(`${folderName}/`) &&
|
||||
configPage.platforms.includes(platform)
|
||||
);
|
||||
|
||||
if (hasContentInFolder) {
|
||||
currentSectionPages.push(page);
|
||||
}
|
||||
}
|
||||
// Regular page
|
||||
else {
|
||||
const pagePath = `${page}.mdx`;
|
||||
if (shouldIncludeFileForPlatform(platform, pagePath)) {
|
||||
currentSectionPages.push(page);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't forget the last section (or remaining pages)
|
||||
if (currentSectionPages.length > 0) {
|
||||
if (currentSectionHeader !== null) {
|
||||
cleanedPages.push(currentSectionHeader);
|
||||
}
|
||||
cleanedPages.push(...currentSectionPages);
|
||||
}
|
||||
|
||||
metaData.pages = cleanedPages;
|
||||
}
|
||||
}
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
||||
|
||||
// Write the processed meta.json
|
||||
fs.writeFileSync(destPath, JSON.stringify(metaData, null, 2));
|
||||
console.log(`Generated platform-specific meta.json for ${platform}: ${destPath}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy assets from template to platform-specific directories
|
||||
*/
|
||||
function copyAssets() {
|
||||
const assetDirs = ['imgs'];
|
||||
|
||||
for (const dir of assetDirs) {
|
||||
const srcDir = path.join(TEMPLATE_DIR, dir);
|
||||
|
||||
if (fs.existsSync(srcDir)) {
|
||||
// Copy assets to each platform directory
|
||||
for (const platform of PLATFORMS) {
|
||||
const folderName = getFolderName(platform);
|
||||
const destDir = path.join(OUTPUT_BASE_DIR, folderName, dir);
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
|
||||
// Find and copy all files
|
||||
const files = glob.sync('**/*', { cwd: srcDir, nodir: true });
|
||||
for (const file of files) {
|
||||
const srcFile = path.join(srcDir, file);
|
||||
const destFile = path.join(destDir, file);
|
||||
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
||||
fs.copyFileSync(srcFile, destFile);
|
||||
console.log(`Copied asset: ${srcFile} -> ${destFile}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main function to generate platform-specific docs
|
||||
*/
|
||||
function generateDocs() {
|
||||
// Find all MDX files in the template directory
|
||||
const templateFiles = glob.sync('**/*.mdx', { cwd: TEMPLATE_DIR });
|
||||
|
||||
if (templateFiles.length === 0) {
|
||||
console.warn(`No template files found in ${TEMPLATE_DIR}`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Found ${templateFiles.length} template files`);
|
||||
|
||||
// Process for each platform
|
||||
for (const platform of PLATFORMS) {
|
||||
const folderName = getFolderName(platform);
|
||||
const outputDir = path.join(OUTPUT_BASE_DIR, folderName);
|
||||
|
||||
// Create the output directory
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
|
||||
// Process each template file
|
||||
for (const file of templateFiles) {
|
||||
// Check if this file should be included for this platform
|
||||
if (!shouldIncludeFileForPlatform(platform, file)) {
|
||||
console.log(`Skipped file (not configured for platform): ${file} for ${platform}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const inputFile = path.join(TEMPLATE_DIR, file);
|
||||
const outputFile = path.join(outputDir, file);
|
||||
|
||||
// Read the template
|
||||
const templateContent = fs.readFileSync(inputFile, 'utf8');
|
||||
|
||||
// Process for this platform
|
||||
const processedContent = processTemplateForPlatform(templateContent, platform);
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
|
||||
|
||||
// Write the processed content
|
||||
fs.writeFileSync(outputFile, processedContent);
|
||||
|
||||
console.log(`Generated: ${outputFile}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate meta.json files for navigation
|
||||
generateMetaFiles();
|
||||
|
||||
// Copy assets (images, etc.)
|
||||
copyAssets();
|
||||
|
||||
console.log('Documentation generation complete!');
|
||||
}
|
||||
|
||||
// Run the generator
|
||||
generateDocs();
|
||||
@@ -0,0 +1,536 @@
|
||||
import fs from 'fs';
|
||||
import { generateFiles } from 'fumadocs-openapi';
|
||||
import path from 'path';
|
||||
|
||||
// Use relative paths to avoid path duplication issues
|
||||
const OPENAPI_DIR = './openapi';
|
||||
const OUTPUT_DIR = './content/api';
|
||||
const TEMPLATES_API_DIR = './templates-api';
|
||||
|
||||
// Define the functional tag order based on user requirements
|
||||
const FUNCTIONAL_TAGS = [
|
||||
'Anonymous',
|
||||
'API Keys',
|
||||
'CLI Authentication',
|
||||
'Contact Channels',
|
||||
'Oauth', // Note: OpenAPI uses "Oauth" not "OAuth"
|
||||
'OTP',
|
||||
'Password',
|
||||
'Permissions',
|
||||
'Projects',
|
||||
'Sessions',
|
||||
'Teams',
|
||||
'Users',
|
||||
'Others' // For any miscellaneous endpoints
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a filtered OpenAPI spec containing only endpoints with the specified tag
|
||||
*/
|
||||
function createTagFilteredSpec(originalSpec, targetTag) {
|
||||
const filteredSpec = {
|
||||
...originalSpec,
|
||||
paths: {},
|
||||
webhooks: {}
|
||||
};
|
||||
|
||||
// Filter regular API paths
|
||||
if (originalSpec.paths) {
|
||||
for (const [path, methods] of Object.entries(originalSpec.paths)) {
|
||||
const filteredMethods = {};
|
||||
|
||||
for (const [method, endpoint] of Object.entries(methods)) {
|
||||
if (endpoint.tags && endpoint.tags.includes(targetTag)) {
|
||||
filteredMethods[method] = endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
// Only include the path if it has methods with the target tag
|
||||
if (Object.keys(filteredMethods).length > 0) {
|
||||
filteredSpec.paths[path] = filteredMethods;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter webhooks
|
||||
if (originalSpec.webhooks) {
|
||||
for (const [webhookName, methods] of Object.entries(originalSpec.webhooks)) {
|
||||
const filteredMethods = {};
|
||||
|
||||
for (const [method, endpoint] of Object.entries(methods)) {
|
||||
if (endpoint.tags && endpoint.tags.includes(targetTag)) {
|
||||
filteredMethods[method] = endpoint;
|
||||
}
|
||||
}
|
||||
|
||||
// Only include the webhook if it has methods with the target tag
|
||||
if (Object.keys(filteredMethods).length > 0) {
|
||||
filteredSpec.webhooks[webhookName] = filteredMethods;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredSpec;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all unique tags from an OpenAPI spec
|
||||
*/
|
||||
function extractTags(spec) {
|
||||
const tags = new Set();
|
||||
|
||||
// Handle regular API paths
|
||||
if (spec.paths) {
|
||||
for (const methods of Object.values(spec.paths)) {
|
||||
for (const endpoint of Object.values(methods)) {
|
||||
if (endpoint.tags) {
|
||||
endpoint.tags.forEach(tag => tags.add(tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle webhooks (different structure)
|
||||
if (spec.webhooks) {
|
||||
for (const webhookMethods of Object.values(spec.webhooks)) {
|
||||
for (const endpoint of Object.values(webhookMethods)) {
|
||||
if (endpoint.tags) {
|
||||
endpoint.tags.forEach(tag => tags.add(tag));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert tag name to a URL-friendly slug
|
||||
*/
|
||||
function tagToSlug(tag) {
|
||||
return tag.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/[^a-z0-9-]/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert tag name to a readable folder name
|
||||
*/
|
||||
function tagToFolderName(tag) {
|
||||
// Special case mappings
|
||||
const specialCases = {
|
||||
'Oauth': 'oauth',
|
||||
'API Keys': 'api-keys',
|
||||
'CLI Authentication': 'cli-authentication',
|
||||
'Contact Channels': 'contact-channels',
|
||||
'OTP': 'otp'
|
||||
};
|
||||
|
||||
return specialCases[tag] || tag.toLowerCase().replace(/\s+/g, '-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively find all MDX files in a directory
|
||||
*/
|
||||
function findMdxFiles(dir) {
|
||||
const files = [];
|
||||
|
||||
if (!fs.existsSync(dir)) {
|
||||
return files;
|
||||
}
|
||||
|
||||
const items = fs.readdirSync(dir);
|
||||
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dir, item);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
files.push(...findMdxFiles(fullPath));
|
||||
} else if (item.endsWith('.mdx')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten the generated file structure
|
||||
*/
|
||||
function flattenGeneratedFiles(functionalCategoryPath) {
|
||||
// Skip flattening - fumadocs expects the directory structure to match routing
|
||||
// The original nested structure from fumadocs-openapi is what we want to keep
|
||||
console.log(` 📁 Keeping original directory structure for ${functionalCategoryPath}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update document references in MDX files to point to permanent filtered OpenAPI files
|
||||
*/
|
||||
function updateDocumentReferences(functionalCategoryPath, newDocumentPath) {
|
||||
const mdxFiles = findMdxFiles(functionalCategoryPath);
|
||||
|
||||
for (const filePath of mdxFiles) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Update the document reference in the APIPage component
|
||||
// Match both old public/openapi paths and temp files
|
||||
const updatedContent = content.replace(
|
||||
/document=\{"(public\/openapi\/|openapi\/)[^"]+"\}/g,
|
||||
`document={"${newDocumentPath}"}`
|
||||
);
|
||||
|
||||
if (content !== updatedContent) {
|
||||
fs.writeFileSync(filePath, updatedContent);
|
||||
console.log(` 🔗 Updated document reference in ${path.basename(filePath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace APIPage with EnhancedAPIPage in generated MDX files
|
||||
*/
|
||||
function replaceAPIPageWithEnhanced(functionalCategoryPath) {
|
||||
const mdxFiles = findMdxFiles(functionalCategoryPath);
|
||||
|
||||
for (const filePath of mdxFiles) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Replace APIPage with EnhancedAPIPage
|
||||
const updatedContent = content.replace(
|
||||
/<APIPage\s+/g,
|
||||
'<EnhancedAPIPage '
|
||||
);
|
||||
|
||||
if (content !== updatedContent) {
|
||||
fs.writeFileSync(filePath, updatedContent);
|
||||
console.log(` 🔄 Replaced APIPage with EnhancedAPIPage in ${path.basename(filePath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace APIPage with WebhooksAPIPage in generated MDX files for webhooks
|
||||
*/
|
||||
function replaceAPIPageWithWebhooks(functionalCategoryPath) {
|
||||
const mdxFiles = findMdxFiles(functionalCategoryPath);
|
||||
|
||||
for (const filePath of mdxFiles) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Replace APIPage with WebhooksAPIPage
|
||||
const updatedContent = content.replace(
|
||||
/<APIPage\s+/g,
|
||||
'<WebhooksAPIPage '
|
||||
);
|
||||
|
||||
if (content !== updatedContent) {
|
||||
fs.writeFileSync(filePath, updatedContent);
|
||||
console.log(` 🔄 Replaced APIPage with WebhooksAPIPage in ${path.basename(filePath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add description prop to EnhancedAPIPage components from frontmatter
|
||||
*/
|
||||
function addDescriptionToEnhancedAPIPage(functionalCategoryPath) {
|
||||
const mdxFiles = findMdxFiles(functionalCategoryPath);
|
||||
|
||||
for (const filePath of mdxFiles) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Extract description from frontmatter
|
||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!frontmatterMatch) continue;
|
||||
|
||||
const frontmatterContent = frontmatterMatch[1];
|
||||
let description = null;
|
||||
|
||||
// Handle multiline YAML descriptions (">-" or ">" syntax)
|
||||
if (frontmatterContent.includes('description: >')) {
|
||||
// Extract multiline description
|
||||
const multilineMatch = frontmatterContent.match(/description:\s*>-?\s*\n((?:\s{2,}.*\n?)*)/);
|
||||
if (multilineMatch) {
|
||||
// Join the indented lines and clean up
|
||||
description = multilineMatch[1]
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^\s{2,}/, '')) // Remove leading indentation
|
||||
.filter(line => line.trim()) // Remove empty lines
|
||||
.join(' ')
|
||||
.trim();
|
||||
}
|
||||
} else {
|
||||
// Handle single-line descriptions
|
||||
const singleLineMatch = frontmatterContent.match(/description:\s*['"]?([^'"]+?)['"]?\s*$/m);
|
||||
if (singleLineMatch) {
|
||||
description = singleLineMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (!description) continue;
|
||||
|
||||
// Add description prop to EnhancedAPIPage if not already present
|
||||
const updatedContent = content.replace(
|
||||
/(<EnhancedAPIPage[^>]*?)(\s+\/?>)/g,
|
||||
(match, componentStart, componentEnd) => {
|
||||
// Check if description prop already exists
|
||||
if (componentStart.includes('description=')) {
|
||||
return match;
|
||||
}
|
||||
// Add description prop
|
||||
return `${componentStart} description={"${description.replace(/"/g, '\\"')}"}${componentEnd}`;
|
||||
}
|
||||
);
|
||||
|
||||
if (content !== updatedContent) {
|
||||
fs.writeFileSync(filePath, updatedContent);
|
||||
console.log(` 📝 Added description prop to EnhancedAPIPage in ${path.basename(filePath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add description prop to WebhooksAPIPage components from frontmatter
|
||||
*/
|
||||
function addDescriptionToWebhooksAPIPage(functionalCategoryPath) {
|
||||
const mdxFiles = findMdxFiles(functionalCategoryPath);
|
||||
|
||||
for (const filePath of mdxFiles) {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
|
||||
// Extract description from frontmatter
|
||||
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (!frontmatterMatch) continue;
|
||||
|
||||
const frontmatterContent = frontmatterMatch[1];
|
||||
let description = null;
|
||||
|
||||
// Handle multiline YAML descriptions (">-" or ">" syntax)
|
||||
if (frontmatterContent.includes('description: >')) {
|
||||
// Extract multiline description
|
||||
const multilineMatch = frontmatterContent.match(/description:\s*>-?\s*\n((?:\s{2,}.*\n?)*)/);
|
||||
if (multilineMatch) {
|
||||
// Join the indented lines and clean up
|
||||
description = multilineMatch[1]
|
||||
.split('\n')
|
||||
.map(line => line.replace(/^\s{2,}/, '')) // Remove leading indentation
|
||||
.filter(line => line.trim()) // Remove empty lines
|
||||
.join(' ')
|
||||
.trim();
|
||||
}
|
||||
} else {
|
||||
// Handle single-line descriptions
|
||||
const singleLineMatch = frontmatterContent.match(/description:\s*['"]?([^'"]+?)['"]?\s*$/m);
|
||||
if (singleLineMatch) {
|
||||
description = singleLineMatch[1].trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (!description) continue;
|
||||
|
||||
// Add description prop to WebhooksAPIPage if not already present
|
||||
const updatedContent = content.replace(
|
||||
/(<WebhooksAPIPage[^>]*?)(\s+\/?>)/g,
|
||||
(match, componentStart, componentEnd) => {
|
||||
// Check if description prop already exists
|
||||
if (componentStart.includes('description=')) {
|
||||
return match;
|
||||
}
|
||||
// Add description prop
|
||||
return `${componentStart} description={"${description.replace(/"/g, '\\"')}"}${componentEnd}`;
|
||||
}
|
||||
);
|
||||
|
||||
if (content !== updatedContent) {
|
||||
fs.writeFileSync(filePath, updatedContent);
|
||||
console.log(` 📝 Added description prop to WebhooksAPIPage in ${path.basename(filePath)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the API overview page from template
|
||||
*/
|
||||
function copyAPIOverviewFromTemplate() {
|
||||
console.log('📄 Copying API overview page from template...');
|
||||
|
||||
const templatePath = path.join(TEMPLATES_API_DIR, 'overview.mdx');
|
||||
const outputPath = path.join(OUTPUT_DIR, 'overview.mdx');
|
||||
|
||||
if (!fs.existsSync(templatePath)) {
|
||||
console.error(`❌ Template file not found: ${templatePath}`);
|
||||
console.log(' Please create the template file first.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure output directory exists
|
||||
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||
|
||||
// Copy the template file
|
||||
fs.copyFileSync(templatePath, outputPath);
|
||||
console.log('✅ Copied API overview page from template');
|
||||
}
|
||||
|
||||
async function generateFunctionalAPIDocs() {
|
||||
console.log('🚀 Starting functional OpenAPI documentation generation...\n');
|
||||
|
||||
// Ensure the OpenAPI directory exists
|
||||
if (!fs.existsSync(OPENAPI_DIR)) {
|
||||
console.log('Creating OpenAPI directory...');
|
||||
fs.mkdirSync(OPENAPI_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Process each API type in complete isolation to avoid fumadocs conflicts
|
||||
const apiTypes = ['client', 'server', 'admin', 'webhooks'];
|
||||
|
||||
for (const apiType of apiTypes) {
|
||||
await processApiTypeInIsolation(apiType);
|
||||
}
|
||||
|
||||
// Copy API overview page from template
|
||||
copyAPIOverviewFromTemplate();
|
||||
|
||||
// Generate main API meta.json
|
||||
console.log('📁 Generating main API navigation...');
|
||||
const mainApiMetaPath = path.join(OUTPUT_DIR, 'meta.json');
|
||||
const mainApiMeta = {
|
||||
pages: [
|
||||
'overview',
|
||||
'client',
|
||||
'server',
|
||||
'admin',
|
||||
'webhooks'
|
||||
]
|
||||
};
|
||||
|
||||
fs.writeFileSync(mainApiMetaPath, JSON.stringify(mainApiMeta, null, 2));
|
||||
console.log('✅ Generated main API meta.json');
|
||||
|
||||
console.log('\n🎉 Functional OpenAPI documentation generation complete!');
|
||||
console.log(`📂 Documentation generated in: ${path.resolve(OUTPUT_DIR)}/`);
|
||||
console.log('\n📋 Structure:');
|
||||
console.log(' /overview.mdx');
|
||||
console.log(' /client/{functional-categories}/');
|
||||
console.log(' /server/{functional-categories}/');
|
||||
console.log(' /admin/{functional-categories}/');
|
||||
console.log(' /webhooks/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a single API type in complete isolation
|
||||
*/
|
||||
async function processApiTypeInIsolation(apiType) {
|
||||
const jsonFile = path.join(OPENAPI_DIR, `${apiType}.json`);
|
||||
|
||||
if (!fs.existsSync(jsonFile)) {
|
||||
console.log(`⚠️ OpenAPI file not found: ${jsonFile}`);
|
||||
console.log(` Run 'pnpm run generate-openapi-fumadocs' from the root to generate OpenAPI schemas first.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`🔄 Processing ${apiType} API in isolation...`);
|
||||
|
||||
// Read and parse the OpenAPI spec
|
||||
const originalSpec = JSON.parse(fs.readFileSync(jsonFile, 'utf8'));
|
||||
|
||||
// Extract all tags from this API
|
||||
const availableTags = extractTags(originalSpec);
|
||||
console.log(` 📋 Found tags: ${availableTags.join(', ')}`);
|
||||
|
||||
// Process each functional tag for this API type
|
||||
const processedTags = [];
|
||||
for (const tag of FUNCTIONAL_TAGS) {
|
||||
if (!availableTags.includes(tag)) {
|
||||
continue; // Skip tags not present in this API
|
||||
}
|
||||
|
||||
console.log(` 📄 Generating docs for ${tag}...`);
|
||||
|
||||
// Create filtered spec for this tag
|
||||
const tagFilteredSpec = createTagFilteredSpec(originalSpec, tag);
|
||||
|
||||
// Create temporary file for this tag's spec
|
||||
const tempSpecPath = path.join(OPENAPI_DIR, `temp-${apiType}-${tagToSlug(tag)}.json`);
|
||||
fs.writeFileSync(tempSpecPath, JSON.stringify(tagFilteredSpec, null, 2));
|
||||
|
||||
try {
|
||||
// Generate docs for this tag in isolation
|
||||
const outputPath = path.join(OUTPUT_DIR, apiType, tagToFolderName(tag));
|
||||
|
||||
// Create permanent filtered OpenAPI file
|
||||
const permanentSpecPath = path.join(OPENAPI_DIR, `${apiType}-${tagToSlug(tag)}.json`);
|
||||
fs.writeFileSync(permanentSpecPath, JSON.stringify(tagFilteredSpec, null, 2));
|
||||
|
||||
// Process this tag completely in isolation - no fumadocs state shared between calls
|
||||
await generateFiles({
|
||||
input: [tempSpecPath],
|
||||
output: outputPath,
|
||||
includeDescription: false,
|
||||
frontmatter: (title, description) => ({
|
||||
title,
|
||||
description,
|
||||
full: true, // Use full-width layout for API docs
|
||||
}),
|
||||
});
|
||||
|
||||
console.log(` ✅ Generated ${tag} docs for ${apiType}`);
|
||||
processedTags.push(tag);
|
||||
|
||||
// Keep original directory structure for proper fumadocs routing
|
||||
flattenGeneratedFiles(outputPath);
|
||||
|
||||
// Update document references in MDX files
|
||||
console.log(` 🔗 Updating document references for ${tag}...`);
|
||||
updateDocumentReferences(outputPath, `openapi/${apiType}-${tagToSlug(tag)}.json`);
|
||||
|
||||
// Replace APIPage with appropriate component based on API type
|
||||
if (apiType === 'webhooks') {
|
||||
console.log(` 🔄 Replacing APIPage with WebhooksAPIPage for ${tag}...`);
|
||||
replaceAPIPageWithWebhooks(outputPath);
|
||||
|
||||
// Add description prop to WebhooksAPIPage
|
||||
console.log(` 📝 Adding description prop to WebhooksAPIPage for ${tag}...`);
|
||||
addDescriptionToWebhooksAPIPage(outputPath);
|
||||
} else {
|
||||
console.log(` 🔄 Replacing APIPage with EnhancedAPIPage for ${tag}...`);
|
||||
replaceAPIPageWithEnhanced(outputPath);
|
||||
|
||||
// Add description prop to EnhancedAPIPage
|
||||
console.log(` 📝 Adding description prop to EnhancedAPIPage for ${tag}...`);
|
||||
addDescriptionToEnhancedAPIPage(outputPath);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(` ❌ Error generating ${tag} docs for ${apiType}:`, error);
|
||||
} finally {
|
||||
// Clean up temporary file
|
||||
if (fs.existsSync(tempSpecPath)) {
|
||||
fs.unlinkSync(tempSpecPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Generate meta.json for this API type
|
||||
if (processedTags.length > 0) {
|
||||
console.log(` 📁 Generating navigation meta for ${apiType}...`);
|
||||
|
||||
const apiMetaPath = path.join(OUTPUT_DIR, apiType, 'meta.json');
|
||||
const apiMeta = {
|
||||
pages: processedTags.map(tag => tagToFolderName(tag))
|
||||
};
|
||||
|
||||
fs.mkdirSync(path.dirname(apiMetaPath), { recursive: true });
|
||||
fs.writeFileSync(apiMetaPath, JSON.stringify(apiMeta, null, 2));
|
||||
|
||||
console.log(` ✅ Generated meta.json for ${apiType} API`);
|
||||
}
|
||||
|
||||
console.log(`🎯 Completed ${apiType} API processing (${processedTags.length} functional categories)\n`);
|
||||
}
|
||||
|
||||
// Run the generator
|
||||
generateFunctionalAPIDocs().catch((error) => {
|
||||
console.error('❌ Failed to generate functional API documentation:', error);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user