vue-quick-starter-cli
Version:
一个简单易用的Vue项目脚手架工具,快速创建Vue项目
754 lines (639 loc) • 19.1 kB
JavaScript
/**
* @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();