UNPKG

@luxine/create-vue

Version:

A CLI scaffold for creating Vue projects

249 lines (221 loc) 8.04 kB
#!/usr/bin/env node import { Command } from 'commander'; import inquirer from 'inquirer'; import fs from 'fs-extra'; import { resolve } from 'node:path'; import chalk from 'chalk'; import ora from 'ora'; import degit from 'degit'; import symbols from 'log-symbols'; import boxen from 'boxen'; import { execa } from 'execa'; // ── 日志工具 ─────────────────────────────────────────────── const log = { info: (msg) => console.log(symbols.info, chalk.cyan(msg)), ok: (msg) => console.log(symbols.success, chalk.green(msg)), warn: (msg) => console.log(symbols.warning, chalk.yellow(msg)), error: (msg) => console.error(symbols.error, chalk.red(msg)), step: (title) => console.log('\n' + chalk.bold.magenta('›') + ' ' + chalk.bold(title)) }; const isCommandAvailable = async (cmd) => { try { await execa.command(`${cmd} --version`, { stdio: 'ignore' }); return true; } catch { return false; } }; const runWithSpinner = async (commandLabel, commandFn) => { const spinner = ora(commandLabel).start(); try { const result = await commandFn(); spinner.succeed(chalk.green(commandLabel)); return result; } catch (err) { spinner.fail(chalk.red(commandLabel)); throw err; } }; // ── CLI 配置 ─────────────────────────────────────────────── const program = new Command(); program .name('luxine-vue-template') .description('初始化项目模板,自动安装依赖并启动开发服务') .option('-n, --name <projectName>', 'project name') .option('-d, --dir <directory>', 'target directory', process.cwd()) .option('-p, --package-manager <npm|yarn|pnpm>', '优先使用的包管理器') .option('--no-start', '跳过自动启动开发服务') .parse(process.argv); const options = program.opts(); // ── 主逻辑 ──────────────────────────────────────────────── (async () => { try { let template_url = 'luxine/Vue-template#main'; // 可以改成参数化 // 1. 项目名 const { projectName: rawName } = await inquirer.prompt([ { type: 'input', name: 'projectName', message: chalk.bold('Project name:'), default: options.name || 'vue-project', validate: (v) => { if (!v || !v.trim()) return '名称不能为空'; return true; } } ]); // 简单清洗 const projectName = rawName.trim().replace(/\s+/g, '-'); const availablePMs = ['pnpm', 'yarn', 'npm']; let packageManager = null; if (options.packageManager) { if (availablePMs.includes(options.packageManager)) { packageManager = options.packageManager; } else { log.warn(`指定的包管理器 "${options.packageManager}" 不可用,自动降级使用可用的:${availablePMs.join(', ')}`); packageManager = availablePMs[0]; } } else { // 交互选择,默认第一个(优先 pnpm) const { pm } = await inquirer.prompt([ { type: 'list', name: 'pm', message: '选择包管理器:', choices: availablePMs, default: availablePMs[0] } ]); packageManager = pm; } // 交互选择 const { docker } = await inquirer.prompt([ { type: 'input', name: 'init dockerfile', message: '是否初始化 Dockerfile?(y/n)', default: 'y', validate: (v) => { if (v.trim().toLowerCase() === 'y' || v.trim().toLowerCase() === 'n') { return true; } else { '请输入 y 或 n' return false; } }, } ]); if (docker === 'y') { template_url = 'luxine/Vue-template#main' } const { nginx } = await inquirer.prompt([ { type: 'input', name: 'init nginx', message: '是否初始化 nginx 相关 ( nginx.conf / nssm ) ?(y/n)', default: 'y', validate: (v) => { if (v.trim().toLowerCase() === 'y' || v.trim().toLowerCase() === 'n') { return true; } else { '请输入 y 或 n' return false; } }, } ]); if (nginx === 'y') { template_url = 'luxine/Vue-template#main' } log.step(`使用包管理器: ${chalk.bold(packageManager)}`); // 3. 目标目录 const targetDir = resolve(options.dir, projectName); // 4. 目录存在性检查 if (await fs.pathExists(targetDir)) { log.error(`目录已存在:${targetDir}`); process.exit(1); } // 5. 拉取远程模板 await runWithSpinner(`初始化模板 → ${targetDir}`, async () => { const emitter = degit(template_url, { cache: false, force: true, verbose: false }); await emitter.clone(targetDir); }); log.ok(`模板初始化完成:${chalk.bold(targetDir)}`); // 6. 安装依赖 const installCmdMap = { pnpm: ['pnpm', ['install']], yarn: ['yarn', []], // yarn 默认安装 npm: ['npm', ['install']] }; const [installBin, installArgs] = installCmdMap[packageManager]; await runWithSpinner(`安装必要的依赖( ${packageManager} )`, async () => { await execa(installBin, installArgs, { cwd: targetDir + '/base', stdio: 'inherit' }); }); log.ok('依赖安装完成'); // 7. 识别 dev/start 脚本 let scriptName = null; const pkgJsonPath = resolve(targetDir, 'package.json'); if (await fs.pathExists(pkgJsonPath)) { const pkg = await fs.readJson(pkgJsonPath); if (pkg.scripts?.dev) scriptName = 'dev'; else if (pkg.scripts?.start) scriptName = 'start'; } else { log.warn('package.json 不存在,无法自动启动开发服务。'); } if (!scriptName) { log.warn('未在 package.json 里发现 dev 或 start 脚本。'); } // 8. 展示最终说明(如果跳过启动或者找不到脚本) if (!options.start || !scriptName) { const fallbackInstructions = ` ${chalk.cyan('cd')} ${projectName + '/base'} ${chalk.cyan(packageManager)} ${scriptName ? (packageManager === 'npm' ? `run ${scriptName}` : scriptName) : 'dev'} `; console.log( boxen(fallbackInstructions.trim(), { padding: 1, margin: 1, borderStyle: 'round', borderColor: 'green' }) ); if (!options.start) { log.ok('已跳过自动启动开发服务。'); process.exit(0); } if (!scriptName) { process.exit(0); } } // 9. 启动开发服务(阻塞,输出直接透传) log.step(`启动开发服务:${scriptName}`); const startCmdMap = { pnpm: ['pnpm', [scriptName]], yarn: [scriptName === 'start' ? 'yarn' : 'yarn', [scriptName]], // yarn dev / yarn start npm: ['npm', ['run', scriptName]] }; const [startBin, startArgs] = startCmdMap[packageManager]; const devProcess = execa(startBin, startArgs, { cwd: targetDir, stdio: 'inherit' }); // Ctrl+C 传递给子进程 const handleSigint = () => { if (!devProcess.killed) { devProcess.kill('SIGINT'); } process.exit(0); }; process.on('SIGINT', handleSigint); process.on('SIGTERM', handleSigint); await devProcess; log.ok('开发服务已退出。'); process.exit(0); } catch (err) { log.error(typeof err === 'object' ? (err.message || JSON.stringify(err)) : String(err)); process.exit(1); } })();