UNPKG

@voilajsx/uikit

Version:

Cross-platform React components with beautiful themes and OKLCH color science

419 lines (356 loc) • 10.8 kB
#!/usr/bin/env node /** * @fileoverview voila-bundle CLI - Theme bundling tool * @description Bundles multiple theme presets into unified CSS for consumer projects * @package @voilajsx/uikit * @file /bin/theme-bundler.js */ import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Create require function for loading CommonJS modules const require = createRequire(import.meta.url); // Consumer project theme directories to search const THEME_SEARCH_PATHS = [ 'src/themes', 'src/themes/presets', 'themes', 'themes/presets', 'src/styles/themes', 'styles/themes', ]; // CLI Arguments const args = process.argv.slice(2); const outputFile = args.find((arg) => arg.startsWith('--output='))?.split('=')[1] || 'src/themes.css'; const watch = args.includes('--watch'); const verbose = args.includes('--verbose') || args.includes('-v'); const help = args.includes('--help') || args.includes('-h'); /** * Show CLI help */ function showHelp() { console.log(` šŸŽØ voila-bundle - Theme Bundler for @voilajsx/uikit USAGE: npx voila-bundle [options] OPTIONS: --output=<path> Output file path (default: src/themes.css) --watch Watch for changes and rebuild automatically --verbose, -v Verbose logging --help, -h Show this help EXAMPLES: npx voila-bundle npx voila-bundle --output=public/themes.css npx voila-bundle --watch --verbose THEME STRUCTURE: Your theme files should export a default object with this structure: export default { name: 'My Theme', id: 'my-theme', light: { background: 'oklch(1 0 0)', foreground: 'oklch(0.2 0.1 250)', // ... more colors }, dark: { background: 'oklch(0.1 0.1 250)', foreground: 'oklch(0.9 0.1 250)', // ... more colors } } SEARCH PATHS: voila-bundle will search these directories for theme files: ${THEME_SEARCH_PATHS.map((p) => ` - ${p}/`).join('\n')} `); } /** * Log with optional verbose mode */ function log(message, isVerbose = false) { if (!isVerbose || verbose) { console.log(message); } } /** * Generate CSS for a single theme */ function generateThemeCSS(theme) { if (!theme || !theme.id || !theme.light || !theme.dark) { console.warn( `āš ļø Invalid theme structure for ${ theme?.id || 'unknown' } - missing required properties` ); return ''; } const lightVariables = Object.entries(theme.light) .map(([key, value]) => { const cssKey = `--color-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`; return ` ${cssKey}: ${value};`; }) .join('\n'); const darkVariables = Object.entries(theme.dark) .map(([key, value]) => { const cssKey = `--color-${key.replace(/([A-Z])/g, '-$1').toLowerCase()}`; return ` ${cssKey}: ${value};`; }) .join('\n'); return `/* ${theme.name} Theme */ .theme-${theme.id} { ${lightVariables} } .theme-${theme.id}.dark { ${darkVariables} }`; } /** * Load a single theme file with both ESM and CommonJS support */ async function loadThemeFile(themePath, fileName) { try { // Clear any cached version first delete require.cache[themePath]; let theme = null; // Try ES modules first try { const themeUrl = `file://${themePath.replace( /\\/g, '/' )}?timestamp=${Date.now()}`; const themeModule = await import(themeUrl); theme = themeModule.default || themeModule; } catch (esmError) { log(` ESM failed for ${fileName}: ${esmError.message}`, true); // Fall back to CommonJS try { theme = require(themePath); // Handle both module.exports = theme and module.exports.default = theme if (theme.default) { theme = theme.default; } } catch (cjsError) { log(` CJS failed for ${fileName}: ${cjsError.message}`, true); throw new Error( `Both ESM and CJS import failed. ESM: ${esmError.message}, CJS: ${cjsError.message}` ); } } return theme; } catch (err) { throw new Error(`Failed to load ${fileName}: ${err.message}`); } } /** * Find and load themes from consumer project */ async function loadThemes() { const themes = []; const cwd = process.cwd(); const foundFiles = []; log('šŸ” Searching for theme files...', true); for (const themesDir of THEME_SEARCH_PATHS) { try { const fullPath = path.resolve(cwd, themesDir); if (!fs.existsSync(fullPath)) { log(` āŒ ${themesDir}/ - not found`, true); continue; } const files = fs.readdirSync(fullPath).filter( (file) => file.endsWith('.js') && !file.startsWith('index.') && !file.includes('theme-generator') // Skip build scripts ); if (files.length === 0) { log(` šŸ“ ${themesDir}/ - no theme files`, true); continue; } log( ` šŸ“ ${themesDir}/ - found ${files.length} files: ${files.join( ', ' )}`, true ); for (const file of files) { try { const themePath = path.resolve(fullPath, file); log(` šŸ”„ Loading ${file}...`, true); const theme = await loadThemeFile(themePath, file); if (theme && theme.id && theme.name && theme.light && theme.dark) { themes.push(theme); foundFiles.push(`${themesDir}/${file}`); log(` āœ… ${theme.name} (${theme.id})`, true); } else { console.warn(` āš ļø ${file} - invalid theme structure`); log(` Expected: { id, name, light: {}, dark: {} }`, true); log( ` Got: ${JSON.stringify( Object.keys(theme || {}), null, 2 )}`, true ); } } catch (err) { console.warn(` āŒ ${file} - ${err.message}`); log(` Full error: ${err.stack}`, true); } } } catch (err) { log(` āŒ ${themesDir}/ - error: ${err.message}`, true); } } return { themes, foundFiles }; } /** * Bundle themes into CSS */ async function bundleThemes() { const startTime = Date.now(); try { const { themes, foundFiles } = await loadThemes(); if (themes.length === 0) { console.log('āŒ No valid themes found!'); console.log( '\nšŸ“ Make sure your theme files are in one of these directories:' ); THEME_SEARCH_PATHS.forEach((dir) => console.log(` - ${dir}/`)); console.log('\nšŸ’” Theme files should export a default object like:'); console.log( ' export default { id: "my-theme", name: "My Theme", light: {...}, dark: {...} }' ); console.log('\nšŸ”§ Run with --verbose to see detailed loading attempts'); return false; } // Generate CSS const themeCSS = themes.map(generateThemeCSS).filter(Boolean).join('\n\n'); if (!themeCSS) { console.log('āŒ No valid CSS generated from themes'); return false; } // Create output content const outputContent = `/* * šŸŽØ Bundled Themes for @voilajsx/uikit * * Auto-generated by voila-bundle * Generated: ${new Date().toISOString()} * Themes: ${themes.length} * * DO NOT EDIT THIS FILE DIRECTLY * Edit your theme source files and run "npx voila-bundle" to regenerate */ ${themeCSS} `; // Ensure output directory exists const outputPath = path.resolve(process.cwd(), outputFile); const outputDir = path.dirname(outputPath); if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); log(`šŸ“ Created directory: ${outputDir}`, true); } // Write the file fs.writeFileSync(outputPath, outputContent); const duration = Date.now() - startTime; console.log( `\nāœ… Successfully bundled ${themes.length} themes in ${duration}ms` ); themes.forEach((theme) => console.log(` šŸŽØ ${theme.name} (${theme.id})`)); console.log(`\nšŸ“¦ Output: ${outputPath}`); console.log(`šŸ“„ Size: ${(outputContent.length / 1024).toFixed(1)}kb`); if (!watch) { console.log('\nšŸ’” Import in your app: import "./themes.css"'); } return true; } catch (err) { console.error('āŒ Bundle failed:', err.message); if (verbose) { console.error(err.stack); } return false; } } /** * Watch mode - rebuild on file changes */ async function watchMode() { console.log('šŸ‘€ Watch mode enabled - monitoring theme files...\n'); let timeout; const debounceMs = 300; const rebuild = () => { clearTimeout(timeout); timeout = setTimeout(async () => { console.log('\nšŸ”„ Change detected, rebuilding...'); await bundleThemes(); console.log('šŸ‘€ Watching for changes...\n'); }, debounceMs); }; // Initial build const success = await bundleThemes(); if (!success) return; // Watch all theme directories const cwd = process.cwd(); const watchers = []; for (const themesDir of THEME_SEARCH_PATHS) { const fullPath = path.resolve(cwd, themesDir); if (fs.existsSync(fullPath)) { try { const watcher = fs.watch( fullPath, { recursive: true }, (eventType, filename) => { if ( filename && filename.endsWith('.js') && !filename.includes('theme-generator') ) { log(`šŸ“ ${eventType}: ${themesDir}/${filename}`, true); rebuild(); } } ); watchers.push(watcher); log(`šŸ‘€ Watching: ${themesDir}/`, true); } catch (err) { console.warn(`āš ļø Could not watch ${themesDir}/: ${err.message}`); } } } if (watchers.length === 0) { console.log('āŒ No directories to watch'); return; } console.log('šŸ‘€ Watching for changes... (Press Ctrl+C to stop)\n'); // Handle graceful shutdown process.on('SIGINT', () => { console.log('\nšŸ›‘ Stopping watch mode...'); watchers.forEach((watcher) => watcher.close()); process.exit(0); }); } /** * Main CLI function */ async function main() { if (help) { showHelp(); return; } console.log('šŸŽØ voila-bundle - Theme Bundler for @voilajsx/uikit\n'); if (watch) { await watchMode(); } else { const success = await bundleThemes(); process.exit(success ? 0 : 1); } } // Run CLI main().catch((err) => { console.error('šŸ’„ Unexpected error:', err); process.exit(1); });