UNPKG

@falconix/ymcli

Version:

A CLI tool for creating and building multi-platform Vue projects

648 lines (566 loc) 17.3 kB
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, };