@knowcode/doc-builder
Version:
Reusable documentation builder for markdown-based sites with Vercel deployment support
526 lines (472 loc) • 17.4 kB
JavaScript
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
};