UNPKG

b0nes

Version:

Zero-dependency component library and SSR/SSG framework

372 lines (306 loc) 10.1 kB
#!/usr/bin/env node /** * b0nes CLI - Project scaffolding tool * Creates new b0nes projects with zero dependencies * * Usage: * npx b0nes my-app * npx b0nes my-blog --template blog */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { execSync } from 'node:child_process'; import readline from 'node:readline'; // import { homeComponents, homeRoutes } from '../src/pages/examples/basic/index.js'; // import { homeComponents, blogComponents, postComponents, blogRoutes } from '../src/pages/examples/blogPost/index.js'; // import { docsComponents, docsRoutes } from '../src/pages/examples/documentation/index.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // Get the source directory (where b0nes package is installed) const getSourceDir = () => { // When running via npx, we're in node_modules/b0nes/bin/ // Source code is in node_modules/b0nes/src/ return path.resolve(__dirname, '..'); }; // Template registry const TEMPLATES = { basic: { name: 'Basic Site', description: 'Simple landing page with header, hero, and footer', pages: 'basic' }, blog: { name: 'Blog', description: 'Blog with posts, categories, and dynamic routes', pages: 'blog' }, docs: { name: 'Documentation', description: 'Documentation site with navigation', pages: 'documentation' } }; // Colors for terminal output const colors = { reset: '\x1b[0m', bright: '\x1b[1m', green: '\x1b[32m', blue: '\x1b[34m', yellow: '\x1b[33m', red: '\x1b[31m', cyan: '\x1b[36m' }; const log = { info: (msg) => console.log(`${colors.blue}${colors.reset} ${msg}`), success: (msg) => console.log(`${colors.green}${colors.reset} ${msg}`), error: (msg) => console.log(`${colors.red}${colors.reset} ${msg}`), warn: (msg) => console.log(`${colors.yellow}${colors.reset} ${msg}`), title: (msg) => console.log(`\n${colors.bright}${colors.cyan}${msg}${colors.reset}\n`) }; /** * Parse CLI arguments */ function parseArgs() { const args = process.argv.slice(2); if (args.length === 0 || args.includes('--help') || args.includes('-h')) { showHelp(); process.exit(0); } const projectName = args[0]; const templateFlag = args.indexOf('--template') !== -1 ? args[args.indexOf('--template') + 1] : null; const skipGit = args.includes('--skip-git'); return { projectName, templateFlag, skipGit }; } /** * Show help message */ function showHelp() { console.log(` ${colors.bright}${colors.cyan}b0nes${colors.reset} - Zero-dependency web framework ${colors.bright}Usage:${colors.reset} npx b0nes <project-name> [options] ${colors.bright}Options:${colors.reset} --template <name> Use a specific template (basic, blog, docs) --skip-git Skip git initialization -h, --help Show this help message ${colors.bright}Examples:${colors.reset} npx b0nes my-site npx b0nes my-blog --template blog npx b0nes my-docs --template docs --skip-git ${colors.bright}Available Templates:${colors.reset} ${Object.entries(TEMPLATES).map(([key, t]) => ` ${colors.cyan}${key.padEnd(12)}${colors.reset} ${t.description}` ).join('\n')} `); } /** * Prompt user for input */ async function prompt(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); return new Promise((resolve) => { rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); }); }); } /** * Select template interactively */ async function selectTemplate() { log.title('Select a template:'); const templateKeys = Object.keys(TEMPLATES); templateKeys.forEach((key, index) => { const template = TEMPLATES[key]; console.log(` ${colors.cyan}${index + 1}${colors.reset}. ${colors.bright}${template.name}${colors.reset}`); console.log(` ${template.description}\n`); }); const answer = await prompt('Enter template number (default: 1): '); const index = parseInt(answer || '1') - 1; if (index < 0 || index >= templateKeys.length) { log.error('Invalid template selection'); process.exit(1); } return templateKeys[index]; } /** * Create project directory */ function createProjectDir(projectName) { const projectPath = path.join(process.cwd(), projectName); if (fs.existsSync(projectPath)) { log.error(`Directory "${projectName}" already exists`); process.exit(1); } fs.mkdirSync(projectPath, { recursive: true }); return projectPath; } /** * Generate package.json */ function generatePackageJson(projectName) { return { name: projectName, version: '0.1.0', private: true, type: 'module', description: `A b0nes project`, scripts: { dev: 'node src/framework/server.js', 'dev:watch': 'node --watch src/framework/server.js', build: 'node src/framework/utils/build/index.js public', preview: 'npx serve public', test: 'node src/components/utils/tester.js', generate: 'node src/components/utils/generator/index.js', "install-component": "node src/scripts/install-component.js", "create": "node create/index.js", }, keywords: ['b0nes', 'website'], author: '', license: 'MIT', engines: { node: '>=20.0.0' } }; } /** * Generate routes.js based on template */ // function generateRoutes(projectPath, templateName) { // const routesContent = { // basic: basicRoutes, // blog: blogRoutes, // docs: docsRoutes // }; // fs.writeFileSync( // path.join(projectPath, 'src/framework/routes.js'), // routesContent[templateName] || routesContent.basic // ); // } // /** // * Generate page files // */ // function generatePage(projectPath, pageName, templateName) { // const pages = { // home: homeExample, // blog: blogExample, // post: postExample, // docs: docsExample // }; // const pagesDir = path.join(projectPath, 'src/framework/pages'); // if (!fs.existsSync(pagesDir)) { // fs.mkdirSync(pagesDir, { recursive: true }); // } // fs.writeFileSync( // path.join(pagesDir, `${pageName}.js`), // pages[pageName] || pages.home // ); // } /** * Initialize git */ function initGit(projectPath) { log.info('Initializing git repository...'); try { execSync('git init', { cwd: projectPath, stdio: 'ignore' }); execSync('git add .', { cwd: projectPath, stdio: 'ignore' }); execSync('git commit -m "Initial commit from b0nes"', { cwd: projectPath, stdio: 'ignore' }); log.success('Git repository initialized'); } catch (error) { log.warn('Failed to initialize git'); } } /** * Show success message */ function showSuccess(projectName, templateName) { const template = TEMPLATES[templateName]; console.log(` ${colors.bright}${colors.green}✓ Success!${colors.reset} Created ${colors.cyan}${projectName}${colors.reset} using ${colors.cyan}${template.name}${colors.reset} template ${colors.bright}Next steps:${colors.reset} ${colors.cyan}cd ${projectName}${colors.reset} ${colors.cyan}npm run dev${colors.reset} ${colors.bright}Available commands:${colors.reset} ${colors.cyan}npm run dev${colors.reset} Start development server with hot reload ${colors.cyan}npm run build${colors.reset} Build static site for production ${colors.cyan}npm run preview${colors.reset} Preview production build ${colors.cyan}npm run test${colors.reset} Run component tests ${colors.cyan}npm run generate${colors.reset} Generate new component ${colors.bright}Learn more:${colors.reset} ${colors.blue}https://github.com/iggydotdev/b0nes${colors.reset} Happy building! 🦴 `); } /** * Main function */ async function main() { log.title('🦴 b0nes'); const { projectName, templateFlag, skipGit } = parseArgs(); // Validate project name if (!/^[a-z0-9-]+$/.test(projectName)) { log.error('Project name can only contain lowercase letters, numbers, and hyphens'); process.exit(1); } // Select template let templateName = templateFlag; if (!templateName || !TEMPLATES[templateName]) { templateName = await selectTemplate(); } log.info(`Creating project: ${colors.cyan}${projectName}${colors.reset}`); log.info(`Using template: ${colors.cyan}${TEMPLATES[templateName].name}${colors.reset}`); // Create project const projectPath = createProjectDir(projectName); const sourceDir = getSourceDir(); const targetDir = projectPath; const source = path.join(sourceDir, 'src'); log.info(`Copy project from: ${colors.cyan}${source}${colors.reset}`); const target = path.join(targetDir, 'src'); log.info(`Copy project to: ${colors.cyan}${target}${colors.reset}`); await fs.promises.cp(source, target, { recursive: true }); // 2. Override only userland pages (and routes if needed) if (TEMPLATES[templateFlag]) { const templatePath = path.join(sourceDir, 'src/pages/examples', TEMPLATES[templateFlag].pages); if (fs.existsSync(templatePath)) { await fs.promises.cp(templatePath, path.join(targetDir, 'src/pages'), { recursive: true, force: true // overwrite }); console.log(`Applied template: ${templateFlag}`); } } // Copy root files const rootFiles = ['README.md', 'llms.txt', 'LICENSE', '.gitignore']; for (const file of rootFiles) { if (fs.existsSync(path.join(sourceDir, file))) { await fs.promises.copyFile(path.join(sourceDir, file), path.join(targetDir, file)); } } // Generate files const packageJson = generatePackageJson(projectName); fs.writeFileSync( path.join(projectPath, 'package.json'), JSON.stringify(packageJson, null, 2) ); // copyFrameworkFiles(projectPath); // generateTemplateFiles(projectPath, templateName); // Initialize git if (!skipGit) { initGit(projectPath); } // Success! showSuccess(projectName, templateName); } // Run main().catch(error => { log.error(`Failed to create project: ${error.message}`); process.exit(1); });