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
729 lines (619 loc) ⢠19.7 kB
JavaScript
const fs = require('fs-extra');
const path = require('path');
const chalk = require('chalk');
const ora = require('ora');
// MusPE Server-Side Rendering (SSR) System
class MusPESSR {
constructor(config = {}) {
this.config = {
output: config.output || 'dist-ssr',
staticDir: config.staticDir || 'static',
routes: config.routes || [],
prerenderRoutes: config.prerenderRoutes || [],
serverPort: config.serverPort || 3000,
compression: config.compression !== false,
caching: config.caching !== false,
...config
};
this.routes = new Map();
this.middleware = [];
this.plugins = [];
}
// Register route handlers
route(path, handler) {
this.routes.set(path, handler);
return this;
}
// Add middleware
use(middleware) {
this.middleware.push(middleware);
return this;
}
// Add plugins
plugin(plugin) {
this.plugins.push(plugin);
return this;
}
// Build SSR application
async build(projectRoot) {
const spinner = ora('Building SSR application...').start();
try {
// Create output directories
await this.createOutputDirectories(projectRoot);
// Build client-side assets
await this.buildClientAssets(projectRoot);
// Build server-side assets
await this.buildServerAssets(projectRoot);
// Generate static pages
await this.generateStaticPages(projectRoot);
// Copy static assets
await this.copyStaticAssets(projectRoot);
// Generate server configuration
await this.generateServerConfig(projectRoot);
spinner.succeed('SSR build completed');
console.log(chalk.green('\n⨠SSR build completed!'));
console.log(chalk.cyan('\nš¦ Build output:'));
console.log(` ${chalk.gray('Client:')} ${this.config.output}/client/`);
console.log(` ${chalk.gray('Server:')} ${this.config.output}/server/`);
console.log(` ${chalk.gray('Static:')} ${this.config.output}/static/`);
} catch (error) {
spinner.fail('SSR build failed');
throw error;
}
}
async createOutputDirectories(projectRoot) {
const outputPath = path.join(projectRoot, this.config.output);
await fs.ensureDir(path.join(outputPath, 'client'));
await fs.ensureDir(path.join(outputPath, 'server'));
await fs.ensureDir(path.join(outputPath, 'static'));
}
async buildClientAssets(projectRoot) {
// Generate client-side bundle
const clientBundle = this.generateClientBundle();
const clientPath = path.join(projectRoot, this.config.output, 'client', 'bundle.js');
await fs.writeFile(clientPath, clientBundle);
// Generate client-side CSS
const clientCSS = this.generateClientCSS();
const cssPath = path.join(projectRoot, this.config.output, 'client', 'styles.css');
await fs.writeFile(cssPath, clientCSS);
}
async buildServerAssets(projectRoot) {
// Generate server bundle
const serverBundle = this.generateServerBundle();
const serverPath = path.join(projectRoot, this.config.output, 'server', 'index.js');
await fs.writeFile(serverPath, serverBundle);
// Generate package.json for server
const serverPackage = this.generateServerPackageJson();
const packagePath = path.join(projectRoot, this.config.output, 'server', 'package.json');
await fs.writeFile(packagePath, JSON.stringify(serverPackage, null, 2));
}
async generateStaticPages(projectRoot) {
if (this.config.prerenderRoutes.length === 0) return;
console.log(chalk.blue('Pre-rendering static pages...'));
for (const route of this.config.prerenderRoutes) {
const html = await this.renderRoute(route);
const routePath = route === '/' ? 'index' : route.slice(1).replace(/\//g, '-');
const htmlPath = path.join(projectRoot, this.config.output, 'static', `${routePath}.html`);
await fs.writeFile(htmlPath, html);
console.log(chalk.gray(` Generated: ${routePath}.html`));
}
}
async copyStaticAssets(projectRoot) {
const staticSrc = path.join(projectRoot, 'public');
const staticDest = path.join(projectRoot, this.config.output, 'static');
if (await fs.pathExists(staticSrc)) {
await fs.copy(staticSrc, staticDest);
}
}
async generateServerConfig(projectRoot) {
const serverConfig = this.generateExpressServer();
const configPath = path.join(projectRoot, this.config.output, 'server.js');
await fs.writeFile(configPath, serverConfig);
// Generate ecosystem.config.js for PM2
const pm2Config = this.generatePM2Config();
const pm2Path = path.join(projectRoot, this.config.output, 'ecosystem.config.js');
await fs.writeFile(pm2Path, pm2Config);
}
generateClientBundle() {
return `// MusPE Client-Side Bundle (SSR)
(function() {
'use strict';
// Import MusPE core
${this.getMusPECore()}
// Hydrate SSR content
window.addEventListener('DOMContentLoaded', function() {
if (window.MusPE) {
// Initialize client-side hydration
MusPE.hydrate();
// Initialize router for SPA navigation
if (MusPE.Router) {
MusPE.Router.init({
mode: 'history',
base: '/',
ssr: true
});
}
// Initialize components
MusPE.components.init();
console.log('š MusPE SSR client hydrated');
}
});
// Client-side routing for SPA behavior
function handleNavigation(event) {
const target = event.target.closest('a[href]');
if (target && target.href.startsWith(window.location.origin)) {
event.preventDefault();
const url = new URL(target.href);
window.history.pushState({}, '', url.pathname);
// Load page content via AJAX
loadPage(url.pathname);
}
}
async function loadPage(path) {
try {
const response = await fetch(path, {
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
});
if (response.ok) {
const html = await response.text();
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Update page content
const main = document.querySelector('main') || document.body;
const newMain = doc.querySelector('main') || doc.body;
main.innerHTML = newMain.innerHTML;
// Update page title
document.title = doc.title;
// Re-initialize components
if (window.MusPE) {
MusPE.components.init();
}
}
} catch (error) {
console.error('Navigation error:', error);
window.location.href = path;
}
}
// Handle browser back/forward
window.addEventListener('popstate', function(event) {
loadPage(window.location.pathname);
});
// Attach navigation handler
document.addEventListener('click', handleNavigation);
})();`;
}
generateClientCSS() {
return `/* MusPE SSR Client Styles */
/* Loading states */
.muspe-loading {
opacity: 0.7;
pointer-events: none;
transition: opacity 0.3s ease;
}
/* Hydration styles */
.muspe-hydrating {
visibility: hidden;
}
.muspe-hydrated {
visibility: visible;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Critical CSS for above-the-fold content */
.muspe-header {
position: sticky;
top: 0;
z-index: 1000;
background: white;
border-bottom: 1px solid #e2e8f0;
}
.muspe-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
max-width: 1200px;
margin: 0 auto;
}
.muspe-main {
min-height: calc(100vh - 80px);
padding: 2rem 1rem;
max-width: 1200px;
margin: 0 auto;
}
/* Mobile-first responsive design */
@media (max-width: 768px) {
.muspe-nav {
flex-direction: column;
gap: 1rem;
}
.muspe-main {
padding: 1rem;
}
}
/* Performance optimizations */
img {
max-width: 100%;
height: auto;
loading: lazy;
}
/* Print styles */
@media print {
.muspe-nav,
.muspe-sidebar {
display: none;
}
.muspe-main {
padding: 0;
max-width: none;
}
}`;
}
generateServerBundle() {
return `// MusPE Server-Side Rendering Bundle
const express = require('express');
const path = require('path');
const fs = require('fs');
const compression = require('compression');
const helmet = require('helmet');
class MusPESSRServer {
constructor(options = {}) {
this.app = express();
this.options = {
port: process.env.PORT || ${this.config.serverPort},
staticDir: '${this.config.staticDir}',
compression: ${this.config.compression},
caching: ${this.config.caching},
...options
};
this.setupMiddleware();
this.setupRoutes();
}
setupMiddleware() {
// Security headers
this.app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
}));
// Compression
if (this.options.compression) {
this.app.use(compression());
}
// Static files
this.app.use('/static', express.static(path.join(__dirname, 'static'), {
maxAge: this.options.caching ? '1y' : '0',
etag: true,
lastModified: true
}));
// Client assets
this.app.use('/client', express.static(path.join(__dirname, 'client'), {
maxAge: this.options.caching ? '1y' : '0',
etag: true,
lastModified: true
}));
}
setupRoutes() {
// Health check
this.app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// API routes
this.app.get('/api/*', (req, res) => {
res.status(404).json({ error: 'API endpoint not found' });
});
// SSR routes
this.app.get('*', (req, res) => {
this.renderPage(req, res);
});
}
async renderPage(req, res) {
try {
const url = req.url;
const isAjax = req.headers['x-requested-with'] === 'XMLHttpRequest';
// Check for pre-rendered static page
const staticPagePath = this.getStaticPagePath(url);
if (staticPagePath && fs.existsSync(staticPagePath)) {
return res.sendFile(staticPagePath);
}
// Generate dynamic page
const html = await this.generatePage(url, req);
if (isAjax) {
// Return only the content for AJAX requests
const contentMatch = html.match(/<main[^>]*>(.*?)<\\/main>/s);
return res.send(contentMatch ? contentMatch[1] : html);
}
res.send(html);
} catch (error) {
console.error('SSR Error:', error);
res.status(500).send(this.generateErrorPage(error));
}
}
getStaticPagePath(url) {
const routePath = url === '/' ? 'index' : url.slice(1).replace(/\\//g, '-');
return path.join(__dirname, 'static', \`\${routePath}.html\`);
}
async generatePage(url, req) {
const route = this.matchRoute(url);
const pageData = await this.getPageData(url, req);
return \`
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>\${pageData.title || 'MusPE App'}</title>
<meta name="description" content="\${pageData.description || 'Built with MusPE Framework'}">
<!-- Open Graph -->
<meta property="og:title" content="\${pageData.title || 'MusPE App'}">
<meta property="og:description" content="\${pageData.description || 'Built with MusPE Framework'}">
<meta property="og:type" content="website">
<meta property="og:url" content="\${req.protocol}://\${req.get('host')}\${req.originalUrl}">
<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="\${pageData.title || 'MusPE App'}">
<meta name="twitter:description" content="\${pageData.description || 'Built with MusPE Framework'}">
<!-- Critical CSS -->
<link rel="stylesheet" href="/client/styles.css">
<!-- Preload resources -->
<link rel="preload" href="/client/bundle.js" as="script">
<!-- PWA -->
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#3b82f6">
<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/static/favicon.ico">
</head>
<body>
<div id="app">
<header class="muspe-header">
<nav class="muspe-nav">
<div class="muspe-logo">
<a href="/">MusPE App</a>
</div>
<div class="muspe-nav-links">
<a href="/">Home</a>
<a href="/about">About</a>
<a href="/contact">Contact</a>
</div>
</nav>
</header>
<main class="muspe-main">
\${await this.renderRoute(route, pageData)}
</main>
<footer class="muspe-footer">
<p>© 2024 MusPE App. Built with MusPE Framework.</p>
</footer>
</div>
<!-- State hydration -->
<script>
window.__MUSPE_STATE__ = \${JSON.stringify(pageData)};
</script>
<!-- Client bundle -->
<script src="/client/bundle.js"></script>
</body>
</html>\`;
}
matchRoute(url) {
// Simple route matching - extend this for more complex routing
const routes = {
'/': 'home',
'/about': 'about',
'/contact': 'contact'
};
return routes[url] || '404';
}
async getPageData(url, req) {
// Mock data - replace with actual data fetching logic
const pageData = {
title: 'MusPE App',
description: 'Built with MusPE Framework',
url: url,
timestamp: new Date().toISOString()
};
// Route-specific data
switch (url) {
case '/':
pageData.title = 'Home - MusPE App';
pageData.description = 'Welcome to MusPE App';
break;
case '/about':
pageData.title = 'About - MusPE App';
pageData.description = 'Learn more about MusPE App';
break;
case '/contact':
pageData.title = 'Contact - MusPE App';
pageData.description = 'Get in touch with us';
break;
}
return pageData;
}
async renderRoute(route, data) {
const templates = {
home: \`
<div class="hero">
<h1>Welcome to MusPE App</h1>
<p>Built with the powerful MusPE Framework</p>
<a href="/about" class="btn btn-primary">Learn More</a>
</div>
<div class="features">
<div class="feature">
<h3>Fast</h3>
<p>Lightning-fast performance with SSR</p>
</div>
<div class="feature">
<h3>Modern</h3>
<p>Built with modern web standards</p>
</div>
<div class="feature">
<h3>Mobile-First</h3>
<p>Designed for mobile devices</p>
</div>
</div>
\`,
about: \`
<div class="page-header">
<h1>About MusPE</h1>
</div>
<div class="content">
<p>MusPE is a modern, mobile-first web framework designed for building fast, responsive applications.</p>
<p>With server-side rendering, progressive enhancement, and a focus on performance, MusPE makes it easy to create great user experiences.</p>
</div>
\`,
contact: \`
<div class="page-header">
<h1>Contact Us</h1>
</div>
<div class="contact-form">
<form action="/api/contact" method="post">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" required>
</div>
<div class="form-group">
<label for="email">Email</label>
<input type="email" id="email" name="email" required>
</div>
<div class="form-group">
<label for="message">Message</label>
<textarea id="message" name="message" required></textarea>
</div>
<button type="submit" class="btn btn-primary">Send Message</button>
</form>
</div>
\`,
404: \`
<div class="error-page">
<h1>404 - Page Not Found</h1>
<p>The page you're looking for doesn't exist.</p>
<a href="/" class="btn btn-primary">Go Home</a>
</div>
\`
};
return templates[route] || templates['404'];
}
generateErrorPage(error) {
return \`
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Error - MusPE App</title>
</head>
<body>
<div class="error-page">
<h1>Something went wrong</h1>
<p>We're sorry, but something went wrong on our end.</p>
<a href="/" class="btn btn-primary">Go Home</a>
</div>
</body>
</html>\`;
}
start() {
this.app.listen(this.options.port, () => {
console.log(\`š MusPE SSR server running on port \${this.options.port}\`);
});
}
}
// Start server if this file is run directly
if (require.main === module) {
const server = new MusPESSRServer();
server.start();
}
module.exports = MusPESSRServer;`;
}
generateServerPackageJson() {
return {
name: 'muspe-ssr-server',
version: '1.0.0',
description: 'MusPE SSR Server',
main: 'index.js',
scripts: {
start: 'node index.js',
dev: 'nodemon index.js'
},
dependencies: {
express: '^4.18.2',
compression: '^1.7.4',
helmet: '^7.0.0'
},
devDependencies: {
nodemon: '^3.0.1'
},
engines: {
node: '>=14.0.0'
}
};
}
generateExpressServer() {
return `// MusPE SSR Express Server
const MusPESSRServer = require('./server/index.js');
const server = new MusPESSRServer({
port: process.env.PORT || ${this.config.serverPort},
compression: ${this.config.compression},
caching: ${this.config.caching}
});
// Graceful shutdown
process.on('SIGTERM', () => {
console.log('SIGTERM signal received: closing HTTP server');
process.exit(0);
});
process.on('SIGINT', () => {
console.log('SIGINT signal received: closing HTTP server');
process.exit(0);
});
server.start();`;
}
generatePM2Config() {
return `// PM2 Configuration for MusPE SSR
module.exports = {
apps: [{
name: 'muspe-ssr',
script: './server.js',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: ${this.config.serverPort}
},
env_production: {
NODE_ENV: 'production',
PORT: ${this.config.serverPort}
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_file: './logs/combined.log',
time: true,
watch: false,
max_memory_restart: '1G',
node_args: '--max_old_space_size=4096'
}]
};`;
}
getMusPECore() {
// This would include the core MusPE framework code
return `
// MusPE Core Framework (client-side)
// This would include the actual MusPE framework code
console.log('MusPE Core loaded for SSR hydration');
`;
}
async renderRoute(route) {
// Mock route rendering - replace with actual template rendering
return `<h1>Route: ${route}</h1><p>This is a server-rendered page.</p>`;
}
}
module.exports = { MusPESSR };