@luxine/create-vue
Version:
A CLI scaffold for creating Vue projects
249 lines (221 loc) • 8.04 kB
JavaScript
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);
}
})();