UNPKG

vue3-quickstart-cli

Version:

一个用于快速创建 Vue3 项目的脚手架工具。

299 lines (297 loc) 10.3 kB
#!/usr/bin/env node 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); } })();