UNPKG

@knowcode/doc-builder

Version:

Reusable documentation builder for markdown-based sites with Vercel deployment support

526 lines (472 loc) 17.4 kB
const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const semver = require('semver'); const sharedAuth = require('./shared-auth-config'); /** * Default configuration */ const defaultConfig = { // Configuration version - updated when new config options are added configVersion: require('../package.json').version, // Source and output directories docsDir: 'docs', outputDir: 'html', // Site metadata siteName: 'Documentation', siteDescription: 'Documentation site built with @knowcode/doc-builder', // Favicon - can be an emoji or a path to an image file favicon: '✨', // Features features: { authentication: false, // false (no auth) or 'supabase' (secure auth) changelog: true, mermaid: true, mermaidEnhanced: true, // Enhanced Mermaid styling with rounded corners and better themes tooltips: true, search: false, darkMode: true, phosphorIcons: true, phosphorWeight: 'regular', // Options: thin, light, regular, bold, fill, duotone phosphorSize: '1.2em', // Relative to text size normalizeTitle: true, // Auto-normalize all-caps titles to title case showPdfDownload: true, // Show PDF download icon in header menuDefaultOpen: true, // Menu/sidebar open by default attachments: true, // Copy attachments (Excel, PDF, etc.) to output dynamicNavIcons: true, // Use status-based icons in navigation subtleColors: true, // Apply subtle colors to status icons privateDirectoryAuth: false, // Enable auth for private directories only (auto-detected) banner: false, // Show preview banner at top of pages staticOutput: true // Generate static version without auth (default: true) }, // Static output configuration staticOutputDir: 'html-static', // Directory for static HTML output (no auth, no private content) // Authentication - Supabase only (basic auth removed for security) auth: { supabaseUrl: sharedAuth.supabaseUrl, supabaseAnonKey: sharedAuth.supabaseAnonKey }, // Changelog settings changelog: { daysBack: 14, enabled: true }, // Banner configuration banner: { enabled: false, text: 'This documentation is a preview version - some content may be incomplete', icon: 'fas fa-exclamation-triangle', type: 'warning', // warning, info, success, error dismissible: true }, // Navigation configuration folderOrder: [], folderDescriptions: {}, folderIcons: {}, // Deployment deployment: { platform: 'vercel', outputDirectory: 'html' }, // SEO configuration seo: { enabled: true, siteUrl: '', // Required for proper SEO author: '', twitterHandle: '', language: 'en-US', keywords: [], titleTemplate: '{pageTitle} | {siteName}', // Customizable title format autoKeywords: true, // Extract keywords from content keywordLimit: 7, // Max keywords per page descriptionFallback: 'smart', // 'smart' or 'first-paragraph' organization: { name: '', url: '', logo: '' }, ogImage: '', // Default Open Graph image generateSitemap: true, generateRobotsTxt: true, customMetaTags: [] }, // Attachment file types to copy attachmentTypes: [ // Documents '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.csv', '.ppt', '.pptx', '.txt', '.rtf', // Web files '.html', '.htm', // Archives '.zip', '.tar', '.gz', '.7z', '.rar', // Images '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico', '.bmp', // Data files '.json', '.xml', '.yaml', '.yml', '.toml', // Other '.mp4', '.mp3', '.wav', '.avi', '.mov' ] }; /** * Notion-inspired preset - clean, modern documentation style */ const notionInspiredPreset = { docsDir: 'docs', outputDir: 'html', siteName: 'Documentation', // Clean default name siteDescription: 'Transforming complex sales through intelligent automation', features: { authentication: false, // Default to no auth - can be enabled with 'supabase' changelog: true, mermaid: true, mermaidEnhanced: true, // Enhanced Mermaid styling with rounded corners and better themes tooltips: true, search: false, darkMode: true, phosphorIcons: true, phosphorWeight: 'regular', phosphorSize: '1.2em', normalizeTitle: true, showPdfDownload: true, menuDefaultOpen: true, attachments: true, banner: false, // Show preview banner at top of pages staticOutput: true // Generate static version without auth (default: true) }, staticOutputDir: 'html-static', // Directory for static HTML output auth: { supabaseUrl: process.env.SUPABASE_URL || sharedAuth.supabaseUrl, supabaseAnonKey: process.env.SUPABASE_ANON_KEY || sharedAuth.supabaseAnonKey }, changelog: { daysBack: 14, enabled: true }, // Banner configuration banner: { enabled: false, text: 'This documentation is a preview version - some content may be incomplete', icon: 'fas fa-exclamation-triangle', type: 'warning', dismissible: true }, // Folder descriptions from existing code folderDescriptions: { 'product-roadmap': 'Strategic vision, timeline, and feature planning', 'product-requirements': 'Detailed product specifications, requirements documents, and feature definitions', 'architecture': 'System design, data flows, and technical infrastructure documentation', 'system-analysis': 'Comprehensive system analysis, functional requirements, and cross-component documentation', 'bubble': 'Core application platform - business logic, UI/UX, and user workflows', 'quickbase': 'Database schema, data management, and backend operations', 'activecampaign': 'Marketing automation integration and lead management system', 'juno-signer': 'Document signing service for digital signatures and PDF generation', 'juno-api-deprecated': 'Legacy API documentation (deprecated, for reference only)', 'postman': 'API testing tools, collections, and test automation', 'mcp': 'Model Context Protocol setup and configuration guides', 'team': 'Team roles, responsibilities, and task assignments', 'thought-leadership': 'Strategic thinking, industry insights, and future vision', 'paths': 'Dynamic pathway routing system for insurance applications', 'testing': 'Testing procedures, checklists, and quality assurance', 'technical': 'Technical documentation and implementation details', 'application': 'Application components, data types, and workflows' }, folderIcons: { 'root': 'fas fa-home', 'product-roadmap': 'fas fa-road', 'product-requirements': 'fas fa-clipboard-list', 'architecture': 'fas fa-sitemap', 'system-analysis': 'fas fa-analytics', 'system': 'fas fa-cogs', 'bubble': 'fas fa-bubble', 'quickbase': 'fas fa-database', 'activecampaign': 'fas fa-envelope', 'juno-signer': 'fas fa-signature', 'juno-api-deprecated': 'fas fa-archive', 'postman': 'fas fa-flask', 'mcp': 'fas fa-puzzle-piece', 'team': 'fas fa-users', 'thought-leadership': 'fas fa-lightbulb', 'middleware': 'fas fa-layer-group', 'paths': 'fas fa-route', 'testing': 'fas fa-vial', 'juno-api': 'fas fa-plug' }, folderOrder: [ 'product-roadmap', 'product-requirements', 'architecture', 'system-analysis', 'bubble', 'quickbase', 'activecampaign', 'juno-signer', 'juno-api-deprecated', 'postman', 'mcp', 'team', 'thought-leadership' ], // SEO configuration for notion preset seo: { enabled: true, siteUrl: '', author: '', twitterHandle: '', language: 'en-US', keywords: ['documentation', 'api', 'guide'], organization: { name: '', url: '', logo: '' }, ogImage: '', generateSitemap: true, generateRobotsTxt: true, customMetaTags: [] } }; /** * Check if config migration is needed * @param {object} userConfig - Current user configuration * @param {string} packageVersion - Current package version * @returns {boolean} - Whether migration is needed */ function needsMigration(userConfig, packageVersion) { // If no configVersion in user config, it needs migration if (!userConfig.configVersion) { return true; } try { // Use semver to compare versions return semver.lt(userConfig.configVersion, packageVersion); } catch (error) { console.warn(chalk.yellow(`Warning: Invalid version format in config: ${userConfig.configVersion}`)); return true; // Migrate on version parse errors } } /** * Deep merge objects, preserving user customizations * @param {object} target - Target object (defaults) * @param {object} source - Source object (user config) * @returns {object} - Merged object */ function deepMerge(target, source) { const result = { ...target }; for (const key in source) { if (source.hasOwnProperty(key)) { if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) { // Recursively merge objects result[key] = deepMerge(target[key] || {}, source[key]); } else { // Use source value (user customization takes precedence) result[key] = source[key]; } } } return result; } /** * Migrate user configuration to current version * @param {object} userConfig - Current user configuration * @param {string} targetVersion - Target package version * @returns {object} - Migrated configuration */ function migrateConfig(userConfig, targetVersion) { console.log(chalk.blue(`🔄 Migrating config from ${userConfig.configVersion || 'unknown'} to ${targetVersion}`)); // Start with current defaults let migratedConfig = { ...defaultConfig }; // Deep merge user customizations migratedConfig = deepMerge(migratedConfig, userConfig); // Update the config version migratedConfig.configVersion = targetVersion; // Log new features added const newFeatures = []; if (!userConfig.features?.dynamicNavIcons) { newFeatures.push('Dynamic navigation icons'); } if (!userConfig.features?.subtleColors) { newFeatures.push('Subtle status colors'); } if (!userConfig.features?.phosphorWeight) { newFeatures.push('Phosphor icon weight options'); } if (!userConfig.features?.phosphorSize) { newFeatures.push('Phosphor icon sizing'); } if (userConfig.features?.banner === undefined) { newFeatures.push('Configurable preview banner'); } if (userConfig.features?.mermaidEnhanced === undefined) { newFeatures.push('Enhanced Mermaid diagram styling'); } if (newFeatures.length > 0) { console.log(chalk.green(`✨ Added new features: ${newFeatures.join(', ')}`)); } return migratedConfig; } /** * Save configuration to file * @param {string} configPath - Path to config file * @param {object} config - Configuration object to save */ async function saveConfig(configPath, config) { // Create backup of existing config if (fs.existsSync(configPath)) { const backupPath = `${configPath}.backup.${Date.now()}`; await fs.copy(configPath, backupPath); console.log(chalk.gray(`💾 Backed up existing config to ${path.basename(backupPath)}`)); } // Format config as module.exports const configContent = `module.exports = ${JSON.stringify(config, null, 2)}; `; await fs.writeFile(configPath, configContent); console.log(chalk.green(`✅ Updated config file: ${path.basename(configPath)}`)); } /** * Load configuration */ async function loadConfig(configPath, options = {}) { let config = { ...defaultConfig }; // Apply preset if specified if (options.preset === 'notion-inspired') { config = { ...config, ...notionInspiredPreset }; } // Load custom config file if it exists const customConfigPath = path.join(process.cwd(), configPath); if (fs.existsSync(customConfigPath)) { try { const customConfig = require(customConfigPath); const packageJson = require('../package.json'); // Check if migration is needed if (needsMigration(customConfig, packageJson.version)) { config = migrateConfig(customConfig, packageJson.version); await saveConfig(customConfigPath, config); } else { config = { ...config, ...customConfig }; } // Handle alternative config formats if (customConfig.site) { // Map site.title to siteName if (customConfig.site.title) { config.siteName = customConfig.site.title; console.log(chalk.gray(` Using site name: ${config.siteName}`)); } if (customConfig.site.description) { config.siteDescription = customConfig.site.description; } } // Map input/output to docsDir/outputDir if (customConfig.input) { config.docsDir = customConfig.input; } if (customConfig.output) { config.outputDir = customConfig.output; } console.log(chalk.gray(`Loaded config from ${configPath}`)); } catch (error) { console.warn(chalk.yellow(`Warning: Failed to load config file: ${error.message}`)); } } else if (!options.preset && !options.legacy) { console.log(chalk.gray('No config file found, creating default config...')); // Create default config file with all settings await saveConfig(customConfigPath, config); console.log(chalk.green(`✅ Created ${configPath} with default settings`)); } // Apply CLI options (these override config file) if (options.input) { config.docsDir = options.input; } if (options.output) { config.outputDir = options.output; } // Note: We don't apply auth=false here anymore because private directory detection // should always override to ensure security if (options.changelog === false) { config.features.changelog = false; config.changelog.enabled = false; } if (options.pdf === false) { config.features.showPdfDownload = false; } if (options.menuClosed === true) { config.features.menuDefaultOpen = false; } if (options.attachments === false) { config.features.attachments = false; } // Legacy mode - auto-detect structure if (options.legacy) { const docsPath = path.join(process.cwd(), 'docs'); const htmlPath = path.join(process.cwd(), 'html'); if (fs.existsSync(docsPath) && fs.existsSync(htmlPath)) { console.log(chalk.gray('Legacy mode: detected docs/ and html/ structure')); config.docsDir = 'docs'; config.outputDir = 'html'; } } // Validate paths - but be lenient for commands that will create directories const docsPath = path.join(process.cwd(), config.docsDir); if (!fs.existsSync(docsPath)) { // Only show warning for commands that don't auto-create the directory // The build command will create it automatically if (!options || !['build', 'dev', 'deploy'].includes(options.command)) { console.warn(chalk.yellow(`Warning: Documentation directory not found: ${config.docsDir}`)); console.log(chalk.gray(`Create it with: mkdir ${config.docsDir} && echo "# Documentation" > ${config.docsDir}/README.md`)); } // Don't throw error - let commands handle missing directories appropriately } else { // Check for private directory to inform user about authentication options const privatePath = path.join(docsPath, 'private'); if (fs.existsSync(privatePath) && fs.statSync(privatePath).isDirectory()) { if (config.features.authentication === 'supabase') { console.log(chalk.blue('🔐 Found private directory - global Supabase authentication is enabled')); console.log(chalk.yellow(' Note: Grant users access by adding domain to the docbuilder_access table')); } else { console.log(chalk.blue('🔐 Found private directory - will be protected with Supabase authentication')); console.log(chalk.gray(' Public pages remain accessible, only /private/ pages require login')); console.log(chalk.yellow(' Note: Grant users access by adding domain to the docbuilder_access table')); // Set a flag to indicate private directory authentication is needed config.features.privateDirectoryAuth = true; } } } return config; } /** * Create default configuration */ async function createDefaultConfig() { const answers = await require('prompts')([ { type: 'text', name: 'siteName', message: 'Site name:', initial: 'My Documentation' }, { type: 'text', name: 'siteDescription', message: 'Site description:', initial: 'Documentation for my project' }, { type: 'confirm', name: 'authentication', message: 'Enable Supabase authentication?', initial: false } ]); const config = { ...defaultConfig }; config.siteName = answers.siteName; config.siteDescription = answers.siteDescription; config.features.authentication = answers.authentication ? 'supabase' : false; return config; } module.exports = { defaultConfig, notionInspiredPreset, loadConfig, createDefaultConfig, needsMigration, migrateConfig, saveConfig };