create-revo
Version:
Project setup tool for ReactJS and NextJS
402 lines (331 loc) • 10.2 kB
JavaScript
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import readline from "readline";
import { createRequire } from "module";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const { version: VERSION } = require('./package.json');
// ============================================================================
// CONSTANTS
// ============================================================================
const TEMPLATES = {
reactjs: 'React.js + TypeScript + TailwindCSS + Vite',
nextjs: 'Next.js + TypeScript + TailwindCSS'
};
const IGNORE_PATTERNS = ['node_modules', '.git', 'dist', 'build', '.next', '.cache'];
// ============================================================================
// UTILITIES
// ============================================================================
/**
* Validate project name
*/
function validateProjectName(name) {
if (!name || name.trim() === '') {
return { valid: false, error: 'Project name cannot be empty' };
}
if (!/^[a-zA-Z0-9-_~]+$/.test(name)) {
return {
valid: false,
error: 'Project name can only contain letters, numbers, hyphens, underscores, and tildes'
};
}
return { valid: true };
}
/**
* Sanitize project name for package.json
*/
function sanitizeProjectName(name) {
return name
.toLowerCase()
.replace(/[^a-z0-9-_~]/g, '-')
.replace(/^-+|-+$/g, '');
}
/**
* Detect package manager
*/
function detectPackageManager() {
const userAgent = process.env.npm_config_user_agent || '';
if (userAgent.includes('bun')) return 'bun';
if (userAgent.includes('pnpm')) return 'pnpm';
if (userAgent.includes('yarn')) return 'yarn';
return 'npm';
}
/**
* Get commands for package manager
*/
function getCommands(pm) {
const commands = {
npm: { install: 'npm install', dev: 'npm run dev' },
yarn: { install: 'yarn', dev: 'yarn dev' },
pnpm: { install: 'pnpm install', dev: 'pnpm dev' },
bun: { install: 'bun install', dev: 'bun dev' }
};
return commands[pm] || commands.npm;
}
// ============================================================================
// PROMPTS
// ============================================================================
/**
* Ask for project name
*/
function askProjectName() {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const ask = () => {
rl.question('Project name: ', (answer) => {
const name = answer.trim();
const validation = validateProjectName(name);
if (!validation.valid) {
console.log(`Error: ${validation.error}`);
ask();
} else {
rl.close();
resolve(name);
}
});
};
ask();
});
}
/**
* Ask for template choice
*/
function askTemplateChoice() {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log('\nSelect template:');
console.log('1. React.js + TypeScript + TailwindCSS + Vite');
console.log('2. Next.js + TypeScript + TailwindCSS');
const ask = () => {
rl.question('\nChoice (1 or 2): ', (answer) => {
const choice = answer.trim().toLowerCase();
if (choice === '1' || choice === 'react' || choice === 'reactjs') {
rl.close();
resolve('reactjs');
} else if (choice === '2' || choice === 'next' || choice === 'nextjs') {
rl.close();
resolve('nextjs');
} else {
console.log('Invalid choice. Please enter 1 or 2.');
ask();
}
});
};
ask();
});
}
// ============================================================================
// FILE OPERATIONS
// ============================================================================
/**
* Copy files recursively
*/
function copyRecursive(source, target) {
if (fs.statSync(source).isDirectory()) {
fs.mkdirSync(target, { recursive: true });
const files = fs.readdirSync(source);
for (const file of files) {
if (IGNORE_PATTERNS.includes(file)) continue;
const sourcePath = path.join(source, file);
const targetPath = path.join(target, file);
copyRecursive(sourcePath, targetPath);
}
} else {
fs.copyFileSync(source, target);
}
}
/**
* Replace placeholders in files
*/
function replacePlaceholders(directory, projectName) {
const sanitized = sanitizeProjectName(projectName);
const files = fs.readdirSync(directory);
for (const file of files) {
const filePath = path.join(directory, file);
if (IGNORE_PATTERNS.includes(file)) continue;
if (fs.statSync(filePath).isDirectory()) {
replacePlaceholders(filePath, projectName);
} else if (file.endsWith('.json') || file.endsWith('.html') ||
file.endsWith('.tsx') || file.endsWith('.ts') ||
file.endsWith('.jsx') || file.endsWith('.js') ||
file.endsWith('.md')) {
let content = fs.readFileSync(filePath, "utf8");
if (file === "package.json") {
try {
const json = JSON.parse(content);
json.name = sanitized;
content = JSON.stringify(json, null, 2);
} catch (e) {
content = content.replace(/\{\{projectName\}\}/g, sanitized);
}
} else {
content = content.replace(/\{\{projectName\}\}/g, projectName);
}
fs.writeFileSync(filePath, content);
}
}
}
/**
* Create .gitignore
*/
function createGitignore(projectDir) {
const content = `# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
/dist
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
bun-debug.log*
# env files
.env*
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
`;
fs.writeFileSync(path.join(projectDir, '.gitignore'), content);
}
// ============================================================================
// CLI ARGS
// ============================================================================
/**
* Parse CLI arguments
*/
function parseArgs() {
const args = process.argv.slice(2);
const options = {
projectName: null,
template: null,
help: false,
version: false
};
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '-h' || arg === '--help') {
options.help = true;
} else if (arg === '-v' || arg === '--version') {
options.version = true;
} else if (arg === '-t' || arg === '--template') {
options.template = args[++i];
} else if (!arg.startsWith('-') && !options.projectName) {
options.projectName = arg;
}
}
return options;
}
/**
* Show help
*/
function showHelp() {
console.log('\nUsage: create-revo <project-name> [options]');
console.log('\nOptions:');
console.log(' -t, --template <type> Template type (react or next)');
console.log(' -h, --help Show help');
console.log(' -v, --version Show version');
console.log('\nExamples:');
console.log(' create-revo my-app');
console.log(' create-revo my-app --template react');
console.log(' create-revo my-app -t next\n');
}
// ============================================================================
// MAIN
// ============================================================================
async function main() {
const options = parseArgs();
if (options.help) {
showHelp();
process.exit(0);
}
if (options.version) {
console.log(VERSION);
process.exit(0);
}
try {
// Get project name
let projectName = options.projectName;
if (!projectName) {
projectName = await askProjectName();
} else {
const validation = validateProjectName(projectName);
if (!validation.valid) {
console.log(`Error: ${validation.error}`);
process.exit(1);
}
}
// Check if directory exists
const targetDir = path.join(process.cwd(), projectName);
if (fs.existsSync(targetDir)) {
console.log(`Error: Directory '${projectName}' already exists`);
process.exit(1);
}
// Get template
let template = options.template;
if (template) {
if (template === 'react' || template === '1') template = 'reactjs';
else if (template === 'next' || template === '2') template = 'nextjs';
if (!TEMPLATES[template]) {
console.log(`Error: Invalid template '${options.template}'`);
console.log('Valid templates: react, next');
process.exit(1);
}
} else {
template = await askTemplateChoice();
}
// Create project
const templateDir = path.join(__dirname, `template-${template}`);
if (!fs.existsSync(templateDir)) {
console.log(`Error: Template directory not found`);
process.exit(1);
}
console.log(`\nCreating project...`);
const start = Date.now();
// Copy files
fs.mkdirSync(targetDir, { recursive: true });
copyRecursive(templateDir, targetDir);
// Configure
replacePlaceholders(targetDir, projectName);
createGitignore(targetDir);
const duration = Date.now() - start;
// Done
const pm = detectPackageManager();
const cmds = getCommands(pm);
console.log(`\nDone in ${duration}ms\n`);
console.log('Next steps:');
console.log(` cd ${projectName}`);
console.log(` ${cmds.install}`);
console.log(` ${cmds.dev}\n`);
} catch (error) {
console.log(`\nError: ${error.message}\n`);
process.exit(1);
}
}
main();