create-vvt
Version:
一个基于 Vite + Vue3 + TypeScript/JavaScript 的项目模板脚手架
395 lines (352 loc) • 11.8 kB
JavaScript
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { Command } from 'commander';
import inquirer from 'inquirer';
import chalk from 'chalk';
import ora from 'ora';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const program = new Command();
// 定义可选插件
const PLUGINS = [
{
name: 'postcss-pxtorem',
value: 'pxtorem',
description: '将 px 单位转换为 rem 单位',
devDependencies: {
'postcss-pxtorem': '^6.1.0'
}
},
{
name: 'tailwindcss(v3.x)',
value: 'tailwind',
description: '功能优先的 CSS 框架',
devDependencies: {
tailwindcss: '3'
}
},
{
name: 'vite-svg-loader',
value: 'svgLoader',
description: '以组件形式加载 SVG',
devDependencies: {
'vite-svg-loader': '^5.1.0'
}
}
];
// 语言选项
const LANGUAGES = {
typescript: {
name: 'TypeScript',
value: 'typescript',
description: '使用 TypeScript 语法(类型安全的 JavaScript 超集)',
templateDir: 'templates/typescript'
},
javascript: {
name: 'JavaScript',
value: 'javascript',
description: '使用 JavaScript 语法(更简洁,无类型检查)',
templateDir: 'templates/javascript'
}
};
program
.name('create-vite-vue3-ts')
.description('基于 Vite + Vue3 + TypeScript/JavaScript 的项目模板')
.version('0.1.0')
.argument('[project-name]', '项目名称')
.action(async (projectName) => {
try {
await createProject(projectName);
} catch (error) {
console.error(chalk.red('错误:') + error.message);
process.exit(1);
}
});
program.parse(process.argv);
async function createProject(projectName) {
try {
// 获取项目名称
const { name, description, author } = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: '请输入项目名称:',
default: projectName || 'vite-vue3-project',
validate: (input) => {
if (!input.trim()) {
return '项目名称不能为空';
}
return true;
}
},
{
type: 'input',
name: 'description',
message: '请输入项目描述:',
default: '基于 Vite + Vue3 的项目模板'
},
{
type: 'input',
name: 'author',
message: '请输入作者名称:',
default: 'egg'
}
]);
// 选择语言
const { language } = await inquirer.prompt([
{
type: 'list',
name: 'language',
message: '请选择开发语言:',
choices: Object.values(LANGUAGES).map((lang) => ({
name: `${lang.name} (${lang.description})`,
value: lang.value
})),
default: 'typescript'
}
]);
const targetDir = path.join(process.cwd(), name);
// 检查目录是否存在
if (fs.existsSync(targetDir)) {
const { overwrite } = await inquirer.prompt([
{
type: 'confirm',
name: 'overwrite',
message: `目标目录 ${chalk.cyan(name)} 已存在。是否要覆盖?`,
default: true
}
]);
if (!overwrite) {
throw new Error('操作取消');
}
const spinner = ora('正在清理目录...').start();
await fs.promises.rm(targetDir, { recursive: true, force: true });
spinner.succeed(chalk.green('目录清理完成'));
}
// 逐个选择插件
const selectedPlugins = [];
for (const plugin of PLUGINS) {
const { install } = await inquirer.prompt([
{
type: 'confirm',
name: 'install',
message: `是否安装 ${plugin.name}(${plugin.description})?`,
default: false
}
]);
if (install) {
selectedPlugins.push(plugin.value);
}
}
// 创建项目
const spinner = ora(chalk.bgYellow('正在创建项目...')).start();
// 复制模板
const templateDir = path.resolve(
__dirname,
'..',
LANGUAGES[language].templateDir
);
fs.mkdirSync(targetDir, { recursive: true });
await copyTemplate(templateDir, targetDir);
// 更新配置文件
spinner.text = '正在更新配置文件...';
await updateProjectFiles(targetDir, selectedPlugins, {
name,
description,
author,
language
});
await updatePackageJson(targetDir, selectedPlugins, {
name,
description,
author
});
spinner.succeed(chalk.green('项目创建成功!'));
// 输出使用说明
console.log('\n使用说明:');
console.log(chalk.cyan(` cd ${name}`));
console.log(chalk.cyan(' pnpm install'));
console.log(chalk.cyan(' pnpm dev\n'));
} catch (error) {
const spinner = ora();
spinner.fail(chalk.red('项目创建失败:' + error.message));
throw error;
}
}
async function updateProjectFiles(root, selectedPlugins, projectInfo) {
// 根据选择的语言决定入口文件的扩展名
const mainExtension = projectInfo.language === 'typescript' ? '.ts' : '.js';
const mainPath = path.join(root, `src/main${mainExtension}`);
if (!fs.existsSync(mainPath)) {
throw new Error(`找不到主入口文件: ${mainPath}`);
}
let mainContent = fs.readFileSync(mainPath, 'utf-8');
// 处理 CSS 配置文件
const cssConfigExtension =
projectInfo.language === 'typescript' ? '.ts' : '.js';
const cssConfigPath = path.join(
root,
`viteConfig/css/index${cssConfigExtension}`
);
// 根据选择的插件修改配置
if (fs.existsSync(cssConfigPath)) {
let cssConfig = fs.readFileSync(cssConfigPath, 'utf-8');
// 处理 pxtorem 插件
if (!selectedPlugins.includes('pxtorem')) {
const remUnitExtension =
projectInfo.language === 'typescript' ? '.ts' : '.js';
const remUnitPath = path.join(root, `lib/remUnit${remUnitExtension}`);
if (fs.existsSync(remUnitPath)) {
fs.unlinkSync(remUnitPath);
// 如果 lib 目录为空,也删除该目录
const libDir = path.dirname(remUnitPath);
if (fs.readdirSync(libDir).length === 0) {
fs.rmdirSync(libDir);
}
}
// TypeScript特有的注释,在JavaScript中可能不存在
if (projectInfo.language === 'typescript') {
cssConfig = cssConfig.replace(/\/\/ @ts-expect-error.*\n/, '');
}
cssConfig = cssConfig.replace(/import pxtorem.*;\n/, '');
cssConfig = cssConfig.replace(/\s*pxtorem\({[^}]+}\),?\n?/, '');
mainContent = mainContent.replace(/import '\.\.\/lib\/remUnit';\n/, '');
}
// 处理 tailwindcss 插件
if (!selectedPlugins.includes('tailwind')) {
cssConfig = cssConfig.replace(/import tailwindcss.*;\n/, '');
cssConfig = cssConfig.replace(/\s*tailwindcss\(\),?\n?/, '');
mainContent = mainContent.replace(/import '\.\/tailwind.css';\n/, '');
const tailwindPath = path.join(root, 'src/tailwind.css');
if (fs.existsSync(tailwindPath)) {
fs.unlinkSync(tailwindPath);
}
const tailwindConfigExtension =
projectInfo.language === 'typescript' ? '.ts' : '.js';
const tailwindConfigPath = path.join(
root,
`tailwind.config${tailwindConfigExtension}`
);
if (fs.existsSync(tailwindConfigPath)) {
fs.unlinkSync(tailwindConfigPath);
}
}
fs.writeFileSync(cssConfigPath, cssConfig);
fs.writeFileSync(mainPath, mainContent);
}
const commonPluginsExtension =
projectInfo.language === 'typescript' ? '.ts' : '.js';
const commonPluginsPath = path.join(
root,
`viteConfig/plugins/common${commonPluginsExtension}`
);
if (fs.existsSync(commonPluginsPath)) {
let commonPlugins = fs.readFileSync(commonPluginsPath, 'utf-8');
// 处理 tailwindcss 插件
if (!selectedPlugins.includes('tailwind')) {
// 处理tailwindcss导入,路径可能在JavaScript和TypeScript中不同
commonPlugins = commonPlugins.replace(
/import ['"]@tailwindcss\/vite['"];\n/,
''
);
commonPlugins = commonPlugins.replace(/\s*tailwindcss\(\),?\n?/, '');
mainContent = mainContent.replace(/import '\.\/tailwind.css';\n/, '');
const tailwindPath = path.join(root, 'src/tailwind.css');
if (fs.existsSync(tailwindPath)) {
fs.unlinkSync(tailwindPath);
}
fs.writeFileSync(commonPluginsPath, commonPlugins);
fs.writeFileSync(mainPath, mainContent);
}
}
const staticPerfConfigExtension =
projectInfo.language === 'typescript' ? '.ts' : '.js';
const staticPerfConfigPath = path.join(
root,
`viteConfig/plugins/staticPerf${staticPerfConfigExtension}`
);
if (fs.existsSync(staticPerfConfigPath)) {
let staticPerfConfig = fs.readFileSync(staticPerfConfigPath, 'utf-8');
// 处理 svgLoader 插件
if (!selectedPlugins.includes('svgLoader')) {
const appVuePath = path.join(root, 'src/App.vue');
if (fs.existsSync(appVuePath)) {
let appContent = fs.readFileSync(appVuePath, 'utf-8');
// 删除 import 语句
appContent = appContent.replace(/import VueView.*;\n/, '');
appContent = appContent.replace(/import ViteView.*;\n/, '');
// 删除<script>标签 - 确保能匹配任何script标签
appContent = appContent.replace(
/<script(\s+setup)?(\s+lang="ts")?>\s*[\s\S]*?<\/script>/,
''
);
// 替换 SVG 组件为 img 标签
appContent = appContent.replace(
/<ViteView width="40" height="40" class="logo" \/>/,
'<img src="./assets/icons/vite.svg" width="40" height="40" class="logo" alt="Vite logo" />'
);
appContent = appContent.replace(
/<VueView width="40" height="40" class="logo" \/>/,
'<img src="./assets/icons/vue.svg" width="40" height="40" class="logo vue" alt="Vue logo" />'
);
fs.writeFileSync(appVuePath, appContent);
}
staticPerfConfig = staticPerfConfig.replace(
/import.*vite-svg-loader.*;\n/,
''
);
staticPerfConfig = staticPerfConfig.replace(
/\s*svgLoader\([^)]*\),?\n?/,
''
);
}
fs.writeFileSync(staticPerfConfigPath, staticPerfConfig);
}
}
async function updatePackageJson(root, selectedPlugins, projectInfo) {
const pkgPath = path.join(root, 'package.json');
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
// 更新项目信息
pkg.name = projectInfo.name;
pkg.description = projectInfo.description;
pkg.author = projectInfo.author;
// 获取选中插件的依赖
const devDependencies = {};
for (const plugin of PLUGINS.filter((p) =>
selectedPlugins.includes(p.value)
)) {
Object.assign(devDependencies, plugin.devDependencies);
}
// 更新 package.json
pkg.devDependencies = {
...pkg.devDependencies,
...devDependencies
};
// 移除未选中插件的依赖
PLUGINS.forEach((plugin) => {
if (!selectedPlugins.includes(plugin.value)) {
Object.keys(plugin.devDependencies).forEach((dep) => {
delete pkg.dependencies[dep];
delete pkg.devDependencies[dep];
});
}
});
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
}
function copyTemplate(src, dest) {
const stat = fs.statSync(src);
if (stat.isDirectory()) {
copyDir(src, dest);
} else {
fs.copyFileSync(src, dest);
}
}
function copyDir(srcDir, destDir) {
fs.mkdirSync(destDir, { recursive: true });
for (const file of fs.readdirSync(srcDir)) {
const srcFile = path.resolve(srcDir, file);
const destFile = path.resolve(destDir, file);
copyTemplate(srcFile, destFile);
}
}