UNPKG

vue-quick-starter-cli

Version:

一个简单易用的Vue项目脚手架工具,快速创建Vue项目

754 lines (639 loc) 19.1 kB
/** * @fileoverview 脚手架核心功能模块 * @author vue-easy-cli * @version 1.0.7 */ // 导入依赖包 const commander = require('commander'); const chalk = require('chalk'); const ora = require('ora'); const inquirer = require('inquirer'); const fs = require('fs'); const path = require('path'); const { execSync, spawn } = require('child_process'); const fsExtra = require('fs-extra'); const execa = require('execa'); // 获取package.json中的版本号 const packageJson = require('../package.json'); /** * 初始化命令行工具 * @returns {void} */ function initCli() { // 设置版本号 commander.version(packageJson.version); // 定义create命令 commander .command('create <project-name>') .description('创建一个新的Vue项目') .action(async (projectName) => { console.log(chalk.yellow('🚀 欢迎使用 vue-quick-cli 下载工具...')); try { await createProject(projectName); console.log(chalk.green('✨ 项目创建成功!')); } catch (error) { console.error(chalk.red(`❌ 项目创建失败: ${error.message}`)); process.exit(1); } }); // 解析命令行参数 commander.parse(process.argv); // 如果没有传递任何参数,显示帮助信息 if (!process.argv.slice(2).length) { commander.outputHelp(); } } /** * 创建项目 * @param {string} projectName - 项目名称 * @returns {Promise<void>} */ async function createProject(projectName) { // 检查项目名称是否已存在 const targetDir = path.join(process.cwd(), projectName); if (fs.existsSync(targetDir)) { const { overwrite } = await inquirer.prompt([ { type: 'confirm', name: 'overwrite', message: `目录 ${projectName} 已存在,是否覆盖?`, default: false } ]); if (overwrite) { console.log(chalk.blue(`\n🗑️ 正在删除 ${projectName}...`)); fs.rmSync(targetDir, { recursive: true, force: true }); } else { console.log(chalk.red('❌ 操作取消')); return; } } // 获取用户选择的配置 const answers = await inquirer.prompt([ { type: 'list', name: 'vueVersion', message: '请选择Vue版本:', choices: ['Vue 3', 'Vue 2'], default: 'Vue 3' }, { type: 'checkbox', name: 'features', message: '请选择需要的功能:', choices: [ { name: 'TypeScript', value: 'typescript' }, { name: 'Router', value: 'router' }, { name: 'Vuex/Pinia', value: 'store' }, { name: 'ESLint', value: 'eslint' }, { name: 'Prettier', value: 'prettier' } ] } ]); // 创建项目 const spinner = ora('正在初始化项目...').start(); try { // 根据Vue版本选择模板 const template = answers.vueVersion === 'Vue 3' ? 'vue' : 'vue-ts'; // 使用 create-vite 创建基础项目 spinner.text = '使用 Vite 初始化项目...'; // 创建项目目录 fs.mkdirSync(targetDir, { recursive: true }); // 进入项目目录 process.chdir(targetDir); // 使用 npm create vite@latest 创建项目 try { const viteArgs = [ 'create', 'vite@latest', '.', '--template', answers.features.includes('typescript') ? `${template}-ts` : template, '--', '--no-git' ]; execSync(`npm ${viteArgs.join(' ')}`, { stdio: 'ignore' }); spinner.succeed('Vite 项目初始化完成'); } catch (error) { spinner.fail('Vite 项目初始化失败'); throw new Error(`创建 Vite 项目失败: ${error.message}`); } // 安装依赖 spinner.text = '开始安装依赖...'; // 安装基础依赖 try { await installDependencies(spinner); } catch (error) { spinner.warn('基础依赖安装可能不完整,请在项目创建后手动检查'); } // 安装并配置其他功能 if (answers.features.includes('router')) { spinner.text = '安装 Vue Router...'; // 安装 vue-router const routerVersion = answers.vueVersion === 'Vue 3' ? '^4.2.5' : '^3.6.5'; try { await installPackage('vue-router', routerVersion, false, spinner); // 创建路由配置文件 createRouterFiles(targetDir, answers); spinner.succeed('Vue Router 安装完成'); } catch (error) { spinner.warn(`Vue Router 安装失败: ${error.message}`); } } // 安装存储库 if (answers.features.includes('store')) { spinner.text = '安装状态管理库...'; try { if (answers.vueVersion === 'Vue 3') { await installPackage('pinia', '^2.1.6', false, spinner); createPiniaFiles(targetDir, answers); spinner.succeed('Pinia 安装完成'); } else { await installPackage('vuex', '^3.6.2', false, spinner); createVuexFiles(targetDir, answers); spinner.succeed('Vuex 安装完成'); } } catch (error) { spinner.warn(`状态管理库安装失败: ${error.message}`); } } // 安装 ESLint if (answers.features.includes('eslint')) { spinner.text = '安装 ESLint...'; try { await installPackage('eslint', '^8.49.0', true, spinner); await installPackage('eslint-plugin-vue', '^9.17.0', true, spinner); createEslintConfig(targetDir, answers); spinner.succeed('ESLint 安装完成'); } catch (error) { spinner.warn(`ESLint 安装失败: ${error.message}`); } } // 安装 Prettier if (answers.features.includes('prettier')) { spinner.text = '安装 Prettier...'; try { await installPackage('prettier', '^3.0.3', true, spinner); createPrettierConfig(targetDir); spinner.succeed('Prettier 安装完成'); } catch (error) { spinner.warn(`Prettier 安装失败: ${error.message}`); } } // 修改 package.json, 添加 serve 脚本 try { const pkgPath = path.join(targetDir, 'package.json'); const pkg = require(pkgPath); // 添加 serve 脚本指向 dev 命令 if (!pkg.scripts.serve) { pkg.scripts.serve = pkg.scripts.dev || 'vite'; fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); } } catch (error) { spinner.warn('无法更新 package.json, 请手动添加 serve 脚本'); } spinner.succeed('所有依赖安装完成'); // 显示项目使用指南 showProjectGuide(projectName); } catch (error) { spinner.fail('创建项目失败'); console.error(chalk.red(`错误: ${error.message}`)); // 清理目录 if (fs.existsSync(targetDir)) { fs.rmSync(targetDir, { recursive: true, force: true }); } throw error; } } /** * 安装依赖 * @param {Object} spinner - ora实例 * @returns {Promise<void>} */ async function installDependencies(spinner) { spinner.text = '安装项目依赖...'; return new Promise((resolve, reject) => { const npmInstall = spawn('npm', ['install'], { stdio: ['ignore', 'pipe', 'pipe'] }); npmInstall.on('close', (code) => { if (code === 0) { spinner.succeed('基础依赖安装完成'); resolve(); } else { spinner.warn('基础依赖安装失败,将尝试继续'); resolve(); // 继续流程,不要中断 } }); npmInstall.on('error', (error) => { spinner.warn(`安装失败: ${error.message}`); resolve(); // 继续流程,不要中断 }); }); } /** * 安装指定的包 * @param {string} packageName - 包名 * @param {string} version - 版本号 * @param {boolean} isDev - 是否为开发依赖 * @param {Object} spinner - ora实例 * @returns {Promise<void>} */ async function installPackage(packageName, version, isDev, spinner) { return new Promise((resolve, reject) => { const packageWithVersion = version ? `${packageName}@${version}` : packageName; const args = ['install', packageWithVersion]; if (isDev) { args.push('--save-dev'); } else { args.push('--save'); } const install = spawn('npm', args, { stdio: ['ignore', 'pipe', 'pipe'] }); install.on('close', (code) => { if (code === 0) { resolve(); } else { reject(new Error(`安装 ${packageName} 失败`)); } }); install.on('error', (error) => { reject(error); }); }); } /** * 创建Vue Router相关文件 * @param {string} targetDir - 目标目录 * @param {Object} answers - 用户选择的配置 */ function createRouterFiles(targetDir, answers) { const srcDir = path.join(targetDir, 'src'); const routerDir = path.join(srcDir, 'router'); const viewsDir = path.join(srcDir, 'views'); // 创建目录 if (!fs.existsSync(routerDir)) { fs.mkdirSync(routerDir, { recursive: true }); } if (!fs.existsSync(viewsDir)) { fs.mkdirSync(viewsDir, { recursive: true }); } // 创建路由配置文件 const isVue3 = answers.vueVersion === 'Vue 3'; const isTS = answers.features.includes('typescript'); const fileExt = isTS ? '.ts' : '.js'; // 路由配置 let routerContent = ''; if (isVue3) { routerContent = `import { createRouter, createWebHistory } from 'vue-router' import HomeView from '../views/HomeView.vue' const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ { path: '/', name: 'home', component: HomeView }, { path: '/about', name: 'about', // 路由懒加载 component: () => import('../views/AboutView.vue') } ] }) export default router `; } else { routerContent = `import Vue from 'vue' import VueRouter from 'vue-router' import HomeView from '../views/HomeView.vue' Vue.use(VueRouter) const routes = [ { path: '/', name: 'home', component: HomeView }, { path: '/about', name: 'about', // 路由懒加载 component: () => import('../views/AboutView.vue') } ] const router = new VueRouter({ mode: 'history', base: process.env.BASE_URL, routes }) export default router `; } fs.writeFileSync(path.join(routerDir, `index${fileExt}`), routerContent); // 创建视图文件 const homeViewContent = `<template> <div class="home"> <h1>Home Page</h1> <p>Welcome to your new Vue.js application!</p> </div> </template> <script${isVue3 ? ' setup' : ''}> ${isVue3 ? '' : ` export default { name: 'HomeView' } `} </script> <style> .home { padding: 20px; } </style>`; const aboutViewContent = `<template> <div class="about"> <h1>About Page</h1> <p>This is an about page</p> </div> </template> <script${isVue3 ? ' setup' : ''}> ${isVue3 ? '' : ` export default { name: 'AboutView' } `} </script> <style> .about { padding: 20px; } </style>`; fs.writeFileSync(path.join(viewsDir, 'HomeView.vue'), homeViewContent); fs.writeFileSync(path.join(viewsDir, 'AboutView.vue'), aboutViewContent); // 更新 App.vue 以包含路由视图 const appVuePath = path.join(srcDir, 'App.vue'); if (fs.existsSync(appVuePath)) { const appVueContent = `<template> <div id="app"> <nav> <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> </nav> <router-view/> </div> </template> <style> #app { font-family: Avenir, Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; margin-top: 60px; } nav { padding: 30px; } nav a { font-weight: bold; color: #2c3e50; } nav a.router-link-exact-active { color: #42b983; } </style>`; fs.writeFileSync(appVuePath, appVueContent); } // 更新 main.js 以使用路由 const mainPath = path.join(srcDir, isTS ? 'main.ts' : 'main.js'); if (fs.existsSync(mainPath)) { const mainContent = isVue3 ? `import { createApp } from 'vue' import App from './App.vue' import router from './router' createApp(App).use(router).mount('#app') ` : `import Vue from 'vue' import App from './App.vue' import router from './router' Vue.config.productionTip = false new Vue({ router, render: h => h(App) }).$mount('#app') `; fs.writeFileSync(mainPath, mainContent); } } /** * 创建Pinia状态管理文件 (Vue 3) * @param {string} targetDir - 目标目录 * @param {Object} answers - 用户选择的配置 */ function createPiniaFiles(targetDir, answers) { const srcDir = path.join(targetDir, 'src'); const storesDir = path.join(srcDir, 'stores'); // 创建目录 if (!fs.existsSync(storesDir)) { fs.mkdirSync(storesDir, { recursive: true }); } // 创建状态库文件 const isTS = answers.features.includes('typescript'); const fileExt = isTS ? '.ts' : '.js'; const counterStoreContent = `import { defineStore } from 'pinia' export const useCounterStore = defineStore('counter', { state: () => ({ count: 0 }), getters: { doubleCount: (state) => state.count * 2 }, actions: { increment() { this.count++ } } }) `; fs.writeFileSync(path.join(storesDir, `counter${fileExt}`), counterStoreContent); // 更新 main.js 以使用 Pinia const mainPath = path.join(srcDir, isTS ? 'main.ts' : 'main.js'); if (fs.existsSync(mainPath)) { let mainContent = fs.readFileSync(mainPath, 'utf8'); // 如果没有导入 pinia if (!mainContent.includes('pinia')) { if (mainContent.includes('createApp')) { // 添加 Pinia 导入 mainContent = `import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' ${mainContent.includes('router') ? "import router from './router'\n" : ''} const app = createApp(App) app.use(createPinia()) ${mainContent.includes('router') ? 'app.use(router)\n' : ''} app.mount('#app') `; } fs.writeFileSync(mainPath, mainContent); } } } /** * 创建Vuex状态管理文件 (Vue 2) * @param {string} targetDir - 目标目录 * @param {Object} answers - 用户选择的配置 */ function createVuexFiles(targetDir, answers) { const srcDir = path.join(targetDir, 'src'); const storeDir = path.join(srcDir, 'store'); // 创建目录 if (!fs.existsSync(storeDir)) { fs.mkdirSync(storeDir, { recursive: true }); } // 创建状态库文件 const isTS = answers.features.includes('typescript'); const fileExt = isTS ? '.ts' : '.js'; const storeContent = `import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { count: 0 }, getters: { doubleCount: state => state.count * 2 }, mutations: { increment(state) { state.count++ } }, actions: { increment({ commit }) { commit('increment') } }, modules: { } }) `; fs.writeFileSync(path.join(storeDir, `index${fileExt}`), storeContent); // 更新 main.js 以使用 Vuex const mainPath = path.join(srcDir, isTS ? 'main.ts' : 'main.js'); if (fs.existsSync(mainPath)) { let mainContent = fs.readFileSync(mainPath, 'utf8'); // 如果没有导入 vuex if (!mainContent.includes('store')) { mainContent = `import Vue from 'vue' import App from './App.vue' ${mainContent.includes('router') ? "import router from './router'\n" : ''} import store from './store' Vue.config.productionTip = false new Vue({ ${mainContent.includes('router') ? ' router,\n' : ''} store, render: h => h(App) }).$mount('#app') `; fs.writeFileSync(mainPath, mainContent); } } } /** * 创建 ESLint 配置文件 * @param {string} targetDir - 目标目录 * @param {Object} answers - 用户选择的配置 */ function createEslintConfig(targetDir, answers) { const isVue3 = answers.vueVersion === 'Vue 3'; const eslintConfig = { root: true, env: { node: true, browser: true, es2021: true }, extends: [ 'eslint:recommended', isVue3 ? 'plugin:vue/vue3-recommended' : 'plugin:vue/recommended' ], parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, rules: { 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off' } }; // 如果使用 TypeScript, 添加 TypeScript 解析器 if (answers.features.includes('typescript')) { eslintConfig.parser = 'vue-eslint-parser'; eslintConfig.parserOptions.parser = '@typescript-eslint/parser'; eslintConfig.extends.push('plugin:@typescript-eslint/recommended'); } // 写入 .eslintrc.js const eslintContent = `module.exports = ${JSON.stringify(eslintConfig, null, 2)}`; fs.writeFileSync(path.join(targetDir, '.eslintrc.js'), eslintContent); // 写入 .eslintignore const eslintIgnore = `node_modules dist *.log `; fs.writeFileSync(path.join(targetDir, '.eslintignore'), eslintIgnore); // 更新 package.json 添加 lint 脚本 try { const pkgPath = path.join(targetDir, 'package.json'); const pkg = require(pkgPath); // 添加 lint 脚本 pkg.scripts.lint = 'eslint --ext .js,.vue src'; if (answers.features.includes('typescript')) { pkg.scripts.lint = 'eslint --ext .js,.vue,.ts,.tsx src'; } fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2)); } catch (error) { console.warn(`无法更新 package.json: ${error.message}`); } } /** * 创建 Prettier 配置文件 * @param {string} targetDir - 目标目录 */ function createPrettierConfig(targetDir) { const prettierConfig = { semi: true, singleQuote: true, tabWidth: 2, trailingComma: 'none', printWidth: 100 }; // 写入 .prettierrc fs.writeFileSync( path.join(targetDir, '.prettierrc'), JSON.stringify(prettierConfig, null, 2) ); // 写入 .prettierignore const prettierIgnore = `node_modules dist *.log .git `; fs.writeFileSync(path.join(targetDir, '.prettierignore'), prettierIgnore); } /** * 显示项目使用指南 * @param {string} projectName - 项目名称 * @returns {void} */ function showProjectGuide(projectName) { console.log(chalk.blue('\n📝 项目初始化完成!')); console.log(chalk.green('✅ 项目结构已创建')); console.log(chalk.green('✅ 依赖已安装')); console.log(chalk.green('✅ 配置文件已生成')); console.log(chalk.blue(`\n🚀 开始使用:`)); console.log(chalk.cyan(` cd ${projectName}`)); console.log(chalk.cyan(` npm run dev\n`)); console.log(chalk.blue(`🔥 其他常用命令:`)); console.log(chalk.cyan(` npm run build # 构建生产版本`)); console.log(chalk.cyan(` npm run preview # 预览构建版本`)); console.log(chalk.cyan(` npm run lint # 代码检查和修复\n`)); } // 执行初始化 initCli();