UNPKG

create-bini-app

Version:

Bini.js: Ultra-high-performance enterprise-grade React framework with automatic source code protection, Next.js-like file-based routing, head-only SSR, production-ready API routes, Fastify server, TypeScript support, and built-in security middleware. The

1,614 lines (1,373 loc) 132 kB
#!/usr/bin/env node import inquirer from "inquirer"; import fs from "fs"; import path from "path"; import os from "os"; import { execSync, spawn } from 'child_process'; import { fileURLToPath } from 'url'; import sharp from "sharp"; // ES module equivalents const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Read version from package.json const CLI_PACKAGE_PATH = path.join(__dirname, 'package.json'); const cliPackageJson = JSON.parse(fs.readFileSync(CLI_PACKAGE_PATH, 'utf-8')); const BINIJS_VERSION = cliPackageJson.version; // Color definitions const COLORS = { CYAN: '\x1b[36m', // Cyan for header RESET: '\x1b[0m', // Reset GREEN: '\x1b[32m' // Green for checkmarks }; const LOGO = ` ██████╗ ██╗███╗ ██╗██╗ ██╗███████╗ ██╔══██╗██║████╗ ██║██║ ██║██╔════╝ ██████╔╝██║██╔██╗ ██║██║ ██║███████╗ ██╔══██╗██║██║╚██╗██║██║ ██╗ ██║╚════██║ ██████╔╝██║██║ ╚████║██║ ╚█████╔╝███████║ ╚═════╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚════╝ ╚══════╝ Developed By Binidu `; class ProgressLogger { constructor(totalSteps) { this.totalSteps = totalSteps; this.currentStep = 0; this.startTime = Date.now(); } start(message) { this.currentStep++; process.stdout.write(`[${this.currentStep}/${this.totalSteps}] ${message}... `); } success() { process.stdout.write('✅\n'); } warn() { process.stdout.write('⚠️\n'); } fail(error) { process.stdout.write('❌\n'); throw error; } complete() { const duration = ((Date.now() - this.startTime) / 1000).toFixed(1); console.log(`\n🎉 Completed ${this.totalSteps} steps in ${duration}s`); } } const REQUIRED_NODE = 'v18.0.0'; function checkNodeVersion() { const [major, minor] = process.version.slice(1).split('.').map(Number); const [reqMajor, reqMinor] = REQUIRED_NODE.slice(1).split('.').map(Number); if (major < reqMajor || (major === reqMajor && minor < reqMinor)) { console.error(`❌ Node.js ${REQUIRED_NODE} or higher required. Current: ${process.version}`); console.error('💡 Please update Node.js from https://nodejs.org'); process.exit(1); } } function parseArguments() { const args = process.argv.slice(2); if (args.includes('--version') || args.includes('-v')) { console.log(`Bini.js CLI v${BINIJS_VERSION}`); process.exit(0); } if (args.includes('--help') || args.includes('-h')) { console.log(` Usage: create-bini-app [project-name] [options] Options: --version, -v Show version number --help, -h Show help --typescript Use TypeScript --javascript Use JavaScript (default) --tailwind Use Tailwind CSS --css-modules Use CSS Modules --force Overwrite existing directory --minimal Minimal setup with fewer files --verbose Show detailed logs Examples: create-bini-app my-app create-bini-app my-app --typescript --tailwind create-bini-app my-app --javascript --force create-bini-app my-app --minimal `); process.exit(0); } const hasExplicitTypeScript = args.includes('--typescript'); const hasExplicitJavaScript = args.includes('--javascript') || args.includes('--no-typescript'); return { projectName: args.find(arg => !arg.startsWith('--')), flags: { force: args.includes('--force'), typescript: hasExplicitTypeScript ? true : (hasExplicitJavaScript ? false : undefined), javascript: hasExplicitJavaScript, tailwind: args.includes('--tailwind'), cssModules: args.includes('--css-modules'), minimal: args.includes('--minimal'), verbose: args.includes('--verbose') } }; } function validateProjectName(name) { const invalidPatterns = [ /\.\./, /[<>:"|?*]/, /^(npm|node|bini|\.)/, /[^\w\-\.]/ ]; return !invalidPatterns.some(pattern => pattern.test(name)) && name.length > 0 && name.length <= 50; } function robustMkdirSync(dirPath) { try { if (!fs.existsSync(dirPath)) { fs.mkdirSync(dirPath, { recursive: true, mode: 0o755 }); } return true; } catch (error) { throw new Error(`Cannot write to directory: ${dirPath}. Check permissions.`); } } function secureWriteFile(filePath, content, options = {}) { const resolved = filePath; if (fs.existsSync(resolved) && !options.force) { throw new Error(`File already exists: ${resolved}. Use --force to overwrite.`); } const dir = path.dirname(resolved); robustMkdirSync(dir); fs.writeFileSync(resolved, content, { mode: options.mode || 0o644, flag: options.flag || 'w' }); } function safeRm(p, { allowedBase = process.cwd() } = {}) { if (!p) throw new Error('Path required'); const resolved = p; const base = path.resolve(allowedBase); if (resolved === base) throw new Error('Refusing to rm project root'); if (!resolved.startsWith(base + path.sep)) { throw new Error('Refusing to rm outside allowed base'); } const forbidden = [ path.resolve('/'), path.resolve(process.env.HOME || ''), path.resolve(process.env.USERPROFILE || ''), path.resolve(process.cwd()), path.resolve(__dirname) ]; if (forbidden.some(forbiddenPath => resolved === forbiddenPath || resolved.startsWith(forbiddenPath + path.sep))) { throw new Error('Refusing to rm forbidden path'); } try { const stat = fs.statSync(resolved); if (stat.isDirectory() && resolved.split(path.sep).length < 3) { throw new Error('Refusing to rm shallow system directory'); } } catch (err) { throw new Error('Path does not exist or is inaccessible'); } fs.rmSync(resolved, { recursive: true, force: true }); } function mkdirRecursive(dirPath) { robustMkdirSync(dirPath); } function getNetworkIp() { const interfaces = os.networkInterfaces(); const candidates = []; for (const name in interfaces) { if (/docker|veth|br-|lo|loopback|vmnet|vbox|utun|tun|tap/i.test(name)) continue; for (const iface of interfaces[name]) { if (iface.internal) continue; if (iface.family === 'IPv4') { candidates.push(iface); } } } const nonLinkLocal = candidates.filter(iface => !iface.address.startsWith('169.254.')); const result = nonLinkLocal[0]?.address || candidates[0]?.address || 'localhost'; return result; } async function checkNetworkConnectivity() { try { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10000); const response = await fetch('https://registry.npmjs.org', { signal: controller.signal }); clearTimeout(timeout); return response.ok; } catch (error) { console.warn('⚠️ Network connectivity issue detected'); console.log('💡 Continuing with offline capabilities...'); return false; } } function executeCommand(command, options = {}) { const isWindows = process.platform === 'win32'; const shell = isWindows ? 'cmd.exe' : '/bin/sh'; try { return execSync(command, { shell, stdio: options.stdio || 'pipe', timeout: options.timeout || 120000, cwd: options.cwd, windowsHide: true, encoding: 'utf8' }); } catch (error) { throw new Error(`Command failed: ${command}\nError: ${error.message}`); } } async function detectPackageManager() { const packageManagers = [ { name: 'bun', command: 'bun --version', priority: 4 }, { name: 'pnpm', command: 'pnpm --version', priority: 3 }, { name: 'yarn', command: 'yarn --version', priority: 2 }, { name: 'npm', command: 'npm --version', priority: 1 } ]; const availableManagers = []; for (const pm of packageManagers) { try { executeCommand(pm.command, { stdio: 'ignore' }); availableManagers.push({ ...pm, detected: true }); } catch (error) { availableManagers.push({ ...pm, detected: false }); } } const detected = availableManagers.filter(pm => pm.detected); if (detected.length === 0) { throw new Error('No package manager found. Please install npm, yarn, pnpm, or bun.'); } const recommended = detected.sort((a, b) => b.priority - a.priority)[0]; return recommended.name; } async function installDependenciesWithFallbacks(projectPath, packageManager) { const installCommands = { npm: 'npm install --no-audit --no-fund --loglevel=error', yarn: 'yarn install --silent --no-progress', pnpm: 'pnpm install --reporter=silent', bun: 'bun install --silent' }; const fallbackCommands = { npm: 'npm install --no-audit --no-fund --production', yarn: 'yarn install --production', pnpm: 'pnpm install --production', bun: 'bun install --production' }; const progress = new ProgressLogger(2); try { progress.start(`Installing dependencies with ${packageManager}`); executeCommand(installCommands[packageManager], { cwd: projectPath, stdio: 'inherit', timeout: 300000 }); progress.success(); } catch (error) { progress.warn(); console.warn(`⚠️ Primary installation failed, trying fallback...`); try { progress.start(`Installing production dependencies only`); executeCommand(fallbackCommands[packageManager], { cwd: projectPath, stdio: 'inherit', timeout: 300000 }); progress.success(); } catch (fallbackError) { progress.fail(new Error('Dependency installation failed completely')); console.log('💡 You can manually run:'); console.log(` cd ${path.basename(projectPath)}`); console.log(` ${packageManager} install`); return false; } } return true; } function checkDiskSpace(requiredMB = 100) { try { if (fs.statfsSync) { const stats = fs.statfsSync(process.cwd()); const freeBytes = stats.bavail * stats.bsize; const requiredBytes = requiredMB * 1024 * 1024; if (freeBytes < requiredBytes) { const freeMB = Math.floor(freeBytes / (1024 * 1024)); throw new Error(`Insufficient disk space. Required: ${requiredMB}MB, Available: ${freeMB}MB`); } } return true; } catch (error) { if (error.code === 'ENOENT') { return true; } console.warn('⚠️ Could not check disk space:', error.message); return true; } } function checkMemoryUsage() { const used = process.memoryUsage(); const maxMemory = 512 * 1024 * 1024; if (used.heapUsed > maxMemory) { console.warn('⚠️ High memory usage detected. Consider increasing Node.js memory limit.'); } } function shouldUseTypeScript(flags, answers) { // Explicit flags take highest priority if (flags.typescript === true) return true; if (flags.javascript === true) return false; // User answers from prompts return answers.typescript === true; } function getFileExtensions(useTypeScript) { return { main: useTypeScript ? 'tsx' : 'jsx', config: 'mjs', // Always use MJS for config files api: 'js' // API routes always use JS for better compatibility }; } async function askQuestions(flags) { let typescript; let styling; const hasTypeScriptFlag = flags.typescript === true || flags.javascript === true; const hasStylingFlag = flags.tailwind === true || flags.cssModules === true; if (hasTypeScriptFlag) { typescript = flags.typescript === true; console.log(`📝 Using ${typescript ? 'TypeScript' : 'JavaScript'} (from command line flag)`); } else { const tsAnswer = await inquirer.prompt([{ type: "confirm", name: "typescript", message: "Use TypeScript?", default: true, }]); typescript = tsAnswer.typescript; } if (hasStylingFlag) { if (flags.tailwind === true) { styling = "Tailwind"; } else if (flags.cssModules === true) { styling = "CSS Modules"; } console.log(`🎨 Using ${styling} (from command line flag)`); } else { const styleAnswer = await inquirer.prompt([{ type: "list", name: "styling", message: "Styling preference?", choices: ["Tailwind", "CSS Modules", "None"], default: "Tailwind", }]); styling = styleAnswer.styling; } return { typescript, styling }; } async function generateFaviconFiles(publicPath) { // Use your exact ß icon SVG for favicon const betaSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" fill="none"> <!-- Gradient Definition --> <defs> <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"> <stop offset="0%" stop-color="#00CFFF"/> <stop offset="100%" stop-color="#0077FF"/> </linearGradient> </defs> <!-- ß Icon with Gradient --> <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Segoe UI, Arial, sans-serif" font-size="90" font-weight="700" fill="url(#grad)"> ß </text> </svg>`; // Use your exact OG image SVG const ogImageSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 630" fill="none"> <!-- White Background --> <rect width="1200" height="630" fill="#ffffff"/> <!-- Gradient Definition --> <defs> <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"> <stop offset="0%" stop-color="#00CFFF"/> <stop offset="100%" stop-color="#0077FF"/> </linearGradient> </defs> <!-- ß Icon with Gradient --> <text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-family="Segoe UI, Arial, sans-serif" font-size="450" font-weight="700" fill="url(#grad)"> ß </text> </svg>`; // Use your exact website logo SVG const biniLogoSVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 150" fill="none"> <!-- Gradient Definition --> <defs> <linearGradient id="grad" x1="0" y1="0" x2="1" y2="1"> <stop offset="0%" stop-color="#00CFFF"/> <stop offset="100%" stop-color="#0077FF"/> </linearGradient> </defs> <!-- ß Icon with Gradient --> <text x="40" y="105" font-family="Segoe UI, Arial, sans-serif" font-size="90" font-weight="700" fill="url(#grad)"> ß </text> <!-- Bini.js Text in Black --> <text x="100" y="108" font-family="Segoe UI, Arial, sans-serif" font-size="60" font-weight="700" fill="#000000"> Bini.js </text> </svg>`; // Write SVGs (using your exact designs) secureWriteFile(path.join(publicPath, "bini-logo.svg"), biniLogoSVG); secureWriteFile(path.join(publicPath, "favicon.svg"), betaSVG); try { // Convert SVGs to PNGs using sharp const pngSizes = [16, 32, 64, 180, 512]; for (const size of pngSizes) { const output = path.join(publicPath, `favicon-${size}x${size}.png`); await sharp(Buffer.from(betaSVG)) .resize(size, size, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) .png() .toFile(output); } // Default favicon PNG await sharp(Buffer.from(betaSVG)) .resize(512, 512, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) .png() .toFile(path.join(publicPath, "favicon.png")); // Apple touch icon await sharp(Buffer.from(betaSVG)) .resize(180, 180, { fit: 'contain', background: { r: 255, g: 255, b: 255, alpha: 0 } }) .png() .toFile(path.join(publicPath, "apple-touch-icon.png")); // OG image - ß icon await sharp(Buffer.from(ogImageSVG)) .resize(1200, 630) .png() .toFile(path.join(publicPath, "og-image.png")); console.log("✅ Favicons and logos generated successfully!"); } catch (error) { console.warn('⚠️ PNG generation failed, creating placeholder files:', error.message); // Fallback: Create proper sized placeholder PNGs const pngSizes = [16, 32, 64, 180, 512]; for (const size of pngSizes) { // Generate proper sized placeholder instead of generic 1x1 const placeholderBuffer = await sharp({ create: { width: size, height: size, channels: 4, background: { r: 0, g: 207, b: 255, alpha: 255 } } }).png().toBuffer(); secureWriteFile(path.join(publicPath, `favicon-${size}x${size}.png`), placeholderBuffer); } // Generate proper sized placeholders for other files const defaultFavicon = await sharp({ create: { width: 512, height: 512, channels: 4, background: { r: 0, g: 207, b: 255, alpha: 255 } } }).png().toBuffer(); const appleIcon = await sharp({ create: { width: 180, height: 180, channels: 4, background: { r: 0, g: 207, b: 255, alpha: 255 } } }).png().toBuffer(); const ogImage = await sharp({ create: { width: 1200, height: 630, channels: 4, background: { r: 0, g: 207, b: 255, alpha: 255 } } }).png().toBuffer(); secureWriteFile(path.join(publicPath, "favicon.png"), defaultFavicon); secureWriteFile(path.join(publicPath, "apple-touch-icon.png"), appleIcon); secureWriteFile(path.join(publicPath, "og-image.png"), ogImage); console.log("✅ Basic favicon placeholders created"); } } function generateWebManifest(projectPath) { const manifest = { name: "Bini.js App", short_name: "BiniApp", description: "Modern React application built with Bini.js", start_url: "/", display: "standalone", background_color: "#ffffff", theme_color: "#00CFFF", icons: [ { src: "/favicon-16x16.png", sizes: "16x16", type: "image/png" }, { src: "/favicon-32x32.png", sizes: "32x32", type: "image/png" }, { src: "/favicon-64x64.png", sizes: "64x64", type: "image/png" }, { src: "/favicon-180x180.png", sizes: "180x180", type: "image/png" }, { src: "/favicon-512x512.png", sizes: "512x512", type: "image/png" } ] }; secureWriteFile( path.join(projectPath, "public", "site.webmanifest"), JSON.stringify(manifest, null, 2) ); } function generateBiniInternals(projectPath, useTypeScript) { const biniInternalPath = path.join(projectPath, "bini/internal"); const pluginsPath = path.join(biniInternalPath, "plugins"); mkdirRecursive(pluginsPath); // ============================================================================= // ENV CHECKER - UPDATED: Only show Environments and Ready, let Vite handle URLs // ============================================================================= secureWriteFile(path.join(biniInternalPath, "env-checker.js"), `import fs from 'fs'; import path from 'path'; import os from 'os'; const BINI_LOGO = 'ß'; // Your Bini.js logo const BINI_VERSION = '${BINIJS_VERSION}'; // Color definitions const COLORS = { CYAN: '\\x1b[36m', // Cyan for header RESET: '\\x1b[0m', // Reset GREEN: '\\x1b[32m' // Green for checkmarks }; /** * Detect which .env files exist in project root * Returns array of found .env files in load order */ export function detectEnvFiles(projectRoot = process.cwd()) { const nodeEnv = process.env.NODE_ENV || 'development'; // Check in priority order (what Next.js loads) const envFiles = [ '.env.local', // Always loaded first \`.env.\${nodeEnv}.local\`, // Environment-specific local \`.env.\${nodeEnv}\`, // Environment-specific '.env' // Base ]; const found = []; for (const file of envFiles) { const filePath = path.join(projectRoot, file); if (fs.existsSync(filePath)) { found.push(file); } } return found; } /** * Format environment files for console display (Next.js style) * Shows: "✓ Environments: .env, .env.local" */ export function formatEnvFilesForDisplay(envFiles) { if (envFiles.length === 0) { return ''; } const envString = envFiles.join(', '); return \`\${COLORS.GREEN}✓\${COLORS.RESET} Environments: \${envString}\`; } /** * Single function to show on server startup * Works for Vite, Production, or any server */ export function displayEnvFiles(projectRoot = process.cwd()) { const found = detectEnvFiles(projectRoot); const formatted = formatEnvFilesForDisplay(found); if (formatted) { console.log(\` \${formatted}\`); } } /** * Bini.js startup display - ONLY shows Environments and Ready * Let Vite handle the Local/Network URLs */ export function displayBiniStartup(options = {}) { const { mode = 'dev' // 'dev', 'preview', or 'prod' } = options; const projectRoot = process.cwd(); const found = detectEnvFiles(projectRoot); // Determine mode label let modeLabel = ''; if (mode === 'preview') { modeLabel = '(preview)'; } else if (mode === 'prod') { modeLabel = '(prod)'; } else { modeLabel = '(dev)'; } // Main header with colors console.log(\`\\n \${COLORS.CYAN}\${BINI_LOGO} Bini.js \${BINI_VERSION}\${COLORS.RESET} \${modeLabel}\`); // Show environments if found - Vite will show Local/Network URLs if (found.length > 0) { const envString = found.join(', '); console.log(\` \${COLORS.GREEN}✓\${COLORS.RESET} Environments: \${envString}\`); } // Status console.log(\` \${COLORS.GREEN}✓\${COLORS.RESET} Ready\\n\`); } export default { detectEnvFiles, formatEnvFilesForDisplay, displayEnvFiles, displayBiniStartup };`); // Router Plugin - FIXED with race condition protection secureWriteFile(path.join(pluginsPath, "router.js"), `import fs from 'fs'; import path from 'path'; let regenerationLock = null; const regenerationQueue = []; function scanRoutes(dir, baseRoute = '') { const routes = []; try { if (!fs.existsSync(dir)) { return routes; } const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name === 'node_modules' || entry.name.startsWith('.')) { continue; } const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { const pageFiles = ['page.tsx', 'page.jsx', 'page.ts', 'page.js']; const pageFile = pageFiles.find(f => fs.existsSync(path.join(fullPath, f))); const isDynamic = entry.name.startsWith('[') && entry.name.endsWith(']'); let currentRoutePath; if (isDynamic) { const paramName = entry.name.slice(1, -1); currentRoutePath = baseRoute + '/:' + paramName; } else { currentRoutePath = baseRoute + '/' + entry.name; } if (pageFile) { routes.push({ path: currentRoutePath, file: path.join(fullPath, pageFile), dynamic: isDynamic, params: isDynamic ? [entry.name.slice(1, -1)] : [], name: entry.name }); } routes.push(...scanRoutes(fullPath, currentRoutePath)); } } } catch (error) { console.warn(\`⚠️ Could not scan routes in \${dir}:\`, error.message); return routes; } return routes; } function generateRouterCode(appDir, isTypeScript) { const routes = scanRoutes(appDir); const rootPageFiles = ['page.tsx', 'page.jsx', 'page.ts', 'page.js']; const rootPage = rootPageFiles.find(f => fs.existsSync(path.join(appDir, f))); if (rootPage) { routes.unshift({ path: '/', file: path.join(appDir, rootPage), dynamic: false, params: [], name: 'Home' }); } routes.sort((a, b) => { if (a.dynamic && !b.dynamic) return 1; if (!a.dynamic && b.dynamic) return -1; return a.path.length - b.path.length; }); let imports = \`import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import './app/globals.css'; class ErrorBoundary extends React.Component { constructor(props) { super(props); this.state = { hasError: false, error: null }; } static getDerivedStateFromError(error) { return { hasError: true, error }; } componentDidCatch(error, errorInfo) { console.error('Page Error:', error, errorInfo); } render() { if (this.state.hasError) { return ( <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', fontFamily: 'system-ui, -apple-system, sans-serif', padding: '2rem', background: '#f8f9fa' }}> <div style={{ background: 'white', padding: '2rem', borderRadius: '1rem', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', maxWidth: '600px', textAlign: 'center' }}> <h1 style={{ fontSize: '2rem', marginBottom: '1rem', color: '#e74c3c' }}>⚠️ Page Error</h1> <p style={{ fontSize: '1rem', color: '#666', marginBottom: '1rem' }}> This page has an error. Please check the component: </p> <pre style={{ background: '#f8f9fa', padding: '1rem', borderRadius: '0.5rem', textAlign: 'left', overflow: 'auto', fontSize: '0.875rem', color: '#e74c3c' }}> {this.state.error?.toString()} </pre> <a href="/" style={{ display: 'inline-block', marginTop: '1rem', padding: '0.75rem 1.5rem', background: '#00CFFF', color: 'white', textDecoration: 'none', borderRadius: '0.5rem', fontWeight: '600' }}> ← Go Home </a> </div> </div> ); } return this.props.children; } } function SafeRoute({ component: Component, ...rest }) { return ( <ErrorBoundary> <Component {...rest} /> </ErrorBoundary> ); } function EmptyPage({ pagePath }) { return ( <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', fontFamily: 'system-ui, -apple-system, sans-serif', padding: '2rem', background: '#f8f9fa' }}> <div style={{ background: 'white', padding: '2rem', borderRadius: '1rem', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', maxWidth: '600px', textAlign: 'center' }}> <h1 style={{ fontSize: '2rem', marginBottom: '1rem', color: '#3498db' }}>📄 Empty Page</h1> <p style={{ fontSize: '1rem', color: '#666', marginBottom: '1rem' }}> This page exists but has no content yet. </p> <code style={{ background: '#f8f9fa', padding: '0.5rem 1rem', borderRadius: '0.5rem', fontSize: '0.875rem', color: '#3498db', display: 'block', marginBottom: '1rem' }}> {pagePath} </code> <p style={{ fontSize: '0.875rem', color: '#999', marginBottom: '1.5rem' }}> Add a default export to this file and it will hot reload automatically! </p> <a href="/" style={{ display: 'inline-block', padding: '0.75rem 1.5rem', background: '#00CFFF', color: 'white', textDecoration: 'none', borderRadius: '0.5rem', fontWeight: '600' }}> ← Go Home </a> </div> </div> ); }\n\`; const importMap = new Map(); let componentIndex = 0; routes.forEach(route => { const componentName = 'Page' + componentIndex++; const relativePath = path.relative(path.join(process.cwd(), 'src'), route.file).replace(/\\\\/g, '/'); let isEmpty = false; try { const fileContent = fs.readFileSync(route.file, 'utf8').trim(); if (fileContent.length === 0 || !fileContent.includes('export default')) { isEmpty = true; } } catch (err) { isEmpty = true; } if (isEmpty) { importMap.set(route.file, { empty: true, path: relativePath }); } else { imports += \`import \${componentName} from './\${relativePath.replace(/\\.tsx?$/, '').replace(/\\.jsx?$/, '')}';\n\`; importMap.set(route.file, { empty: false, component: componentName }); } }); let routesCode = \`\nexport default function App() { return ( <Router> <Routes>\n\`; routes.forEach(route => { const importInfo = importMap.get(route.file); if (!importInfo) return; const comment = route.dynamic ? \` {/* Dynamic: \${route.path} */}\` : ''; if (importInfo.empty) { routesCode += \` <Route path="\${route.path}" element={<EmptyPage pagePath="\${importInfo.path}" />} />\${comment}\n\`; } else { routesCode += \` <Route path="\${route.path}" element={<SafeRoute component={\${importInfo.component}} />} />\${comment}\n\`; } }); routesCode += \` <Route path="*" element={<NotFound />} /> </Routes> </Router> ); } function NotFound() { return ( <div style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', fontFamily: 'system-ui, -apple-system, sans-serif', background: 'linear-gradient(135deg, #00CFFF 0%, #0077FF 100%)', color: 'white' }}> <h1 style={{ fontSize: '4rem', marginBottom: '1rem', fontWeight: 'bold' }}>404</h1> <p style={{ fontSize: '1.5rem', marginBottom: '0.5rem' }}>Page not found</p> <p style={{ fontSize: '1rem', opacity: 0.8, marginBottom: '2rem' }}> The page you're looking for doesn't exist </p> <a href="/" style={{ padding: '1rem 2rem', background: 'white', color: '#00CFFF', textDecoration: 'none', borderRadius: '0.5rem', fontWeight: '600', fontSize: '1.1rem', transition: 'transform 0.2s, box-shadow 0.2s', boxShadow: '0 4px 6px rgba(0,0,0,0.1)' }} onMouseOver={(e) => { e.target.style.transform = 'translateY(-2px)'; e.target.style.boxShadow = '0 6px 12px rgba(0,0,0,0.15)'; }} onMouseOut={(e) => { e.target.style.transform = 'translateY(0)'; e.target.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)'; }}> ← Back to Home </a> </div> ); }\`; return imports + routesCode; } let regenerateTimeout = null; let lastRoutes = ''; async function regenerateRoutesWithLock(routerPlugin, changeType = 'update') { // If already regenerating, queue the request if (regenerationLock) { return new Promise((resolve) => { regenerationQueue.push(resolve); }); } regenerationLock = true; try { clearTimeout(routerPlugin.regenerateTimeout); routerPlugin.regenerateTimeout = setTimeout(async () => { try { await routerPlugin.regenerateRoutes(changeType); // Process any queued regenerations while (regenerationQueue.length > 0) { const nextResolve = regenerationQueue.shift(); await routerPlugin.regenerateRoutes('queued'); nextResolve(); } } catch (error) { console.error('❌ Route regeneration failed:', error); // Clear queue on error regenerationQueue.length = 0; } finally { regenerationLock = null; } }, 100); } catch (error) { regenerationLock = null; throw error; } } export function biniRouterPlugin() { return { name: 'bini-router-plugin', config() { const appDir = path.join(process.cwd(), 'src/app'); const appTsxPath = path.join(process.cwd(), 'src/App.tsx'); const appJsxPath = path.join(process.cwd(), 'src/App.jsx'); const isTypeScript = fs.existsSync(appTsxPath); const targetPath = isTypeScript ? appTsxPath : appJsxPath; if (fs.existsSync(appDir)) { const newCode = generateRouterCode(appDir, isTypeScript); fs.writeFileSync(targetPath, newCode, 'utf8'); lastRoutes = newCode; } }, configureServer(server) { const appDir = path.join(process.cwd(), 'src/app'); if (!fs.existsSync(appDir)) { return; } server.watcher.add(appDir); const regenerateApp = async (reason = 'File changed') => { if (regenerateTimeout) { clearTimeout(regenerateTimeout); } regenerateTimeout = setTimeout(async () => { const appTsxPath = path.join(process.cwd(), 'src/App.tsx'); const appJsxPath = path.join(process.cwd(), 'src/App.jsx'); const isTypeScript = fs.existsSync(appTsxPath); const targetPath = isTypeScript ? appTsxPath : appJsxPath; const newCode = generateRouterCode(appDir, isTypeScript); if (newCode !== lastRoutes) { fs.writeFileSync(targetPath, newCode, 'utf8'); lastRoutes = newCode; setTimeout(() => { server.ws.send({ type: 'full-reload', path: '*' }); }, 100); } }, 300); }; server.watcher.on('add', (file) => { if (file.includes('src' + path.sep + 'app') && /page\\.(tsx|jsx|ts|js)$/.test(file)) { const pageName = path.basename(path.dirname(file)); regenerateApp(\`New page: \${pageName}\`); } }); server.watcher.on('unlink', (file) => { if (file.includes('src' + path.sep + 'app') && /page\\.(tsx|jsx|ts|js)$/.test(file)) { const pageName = path.basename(path.dirname(file)); regenerateApp(\`Deleted page: \${pageName}\`); } }); server.watcher.on('addDir', (dir) => { if (dir.includes('src' + path.sep + 'app') && !dir.includes('node_modules')) { const dirName = path.basename(dir); setTimeout(() => { const pageFiles = ['page.tsx', 'page.jsx', 'page.ts', 'page.js']; const hasPage = pageFiles.some(f => fs.existsSync(path.join(dir, f))); if (hasPage) { regenerateApp(\`New directory: \${dirName}\`); } }, 500); } }); server.watcher.on('unlinkDir', (dir) => { if (dir.includes('src' + path.sep + 'app') && !dir.includes('node_modules')) { const dirName = path.basename(dir); regenerateApp(\`Deleted directory: \${dirName}\`); } }); server.watcher.on('change', (file) => { if (file.includes('src' + path.sep + 'app') && /page\\.(tsx|jsx|ts|js)$/.test(file)) { try { const fileContent = fs.readFileSync(file, 'utf8').trim(); const hasExport = fileContent.length > 0 && fileContent.includes('export default'); const pageName = path.basename(path.dirname(file)); regenerateApp(\`Content updated: \${pageName}\`); } catch (err) { } } }); }, buildStart() { const appDir = path.join(process.cwd(), 'src/app'); const appTsxPath = path.join(process.cwd(), 'src/App.tsx'); const appJsxPath = path.join(process.cwd(), 'src/App.jsx'); const isTypeScript = fs.existsSync(appTsxPath); const targetPath = isTypeScript ? appTsxPath : appJsxPath; if (fs.existsSync(appDir)) { const newCode = generateRouterCode(appDir, isTypeScript); fs.writeFileSync(targetPath, newCode, 'utf8'); } } } }`); // Preview Plugin - UPDATED with simplified display secureWriteFile(path.join(pluginsPath, "preview.js"), `import { displayBiniStartup } from '../env-checker.js'; export function biniPreviewPlugin() { return { name: 'bini-preview-plugin', configurePreviewServer(server) { // Let Vite handle the Local/Network URLs, we just add Environments and Ready setTimeout(() => { displayBiniStartup({ mode: 'preview' }); }, 100); } } }`); // Badge Plugin - UPDATED with simplified display secureWriteFile(path.join(pluginsPath, "badge.js"), `const BINIJS_VERSION = "${BINIJS_VERSION}"; import path from 'path'; import fs from 'fs'; import { displayBiniStartup } from '../env-checker.js'; function getRoutes() { const appDir = path.join(process.cwd(), 'src/app'); const routes = []; if (!fs.existsSync(appDir)) return routes; function scanDir(dir, baseRoute = '') { const entries = fs.readdirSync(dir, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { const pageFiles = ['page.tsx', 'page.jsx', 'page.ts', 'page.js']; const hasPage = pageFiles.some(f => fs.existsSync(path.join(fullPath, f))); if (hasPage) { const routePath = baseRoute + '/' + entry.name; routes.push(routePath === '/' ? '/' : routePath); } scanDir(fullPath, baseRoute + '/' + entry.name); } } } if (fs.existsSync(path.join(appDir, 'page.tsx')) || fs.existsSync(path.join(appDir, 'page.jsx'))) { routes.push('/'); } scanDir(appDir); return routes.sort(); } export function biniBadgePlugin() { let port = 3000; let routes = []; return { name: 'bini-badge-injector', configResolved(config) { port = config.server?.port || 3000; routes = getRoutes(); }, configureServer(server) { server.httpServer?.once('listening', () => { // Use the colored display function - this will show AFTER Vite's output setTimeout(() => { displayBiniStartup({ port: port, mode: 'dev', isDev: true }); }, 100); }); // Watch for route changes const appDir = path.join(process.cwd(), 'src/app'); if (fs.existsSync(appDir)) { server.watcher.add(appDir); server.watcher.on('add', (file) => { if (/page\\.(tsx|jsx|ts|js)$/.test(file)) { routes = getRoutes(); } }); server.watcher.on('unlink', (file) => { if (/page\\.(tsx|jsx|ts|js)$/.test(file)) { routes = getRoutes(); } }); } }, transformIndexHtml: { order: 'post', handler(html) { // Only inject in development mode if (process.env.NODE_ENV !== 'production' && !process.env.DISABLE_BADGE) { const routesJson = JSON.stringify(routes); const versionInfo = BINIJS_VERSION; const badgeScript = \` <style> .bini-dev-badge { position: fixed; bottom: 20px; right: 20px; background: #111; color: #fff; padding: 10px 20px; border-radius: 8px; font-size: 14px; font-weight: bold; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 9999; font-family: system-ui, -apple-system, sans-serif; user-select: none; pointer-events: auto; animation: fadeIn 0.5s ease-in; cursor: pointer; max-width: 300px; transition: all 0.3s ease; } .bini-dev-badge:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.5); } .bini-dev-badge.expanded { padding: 0; border-radius: 12px; overflow: hidden; } .bini-badge-header { padding: 12px 16px; display: flex; align-items: center; justify-content: space-between; cursor: pointer; } .bini-badge-title { display: flex; align-items: center; gap: 6px; font-weight: bold; } .bini-badge-content { display: none; max-height: 0; overflow: hidden; transition: max-height 0.3s ease; } .bini-dev-badge.expanded .bini-badge-content { display: block; max-height: 500px; border-top: 1px solid #333; } .bini-badge-section { padding: 12px 16px; border-bottom: 1px solid #333; font-size: 12px; } .bini-badge-section:last-child { border-bottom: none; } .bini-badge-label { color: #888; font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; } .bini-badge-value { color: #0fb; font-family: 'Monaco', 'Courier New', monospace; word-break: break-all; } .bini-badge-routes { display: flex; flex-direction: column; gap: 4px; } .bini-badge-route { color: #0fb; font-family: 'Monaco', 'Courier New', monospace; font-size: 11px; padding: 4px 0; } .bini-badge-toggle { color: #888; font-size: 12px; cursor: pointer; } .bini-icon { color: #00CFFF; font-weight: bold; } @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } @media (max-width: 640px) { .bini-dev-badge { bottom: 12px; right: 12px; padding: 10px 16px; font-size: 12px; max-width: 280px; } .bini-badge-section { padding: 10px 12px; font-size: 11px; } } </style> <div class="bini-dev-badge" id="bini-dev-badge"> <div class="bini-badge-header" onclick="document.getElementById('bini-dev-badge').classList.toggle('expanded')"> <span class="bini-badge-title"> <span class="bini-icon">ß</span> Bini.js <span style="font-size: 12px; color: #888;">v\${versionInfo}</span> </span> <span class="bini-badge-toggle">⌄</span> </div> <div class="bini-badge-content"> <div class="bini-badge-section"> <div class="bini-badge-label">📁 Routes (\${routes.length})</div> <div class="bini-badge-routes"> \${routes.map(route => \`<div class="bini-badge-route">\${route}</div>\`).join('')} </div> </div> <div class="bini-badge-section"> <div class="bini-badge-label">⚡ Status</div> <div class="bini-badge-value">✓ Ready</div> </div> </div> </div> <script> (function() { window.addEventListener('DOMContentLoaded', function() { function updateMemory() { if (performance.memory) { const used = Math.round(performance.memory.usedJSHeapSize / 1048576); const limit = Math.round(performance.memory.jsHeapSizeLimit / 1048576); document.getElementById('bini-memory').textContent = used + 'MB / ' + limit + 'MB'; } } updateMemory(); setInterval(updateMemory, 2000); }); window.__BINI_ROUTES__ = \${routesJson}; window.__BINI_VERSION__ = '\${versionInfo}'; })(); </script> \`; return html.replace('</body>', badgeScript + '</body>'); } return html; } } } }`); // SSR Plugin secureWriteFile(path.join(pluginsPath, "ssr.js"), `const BINIJS_VERSION = "${BINIJS_VERSION}"; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); function parseMetadata(layoutContent) { const metaTags = { title: 'Bini.js App', description: 'Modern React application built with Bini.js', keywords: '', viewport: 'width=device-width, initial-scale=1.0', openGraph: {}, twitter: {}, icons: {} }; try { // Extract the entire metadata object const metadataMatch = layoutContent.match(/export\\s+const\\s+metadata\\s*=\\s*({[\\s\\S]*?})(?=\\s*export|\\s*function|\\s*const|\\s*$)/); if (metadataMatch) { const metadataStr = metadataMatch[1]; // Helper function to extract properties const extractString = (str, prop) => { const regex = new RegExp(\`\${prop}:\\\\s*['"]([^'"]+)['"]\`, 'i'); const match = str.match(regex); return match ? match[1] : null; }; const extractArray = (str, prop) => { const regex = new RegExp(\`\${prop}:\\\\s*\\\\[([^\\\\]]+)\\\\]\`, 'i'); const match = str.match(regex); if (match) { // Simple array parsing - extract quoted values const arrayContent = match[1]; const items = arrayContent.match(/['"]([^'"]+)['"]/g) || []; return items.map(item => item.replace(/['"]/g, '')); } return null; }; const extractObject = (str, prop) => { const regex = new RegExp(\`\${prop}:\\\\s*{([^}]+)}\`, 'i'); const match = str.match(regex); if (match) { const objContent = match[1]; return { title: extractString(objContent, 'title'), description: extractString(objContent, 'description'), url: extractString(objContent, 'url'), images: extractArray(objContent, 'images') || [] }; } return null; }; // Basic metadata metaTags.title = extractString(metadataStr, 'title') || metaTags.title; metaTags.description = extractString(metadataStr, 'description') || metaTags.description; // Keywords (array) const keywordsArray = extractArray(metadataStr, 'keywords'); if (keywordsArray) { metaTags.keywords = keywordsArray.join(', '); } // Authors const authorsMatch = metadataStr.match(/authors:\\s*\\[\\s*{\\s*name:\\s*['"]([^'"]+)['"]/); if (authorsMatch) metaTags.author = authorsMatch[1]; // Viewport metaTags.viewport = extractString(metadataStr, 'viewport') || metaTags.viewport; // OpenGraph const og = extractObject(metadataStr, 'openGraph'); if (og) metaTags.openGraph = og; // Twitter const twitter = extractObject(metadataStr, 'twitter'); if (twitter) metaTags.twitter = twitter; // Icons const iconsMatch = metadataStr.m