UNPKG

@tianyio/quality-helper

Version:

A comprehensive quality helper tool for project scaffolding and management

879 lines (878 loc) 28.1 kB
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