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

729 lines (619 loc) • 19.7 kB
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 \`<!DOCTYPE html> <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>&copy; 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 \`<!DOCTYPE html> <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 };