vue3-quickstart-cli
Version:
一个用于快速创建 Vue3 项目的脚手架工具。
299 lines (297 loc) • 10.3 kB
text/typescript
import path from 'path';
import fs from 'fs-extra';
import os from 'os';
import minimist from 'minimist';
import { fileURLToPath } from 'url';
import inquirer from 'inquirer';
import plugins from './plugins/index.js';
// 简化消息系统,只保留必要的提示
import { getAvailablePm, checkPm } from './utils.js';
import { execSync } from 'child_process';
import open from 'open';
import chalk from 'chalk';
console.log('欢迎使用 Vue 3 项目初始化工具');
(async () => {
try {
const argv = minimist(process.argv.slice(2));
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const pkgLocal = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../../package.json'), 'utf-8'));
const PRESET_PATH = path.join(os.homedir(), '.my-vue3-cli-presets.json');
const FEATURE_LIST = [
{ name: 'Vue Router', value: 'router' },
{ name: 'Pinia', value: 'pinia' },
{ name: 'ESLint', value: 'eslint' },
{ name: '单元测试', value: 'unitTest' },
{ name: 'axios', value: 'axios' },
{ name: 'vueuse', value: 'vueuse' },
{ name: 'scss', value: 'scss' },
{ name: 'git提交规范(Commitizen)', value: 'commitizen' },
{ name: 'husky', value: 'husky' },
{ name: 'element-plus', value: 'elementPlus' },
{ name: 'TypeScript', value: 'typescript' },
{ name: '国际化(vue-i18n)', value: 'i18n' },
];
// 解析命令行参数
// --template --features=a,b,c --pm=pnpm --lang=zh --name=xxx --preset=xxx
const cliArgs = {
template: argv.template as string | undefined,
features: argv.features ? String(argv.features).split(',') : undefined,
pm: argv.pm as string | undefined,
lang: argv.lang as string | undefined,
name: argv.name as string | undefined,
preset: argv.preset as string | undefined,
};
// 读取预设
let presets = {} as Record<string, string[]>;
if (fs.existsSync(PRESET_PATH)) {
try {
presets = fs.readJsonSync(PRESET_PATH);
} catch {}
}
// 简化语言选择,移除messages依赖
const lang = 'zh'; // 默认使用中文
// 直接定义所需的提示文本
const t = {
projectName: '项目名称',
projectExists: '项目目录已存在,请选择其他名称或删除现有目录。',
features: '选择特性',
inputProjectName: '请输入项目名称:',
projectNameRequired: '项目名称不能为空',
savePreset: '选择预设:',
no: '不使用预设',
nextStep: '下一步操作:',
devTip: '启动开发服务器:',
enjoy: '祝您开发愉快!',
projectSuccess: '项目创建成功!',
openDir: '是否自动打开项目目录?',
openDirSuccess: '已打开项目目录',
openDirFail: '打开项目目录失败',
readmeTitle: '项目说明',
featuresTitle: '已选择特性',
readmeGen: '已生成README.md文件',
selectFeatures: '请选择需要的特性:',
inputPresetName: '请输入预设名称:',
presetNameRequired: '预设名称不能为空',
overwriteDir: '是否覆盖现有目录?'
};
// 预设选择
let usePreset = false;
let presetName = '';
let projectName = '';
let features: string[] = [];
if (cliArgs.preset && presets[cliArgs.preset]) {
usePreset = true;
presetName = cliArgs.preset;
projectName = cliArgs.name || (await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: t.inputProjectName,
validate: (input) => input ? true : t.projectNameRequired,
}
])).projectName;
features = presets[presetName];
} else if (Object.keys(presets).length > 0 && !cliArgs.features) {
const presetNames = Object.keys(presets);
const presetAns = await inquirer.prompt([
{
type: 'list',
name: 'preset',
message: t.savePreset,
choices: [
...presetNames.map(name => ({ name, value: name })),
{ name: t.no, value: '' }
],
default: ''
}
]);
if (presetAns.preset) {
usePreset = true;
presetName = presetAns.preset;
projectName = await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: t.inputProjectName,
validate: (input) => input ? true : t.projectNameRequired,
}
]).then(ans => ans.projectName);
features = presets[presetName];
}
}
if (!usePreset) {
if (cliArgs.name) {
projectName = cliArgs.name;
} else {
const projectNameAns = await inquirer.prompt([
{
type: 'input',
name: 'projectName',
message: t.inputProjectName,
validate: (input) => input ? true : t.projectNameRequired,
}
]);
projectName = projectNameAns.projectName;
}
if (cliArgs.features) {
features = cliArgs.features;
} else {
const featuresAns = await inquirer.prompt([
{
type: 'checkbox',
name: 'features',
message: t.selectFeatures,
choices: FEATURE_LIST.map(f => ({ name: f.name, value: f.value })),
}
]);
features = featuresAns.features;
}
// 选择后询问是否保存为预设
const savePresetAns = await inquirer.prompt([
{
type: 'confirm',
name: 'savePreset',
message: t.savePreset,
default: false,
}
]);
if (savePresetAns.savePreset) {
const presetNameAns = await inquirer.prompt([
{
type: 'input',
name: 'presetName',
message: t.inputPresetName,
validate: (input) => input ? true : t.presetNameRequired,
}
]);
(presets as Record<string, string[]>)[presetNameAns.presetName] = features;
fs.writeJsonSync(PRESET_PATH, presets, { spaces: 2 });
}
}
// 移除了模板选择功能
// 简化包管理器选择
let pm = cliArgs.pm || 'npm';
if (!(await checkPm(pm))) {
console.log(`未找到包管理器: ${pm},将使用npm`);
pm = 'npm';
}
const targetDir = path.resolve(process.cwd(), projectName);
if (fs.existsSync(targetDir)) {
console.log(t.overwriteDir);
process.exit(1);
}
// 插件钩子类型定义
interface UserPlugin {
name: string;
pre?(context: any): void | Promise<void>;
post?(context: any): void | Promise<void>;
}
// 自动扫描用户插件
function loadUserPlugins(projectRoot: string): UserPlugin[] {
const plugins: UserPlugin[] = [];
// 1. 扫描 node_modules/my-vue3-plugin-*
const nm = path.resolve(projectRoot, 'node_modules');
if (fs.existsSync(nm)) {
for (const dir of fs.readdirSync(nm)) {
if (/^my-vue3-plugin-/.test(dir)) {
try {
const mod = require(path.join(nm, dir));
if (mod && (mod.pre || mod.post)) plugins.push({ name: dir, ...mod });
} catch {}
}
}
}
// 2. 扫描项目根目录 plugins 目录
const localPluginsDir = path.resolve(projectRoot, 'plugins');
if (fs.existsSync(localPluginsDir)) {
for (const file of fs.readdirSync(localPluginsDir)) {
if (/\.(js|ts)$/i.test(file)) {
try {
const mod = require(path.join(localPluginsDir, file));
if (mod && (mod.pre || mod.post)) plugins.push({ name: file, ...mod });
} catch {}
}
}
}
return plugins;
}
// 创建基本项目结构
fs.mkdirSync(targetDir, { recursive: true });
console.log(`已创建项目目录: ${targetDir}`);
// 变量注入上下文
const context = {
projectName,
author: os.userInfo().username,
features,
year: new Date().getFullYear(),
lang,
targetDir,
pm,
};
// 创建基础package.json
const pkgPath = path.join(targetDir, 'package.json');
const pkg = {
name: projectName,
version: '1.0.0',
scripts: {
'dev': 'vue-cli-service serve',
'build': 'vue-cli-service build'
},
dependencies: {
'vue': '^3.3.4'
},
devDependencies: {
'@vue/cli-service': '^5.0.8'
},
};
fs.writeJsonSync(pkgPath, pkg, { spaces: 2 });
console.log('已创建基础 package.json 文件');
// 已移除自动安装依赖功能,需手动安装
// 自动 git init (可选)
try {
execSync('git init', { cwd: targetDir, stdio: 'ignore' });
console.log('已初始化 git 仓库');
} catch (e) {
console.log('git 初始化失败,可手动执行 git init');
}
// 自动打开项目目录
const openDirAns = await inquirer.prompt([
{
type: 'confirm',
name: 'openDir',
message: t.openDir,
default: false,
}
]);
if (openDirAns.openDir) {
try {
await open(targetDir);
console.log(chalk.green(t.openDirSuccess));
} catch {
console.log(chalk.yellow(t.openDirFail));
}
}
// 自动生成 README.md
const readmePath = path.join(targetDir, 'README.md');
const readmeContent = `# ${projectName}\n\n${t.readmeTitle}\n\n## ${t.featuresTitle}\n${features.map(f => `- ${f}`).join('\n')}\n`;
fs.writeFileSync(readmePath, readmeContent);
console.log(chalk.green(t.readmeGen));
// 启动提示
console.log('\n项目创建成功!');
console.log(' ' + chalk.cyan(`cd ${projectName}`));
console.log(' ' + chalk.cyan(`${pm} install`));
console.log(' ' + chalk.cyan(`${pm} run dev`));
// 打开项目目录
try {
open(targetDir);
console.log('已打开项目目录');
} catch (e) {
// 忽略打开失败
}
} catch (error) {
console.error(chalk.red('Error:'), error instanceof Error ? error.message : String(error));
process.exit(1);
}
})();