@falconix/ymcli
Version:
A CLI tool for creating and building multi-platform Vue projects
648 lines (566 loc) • 17.3 kB
JavaScript
const fs = require("fs-extra");
const path = require("path");
const { execa } = require("execa");
/**
* 检查路径是否存在
* @param {string} path 路径
* @returns {boolean} 是否存在
*/
async function pathExists(path) {
return await fs.pathExists(path);
}
/**
* 创建目录
* @param {string} dir 目录路径
*/
async function createDirectory(dir) {
await fs.ensureDir(dir);
}
/**
* 执行命令
* @param {string} command 命令
* @param {string[]} args 参数
* @param {object} options 选项
* @returns {Promise} 执行结果
*/
async function runCommand(command, args = [], options = {}) {
return execa(command, args, { stdio: "inherit", ...options });
}
/**
* 创建 .gitignore 文件
* @param {string} projectPath 项目路径
*/
async function createGitignore(projectPath) {
const gitignoreContent = `# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/*.code-snippets
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
`;
const gitignorePath = path.join(projectPath, ".gitignore");
await fs.writeFile(gitignorePath, gitignoreContent);
}
/**
* 在项目根目录创建.vscode配置
* @param {string} rootPath 项目根目录路径
*/
async function createRootVscodeConfig(rootPath) {
const vscodeDir = path.join(rootPath, ".vscode");
await fs.ensureDir(vscodeDir);
// 创建extensions.json
const extensionsConfig = {
recommendations: [
"Vue.volar",
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
],
};
await fs.writeFile(
path.join(vscodeDir, "extensions.json"),
JSON.stringify(extensionsConfig, null, 2),
"utf8"
);
// 创建settings.json
const settingsConfig = {
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
},
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"workbench.colorCustomizations": {
"activityBar.activeBackground": "#c8c255",
"activityBar.background": "#c8c255",
"activityBar.foreground": "#e7e7e7",
"activityBar.inactiveForeground": "#e7e7e799",
"activityBarBadge.background": "#e8e3fb",
"activityBarBadge.foreground": "#15202b",
"commandCenter.border": "#e7e7e799",
"sash.hoverBorder": "#c8c255",
"statusBar.background": "#c8c255",
"statusBar.foreground": "#e7e7e7",
"statusBarItem.hoverBackground": "#c8c255",
"statusBarItem.remoteBackground": "#c8c255",
"statusBarItem.remoteForeground": "#e7e7e7",
"titleBar.activeBackground": "#c8c255",
"titleBar.activeForeground": "#e7e7e7",
"titleBar.inactiveBackground": "#c8c25599",
"titleBar.inactiveForeground": "#e7e7e799",
},
"peacock.color": "#c8c255",
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
},
};
await fs.writeFile(
path.join(vscodeDir, "settings.json"),
JSON.stringify(settingsConfig, null, 2),
"utf8"
);
console.log("Created .vscode configuration in project root");
}
/**
* 创建Vite配置文件,指定输出目录
* @param {string} projectPath 项目路径(web或mobile目录)
* @param {string} platform 平台类型(web或mobile)
*/
async function createViteConfig(projectPath, platform) {
const viteConfigContent = `import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { VantResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [
ElementPlusResolver(),
VantResolver()
],
}),
Components({
resolvers: [
ElementPlusResolver(),
VantResolver()
],
}),
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src')
}
},
build: {
outDir: path.resolve(__dirname, \`../dist/\${${
platform === "web" ? "'web'" : "'mobile'"
}}\`),
assetsDir: 'assets',
sourcemap: false
},
server: {
port: ${platform === "web" ? 3000 : 3001},
open: true,
cors: true
}
})`;
await fs.writeFile(
path.join(projectPath, "vite.config.ts"),
viteConfigContent,
"utf8"
);
console.log(`Created vite.config.ts for ${platform} project`);
}
/**
* 创建环境配置文件
* @param {string} targetDir 目标目录路径
*/
async function createEnvFiles(targetDir) {
// 开发环境配置 (.env.development)
const developmentContent = `VITE_API_BASE_URL=/api
VITE_APP_NAME=鹰脉本地开发环境
VITE_DEBUG_MENUS=["home", "new_project", "rcgs", "work", "clue", "financial", "gantt", "others", "oldfinance", "crew", "recruit", "training", "supply_chain","knowledgeBase"]
VITE_DEBUG_BUTTONS=[":others:clientManagement:edit",":others:clientManagement:delete",":others:clientManagement:export",":others:clientManagement:create",":training:trainingAnnualPlan:tabs:trainingAnnualPlanByDeptList:createSummary",":training:trainingAnnualPlan:tabs:trainingAnnualPlanByDeptList:distributionRequirements",":training:trainingAnnualPlan:tabs:trainingAnnualPlanByTerritoryList:createSummary",":training:trainingMonthlyPlan:tabs:trainingMonthlyPlanList:createSummary",":recruit:recruitAnnualPlan:tabs:recruitAnnualPlanList:createSummary",":recruit:recruitAnnualPlan:tabs:recruitAnnualPlanList:sendReminder",":clue:list:tabs:clueList:export",":new_project:list:tabs:development_project:development_project_create",":new_project:list:tabs:contract_project:contract_project_create"]
VITE_LEGACY_WEBURL=http://localhost:8080
VITE_CHAT_WS_URL=ws://10.168.2.227:8878/im
VITE_AI_BASE_URL=http://10.168.2.227:8001
VITE_VIDEO_WS=10.168.2.140:18866 #课程库websc视频ip地址
VITE_CTYUN_BUCKETNAME=yinglianbangnew
VITE_PREVIEWFILEURL=https://test.yinglianbang.cn:1443/prev/onlinePreview
VITE_FLOW_URL=http://10.168.2.129:8190
`;
// 开发服务器配置 (.env.devserver)
const devserverContent = `VITE_CTYUN_BUCKETNAME=yinglianbangnew
VITE_PREVIEWFILEURL=https://test.yinglianbang.cn:1443/prev/onlinePreview
VITE_FLOW_URL=http://10.168.2.129:8190
`;
// 生产环境配置 (.env.production)
const productionContent = `VITE_API_BASE_URL=https://api.yingmai.net
VITE_APP_NAME=鹰脉
VITE_LEGACY_WEBURL=https://original.yingmai.net
VITE_CHAT_WS_URL=wss://api.yingmai.net:8877/wss/im
VITE_AI_BASE_URL=https://api.yingmai.net:8877/api/ai
VITE_VIDEO_WS=www.yingmai.net:18866 #课程库websc视频ip地址
VITE_CTYUN_BUCKETNAME=yinglianbang
VITE_PREVIEWFILEURL=https://www.yingmai.net/prev/onlinePreview
VITE_FLOW_URL=https://www.yingmai.net:8190
`;
// 测试环境配置 (.env.test)
const testContent = `VITE_API_BASE_URL=https://testapi.yinglianbang.cn:8110/api
VITE_APP_NAME=鹰脉测试环境
VITE_LEGACY_WEBURL=https://test.yinglianbang.cn:1443
VITE_CHAT_WS_URL=wss://testapi.yinglianbang.cn:8877/wss/im
VITE_AI_BASE_URL=https://testapi.yinglianbang.cn:8877/api/ai
VITE_VIDEO_WS=test.yinglianbang.cn:18866 #课程库websc视频ip地址
VITE_CTYUN_BUCKETNAME=yinglianbangnew
VITE_PREVIEWFILEURL=https://test.yinglianbang.cn:1443/prev/onlinePreview
VITE_FLOW_URL=https://test.yinglianbang.cn:8190
`;
// 环境文件配置列表
const envFiles = [
{ name: ".env.development", content: developmentContent },
{ name: ".env.devserver", content: devserverContent },
{ name: ".env.production", content: productionContent },
{ name: ".env.test", content: testContent },
];
// 为每个环境文件写入对应内容
for (const file of envFiles) {
const filePath = path.join(targetDir, file.name);
await fs.writeFile(filePath, file.content, "utf8");
console.log(`Created environment file: ${filePath}`);
}
}
/**
* 调整package.json中的scripts配置
* @param {string} projectPath 项目路径
*/
async function adjustPackageJsonScripts(projectPath) {
const packageJsonPath = path.join(projectPath, "package.json");
const packageJson = await fs.readJson(packageJsonPath);
packageJson.scripts = {
dev: "vite",
build: 'run-p type-check "build-only {@}" --',
"build-test": 'run-p type-check "build-only-test {@}" --',
"build-dev": 'run-p type-check "build-only-devserver {@}" --',
preview: "vite preview",
"build-only": "vite build",
"build-only-test": "vite build --mode test",
"build-only-devserver": "vite build --mode devserver",
"type-check": "vue-tsc --build --force",
lint: "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
format: "prettier --write src/",
prepare: "husky",
};
await fs.writeJson(packageJsonPath, packageJson, { spaces: 2 });
console.log(`Updated scripts in package.json: ${packageJsonPath}`);
}
/**
* 创建网络请求相关文件(axios配置和api)
* @param {string} srcPath 项目的src目录路径
*/
async function createNetworkAndApiFiles(srcPath) {
const networkDir = path.join(srcPath, "network");
await fs.ensureDir(networkDir);
const axiosContent = `import axios, { AxiosError } from 'axios';
declare global {
interface Window {
$wujie?: {
props?: {
getToken?: () => string;
bus?: {
emit: (event: string, ...args: any[]) => void;
};
};
};
__POWERED_BY_WUJIE__?: boolean;
}
}
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 30000,
headers: {
'Content-Type': 'application/json;charset=utf-8',
},
});
service.interceptors.request.use(
(config) => {
if (window.$wujie?.props?.getToken) {
const token = window.$wujie.props.getToken();
if (token) {
config.headers.Authorization = token;
}
} else {
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = token;
}
}
return config;
},
(error: AxiosError) => {
return Promise.reject(error);
}
);
service.interceptors.response.use(
(response) => {
const resData = response.data;
if (resData instanceof Blob) {
return response;
}
if (resData.code === 403) {
handle403Error();
return Promise.reject('Login expired');
}
return response;
},
(error: AxiosError) => {
return Promise.reject(error);
}
);
function handle403Error() {
if (window.__POWERED_BY_WUJIE__ && window.$wujie?.props?.bus) {
window.$wujie.props.bus.emit('loginExpired');
} else {
window.location.href = '/login';
}
}
export default service;`;
await fs.writeFile(path.join(networkDir, "axios.ts"), axiosContent, "utf8");
const apiDir = path.join(networkDir, "api");
await fs.ensureDir(apiDir);
const adminContent = `import type { PageParams } from '.'
import request from '../axios'
// 示例代码,可根据实际需求修改
export interface GetUserListFilterData {
searchType?: number
searchKeyWord: string
statusType?: number
}
export function getUserList(data: GetUserListFilterData & PageParams) {
return request({
method: 'post',
url: '/system/v2/user/list',
data
})
}`;
await fs.writeFile(path.join(apiDir, "admin.ts"), adminContent, "utf8");
const indexContent = `import * as admin from './admin'
export type PageParams = {
pageNo: number
pageSize: number
orderKey?: string
/**0==升序 1==降序 */
orderType?: 0 | 1
}
export default {
admin
}`;
await fs.writeFile(path.join(apiDir, "index.ts"), indexContent, "utf8");
}
/**
* 创建ESLint配置文件
* @param {string} projectPath 项目路径
*/
async function createEslintConfig(projectPath) {
const eslintConfig = {
env: {
browser: true,
es2021: true,
node: true,
},
extends: [
"eslint:recommended",
"plugin:vue/vue3-essential",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
],
parserOptions: {
ecmaVersion: "latest",
parser: "@typescript-eslint/parser",
sourceType: "module",
},
plugins: ["vue", "@typescript-eslint", "prettier"],
rules: {
"prettier/prettier": "error",
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"vue/multi-word-component-names": "off",
"@typescript-eslint/no-explicit-any": "warn",
},
};
await fs.writeFile(
path.join(projectPath, ".eslintrc.js"),
`module.exports = ${JSON.stringify(eslintConfig, null, 2)}`,
"utf8"
);
// 创建prettier配置
const prettierConfig = {
singleQuote: true,
semi: true,
trailingComma: "es5",
printWidth: 100,
tabWidth: 2,
endOfLine: "auto",
};
await fs.writeFile(
path.join(projectPath, ".prettierrc"),
JSON.stringify(prettierConfig, null, 2),
"utf8"
);
// 创建.eslintignore文件
const eslintIgnoreContent = `node_modules/
dist/
*.d.ts
.vscode/
.idea/
.DS_Store
`;
await fs.writeFile(
path.join(projectPath, ".eslintignore"),
eslintIgnoreContent,
"utf8"
);
}
/**
* 配置husky和lint-staged实现提交前校验
* @param {string} projectPath 项目路径
*/
async function setupHuskyAndLintStaged(projectPath) {
await runCommand(
"pnpm",
["add", "-D", "husky", "lint-staged", "prettier", "npm-run-all"],
{ cwd: projectPath }
);
await runCommand("npx", ["husky", "install"], { cwd: projectPath });
const huskyDir = path.join(projectPath, ".husky");
await fs.ensureDir(huskyDir);
const preCommitPath = path.join(huskyDir, "pre-commit");
const preCommitContent = `#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx lint-staged
`;
await fs.writeFile(preCommitPath, preCommitContent, "utf8");
// 赋予钩子执行权限
await runCommand("chmod", ["+x", preCommitPath], { cwd: projectPath });
// 配置lint-staged
const lintStagedConfig = {
"*.{vue,js,ts,jsx,tsx}": ["eslint --fix", "prettier --write"],
"*.{json,md}": ["prettier --write"],
};
await fs.writeFile(
path.join(projectPath, "lint-staged.config.js"),
`module.exports = ${JSON.stringify(lintStagedConfig, null, 2)}`,
"utf8"
);
}
/**
* 创建 PC 或 Web 项目 (Vue + Vite + TypeScript + Element Plus)
* @param {string} projectPath 项目路径
*/
async function createVueElementProject(projectPath) {
await runCommand(
"pnpm",
["create", "vite@latest", ".", "--template", "vue-ts"],
{ cwd: projectPath }
);
await runCommand("pnpm", ["install"], { cwd: projectPath });
await runCommand(
"pnpm",
[
"add",
"element-plus",
"vue-router",
"pinia",
"axios",
"@element-plus/icons-vue",
],
{ cwd: projectPath }
);
await runCommand(
"pnpm",
[
"add",
"-D",
"unplugin-vue-components",
"unplugin-auto-import",
"@types/node",
"eslint",
"eslint-plugin-vue",
"@typescript-eslint/eslint-plugin",
"@typescript-eslint/parser",
"eslint-config-prettier",
"eslint-plugin-prettier",
"vue-tsc",
"typescript",
],
{ cwd: projectPath }
);
// 创建Vite配置文件,指定输出目录
await createViteConfig(projectPath, "web");
await createEslintConfig(projectPath);
await setupHuskyAndLintStaged(projectPath);
const srcPath = path.join(projectPath, "src");
await createNetworkAndApiFiles(srcPath);
await createEnvFiles(projectPath);
await adjustPackageJsonScripts(projectPath);
}
/**
* 创建 Mobile 项目 (Vue + Vite + TypeScript + Vant)
* @param {string} projectPath 项目路径
*/
async function createVueVantProject(projectPath) {
await runCommand(
"pnpm",
["create", "vite@latest", ".", "--template", "vue-ts"],
{ cwd: projectPath }
);
await runCommand("pnpm", ["install"], { cwd: projectPath });
await runCommand(
"pnpm",
["add", "vant", "vue-router", "pinia", "axios", "postcss-px-to-viewport"],
{ cwd: projectPath }
);
await runCommand(
"pnpm",
[
"add",
"-D",
"unplugin-vue-components",
"unplugin-auto-import",
"@types/node",
"eslint",
"eslint-plugin-vue",
"@typescript-eslint/eslint-plugin",
"@typescript-eslint/parser",
"eslint-config-prettier",
"eslint-plugin-prettier",
"vue-tsc",
"typescript",
],
{ cwd: projectPath }
);
await createViteConfig(projectPath, "mobile");
await createEslintConfig(projectPath);
await setupHuskyAndLintStaged(projectPath);
const srcPath = path.join(projectPath, "src");
await createNetworkAndApiFiles(srcPath);
await createEnvFiles(projectPath);
await adjustPackageJsonScripts(projectPath);
}
module.exports = {
pathExists,
createDirectory,
runCommand,
createGitignore,
createRootVscodeConfig,
createViteConfig,
createEnvFiles,
adjustPackageJsonScripts,
createNetworkAndApiFiles,
createEslintConfig,
setupHuskyAndLintStaged,
createVueElementProject,
createVueVantProject,
};