@tianyio/quality-helper
Version:
A comprehensive quality helper tool for project scaffolding and management
783 lines (782 loc) • 24.3 kB
JavaScript
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