UNPKG

@tianyio/quality-helper

Version:

A comprehensive quality helper tool for project scaffolding and management

783 lines (782 loc) 24.3 kB
import { execSync } from "child_process"; import fs, { readFile } from "fs/promises"; import { join } from "path"; import chalk from "chalk"; import ora from "ora"; import { existsSync } from "fs"; import { F as FileOperations } from "./core-file-ops-d43o99eg.js"; class ArrayUtils { /** * 按指定键对数组进行分组 * @param array 要分组的数组 * @param keySelector 键选择器函数 * @returns 分组后的 Map */ static groupBy(array, keySelector) { const groups = /* @__PURE__ */ new Map(); for (const item of array) { const key = keySelector(item); if (!groups.has(key)) { groups.set(key, []); } groups.get(key).push(item); } return groups; } /** * 安全地获取数组元素 * @param array 数组 * @param index 索引 * @returns 元素或 undefined */ static safeGet(array, index) { return array[index]; } } async function detectPackageManager(projectPath) { const lockFiles = { "pnpm-lock.yaml": "pnpm", "yarn.lock": "yarn", "package-lock.json": "npm" }; for (const [lockFile, manager] of Object.entries(lockFiles)) { try { const lockPath = join(projectPath, lockFile); await fs.access(lockPath); return manager; } catch { continue; } } const managers = ["pnpm", "yarn", "npm"]; for (const manager of managers) { try { execSync(`${manager} --version`, { stdio: "ignore" }); return manager; } catch { continue; } } return "npm"; } async function installProjectDependencies(projectPath) { const packageManager = await detectPackageManager(projectPath); try { const installCommand = getInstallCommand(packageManager); if (packageManager === "pnpm") { try { execSync(`${installCommand} --no-frozen-lockfile`, { cwd: projectPath, stdio: "inherit" }); } catch (retryError) { console.error("❌ 重新安装失败:"); console.log("\n💡 请手动执行以下命令解决:"); console.log("1. rm -rf node_modules pnpm-lock.yaml"); console.log("2. pnpm install --no-frozen-lockfile"); throw retryError; } } else { execSync(installCommand, { cwd: projectPath, stdio: "inherit" }); } } catch (error) { console.warn("依赖安装失败:", error); throw error; } } function getInstallCommand(packageManager) { switch (packageManager) { case "pnpm": return "pnpm install"; case "yarn": return "yarn install"; case "npm": default: return "npm install"; } } async function cleanDependencies(projectPath) { const nodeModulesPath = join(projectPath, "node_modules"); const lockFiles = ["pnpm-lock.yaml", "yarn.lock", "package-lock.json"]; const pnpmFiles = [".pnpm-store", ".pnpm-debug.log"]; try { await fs.rm(nodeModulesPath, { recursive: true, force: true }); for (const lockFile of lockFiles) { try { await fs.unlink(join(projectPath, lockFile)); } catch { } } for (const pnpmFile of pnpmFiles) { try { const pnpmPath = join(projectPath, pnpmFile); await fs.rm(pnpmPath, { recursive: true, force: true }); } catch { } } try { const packageManager = await detectPackageManager(projectPath); if (packageManager === "pnpm") { execSync("pnpm store prune", { cwd: projectPath, stdio: "ignore" }); } } catch { } } catch (error) { console.warn("依赖清理失败:", error.message); throw error; } } async function initializeGit(projectPath) { try { execSync("git init", { cwd: projectPath, stdio: "ignore" }); } catch (error) { console.warn("Git仓库初始化失败,请手动初始化"); throw error; } } async function initializeHusky(projectPath) { try { execSync("npx husky init", { cwd: projectPath, stdio: "ignore" }); } catch (error) { console.warn("Husky初始化失败,请手动初始化"); throw error; } } async function formatProject(projectPath, silent = false) { const spinner = !silent ? ora("格式化代码中...").start() : null; try { if (!existsSync(projectPath)) { throw new Error(`项目路径不存在: ${projectPath}`); } const packageJsonPath = join(projectPath, "package.json"); if (!existsSync(packageJsonPath)) { throw new Error("项目中未找到package.json文件"); } let formatSuccess = false; try { execSync("eslint . --fix", { cwd: projectPath, stdio: silent ? "ignore" : "pipe", timeout: 6e4 // 60秒超时 }); formatSuccess = true; } catch { } if (!formatSuccess) { if (spinner) { spinner.warn(chalk.yellow("代码格式化失败,请手动运行格式化命令")); } else if (!silent) { console.log(chalk.yellow("⚠️ 代码格式化失败,请手动运行格式化命令")); } } else { if (spinner) { spinner.succeed(chalk.green("代码格式化成功")); } } } catch (error) { if (spinner) { spinner.fail(chalk.red("代码格式化失败")); } else if (!silent) { console.warn("代码格式化失败:", error); } if (!silent) { console.log(chalk.yellow("提示: 可以稍后手动运行 npm run format 进行代码格式化")); } } } async function getLatestVersion(packageName) { try { const command = `npm view ${packageName} version --json`; const result = execSync(command, { encoding: "utf8", timeout: 3e4, // 增加超时时间到30秒 stdio: ["pipe", "pipe", "pipe"] // 明确指定stdio }); const lines = result.split("\n"); const jsonLines = lines.filter( (line) => !line.startsWith("npm warn") && !line.startsWith("npm notice") && line.trim() !== "" ); const jsonString = jsonLines.join("\n").trim(); if (!jsonString) { console.warn(`No valid JSON response for ${packageName}`); return "latest"; } const version = JSON.parse(jsonString); return typeof version === "string" ? version : version[version.length - 1]; } catch (error) { if (error.code === "ETIMEDOUT" || error.errno === -4039) { console.warn(`Timeout getting version for ${packageName}, using 'latest'`); } else { console.warn(`Failed to get latest version for ${packageName}:`, error.message); } return "latest"; } } async function getLatestVersions(packageNames) { const versions = {}; const promises = packageNames.map(async (packageName) => { const version = await getLatestVersion(packageName); versions[packageName] = version; }); await Promise.all(promises); return versions; } async function resolveDependencyVersions(dependencies) { const resolved = { ...dependencies }; const packagesToResolve = []; for (const [name, version] of Object.entries(dependencies)) { if (!version || version === "latest") { packagesToResolve.push(name); } } if (packagesToResolve.length > 0) { const latestVersions = await getLatestVersions(packagesToResolve); for (const packageName of packagesToResolve) { if (latestVersions[packageName]) { resolved[packageName] = latestVersions[packageName]; } } } return resolved; } const RUNTIME_DEPENDENCIES = { // Vue 核心 vue: "3.5.18", // 路由管理 "vue-router": "4.5.1", // 状态管理 pinia: "3.0.3", // VueUse 工具库 "@vueuse/core": "13.9.0", // 工具库 "lodash-es": "4.17.21", axios: "1.12.2", // 自研工具包 "@tianyio/fast-util-lib": "latest" // @tianyio/ 依赖使用 latest 版本 }; const DEV_DEPENDENCIES = { // 脚手架工具 "@tianyio/quality-helper": "latest", // @tianyio/ 依赖使用 latest 版本 // 构建工具 vite: "7.1.5", "@vitejs/plugin-vue": "6.0.1", "@vitejs/plugin-legacy": "7.2.1", "vite-plugin-vue-devtools": "8.0.0", "vite-plugin-dts": "4.5.4", "@originjs/vite-plugin-federation": "1.4.1", "vite-bundle-analyzer": "1.2.3", "vue-tsc": "3.1.1", // TypeScript typescript: "5.9.2", "@types/node": "24.3.0", "@vue/tsconfig": "0.8.1", // 代码规范 eslint: "9.33.0", "@eslint/js": "9.38.0", "eslint-config-prettier": "10.1.8", "eslint-plugin-prettier": "5.5.4", "eslint-plugin-promise": "7.2.1", "eslint-plugin-vue": "10.4.0", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", "eslint-plugin-complexity": "^1.0.0", "vue-eslint-parser": "10.2.0", prettier: "3.6.2", // Git 工具 husky: "9.1.7", "lint-staged": "16.1.5", "@commitlint/cli": "19.8.1", "@commitlint/config-conventional": "19.8.1", // 自动导入 "unplugin-auto-import": "20.0.0", "unplugin-vue-components": "29.0.0", "unplugin-icons": "22.3.0", // Babel 浏览器兼容 "@babel/core": "7.28.3", "@babel/preset-env": "7.28.3", "@babel/preset-typescript": "7.27.1", "@babel/plugin-proposal-decorators": "7.28.0", "@babel/plugin-syntax-dynamic-import": "7.8.3", "@rollup/plugin-babel": "6.0.4", "@vue/babel-plugin-jsx": "1.5.0", "react-refresh": "0.17.0" }; function getRuntimeDependencies() { return { ...RUNTIME_DEPENDENCIES }; } function getDevDependencies() { return { ...DEV_DEPENDENCIES }; } function isDependencySupported(name) { return name in RUNTIME_DEPENDENCIES || name in DEV_DEPENDENCIES; } const DEPENDENCY_UPDATE_STRATEGY = { // 模板依赖优先级:模板版本覆盖项目版本 templatePriority: true, // 保留用户新增的依赖 preserveUserDependencies: true, // 同步新增的模板依赖到项目 syncNewTemplateDependencies: true }; async function getRuntimeDependenciesWithVersionResolution(resolveVersions = true) { const dependencies = getRuntimeDependencies(); if (resolveVersions) { return await resolveDependencyVersions(dependencies); } return dependencies; } async function getDevDependenciesWithVersionResolution(resolveVersions = true) { const dependencies = getDevDependencies(); if (resolveVersions) { return await resolveDependencyVersions(dependencies); } return dependencies; } class DependencyManager { strategy; customDependencies; customDevDependencies; constructor(strategy = DEPENDENCY_UPDATE_STRATEGY, customConfig) { this.strategy = strategy; if (customConfig?.customDependencies) { this.customDependencies = customConfig.customDependencies; } if (customConfig?.customDevDependencies) { this.customDevDependencies = customConfig.customDevDependencies; } } /** * 直接更新项目依赖到推荐版本 * @returns 是否有更新 */ async updateToRecommendedVersions(projectRoot) { const packageJsonPath = join(projectRoot, "package.json"); const packageJson = JSON.parse(await FileOperations.readFile(packageJsonPath)); const templateRuntimeDeps = this.customDependencies ?? await getRuntimeDependenciesWithVersionResolution(); const templateDevDeps = this.customDevDependencies ?? await getDevDependenciesWithVersionResolution(); packageJson.dependencies ??= {}; packageJson.devDependencies ??= {}; let hasChanges = false; const preservedUserDeps = []; const updatedDeps = []; const addedDeps = []; hasChanges = this.updateDependencyGroup( packageJson.dependencies, templateRuntimeDeps, preservedUserDeps, updatedDeps, addedDeps ) || hasChanges; hasChanges = this.updateDependencyGroup( packageJson.devDependencies, templateDevDeps, preservedUserDeps, updatedDeps, addedDeps ) || hasChanges; if (hasChanges) { await FileOperations.writeFile(packageJsonPath, JSON.stringify(packageJson, null, 2)); } return hasChanges; } /** * 更新依赖组 */ updateDependencyGroup(projectDeps, templateDeps, preservedUserDeps, updatedDeps, addedDeps) { let hasChanges = false; for (const [name, templateVersion] of Object.entries(templateDeps)) { const currentVersion = projectDeps[name]; if (!currentVersion) { projectDeps[name] = templateVersion; addedDeps.push(name); hasChanges = true; } else if (currentVersion !== templateVersion) { projectDeps[name] = templateVersion; updatedDeps.push(name); hasChanges = true; } } if (this.strategy.preserveUserDependencies) { for (const [name] of Object.entries(projectDeps)) { if (!isDependencySupported(name) && !templateDeps[name]) { preservedUserDeps.push(name); } } } return hasChanges; } /** * 分析项目依赖状态 */ async analyzeDependencies(projectRoot) { const packageJsonPath = join(projectRoot, "package.json"); const packageJson = JSON.parse(await FileOperations.readFile(packageJsonPath)); const templateRuntimeDeps = this.customDependencies ?? await getRuntimeDependenciesWithVersionResolution(); const templateDevDeps = this.customDevDependencies ?? await getDevDependenciesWithVersionResolution(); const projectDeps = packageJson.dependencies ?? {}; const projectDevDeps = packageJson.devDependencies ?? {}; const analysis = { toUpdate: {}, toAdd: {}, userAdded: {}, synced: {} }; this.analyzeDepGroup(projectDeps, templateRuntimeDeps, analysis); this.analyzeDepGroup(projectDevDeps, templateDevDeps, analysis); return analysis; } /** * 分析依赖组 */ analyzeDepGroup(projectDeps, templateDeps, analysis) { for (const [name, templateVersion] of Object.entries(templateDeps)) { const projectVersion = projectDeps[name]; if (!projectVersion) { analysis.toAdd[name] = templateVersion; } else if (projectVersion !== templateVersion) { analysis.toUpdate[name] = { current: projectVersion, template: templateVersion }; } else { analysis.synced[name] = templateVersion; } } if (this.strategy.preserveUserDependencies) { for (const [name, version] of Object.entries(projectDeps)) { if (!isDependencySupported(name)) { analysis.userAdded[name] = version; } } } } /** * 生成依赖报告 */ generateReport(analysis) { const { toUpdate, toAdd, userAdded, synced } = analysis; const totalDeps = Object.keys(synced).length + Object.keys(toUpdate).length + Object.keys(toAdd).length + Object.keys(userAdded).length; console.log(chalk.blue("\n📊 依赖分析结果:")); if (Object.keys(synced).length > 0) { console.log(chalk.green(`✅ 已同步: ${Object.keys(synced).length} 个依赖`)); } if (Object.keys(toUpdate).length > 0) { console.log(chalk.yellow(`🔄 需要更新: ${Object.keys(toUpdate).length} 个依赖`)); Object.entries(toUpdate).slice(0, 3).forEach(([name, { current, template }]) => { console.log(chalk.gray(` ${name}: ${current}${template}`)); }); if (Object.keys(toUpdate).length > 3) { console.log(chalk.gray(` ... 还有 ${Object.keys(toUpdate).length - 3} 个`)); } } if (Object.keys(toAdd).length > 0) { console.log(chalk.cyan(`➕ 需要添加: ${Object.keys(toAdd).length} 个依赖`)); } if (Object.keys(userAdded).length > 0) { console.log(chalk.magenta(`👤 用户自定义: ${Object.keys(userAdded).length} 个依赖`)); } console.log(chalk.gray(`总计: ${totalDeps} 个依赖`)); } /** * 检查依赖兼容性 */ checkCompatibility(analysis) { const conflicts = []; for (const [name, { current, template }] of Object.entries(analysis.toUpdate)) { const currentMajor = this.extractMajorVersion(current); const templateMajor = this.extractMajorVersion(template); if (currentMajor !== templateMajor) { conflicts.push(`${name}: ${current}${template} (主版本变更)`); } } if (conflicts.length > 0) { console.log(chalk.red("⚠️ 发现潜在的版本冲突:")); conflicts.forEach((conflict) => { console.log(chalk.red(` ${conflict}`)); }); return false; } return true; } /** * 提取主版本号 */ extractMajorVersion(version) { const match = version.match(/\d+/); return match ? match[0] : "0"; } } function createDependencyManager(strategy) { if (strategy && "customDependencies" in strategy) { const customConfig = strategy; return new DependencyManager(DEPENDENCY_UPDATE_STRATEGY, customConfig); } const fullStrategy = { ...DEPENDENCY_UPDATE_STRATEGY, ...strategy }; return new DependencyManager(fullStrategy); } class PackageNameValidationRule { validate(name) { const result = { isValid: true, errors: [], warnings: [] }; if (!name || typeof name !== "string") { result.isValid = false; result.errors.push("包名不能为空"); return result; } if (name.trim().length === 0) { result.isValid = false; result.errors.push("包名不能为空"); return result; } if (name.startsWith(".") || name.startsWith("_")) { result.isValid = false; result.errors.push("包名不能以点或下划线开头"); return result; } if (name.includes("@")) { const parts = name.split("/"); if (parts.length !== 2) { result.isValid = false; result.errors.push("作用域包格式无效"); return result; } const scope = parts[0]?.substring(1); const [, packageName] = parts; if (!scope || !packageName) { result.isValid = false; result.errors.push("作用域包格式无效"); return result; } if (!/^[a-z0-9_-]+$/.test(scope) || !/^[a-z0-9_-]+$/.test(packageName)) { result.isValid = false; result.errors.push("作用域包名格式无效"); return result; } } else { if (!/^[a-z0-9_-]+$/.test(name)) { result.isValid = false; result.errors.push("包名格式无效"); return result; } } return result; } } class VersionValidationRule { versionPatterns = [ /^\d+\.\d+\.\d+$/, // 1.2.3 /^\d+\.\d+\.\d+-\w+$/, // 1.2.3-beta /^\d+\.\d+\.\d+\+\w+$/, // 1.2.3+build /^\^\d+\.\d+\.\d+$/, // ^1.2.3 /^~\d+\.\d+\.\d+$/, // ~1.2.3 /^>=?\d+\.\d+\.\d+$/, // >=1.2.3 /^<=?\d+\.\d+\.\d+$/, // <=1.2.3 /^latest$/, // latest /^next$/, // next /^beta$/, // beta /^alpha$/, // alpha /^rc$/, // rc /^\*$/, // * /^x$/, // x /^\d+\.\d+\.\d+ - \d+\.\d+\.\d+$/ // 1.2.3 - 2.3.4 ]; validate(version) { const result = { isValid: true, errors: [], warnings: [] }; if (!version || typeof version !== "string") { result.isValid = false; result.errors.push("版本号不能为空"); return result; } if (!this.versionPatterns.some((pattern) => pattern.test(version))) { result.warnings.push(`版本格式可能无效: ${version}`); } return result; } } class ValidationRuleFactory { static createPackageNameRule() { return new PackageNameValidationRule(); } static createVersionRule() { return new VersionValidationRule(); } } class DependencyValidator { packageNameRule = ValidationRuleFactory.createPackageNameRule(); versionRule = ValidationRuleFactory.createVersionRule(); /** * 验证依赖列表文件 * @param filePath 文件路径 * @returns 验证结果 */ async validateDependencyFile(filePath) { const result = { isValid: false, errors: [], warnings: [] }; try { const fileContent = await readFile(filePath, "utf-8"); let parsedContent; try { parsedContent = JSON.parse(fileContent); } catch (parseError) { result.errors.push("文件不是有效的 JSON 格式"); return result; } this.validateBasicStructure(parsedContent, result); if (result.isValid) { result.dependencies = parsedContent.dependencies ?? {}; result.devDependencies = parsedContent.devDependencies ?? {}; this.validateDependencyFormat(result.dependencies, "dependencies", result); this.validateDependencyFormat(result.devDependencies, "devDependencies", result); } return result; } catch (error) { result.errors.push(`读取文件失败: ${error.message}`); return result; } } /** * 验证基本结构 * @param content 解析后的内容 * @param result 验证结果 */ validateBasicStructure(content, result) { const hasDependencies = "dependencies" in content; const hasDevDependencies = "devDependencies" in content; if (!hasDependencies && !hasDevDependencies) { result.errors.push("文件必须包含 dependencies 或 devDependencies 字段"); return; } if (hasDependencies && typeof content.dependencies !== "object") { result.errors.push("dependencies 字段必须是对象类型"); return; } if (hasDevDependencies && typeof content.devDependencies !== "object") { result.errors.push("devDependencies 字段必须是对象类型"); return; } if (hasDependencies && Object.keys(content.dependencies ?? {}).length === 0) { result.warnings.push("dependencies 字段为空"); } if (hasDevDependencies && Object.keys(content.devDependencies ?? {}).length === 0) { result.warnings.push("devDependencies 字段为空"); } result.isValid = true; } /** * 验证依赖格式 * @param dependencies 依赖对象 * @param fieldName 字段名称 * @param result 验证结果 */ validateDependencyFormat(dependencies, fieldName, result) { for (const [name, version] of Object.entries(dependencies)) { if (!this.isValidPackageName(name)) { result.errors.push(`${fieldName}.${name}: 包名格式无效`); continue; } if (!this.isValidVersion(version)) { result.warnings.push(`${fieldName}.${name}: 版本格式可能无效 (${version})`); } } } /** * 验证包名格式 * @param name 包名 * @returns 是否有效 */ isValidPackageName(name) { const result = this.packageNameRule.validate(name); return result.isValid; } /** * 验证版本格式 * @param version 版本号 * @returns 是否有效 */ isValidVersion(version) { const result = this.versionRule.validate(version); return result.isValid; } /** * 打印验证结果 * @param result 验证结果 * @param filePath 文件路径 */ printValidationResult(result, filePath) { console.log(chalk.blue(` 📋 依赖列表文件验证结果: ${filePath}`)); if (result.isValid) { console.log(chalk.green("✅ 文件结构有效")); if (result.dependencies && Object.keys(result.dependencies).length > 0) { console.log(chalk.cyan(`📦 运行时依赖: ${Object.keys(result.dependencies).length} 个`)); } if (result.devDependencies && Object.keys(result.devDependencies).length > 0) { console.log(chalk.cyan(`🔧 开发依赖: ${Object.keys(result.devDependencies).length} 个`)); } } else { console.log(chalk.red("❌ 文件结构无效")); } if (result.errors.length > 0) { console.log(chalk.red("\n❌ 错误:")); result.errors.forEach((error) => { console.log(chalk.red(` • ${error}`)); }); } if (result.warnings.length > 0) { console.log(chalk.yellow("\n⚠️ 警告:")); result.warnings.forEach((warning) => { console.log(chalk.yellow(` • ${warning}`)); }); } } } function createDependencyValidator() { return new DependencyValidator(); } export { ArrayUtils as A, initializeGit as a, initializeHusky as b, createDependencyManager as c, createDependencyValidator as d, cleanDependencies as e, formatProject as f, installProjectDependencies as i }; //# sourceMappingURL=utils-JjdnUrNK.js.map