UNPKG

muspe-cli

Version:

MusPE Advanced Framework v2.1.3 - Mobile User-friendly Simple Progressive Engine with Enhanced CLI Tools, Specialized E-Commerce Templates, Material Design 3, Progressive Enhancement, Mobile Optimizations, Performance Analysis, and Enterprise-Grade Develo

386 lines (311 loc) 11.8 kB
const fs = require('fs-extra'); const path = require('path'); const chalk = require('chalk'); const ora = require('ora'); const spawn = require('cross-spawn'); async function buildProject(options) { const projectRoot = findProjectRoot(); if (!projectRoot) { console.log(chalk.red('Not in a MusPE project directory')); return; } const config = await loadConfig(projectRoot); const outputDir = options.output || config.build?.outDir || 'dist'; const shouldMinify = options.minify || config.build?.minify || false; const shouldAnalyze = options.analyze || false; const spinner = ora('Building for production...').start(); try { const buildPath = path.join(projectRoot, outputDir); // Clean output directory await fs.remove(buildPath); await fs.ensureDir(buildPath); // Build steps await copyPublicFiles(projectRoot, buildPath); await buildCSS(projectRoot, buildPath, config, shouldMinify); await buildJS(projectRoot, buildPath, config, shouldMinify); await generateManifest(projectRoot, buildPath, config); if (config.pwa?.enabled) { await buildPWA(projectRoot, buildPath, config); } const stats = await getBuildStats(buildPath); spinner.succeed('Build completed successfully'); console.log(chalk.green('\n✨ Build completed!')); console.log(chalk.cyan('\n📦 Build Statistics:')); console.log(` ${chalk.gray('Total Size:')} ${chalk.bold(formatBytes(stats.totalSize))}`); console.log(` ${chalk.gray('Files:')} ${chalk.bold(stats.fileCount)}`); console.log(` ${chalk.gray('Output:')} ${chalk.bold(path.relative(projectRoot, buildPath))}`); if (shouldAnalyze) { console.log(chalk.cyan('\n📊 File Analysis:')); stats.files.forEach(file => { console.log(` ${chalk.gray(file.name)} ${chalk.bold(formatBytes(file.size))}`); }); } console.log(chalk.gray('\n🚀 Ready for deployment!\n')); } catch (error) { spinner.fail('Build failed'); console.error(chalk.red(error.message)); if (error.stack) { console.error(chalk.gray(error.stack)); } } } function findProjectRoot() { let currentDir = process.cwd(); while (currentDir !== path.parse(currentDir).root) { const configPath = path.join(currentDir, 'muspe.config.js'); if (fs.existsSync(configPath)) { return currentDir; } currentDir = path.dirname(currentDir); } return null; } async function loadConfig(projectRoot) { const configPath = path.join(projectRoot, 'muspe.config.js'); if (await fs.pathExists(configPath)) { try { delete require.cache[require.resolve(configPath)]; return require(configPath); } catch (error) { console.log(chalk.yellow('Warning: Failed to load muspe.config.js, using defaults')); return {}; } } return {}; } async function copyPublicFiles(projectRoot, buildPath) { const publicDir = path.join(projectRoot, 'public'); if (await fs.pathExists(publicDir)) { await fs.copy(publicDir, buildPath, { filter: (src) => { // Skip service worker and manifest during copy, we'll process them separately const filename = path.basename(src); return !['sw.js', 'manifest.json'].includes(filename); } }); } } async function buildCSS(projectRoot, buildPath, config, shouldMinify) { const srcDir = path.join(projectRoot, 'src'); const stylesDir = path.join(srcDir, 'styles'); const outputCSSDir = path.join(buildPath, 'styles'); await fs.ensureDir(outputCSSDir); if (await fs.pathExists(stylesDir)) { const cssFiles = await fs.readdir(stylesDir); for (const file of cssFiles) { if (path.extname(file) === '.css') { const inputPath = path.join(stylesDir, file); const outputPath = path.join(outputCSSDir, file); let content = await fs.readFile(inputPath, 'utf8'); // Process CSS based on framework if (config.framework === 'tailwind') { content = await processTailwindForProduction(content, projectRoot); } // Minify if requested if (shouldMinify) { content = minifyCSS(content); } await fs.writeFile(outputPath, content); } } } } async function buildJS(projectRoot, buildPath, config, shouldMinify) { const srcDir = path.join(projectRoot, 'src'); const scriptsDir = path.join(srcDir, 'scripts'); const outputJSDir = path.join(buildPath, 'scripts'); await fs.ensureDir(outputJSDir); if (await fs.pathExists(scriptsDir)) { const jsFiles = await fs.readdir(scriptsDir); for (const file of jsFiles) { if (path.extname(file) === '.js') { const inputPath = path.join(scriptsDir, file); const outputPath = path.join(outputJSDir, file); let content = await fs.readFile(inputPath, 'utf8'); // Process imports and dependencies content = await processJSImports(content, srcDir); // Minify if requested if (shouldMinify) { content = minifyJS(content); } await fs.writeFile(outputPath, content); } } } // Copy components and other JS files const componentsDirs = ['components', 'pages', 'services', 'utils']; for (const dir of componentsDirs) { const sourceDir = path.join(srcDir, dir); const targetDir = path.join(buildPath, dir); if (await fs.pathExists(sourceDir)) { await fs.copy(sourceDir, targetDir); } } } async function processTailwindForProduction(content, projectRoot) { // In a real implementation, this would use PostCSS and Tailwind CLI // For now, we'll do a simple processing const tailwindProductionCSS = ` /* Tailwind CSS Production Build */ *, ::before, ::after { box-sizing: border-box; border-width: 0; border-style: solid; border-color: #e5e7eb; } html { line-height: 1.5; -webkit-text-size-adjust: 100%; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } body { margin: 0; line-height: inherit; } /* Core utilities */ .container { max-width: 100%; margin: 0 auto; padding: 0 1rem; } @media (min-width: 640px) { .container { max-width: 640px; } } @media (min-width: 768px) { .container { max-width: 768px; } } @media (min-width: 1024px) { .container { max-width: 1024px; } } .flex { display: flex; } .flex-col { flex-direction: column; } .items-center { align-items: center; } .justify-center { justify-content: center; } .grid { display: grid; } .grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .gap-4 { gap: 1rem; } .space-y-2 > * + * { margin-top: 0.5rem; } .space-y-4 > * + * { margin-top: 1rem; } .p-4 { padding: 1rem; } .p-6 { padding: 1.5rem; } .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; } .px-6 { padding-left: 1.5rem; padding-right: 1.5rem; } .my-6 { margin-top: 1.5rem; margin-bottom: 1.5rem; } .bg-primary-500 { background-color: #3b82f6; } .bg-primary-600 { background-color: #2563eb; } .bg-white { background-color: #ffffff; } .bg-gray-50 { background-color: #f9fafb; } .text-white { color: #ffffff; } .text-center { text-align: center; } .font-bold { font-weight: 700; } .text-2xl { font-size: 1.5rem; } .rounded-lg { border-radius: 0.5rem; } .rounded-xl { border-radius: 0.75rem; } .shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); } .transition-colors { transition-property: color, background-color, border-color; transition-duration: 150ms; } .hover\\:bg-primary-600:hover { background-color: #2563eb; } .max-w-md { max-width: 28rem; } .mx-auto { margin-left: auto; margin-right: auto; } .min-h-screen { min-height: 100vh; } `; return content .replace('@tailwind base;', tailwindProductionCSS) .replace('@tailwind components;', '') .replace('@tailwind utilities;', ''); } async function processJSImports(content, srcDir) { // Simple import processing - in a real implementation, you'd use a bundler return content; } function minifyCSS(css) { return css .replace(/\/\*[\s\S]*?\*\//g, '') // Remove comments .replace(/\s+/g, ' ') // Collapse whitespace .replace(/;\s*}/g, '}') // Remove unnecessary semicolons .replace(/\s*{\s*/g, '{') // Remove spaces around braces .replace(/}\s*/g, '}') // Remove spaces after braces .replace(/;\s*/g, ';') // Remove spaces after semicolons .replace(/:\s*/g, ':') // Remove spaces after colons .trim(); } function minifyJS(js) { return js .replace(/\/\*[\s\S]*?\*\//g, '') // Remove block comments .replace(/\/\/.*$/gm, '') // Remove line comments .replace(/\s+/g, ' ') // Collapse whitespace .replace(/;\s*}/g, '}') // Clean up semicolons .trim(); } async function buildPWA(projectRoot, buildPath, config) { // Copy and process service worker const swPath = path.join(projectRoot, 'public', 'sw.js'); if (await fs.pathExists(swPath)) { const swContent = await fs.readFile(swPath, 'utf8'); const processedSW = processSW(swContent, config); await fs.writeFile(path.join(buildPath, 'sw.js'), processedSW); } // Generate manifest await generateManifest(projectRoot, buildPath, config); } function processSW(content, config) { // Update cache name with build timestamp const timestamp = Date.now(); return content.replace(/CACHE_NAME = '[^']*'/, `CACHE_NAME = 'muspe-v${timestamp}'`); } async function generateManifest(projectRoot, buildPath, config) { const packageJsonPath = path.join(projectRoot, 'package.json'); if (await fs.pathExists(packageJsonPath)) { const packageJson = await fs.readJSON(packageJsonPath); const manifest = { name: packageJson.name, short_name: packageJson.name, description: packageJson.description || `${packageJson.name} - Built with MusPE`, start_url: '/', display: 'standalone', background_color: '#ffffff', theme_color: '#3b82f6', icons: [ { src: './assets/icons/icon-192x192.png', sizes: '192x192', type: 'image/png' }, { src: './assets/icons/icon-512x512.png', sizes: '512x512', type: 'image/png' } ], categories: ['utilities', 'productivity'], orientation: config.mobile?.orientation || 'portrait', scope: '/', ...config.pwa?.manifest }; await fs.writeJSON(path.join(buildPath, 'manifest.json'), manifest, { spaces: 2 }); } } async function getBuildStats(buildPath) { const files = []; let totalSize = 0; async function scanDirectory(dir) { const items = await fs.readdir(dir); for (const item of items) { const fullPath = path.join(dir, item); const stats = await fs.stat(fullPath); if (stats.isDirectory()) { await scanDirectory(fullPath); } else { const relativePath = path.relative(buildPath, fullPath); files.push({ name: relativePath, size: stats.size }); totalSize += stats.size; } } } await scanDirectory(buildPath); // Sort files by size (largest first) files.sort((a, b) => b.size - a.size); return { files, fileCount: files.length, totalSize }; } function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } module.exports = { buildProject };