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
JavaScript
#!/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