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
JavaScript
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 };