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:
Madison
2025-06-20 13:30:01 -07:00
committed by GitHub
co-authored by Konsti Wohlwend Madison Kennedy BilalG1
parent 8ed08ed6b3
commit 4e467c4026
246 changed files with 26524 additions and 7144 deletions
+58
View File
@@ -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();
+340
View File
@@ -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);
});