stack/docs/scripts/generate-docs.js
Madison a5734defba
Adds oauth providers, fixes bottom page navigation with mobile suppor… (#726)
<!--

Fixes generation script, adds new oauth docs pages, fixes bottom
navigation, adds mobile support, sidebar changes.

-->

<!-- ELLIPSIS_HIDDEN -->


----

> [!IMPORTANT]
> This PR adds OAuth provider documentation, enhances mobile navigation,
and updates Python-specific documentation for Stack Auth.
> 
>   - **OAuth Providers**:
> - Adds documentation for GitHub, Google, Facebook, Microsoft, Spotify,
Discord, GitLab, Apple, Bitbucket, LinkedIn, and X (Twitter) in
`docs/templates/concepts/auth-providers/`.
> - Updates `docs/docs-platform.yml` to include new OAuth provider
pages.
>   - **Mobile Support**:
> - Enhances bottom navigation for mobile devices in
`docs/src/app/(home)/layout.tsx` and `docs/src/app/api/layout.tsx`.
> - Introduces `AIChatDrawer` and `AuthPanel` components for
mobile-friendly interactions.
>   - **Documentation Enhancements**:
> - Adds Python-specific documentation for user authentication and team
management in `docs/templates-python/concepts/`.
> - Updates `docs/templates-python/meta.json` to include new Python
documentation pages.
> - Refines search functionality and UI components for better user
experience.
> 
> <sup>This description was created by </sup>[<img alt="Ellipsis"
src="https://img.shields.io/badge/Ellipsis-blue?color=175173">](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral)<sup>
for bf759151d8. You can
[customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this
summary. It will automatically update as commits are pushed.</sup>


<!-- ELLIPSIS_HIDDEN -->

---------

Co-authored-by: Konsti Wohlwend <[email protected]>
2025-07-08 19:51:24 -07:00

452 lines
16 KiB
JavaScript

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 PYTHON_TEMPLATE_DIR = path.resolve(__dirname, '../templates-python');
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);
// For Python platform, prioritize Python-specific templates, but also include shared templates
const templateDir = (platform === 'python' && fs.existsSync(PYTHON_TEMPLATE_DIR)) ? PYTHON_TEMPLATE_DIR : TEMPLATE_DIR;
// Find all meta.json files in the appropriate template directory
const metaFiles = glob.sync('**/meta.json', { cwd: templateDir });
// For Python, also get meta.json files from shared templates (excluding root meta.json to avoid conflicts)
let sharedMetaFiles = [];
if (platform === 'python' && fs.existsSync(PYTHON_TEMPLATE_DIR)) {
sharedMetaFiles = glob.sync('**/meta.json', { cwd: TEMPLATE_DIR }).filter(file => file !== 'meta.json');
}
// Process Python-specific meta files
for (const metaFile of metaFiles) {
const srcPath = path.join(templateDir, 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 {
// Check if this is actually a folder reference vs a page reference
// Check both template directories for Python
let folderPath = path.join(templateDir, page);
let isActualFolder = fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory();
// For Python, also check shared templates directory
if (!isActualFolder && platform === 'python') {
folderPath = path.join(TEMPLATE_DIR, page);
isActualFolder = fs.existsSync(folderPath) && fs.statSync(folderPath).isDirectory();
}
if (isActualFolder) {
// This is a folder reference - check if folder has content for this platform
const hasContentInFolder = platformConfig.pages.some(configPage =>
configPage.path.startsWith(`${page}/`) &&
configPage.platforms.includes(platform)
);
if (hasContentInFolder) {
currentSectionPages.push(page);
}
} else {
// This is a regular page reference
const pagePath = `${page}.mdx`;
const shouldInclude = shouldIncludeFileForPlatform(platform, pagePath);
if (shouldInclude) {
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}`);
}
// For Python, also process shared meta.json files (but not root)
for (const metaFile of sharedMetaFiles) {
const folderPath = path.dirname(metaFile);
// Check if any pages in this folder are included for Python
const hasContentInFolder = platformConfig.pages && platformConfig.pages.some(configPage =>
configPage.path.startsWith(`${folderPath}/`) &&
configPage.platforms.includes(platform)
);
if (hasContentInFolder) {
const srcPath = path.join(TEMPLATE_DIR, metaFile);
const destPath = path.join(OUTPUT_BASE_DIR, folderName, metaFile);
// Read and copy the shared meta.json
const templateContent = fs.readFileSync(srcPath, 'utf8');
// Create directory if it doesn't exist
fs.mkdirSync(path.dirname(destPath), { recursive: true });
// Write the shared meta.json
fs.writeFileSync(destPath, templateContent);
console.log(`Generated shared meta.json for ${platform}: ${destPath}`);
}
}
}
}
/**
* Copy assets from template to platform-specific directories
*/
function copyAssets() {
const assetDirs = ['imgs'];
// Copy assets from main templates directory
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}`);
}
}
}
}
// Copy Python-specific assets if they exist
if (fs.existsSync(PYTHON_TEMPLATE_DIR)) {
for (const dir of assetDirs) {
const srcDir = path.join(PYTHON_TEMPLATE_DIR, dir);
if (fs.existsSync(srcDir)) {
const destDir = path.join(OUTPUT_BASE_DIR, 'python', 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 Python-specific asset: ${srcFile} -> ${destFile}`);
}
}
}
}
}
/**
* Main function to generate platform-specific docs
*/
function generateDocs() {
// Find all MDX files in the main 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} shared template files`);
// Process shared templates 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 shared 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}`);
}
}
// Process Python-specific templates if they exist
if (fs.existsSync(PYTHON_TEMPLATE_DIR)) {
console.log(`Processing Python-specific templates from ${PYTHON_TEMPLATE_DIR}`);
const pythonTemplateFiles = glob.sync('**/*.mdx', { cwd: PYTHON_TEMPLATE_DIR });
if (pythonTemplateFiles.length > 0) {
const pythonOutputDir = path.join(OUTPUT_BASE_DIR, 'python');
for (const file of pythonTemplateFiles) {
const inputFile = path.join(PYTHON_TEMPLATE_DIR, file);
const outputFile = path.join(pythonOutputDir, file);
// Read the Python-specific template
const templateContent = fs.readFileSync(inputFile, 'utf8');
// Create output directory if it doesn't exist
fs.mkdirSync(path.dirname(outputFile), { recursive: true });
// Write the content (no platform processing needed for Python-specific files)
fs.writeFileSync(outputFile, templateContent);
console.log(`Generated Python-specific: ${outputFile}`);
}
}
}
// Generate meta.json files for navigation
generateMetaFiles();
// Copy assets (images, etc.)
copyAssets();
console.log('Documentation generation complete!');
}
// Run the generator
generateDocs();