@tianyio/quality-helper
Version:
A comprehensive quality helper tool for project scaffolding and management
879 lines (878 loc) • 28.1 kB
JavaScript
import chalk from "chalk";
import ora from "ora";
import { join, relative, dirname, basename } from "path";
import { F as FileOperations } from "./core-file-ops-d43o99eg.js";
import { F as FileMerger } from "./core-file-merger-DSyvZF-T.js";
import { D as Discovery } from "./core-discovery-DU1aPJk0.js";
import prompts from "prompts";
import { A as ArrayUtils } from "./utils-JjdnUrNK.js";
import { readdir } from "fs/promises";
class MergeService {
configMerger;
constructor() {
this.configMerger = new FileMerger();
}
/**
* 确定配置文件的更新模式
*/
async determineUpdateMode(targetContent, templateContent, config) {
if (targetContent.trim() === templateContent.trim()) {
return "skip";
}
const needOverride = [".gitconfig", ".editorconfig"];
if (config.group === "git-hooks" || needOverride.includes(config.name) || !targetContent.trim()) {
return "overwrite";
}
return "merge";
}
/**
* 智能合并配置文件内容
*/
async mergeConfigContent(targetContent, templateContent, config) {
return this.configMerger.mergeConfigContent(targetContent, templateContent, config);
}
}
class BuildToolsService {
mergeService;
// 用于跟踪已更新的 monorepo 根目录配置文件,避免重复更新
updatedMonorepoConfigs;
constructor() {
this.mergeService = new MergeService();
this.updatedMonorepoConfigs = /* @__PURE__ */ new Set();
}
/**
* 判断 typescript 配置文件是否拆分基文件和特异文件
*/
isTsConfigSeparated(content) {
try {
const tsConfig = JSON.parse(content);
const checkExtends = (extendsValue) => {
const reg = /^(?:\.\.\/|\.\/|[\w-]+\/)*tsconfig\.json$/;
if (typeof extendsValue === "string") {
const cleanValue = extendsValue.replace(/^["']|["']$/g, "");
return reg.test(cleanValue);
}
if (Array.isArray(extendsValue)) {
return extendsValue.some((item) => {
if (typeof item === "string") {
const cleanValue = item.replace(/^["']|["']$/g, "");
return reg.test(cleanValue);
}
return false;
});
}
return false;
};
return checkExtends(tsConfig.extends ?? "");
} catch {
return false;
}
}
/**
* 判断 vite 配置文件是否拆分基文件和特异文件
*/
isViteConfigSeparated(content) {
const reg = /import\s+.*\s+from\s+["'][.\/]+.*vite\.config\.(?:ts|js)["']/;
return reg.test(content);
}
/**
* 检查配置文件状态
*/
async checkConfigStatus(projectPath, type) {
const fileName = type === "vite" ? "vite.config.ts" : "tsconfig.json";
try {
const configPath = join(projectPath, fileName);
const exists = await FileOperations.exists(configPath);
if (!exists) {
return "not-exists";
}
const content = await FileOperations.readFile(configPath);
let isSeparated;
if (type === "vite") {
isSeparated = this.isViteConfigSeparated(content);
} else {
isSeparated = this.isTsConfigSeparated(content);
}
return isSeparated ? "separated" : "not-separated";
} catch {
return "not-exists";
}
}
/**
* 处理配置文件
*/
async process(configType, templatesDir, projectPath, rootPath, architecture, projectType) {
const status = await this.checkConfigStatus(projectPath, configType);
switch (status) {
case "not-exists":
await this.handleProjectConfigNotExists(
templatesDir,
projectPath,
rootPath,
configType,
architecture,
projectType
);
break;
case "separated":
await this.handleProjectConfigSeparated(
templatesDir,
projectPath,
rootPath,
configType,
architecture,
projectType
);
break;
case "not-separated":
await this.handleProjectConfigNotSeparated(
templatesDir,
projectPath,
rootPath,
configType,
architecture,
projectType
);
break;
}
}
/**
* 计算相对路径
*/
calculateRelativePath(from, to) {
const relativePath = relative(from, to).replace(/\\/g, "/");
if (!relativePath.startsWith("./") && !relativePath.startsWith("../")) {
return `./${relativePath}`;
}
return relativePath;
}
/**
* 更新基配置文件(避免重复更新)
*/
async updateBaseConfigFile(targetPath, templatePath) {
const configKey = targetPath;
if (!this.updatedMonorepoConfigs.has(configKey)) {
const templateContent = await FileOperations.readFile(templatePath);
await FileOperations.writeFile(targetPath, templateContent);
this.updatedMonorepoConfigs.add(configKey);
}
}
/**
* 合并并更新基配置文件(避免重复更新)
*/
async mergeAndUpdateBaseConfigFile(configsFilePath, templatePath, configName) {
const configKey = configsFilePath;
if (!this.updatedMonorepoConfigs.has(configKey)) {
const templateContent = await FileOperations.readFile(templatePath);
const existingContent = await FileOperations.readFile(configsFilePath);
const mergedContent = await this.mergeConfigContent(
existingContent,
templateContent,
configName
);
await FileOperations.writeFile(configsFilePath, mergedContent);
this.updatedMonorepoConfigs.add(configKey);
}
}
/**
* 通用方法:处理项目配置文件不存在的情况
*/
async handleProjectConfigNotExists(templatesDir, projectPath, rootPath, configType, architecture, projectType) {
await FileOperations.ensureDir(join(rootPath, "configs"));
const fileName = configType === "vite" ? "vite.config.ts" : "tsconfig.json";
const baseTemplatePath = join(templatesDir, "configs", "build-tools", fileName);
const baseTargetPath = join(rootPath, "configs", fileName);
await this.updateBaseConfigFile(baseTargetPath, baseTemplatePath);
if (architecture === "monorepo" && projectType === "root") return;
const templatePath = join(
templatesDir,
"configs",
"build-tools",
projectType === "lib" ? "libs" : "apps",
fileName
);
let templateContent = await FileOperations.readFile(templatePath);
const relativeConfigsPath = this.calculateRelativePath(projectPath, join(rootPath, "configs"));
if (configType === "vite") {
templateContent = `import getBaseConfig from '${relativeConfigsPath}/vite.config'
${templateContent}`;
templateContent = templateContent.replace(
/return\s+config/g,
"return mergeConfig(getBaseConfig(env), config)"
);
} else {
const tsConfig = JSON.parse(templateContent);
tsConfig.extends = `${relativeConfigsPath}/tsconfig.json`;
templateContent = JSON.stringify(tsConfig, null, 2);
}
await FileOperations.writeFile(join(projectPath, fileName), templateContent);
}
/**
* 通用方法:处理项目配置文件拆分的情况
*/
async handleProjectConfigSeparated(templatesDir, projectPath, rootPath, configType, architecture, projectType) {
const fileName = configType === "vite" ? "vite.config.ts" : "tsconfig.json";
const baseTargetPath = join(rootPath, "configs", fileName);
const baseTemplatePath = join(templatesDir, "configs", "build-tools", fileName);
if (architecture === "monorepo") {
await this.mergeAndUpdateBaseConfigFile(baseTargetPath, baseTemplatePath, fileName);
} else {
const baseTemplateContent = await FileOperations.readFile(baseTemplatePath);
const baseTargetContent = await FileOperations.readFile(baseTargetPath);
const mergedBaseContent = await this.mergeConfigContent(
baseTargetContent,
baseTemplateContent,
fileName
);
await FileOperations.writeFile(baseTargetPath, mergedBaseContent);
}
if (architecture === "monorepo" && projectType === "root") return;
const templatePath = join(
templatesDir,
"configs",
"build-tools",
projectType === "lib" ? "libs" : "apps",
fileName
);
const templateContent = await FileOperations.readFile(templatePath);
const targetPath = join(projectPath, fileName);
const targetContent = await FileOperations.readFile(targetPath);
const mergedProjectContent = await this.mergeConfigContent(
targetContent,
templateContent,
fileName
);
await FileOperations.writeFile(targetPath, mergedProjectContent);
}
/**
* 通用方法:处理项目配置文件未拆分的情况
*/
async handleProjectConfigNotSeparated(templatesDir, projectPath, rootPath, configType, architecture, projectType) {
const fileName = configType === "vite" ? "vite.config.ts" : "tsconfig.json";
const projectConfigPath = join(projectPath, fileName);
const backupConfigPath = `${projectConfigPath}.backup`;
if (await FileOperations.exists(projectConfigPath)) {
await FileOperations.rename(projectConfigPath, backupConfigPath);
console.log(`原始配置文件已备份为: ${backupConfigPath}`);
}
await this.handleProjectConfigNotExists(
templatesDir,
projectPath,
rootPath,
configType,
architecture,
// 假设为 monorepo 架构
projectType
);
}
/**
* 智能合并配置文件内容
*/
async mergeConfigContent(targetContent, templateContent, configName) {
const commentPrefix = configName === "vite.config.ts" ? "// " : "/*";
const commentSuffix = configName === "vite.config.ts" ? " " : "*/";
return this.mergeService.mergeConfigContent(targetContent, templateContent, {
name: configName,
commentPrefix,
commentSuffix
});
}
}
class FileService {
templatesDir;
mergeService;
buildToolsService;
constructor(templatesDir) {
this.templatesDir = templatesDir;
this.mergeService = new MergeService();
this.buildToolsService = new BuildToolsService();
}
/**
* 更新单个配置文件
*/
async updateConfigFile(config, projectRoot, rootPath, architecture, projectType) {
try {
if (config.name.startsWith("vite.config")) {
await this.buildToolsService.process(
"vite",
this.templatesDir,
projectRoot,
rootPath,
architecture,
projectType
);
return true;
}
if (config.name === "tsconfig.json") {
await this.buildToolsService.process(
"ts",
this.templatesDir,
projectRoot,
rootPath,
architecture,
projectType
);
return true;
}
let updateCtx;
if (config.name === ".pnpmrc" || config.name === ".npmrc") {
updateCtx = await this.getAutoUpdateModeOfNpmrc(config, architecture);
} else {
updateCtx = await this.getAutoUpdateMode(config);
}
if (updateCtx.updateMode === "skip") {
return false;
}
let finalContent;
if (updateCtx.updateMode === "overwrite") {
finalContent = updateCtx.templateContent;
} else {
finalContent = await this.mergeService.mergeConfigContent(
updateCtx.targetContent,
updateCtx.templateContent,
config
);
}
await FileOperations.writeFile(
config.path,
finalContent,
config.group === "git-hooks" ? { mode: 493 } : "utf8"
);
return true;
} catch (error) {
throw new Error(`更新配置文件失败 ${config.name}: ${error.message}`);
}
}
/**
* 自动选择更新模式
*/
async getAutoUpdateMode(config) {
const { templatePath, path: targetPath } = config;
const templateContent = await FileOperations.readFile(templatePath);
const targetExists = await FileOperations.exists(targetPath);
let targetContent = "";
if (targetExists) {
targetContent = await FileOperations.readFile(targetPath);
} else {
await FileOperations.ensureDir(dirname(targetPath));
}
const updateMode = await this.mergeService.determineUpdateMode(
targetContent,
templateContent,
config
);
return {
updateMode,
targetContent,
templateContent
};
}
/**
* 更新 .pnpmrc 文件或 .npmrc 文件
*/
async getAutoUpdateModeOfNpmrc(config, architecture) {
const baseTemplatePath = config.templatePath;
const architectureTemplatePath = join(
this.templatesDir,
"configs",
"npm",
architecture,
config.name.replace(/^\./, "")
);
const baseTemplateContent = await FileOperations.readFile(baseTemplatePath);
const architectureTemplateContent = await FileOperations.readFile(architectureTemplatePath);
const templateContent = await this.mergeService.mergeConfigContent(
architectureTemplateContent,
baseTemplateContent,
config
);
const targetExists = await FileOperations.exists(config.path);
let targetContent = "";
if (targetExists) {
targetContent = await FileOperations.readFile(config.path);
} else {
await FileOperations.ensureDir(dirname(config.path));
}
const updateMode = await this.mergeService.determineUpdateMode(
targetContent,
templateContent,
config
);
return {
updateMode,
targetContent,
templateContent
};
}
}
class FileBrowserService {
/**
* 浏览文件系统选择 package.json 文件
* @param startDir 起始目录
* @returns 选择的文件路径,如果未选择则返回 null
*/
async browseForPackageJson(startDir) {
let currentDir = startDir;
let selectedFile = null;
while (true) {
const { action, file } = await this.browseDirectory(currentDir, startDir);
if (action === "cancel") {
console.log(chalk.yellow("⚠️ 操作已取消"));
return null;
}
if (action === "select" && file) {
selectedFile = file;
break;
}
if (action === "navigate" && file) {
currentDir = file;
}
if (action === "back") {
const parentDir = dirname(currentDir);
if (parentDir !== currentDir) {
currentDir = parentDir;
}
}
}
if (selectedFile) {
console.log(chalk.green(`✓ 已选择: ${basename(selectedFile)}`));
}
return selectedFile;
}
/**
* 浏览目录
*/
async browseDirectory(currentDir, startDir) {
try {
const entries = await readdir(currentDir, { withFileTypes: true });
const relativePath = this.getRelativePath(currentDir, startDir);
console.log(chalk.blue(`
📁 当前目录: ${relativePath}`));
const choices = await this.buildChoices(entries, currentDir);
if (choices.length === 1) {
console.log(chalk.yellow("⚠️ 当前目录中没有找到 package.json 文件"));
return { action: "cancel" };
}
const { selection } = await prompts({
type: "select",
name: "selection",
message: "请选择操作:",
choices,
instructions: false
});
return selection ?? { action: "cancel" };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "未知错误";
console.log(chalk.red(`❌ 无法访问目录: ${errorMessage}`));
return { action: "cancel" };
}
}
/**
* 构建选择列表
*/
async buildChoices(entries, currentDir) {
const choices = [];
if (dirname(currentDir) !== currentDir) {
choices.push({
title: "📁 .. (返回上级目录)",
value: { action: "back" },
description: "返回上级目录"
});
}
const hasPackageJson = entries.some((entry) => entry.isFile() && entry.name === "package.json");
if (hasPackageJson) {
choices.push({
title: "📄 package.json",
value: { action: "select", file: join(currentDir, "package.json") },
description: "选择此文件"
});
}
await this.addDirectoriesWithPackageJson(choices, entries, currentDir);
choices.push({
title: "❌ 取消",
value: { action: "cancel" },
description: "取消选择"
});
return choices;
}
/**
* 添加包含 package.json 的目录
*/
async addDirectoriesWithPackageJson(choices, entries, currentDir) {
const directoriesWithPackageJson = await this.getDirectoriesWithPackageJson(entries, currentDir);
directoriesWithPackageJson.forEach((dir) => {
choices.push({
title: `📁 ${dir.name}/`,
value: { action: "navigate", file: join(currentDir, dir.name) },
description: "进入目录"
});
});
}
/**
* 获取包含 package.json 的目录列表
*/
async getDirectoriesWithPackageJson(entries, currentDir) {
const directoriesWithPackageJson = [];
for (const entry of entries) {
if (entry.isDirectory() && !entry.name.startsWith(".")) {
try {
const subEntries = await readdir(join(currentDir, entry.name), { withFileTypes: true });
const hasPackageJson = subEntries.some(
(subEntry) => subEntry.isFile() && subEntry.name === "package.json"
);
if (hasPackageJson) {
directoriesWithPackageJson.push({ name: entry.name });
}
} catch {
}
}
}
return directoriesWithPackageJson.sort((a, b) => a.name.localeCompare(b.name));
}
/**
* 获取相对路径
*/
getRelativePath(currentDir, startDir) {
return currentDir.replace(startDir, "").replace(/^[\\/]/, "") || ".";
}
}
class UIService {
fileBrowserService;
constructor() {
this.fileBrowserService = new FileBrowserService();
}
/**
* 选择配置文件(交互式)
*/
async selectConfigFiles(allConfigs) {
if (allConfigs.length === 0) {
return [];
}
const groups = ArrayUtils.groupBy(allConfigs, (config) => config.group);
const selectedConfigs = [];
const groupEntries = Array.from(groups.entries());
const totalGroups = groupEntries.length;
for (let i = 0; i < totalGroups; i++) {
const groupEntry = ArrayUtils.safeGet(groupEntries, i);
if (!groupEntry) continue;
const [groupName, groupConfigs] = groupEntry;
const currentIndex = i + 1;
console.log(
`${chalk.cyan(`[${currentIndex}/${totalGroups}]`)} ${chalk.bold(groupName)} ${chalk.gray(`(${groupConfigs.length} 个文件)`)}`
);
const fileChoices = groupConfigs.map((config) => ({
title: config.name,
value: config
}));
const { selectedFiles } = await prompts({
type: "multiselect",
name: "selectedFiles",
message: "选择要更新的文件:",
choices: fileChoices,
instructions: false
});
if (!selectedFiles || selectedFiles.length === 0) {
continue;
}
selectedConfigs.push(...selectedFiles);
}
if (selectedConfigs.length === 0) {
console.log(chalk.yellow("⚠️ 未选择任何配置文件"));
return [];
}
this.displaySelectionSummary(selectedConfigs);
return selectedConfigs;
}
/**
* 选择依赖列表文件(交互式)
* @param projectRoot 项目根目录
* @returns 选择的文件路径,如果未选择则返回 null
*/
async selectDependencyFile(projectRoot) {
console.log(chalk.blue("\n📦 依赖列表文件选择"));
console.log(chalk.gray("您可以选择使用本地 package.json 文件,或使用项目默认的依赖列表"));
const { useCustomFile } = await prompts({
type: "confirm",
name: "useCustomFile",
message: "是否要使用自定义 package.json 文件?",
initial: false
});
if (!useCustomFile) {
console.log(chalk.green("✓ 使用项目默认依赖列表"));
return null;
}
return await this.fileBrowserService.browseForPackageJson(projectRoot);
}
/**
* 显示选择汇总
*/
displaySelectionSummary(selectedConfigs) {
console.log(
`
${chalk.blue("📋")} ${chalk.bold("最终选择汇总:")} 共 ${selectedConfigs.length} 个文件`
);
const finalGroups = ArrayUtils.groupBy(selectedConfigs, (config) => config.group);
Array.from(finalGroups.entries()).forEach(([groupName, configs]) => {
console.log(` ${chalk.green("✓")} ${groupName}: ${configs.length} 个文件`);
});
}
}
class ConfigService {
templatesDir;
fileService;
uiService;
constructor(templatesDir) {
this.templatesDir = templatesDir;
this.fileService = new FileService(templatesDir);
this.uiService = new UIService();
}
/**
* 更新项目配置文件
*/
async updateConfig(options) {
const initialSpinner = options?.silent ? null : ora("正在初始化配置更新...").start();
try {
let projectPath, rootPath;
if (options?.projectPath) {
projectPath = options.projectPath;
rootPath = options.rootPath;
} else {
const { projectPath: discoveredProjectPath, rootPath: discoveredRootPath } = await Discovery.findProjectRoot();
projectPath = discoveredProjectPath;
rootPath = discoveredRootPath;
}
if (!projectPath || !rootPath) {
if (initialSpinner) initialSpinner.fail("未找到项目根目录(package.json)");
throw new Error("未找到项目根目录(package.json)");
}
if (initialSpinner) {
initialSpinner.text = "正在检测项目架构...";
}
const architecture = options?.architectureType ?? await Discovery.detectArchitectureType(rootPath);
const projectType = options?.projectType ?? await Discovery.detectProjectType(projectPath);
if (initialSpinner) {
initialSpinner.succeed(
chalk.gray(
`📋 项目架构: ${architecture};项目类型: ${projectType === "app" ? "应用" : projectType === "lib" ? "库" : "根目录"}`
)
);
}
const allConfigs = await Discovery.getAvailableConfigFiles(
this.templatesDir,
projectPath,
options
);
if (allConfigs.length === 0) {
if (!options?.silent) {
console.log(chalk.yellow("⚠️ 未找到可更新的配置文件"));
}
return;
}
const selectedConfigs = options?.silent ? allConfigs : await this.uiService.selectConfigFiles(allConfigs);
if (selectedConfigs.length === 0) {
return;
}
await this.updateSelectedConfigs(
selectedConfigs,
projectPath,
rootPath,
architecture,
projectType,
options?.silent
);
} catch (error) {
if (initialSpinner) {
initialSpinner.fail("配置更新初始化失败");
}
throw error;
}
}
/**
* 更新选中的配置文件
*/
async updateSelectedConfigs(selectedConfigs, projectRoot, rootPath, architecture, projectType, silent) {
let spinner = silent ? null : ora("正在更新配置文件...").start();
try {
let updatedCount = 0;
let hasEnvFiles = false;
for (const config of selectedConfigs) {
try {
await this.fileService.updateConfigFile(
config,
projectRoot,
rootPath,
architecture,
projectType
);
updatedCount++;
if (config.group === "env" || config.name.startsWith(".env")) {
hasEnvFiles = true;
}
} catch (error) {
if (!silent && spinner) {
spinner.warn(chalk.yellow(`⚠️ ${error.message}`));
spinner = ora("正在更新配置文件...").start();
}
}
}
if (spinner) {
const totalFiles = selectedConfigs.length;
const failedCount = totalFiles - updatedCount;
let message = `配置更新完成!成功更新 ${updatedCount}/${totalFiles} 个文件`;
if (failedCount > 0) {
message += chalk.yellow(` (${failedCount} 个文件更新失败)`);
}
if (hasEnvFiles) {
message += chalk.gray(" • 已更新环境变量配置");
}
spinner.succeed(chalk.green(message));
}
if (hasEnvFiles) {
await Discovery.updatePackageJsonScripts(projectRoot);
}
} catch (error) {
if (spinner) {
spinner.fail(chalk.red("配置文件更新失败"));
}
throw error;
}
}
}
class ProjectOptionsService {
constructor() {
}
/**
* 获取项目创建选项
*/
async getProjectOptions(projectName, options = {}) {
console.log();
console.log();
const name = await this.getProjectName(projectName);
const architecture = await this.getArchitectureType(options);
const framework = await this.getFrameworkType(options);
const targetPath = await this.getTargetPath();
await this.displayProjectSummary({
projectName: name,
architectureType: architecture,
framework,
targetPath
});
return {
projectName: name,
architectureType: architecture,
framework,
targetPath
};
}
/**
* 获取项目名称
*/
async getProjectName(projectName) {
if (projectName) {
return projectName;
}
const response = await prompts(
{
type: "text",
name: "projectName",
message: "请输入项目名称:",
initial: "my-project",
validate: (value) => {
if (!value.trim()) return "项目名称不能为空";
if (!/^[a-zA-Z0-9-_]+$/.test(value)) return "项目名称只能包含字母、数字、连字符和下划线";
return true;
}
},
{
onCancel: () => {
process.exit(0);
}
}
);
if (!response.projectName) {
process.exit(0);
}
return response.projectName;
}
/**
* 获取架构类型
*/
async getArchitectureType(options) {
if (options.architecture) {
return options.architecture;
}
if (options.monorepo) {
return "monorepo";
}
if (options.single) {
return "single";
}
const response = await prompts(
{
type: "select",
name: "architecture",
message: "选择项目架构:",
choices: [
{
title: "单体仓库 (Single Repository)",
value: "single",
description: "适合小到中型项目,所有代码在一个仓库中"
},
{
title: "Monorepo (多包管理)",
value: "monorepo",
description: "适合大型项目,支持多个包的统一管理"
}
],
initial: 0
},
{
onCancel: () => {
process.exit(0);
}
}
);
if (!response.architecture) {
process.exit(0);
}
return response.architecture;
}
/**
* 获取框架类型
*/
async getFrameworkType(options) {
if (options.framework) {
return options.framework;
}
return "vue3";
}
/**
* 获取目标路径
*/
async getTargetPath() {
return process.cwd();
}
/**
* 显示项目配置摘要
*/
async displayProjectSummary(config) {
console.log();
console.log(chalk.gray("─".repeat(50)));
console.log(`${chalk.bold("项目名称:")} ${chalk.cyan(config.projectName)}`);
console.log(
`${chalk.bold("项目架构:")} ${chalk.cyan(config.architectureType === "monorepo" ? "Monorepo" : "Single Repository")}`
);
console.log(`${chalk.bold("开发框架:")} ${chalk.cyan("Vue 3")}`);
console.log(
`${chalk.bold("创建路径:")} ${chalk.cyan(join(config.targetPath, config.projectName))}`
);
console.log(chalk.gray("─".repeat(50)));
console.log();
}
}
export {
ConfigService as C,
ProjectOptionsService as P,
UIService as U
};
//# sourceMappingURL=services-DXX3cT7y.js.map