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 (338 loc) ⢠11.6 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const ora = require('ora');
const http = require('http');
const url = require('url');
const spawn = require('cross-spawn');
async function serveProject(options) {
const projectRoot = findProjectRoot();
if (!projectRoot) {
console.log(chalk.red('Not in a MusPE project directory'));
return;
}
const config = await loadConfig(projectRoot);
const port = options.port || config.server?.port || 3000;
const host = options.host || config.server?.host || 'localhost';
const shouldOpen = options.open || config.server?.open || false;
const spinner = ora('Starting development server...').start();
try {
const server = await createDevServer(projectRoot, config, port, host);
spinner.succeed(`Development server running at http://${host}:${port}`);
console.log(chalk.cyan('\nš± MusPE Development Server'));
console.log(chalk.gray(` Local: http://${host}:${port}`));
console.log(chalk.gray(` Network: http://${getNetworkIP()}:${port}`));
console.log(chalk.gray('\n Press Ctrl+C to stop\n'));
// Open browser if requested
if (shouldOpen) {
await openBrowser(`http://${host}:${port}`);
}
// Keep the process alive
process.on('SIGINT', () => {
console.log(chalk.yellow('\nš Shutting down development server...'));
server.close(() => {
console.log(chalk.green('ā
Server stopped'));
process.exit(0);
});
});
} catch (error) {
spinner.fail('Failed to start development server');
console.error(chalk.red(error.message));
}
}
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 createDevServer(projectRoot, config, port, host) {
const publicDir = path.join(projectRoot, 'public');
const srcDir = path.join(projectRoot, 'src');
const server = http.createServer(async (req, res) => {
const parsedUrl = url.parse(req.url, true);
let pathname = parsedUrl.pathname;
// Handle root
if (pathname === '/') {
pathname = '/index.html';
}
try {
// Try to serve from public directory first
let filePath = path.join(publicDir, pathname);
// If not in public, try src directory
if (!await fs.pathExists(filePath)) {
filePath = path.join(srcDir, pathname);
}
// If still not found, serve index.html for SPA routing
if (!await fs.pathExists(filePath) && !pathname.includes('.')) {
filePath = path.join(publicDir, 'index.html');
}
if (await fs.pathExists(filePath)) {
const ext = path.extname(filePath).toLowerCase();
const mimeTypes = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.eot': 'application/vnd.ms-fontobject'
};
const contentType = mimeTypes[ext] || 'application/octet-stream';
res.setHeader('Content-Type', contentType);
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
// Process CSS files for imports and Tailwind
if (ext === '.css') {
const content = await processCSSFile(filePath, config);
res.writeHead(200);
res.end(content);
} else {
const content = await fs.readFile(filePath);
res.writeHead(200);
res.end(content);
}
} else {
// 404 page
res.writeHead(404, { 'Content-Type': 'text/html' });
res.end(generate404Page());
}
} catch (error) {
console.error('Server error:', error);
res.writeHead(500, { 'Content-Type': 'text/html' });
res.end(generate500Page(error));
}
});
return new Promise((resolve, reject) => {
server.listen(port, host, (error) => {
if (error) {
reject(error);
} else {
resolve(server);
}
});
});
}
async function processCSSFile(filePath, config) {
const content = await fs.readFile(filePath, 'utf8');
// If Tailwind is configured, process Tailwind directives
if (config.framework === 'tailwind') {
return await processTailwindCSS(content, filePath);
}
return content;
}
async function processTailwindCSS(content, filePath) {
// Simple Tailwind processing for development
// In a real implementation, you'd integrate with PostCSS and Tailwind CLI
const tailwindBase = `
/* Tailwind CSS Base Styles */
*, ::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%;
-moz-tab-size: 4;
tab-size: 4;
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
body {
margin: 0;
line-height: inherit;
}
/* Responsive Design */
.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; } }
/* Flexbox */
.flex { display: flex; }
.flex-col { flex-direction: column; }
.items-center { align-items: center; }
.justify-center { justify-content: center; }
.space-y-2 > * + * { margin-top: 0.5rem; }
.space-y-4 > * + * { margin-top: 1rem; }
/* Grid */
.grid { display: grid; }
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.gap-4 { gap: 1rem; }
/* Spacing */
.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; }
.m-0 { margin: 0; }
.my-6 { margin-top: 1.5rem; margin-bottom: 1.5rem; }
/* Colors */
.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-primary-500 { color: #3b82f6; }
/* Typography */
.text-center { text-align: center; }
.font-bold { font-weight: 700; }
.text-2xl { font-size: 1.5rem; }
/* Border */
.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); }
/* Effects */
.transition-colors { transition-property: color, background-color, border-color; transition-duration: 150ms; }
.hover\\:bg-primary-600:hover { background-color: #2563eb; }
/* Responsive */
.max-w-md { max-width: 28rem; }
.mx-auto { margin-left: auto; margin-right: auto; }
.min-h-screen { min-height: 100vh; }
`;
// Replace Tailwind directives with base styles
let processedContent = content
.replace('@tailwind base;', tailwindBase)
.replace('@tailwind components;', '/* Components will be processed here */')
.replace('@tailwind utilities;', '/* Utilities will be processed here */');
return processedContent;
}
function generate404Page() {
return `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>404 - Page Not Found</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 2rem;
text-align: center;
background: #f8f9fa;
color: #333;
}
.container {
max-width: 400px;
margin: 2rem auto;
background: white;
padding: 2rem;
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 { color: #e74c3c; margin-bottom: 1rem; }
.emoji { font-size: 4rem; margin-bottom: 1rem; }
a { color: #3b82f6; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<div class="emoji">š±</div>
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<p><a href="/">ā Back to Home</a></p>
<p><small>MusPE Development Server</small></p>
</div>
</body>
</html>`;
}
function generate500Page(error) {
return `
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>500 - Server Error</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 0;
padding: 2rem;
text-align: center;
background: #f8f9fa;
color: #333;
}
.container {
max-width: 500px;
margin: 2rem auto;
background: white;
padding: 2rem;
border-radius: 1rem;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
h1 { color: #e74c3c; margin-bottom: 1rem; }
.emoji { font-size: 4rem; margin-bottom: 1rem; }
.error {
background: #fee;
padding: 1rem;
border-radius: 0.5rem;
margin: 1rem 0;
text-align: left;
font-family: monospace;
color: #c53030;
}
a { color: #3b82f6; text-decoration: none; }
a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container">
<div class="emoji">ā ļø</div>
<h1>500 - Server Error</h1>
<p>Something went wrong on the server.</p>
<div class="error">${error.message}</div>
<p><a href="/">ā Back to Home</a></p>
<p><small>MusPE Development Server</small></p>
</div>
</body>
</html>`;
}
function getNetworkIP() {
const interfaces = require('os').networkInterfaces();
for (const name of Object.keys(interfaces)) {
for (const interface of interfaces[name]) {
if (interface.family === 'IPv4' && !interface.internal) {
return interface.address;
}
}
}
return 'localhost';
}
async function openBrowser(url) {
const open = (await import('open')).default;
try {
await open(url);
} catch (error) {
console.log(chalk.yellow(`Could not open browser automatically. Please visit: ${url}`));
}
}
module.exports = { serveProject };