@tianyio/quality-helper
Version:
A comprehensive quality helper tool for project scaffolding and management
1,044 lines (910 loc) • 29.4 kB
JavaScript
import { readdirSync, readFileSync, statSync, writeFileSync } from 'fs'
import { execSync } from 'child_process'
import { join, resolve, extname } from 'path'
import prompts from 'prompts'
import chalk from 'chalk'
// ==================== 配置和类型定义 ====================
/**
* 版本类型枚举
*/
const VersionType = {
PATCH: 'patch',
MINOR: 'minor',
MAJOR: 'major',
PRERELEASE: 'prerelease',
PREPATCH: 'prepatch',
PREMINOR: 'preminor',
PREMAJOR: 'premajor',
}
/**
* 默认配置
*/
class Config {
static getDefaultConfig() {
return {
publicRegistry: 'https://registry.npmjs.org',
privateRegistry: 'http://192.168.4.34:4873',
packagesDir: resolve(process.cwd(), 'packages'),
privateScope: '@privateScope',
publicScope: '@publicScope',
}
}
}
// ==================== 错误处理类 ====================
/**
* 自定义发布错误类
*/
class PublishError extends Error {
constructor(message, code = 'PUBLISH_ERROR', cause = null) {
super(message)
this.name = 'PublishError'
this.code = code
this.cause = cause
}
}
/**
* 统一错误处理器
*/
class ErrorHandler {
/**
* 处理错误并尝试恢复状态
* @param {Error} error
* @param {NpmRegistryManager} registryManager
*/
static handleError(error, registryManager = null) {
console.error(chalk.red('❌ 发布过程中出现错误:'))
console.error(chalk.red(error.message))
// 尝试恢复npm注册表地址
if (registryManager) {
try {
registryManager.restoreOriginalRegistry()
console.log(chalk.yellow('🔄 已恢复npm注册表地址'))
} catch (restoreError) {
console.error(
chalk.red(
'⚠️ 恢复npm注册表地址失败,请手动执行: npm config set registry https://registry.npmjs.org'
)
)
}
}
process.exit(1)
}
}
// ==================== 基础抽象类 ====================
/**
* 抽象注册表接口
*/
class IRegistry {
/**
* 设置注册表地址
* @abstract
* @param {string} registry
*/
setRegistry(registry) {
throw new Error('Method must be implemented')
}
/**
* 发布包
* @abstract
* @param {PackageInfo} packageInfo
*/
publish(packageInfo) {
throw new Error('Method must be implemented')
}
/**
* 获取当前注册表地址
* @abstract
* @returns {string}
*/
getCurrentRegistry() {
throw new Error('Method must be implemented')
}
}
// ==================== 注册表管理类 ====================
/**
* npm注册表管理器 - 单一职责:管理npm注册表
* 使用npm原生命令管理注册表
*/
class NpmRegistryManager extends IRegistry {
constructor() {
super()
this.originalRegistry = null
this.config = Config.getDefaultConfig()
}
/**
* 设置npm注册表地址
* @param {string} registry
*/
setRegistry(registry) {
try {
execSync(`npm config set registry ${registry}`, { stdio: 'inherit' })
} catch (error) {
throw new PublishError(`设置npm注册表地址失败: ${error.message}`, 'REGISTRY_ERROR', error)
}
}
/**
* 发布包到注册表
* @param {PackageInfo} packageInfo
* @param {string} targetRegistry 目标注册表地址
* @param {boolean} isPrivateRegistry 是否是私有注册表
*/
publish(packageInfo, targetRegistry = null, isPrivateRegistry = false) {
try {
const registry = targetRegistry || this.getCurrentRegistry()
// 检查是否是预发布版本
const packageJson = packageInfo.readPackageJson()
const { version } = packageJson
const isPrerelease = version.includes('-') // 预发布版本包含连字符
let publishCommand = 'npm publish'
// 如果是预发布版本,需要添加 --tag 参数
if (isPrerelease) {
// 提取标签,例如从 1.0.0-alpha.1 中提取 alpha
const [, prerelease] = version.split('-')
const [tag] = prerelease.split('.')
publishCommand += ` --tag ${tag}`
}
// 私服发布可能需要额外的参数
if (isPrivateRegistry) {
// 可以在这里添加私服特定的参数
}
execSync(publishCommand, {
stdio: 'inherit',
cwd: packageInfo.path,
})
console.log(chalk.green(`✅ 发布到 ${registry} 完成`))
} catch (error) {
throw new PublishError(`发布失败: ${error.message}`, 'PUBLISH_ERROR', error)
}
}
/**
* 获取当前注册表地址
* @returns {string}
*/
getCurrentRegistry() {
try {
const output = execSync('npm config get registry', { encoding: 'utf-8' })
return output.trim()
} catch (error) {
return this.config.publicRegistry
}
}
/**
* 保存当前注册表地址
*/
saveOriginalRegistry() {
this.originalRegistry = this.getCurrentRegistry()
}
/**
* 恢复原始注册表地址
*/
restoreOriginalRegistry() {
if (this.originalRegistry) {
this.setRegistry(this.originalRegistry)
}
}
}
// ==================== 包管理器类 ====================
/**
* 包信息类 - 封装包的数据和行为
*/
class PackageInfo {
/**
* @param {Object} data
* @param {string} data.name
* @param {string} data.path
* @param {string} data.packagePath
* @param {string} data.version
*/
constructor(data) {
this.name = data.name
this.path = data.path
this.packagePath = data.packagePath
this.version = data.version
}
/**
* 读取package.json内容
* @returns {Object}
*/
readPackageJson() {
try {
const content = readFileSync(this.packagePath, 'utf-8')
return JSON.parse(content)
} catch (error) {
throw new PublishError(`读取package.json失败: ${error.message}`, 'FILE_ERROR', error)
}
}
/**
* 写入package.json内容
* @param {Object} packageJson
*/
writePackageJson(packageJson) {
try {
const content = JSON.stringify(packageJson, null, 2)
writeFileSync(this.packagePath, content, 'utf-8')
} catch (error) {
throw new PublishError(`写入package.json失败: ${error.message}`, 'FILE_ERROR', error)
}
}
/**
* 修改包名
* @param {string} fromScope
* @param {string} toScope
* @returns {string} 原始包名
*/
changeScope(fromScope, toScope) {
const packageJson = this.readPackageJson()
const originalName = packageJson.name
// 如果包名已经包含目标作用域,不需要修改
if (originalName.includes(toScope)) {
return originalName
}
if (!originalName.includes(fromScope)) {
throw new PublishError('包名不包含指定作用域,无法修改', 'SCOPE_ERROR')
}
packageJson.name = originalName.replace(fromScope, toScope)
this.writePackageJson(packageJson)
return originalName
}
/**
* 恢复包名
* @param {string} originalName
*/
restoreScope(originalName) {
const packageJson = this.readPackageJson()
packageJson.name = originalName
this.writePackageJson(packageJson)
}
}
/**
* 包发现器 - 单一职责:发现和管理包
*/
class PackageDiscovery {
/**
* @param {Object} config
*/
constructor(config = Config.getDefaultConfig()) {
this.config = config
}
/**
* 获取packages目录下的所有包
* @returns {PackageInfo[]}
*/
getPackages() {
const packages = []
try {
const entries = readdirSync(this.config.packagesDir)
for (const entry of entries) {
const packagePath = join(this.config.packagesDir, entry)
const stat = statSync(packagePath)
if (this.isValidPackage(entry, stat)) {
const packageJsonPath = join(packagePath, 'package.json')
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'))
if (this.isPublishablePackage(packageJson)) {
packages.push(
new PackageInfo({
name: packageJson.name,
path: packagePath,
packagePath: packageJsonPath,
version: packageJson.version,
})
)
}
} catch (error) {
// 跳过无效的包
}
}
}
} catch (error) {
throw new PublishError(`读取packages目录失败: ${error.message}`, 'DIRECTORY_ERROR', error)
}
return packages
}
/**
* 检查是否为有效包目录
* @param {string} entry
* @param {import('fs').Stats} stat
* @returns {boolean}
*/
isValidPackage(entry, stat) {
return stat.isDirectory()
}
/**
* 检查包是否可发布
* @param {Object} packageJson
* @returns {boolean}
*/
isPublishablePackage(packageJson) {
return packageJson.name && !packageJson.private
}
}
// ==================== 版本管理器类 ====================
/**
* 版本管理器 - 单一职责:管理版本更新
*/
class VersionManager {
/**
* 更新版本
* @param {PackageInfo} packageInfo
* @param {string} versionType
*/
async updateVersion(packageInfo, versionType) {
const packageJson = packageInfo.readPackageJson()
const currentVersion = packageJson.version
let newVersion
switch (versionType) {
case 'patch':
newVersion = this.incrementVersion(currentVersion, 'patch')
break
case 'minor':
newVersion = this.incrementVersion(currentVersion, 'minor')
break
case 'major':
newVersion = this.incrementVersion(currentVersion, 'major')
break
case 'prerelease':
newVersion = this.incrementPrerelease(currentVersion)
break
case 'prepatch':
newVersion = `${this.incrementVersion(currentVersion, 'patch')}-alpha.1`
break
case 'preminor':
newVersion = `${this.incrementVersion(currentVersion, 'minor')}-alpha.1`
break
case 'premajor':
newVersion = `${this.incrementVersion(currentVersion, 'major')}-alpha.1`
break
default:
throw new Error(`不支持的版本类型: ${versionType}`)
}
packageJson.version = newVersion
packageInfo.writePackageJson(packageJson)
}
/**
* 增加版本号
* @param {string} version
* @param {string} type
* @returns {string}
*/
incrementVersion(version, type) {
// 移除预发布标识
const [baseVersion] = version.split('-')
const versionParts = baseVersion.split('.').map(Number)
switch (type) {
case 'patch':
versionParts[2]++
break
case 'minor':
versionParts[1]++
versionParts[2] = 0
break
case 'major':
versionParts[0]++
versionParts[1] = 0
versionParts[2] = 0
break
}
return versionParts.join('.')
}
/**
* 增加预发布版本号
* @param {string} version
* @returns {string}
*/
incrementPrerelease(version) {
if (version.includes('-')) {
// 如果是预发布版本,递增预发布标识符
const [baseVersion, prerelease] = version.split('-')
const prereleaseParts = prerelease.split('.')
// 从后往前找最后一个数字部分
let i = prereleaseParts.length - 1
while (i >= 0 && isNaN(prereleaseParts[i])) {
i--
}
if (i >= 0) {
// 找到了数字部分,递增它
prereleaseParts[i] = (parseInt(prereleaseParts[i]) + 1).toString()
// 重置后续的数字部分为0(如果有的话)
for (let j = i + 1; j < prereleaseParts.length; j++) {
if (!isNaN(prereleaseParts[j])) {
prereleaseParts[j] = '0'
}
}
} else {
// 没有找到数字部分,追加.1
prereleaseParts.push('1')
}
return `${baseVersion}-${prereleaseParts.join('.')}`
} else {
// 如果不是预发布版本,则创建预发布版本 (alpha.1)
return `${version}-alpha.1`
}
}
}
// ==================== 构建管理器类 ====================
/**
* 构建管理器 - 单一职责:管理构建过程
*/
class BuildManager {
/**
* 构建项目 - 只构建选中的包
* @param {PackageInfo} packageInfo
*/
buildProject(packageInfo) {
try {
// 只构建选中的包
execSync('pnpm build', {
stdio: 'inherit',
cwd: packageInfo.path,
})
} catch (error) {
throw new PublishError(`构建失败: ${error.message}`, 'BUILD_ERROR', error)
}
}
}
// ==================== 用户交互管理类 ====================
/**
* 用户交互管理器 - 单一职责:处理用户交互
*/
class UserInteractionManager {
/**
* 用户选择发布的包
* @param {PackageInfo[]} packages
* @returns {Promise<PackageInfo|null>}
*/
async selectPackage(packages) {
if (packages.length === 0) {
console.log(chalk.red('❌ 没有找到可发布的包'))
return null
}
const choices = packages.map(pkg => ({
title: pkg.name,
value: pkg,
}))
const result = await prompts({
type: 'select',
name: 'selectedPackage',
message: '请选择要发布的包:',
choices,
initial: 0,
})
return result.selectedPackage
}
/**
* 用户选择版本更新类型
* @returns {Promise<string|null>}
*/
async selectVersionType() {
const result = await prompts({
type: 'select',
name: 'versionType',
message: '请选择版本更新层级:',
choices: [
{ title: 'patch (补丁版本,如 1.0.0 -> 1.0.1)', value: VersionType.PATCH },
{ title: 'minor (次版本,如 1.0.0 -> 1.1.0)', value: VersionType.MINOR },
{ title: 'major (主版本,如 1.0.0 -> 2.0.0)', value: VersionType.MAJOR },
{ title: 'prerelease (预发布版本)', value: VersionType.PRERELEASE },
{ title: 'prepatch (预发布补丁版本)', value: VersionType.PREPATCH },
{ title: 'preminor (预发布次版本)', value: VersionType.PREMINOR },
{ title: 'premajor (预发布主版本)', value: VersionType.PREMAJOR },
],
initial: 0,
})
return result.versionType
}
/**
* 用户选择发布目标
* @returns {Promise<string[]>}
*/
async selectPublishTargets() {
const result = await prompts({
type: 'multiselect',
name: 'targets',
message: '请选择发布目标:',
choices: [
{ title: '公服', value: 'public', selected: true },
{ title: '私服', value: 'private', selected: true },
],
instructions: false,
})
return result.targets || []
}
}
// ==================== 作用域替换管理器类 ====================
/**
* 作用域替换管理器 - 单一职责:管理包作用域的全局替换和还原
*/
class ScopeReplacementManager {
/**
* 替换包作用域
* @param {PackageInfo} packageInfo
* @param {string} fromScope
* @param {string} toScope
*/
replaceScopeInPackage(packageInfo, fromScope, toScope) {
console.log(chalk.blue('🔍 正在替换作用域...'))
// 存储被修改的文件路径,用于后续还原
const modifiedFiles = []
try {
// 1. 替换 package.json 中的包名
const originalName = this.replacePackageName(packageInfo, fromScope, toScope)
if (originalName) {
modifiedFiles.push(packageInfo.packagePath)
}
// 2. 遍历包目录中的所有文件,替换作用域引用
const result = this.replaceScopeInDirectory(
packageInfo.path,
fromScope,
toScope,
modifiedFiles
)
console.log(chalk.green(`✅ 作用域替换完成 (替换文件: ${modifiedFiles.length})`))
return modifiedFiles
} catch (error) {
// 如果替换过程中出现错误,尝试还原已做的更改
console.log(chalk.red(`❌ 作用域替换过程中出现错误: ${error.message}`))
this.restoreModifiedFiles(modifiedFiles, fromScope, toScope)
throw new PublishError(`作用域替换失败: ${error.message}`, 'SCOPE_REPLACEMENT_ERROR', error)
}
}
/**
* 替换 package.json 中的包名
* @param {PackageInfo} packageInfo
* @param {string} fromScope
* @param {string} toScope
* @returns {string|null} 原始包名,如果未修改则返回 null
*/
replacePackageName(packageInfo, fromScope, toScope) {
const packageJson = packageInfo.readPackageJson()
const originalName = packageJson.name
// 如果包名已经包含目标作用域,不需要修改
if (originalName.includes(toScope)) {
return null
}
if (!originalName.includes(fromScope)) {
throw new PublishError(`包名不包含指定作用域: ${originalName}`, 'SCOPE_ERROR')
}
packageJson.name = originalName.replace(fromScope, toScope)
packageInfo.writePackageJson(packageJson)
return originalName
}
/**
* 遍历目录中的文件,替换作用域引用
* @param {string} dirPath
* @param {string} fromScope
* @param {string} toScope
* @param {string[]} modifiedFiles
* @returns {Object} 处理结果统计
*/
replaceScopeInDirectory(dirPath, fromScope, toScope, modifiedFiles) {
// 忽略的目录和文件模式
const ignorePatterns = ['node_modules', 'dist', '.git', '.DS_Store', '~', 'cache']
const isIgnored = filePath => {
// 检查是否包含忽略模式
if (ignorePatterns.some(pattern => filePath.includes(pattern))) {
return true
}
// 特别检查node_modules目录
const normalizedPath = filePath.replace(/\\/g, '/')
if (normalizedPath.includes('/node_modules/') || normalizedPath.endsWith('/node_modules')) {
return true
}
return false
}
let processedFiles = 0
let replacedFiles = 0
const processDirectory = currentPath => {
const entries = readdirSync(currentPath)
for (const entry of entries) {
const fullPath = join(currentPath, entry)
// 跳过忽略的文件和目录
if (isIgnored(fullPath)) {
continue
}
const stat = statSync(fullPath)
if (stat.isDirectory()) {
processDirectory(fullPath)
} else if (stat.isFile()) {
const result = this.replaceScopeInFile(fullPath, fromScope, toScope, modifiedFiles)
processedFiles++
if (result) {
replacedFiles++
}
}
}
}
processDirectory(dirPath)
return { processedFiles, replacedFiles }
}
/**
* 替换单个文件中的作用域引用
* @param {string} filePath
* @param {string} fromScope
* @param {string} toScope
* @param {string[]} modifiedFiles
* @returns {boolean} 是否进行了替换
*/
replaceScopeInFile(filePath, fromScope, toScope, modifiedFiles) {
// 只处理文本文件
const textFileExtensions = [
'.js',
'.ts',
'.json',
'.md',
'.txt',
'.vue',
'.jsx',
'.tsx',
'.html',
'.css',
'.scss',
'.sass',
]
const ext = extname(filePath).toLowerCase()
if (!textFileExtensions.includes(ext)) {
return false
}
try {
const content = readFileSync(filePath, 'utf-8')
// 检查文件是否包含要替换的作用域
if (content.includes(fromScope)) {
const newContent = content.replace(new RegExp(fromScope, 'g'), toScope)
// 只有当内容实际发生变化时才写入文件
if (content !== newContent) {
writeFileSync(filePath, newContent, 'utf-8')
modifiedFiles.push(filePath)
return true
}
}
return false
} catch (error) {
// 读取或写入文件失败时忽略错误,继续处理其他文件
return false
}
}
/**
* 还原被修改的文件
* @param {string[]} modifiedFiles
* @param {string} fromScope
* @param {string} toScope
*/
restoreModifiedFiles(modifiedFiles, fromScope, toScope) {
if (modifiedFiles.length === 0) {
return
}
console.log(chalk.blue('↩️ 正在还原更改...'))
let restoredFiles = 0
for (const filePath of modifiedFiles) {
try {
const content = readFileSync(filePath, 'utf-8')
// 还原逻辑:将文件中当前的 fromScope 替换为 toScope
const originalContent = content.replace(new RegExp(fromScope, 'g'), toScope)
writeFileSync(filePath, originalContent, 'utf-8')
restoredFiles++
} catch (error) {
// 忽略还原错误
}
}
console.log(chalk.green(`✅ 更改还原完成 (成功还原: ${restoredFiles}/${modifiedFiles.length})`))
}
}
// ==================== 发布策略接口和实现 ====================
/**
* 抽象发布策略接口
*/
class IPublishStrategy {
/**
* 执行发布策略
* @abstract
* @param {PackageInfo} packageInfo
* @param {IRegistry} registryManager
*/
async execute(packageInfo, registryManager) {
throw new Error('Method must be implemented')
}
}
/**
* 公服发布策略
*/
class PublicPublishStrategy extends IPublishStrategy {
async execute(packageInfo, registryManager) {
const config = Config.getDefaultConfig()
registryManager.setRegistry(config.publicRegistry)
registryManager.publish(packageInfo, config.publicRegistry)
}
}
/**
* 私服发布策略
*/
class PrivatePublishStrategy extends IPublishStrategy {
async execute(packageInfo, registryManager) {
const config = Config.getDefaultConfig()
const scopeReplacementManager = new ScopeReplacementManager()
let modifiedFiles = []
try {
// 1. 替换作用域
modifiedFiles = scopeReplacementManager.replaceScopeInPackage(
packageInfo,
config.publicScope,
config.privateScope
)
// 2. 重新构建项目
console.log(chalk.blue('🏗️ 正在构建项目...'))
const buildManager = new BuildManager()
buildManager.buildProject(packageInfo)
console.log(chalk.green('✅ 项目构建完成'))
// 3. 发布到私服
console.log(chalk.blue('📦 正在发布到私服...'))
registryManager.setRegistry(config.privateRegistry)
registryManager.publish(packageInfo, config.privateRegistry, true) // true表示是私服发布
console.log(chalk.green('✅ 私服发布完成'))
} catch (error) {
console.log(chalk.red(`❌ 私服发布失败: ${error.message}`))
throw error
} finally {
// 4. 无论如何都要还原作用域
scopeReplacementManager.restoreModifiedFiles(
modifiedFiles,
config.privateScope, // fromScope - 当前文件中的内容 (@privateScope)
config.publicScope // toScope - 要还原成的内容 (@publicScope)
)
}
}
}
// ==================== 工厂类 ====================
/**
* 发布策略工厂 - 工厂模式创建发布策略
*/
class PublishStrategyFactory {
/**
* 创建发布策略
* @param {string} type
* @returns {IPublishStrategy}
*/
static createStrategy(type) {
switch (type) {
case 'public':
return new PublicPublishStrategy()
case 'private':
return new PrivatePublishStrategy()
default:
throw new PublishError(`未知的发布策略类型: ${type}`, 'STRATEGY_ERROR')
}
}
}
// ==================== 主编排器类 ====================
/**
* npm双仓发布编排器 - 主编排器,协调各个组件
*/
class DualPublishOrchestrator {
/**
* @param {Object} config
*/
constructor(config = Config.getDefaultConfig()) {
this.config = config
this.registryManager = new NpmRegistryManager()
this.packageDiscovery = new PackageDiscovery(config)
this.versionManager = new VersionManager()
this.buildManager = new BuildManager()
this.userInteraction = new UserInteractionManager()
}
/**
* 执行完整的发布流程
* 1. 检索packages文件夹下的子项目
* 2. 用户选择需要发布的包
* 3. 用户选择版本更新类型
* 4. 用户选择发布仓库地址(公服、私服、或者公服+私服)
* 5. 版本更新
* 6. 构建
* 7. 发布
*/
async execute() {
try {
console.log(chalk.cyan('🚀 开始npm发布流程\n'))
// 1. 检索packages文件夹下的子项目,作为发布待选包
const packages = await this.discoverPackages()
// 2. 选择需要发布的包(单选,可以不选->退出流程)
const selectedPackage = await this.userInteraction.selectPackage(packages)
if (!selectedPackage) return this.exitGracefully('取消发布')
// 3. 选择版本更新类型
const versionType = await this.userInteraction.selectVersionType()
if (!versionType) return this.exitGracefully('取消发布')
// 4. 选择发布仓库地址(公服、私服、或者公服+私服)
const publishTargets = await this.userInteraction.selectPublishTargets()
if (publishTargets.length === 0) return this.exitGracefully('取消发布')
// 5-7. 执行发布流程
await this.performPublishWorkflow(selectedPackage, versionType, publishTargets)
// 完成发布
this.completePublication(selectedPackage, versionType, publishTargets)
} catch (error) {
ErrorHandler.handleError(error, this.registryManager)
}
}
/**
* 发现可发布的包
* @returns {Promise<PackageInfo[]>}
*/
async discoverPackages() {
const packages = this.packageDiscovery.getPackages()
if (packages.length === 0) {
throw new PublishError('没有找到可发布的包', 'NO_PACKAGES')
}
return packages
}
/**
* 执行发布工作流程
* 5. 版本更新:使用用户选择的版本更新类型,对用户选择的需要发布的包进行版本号更新
* 6. 构建:构建用户选择的需要发布的包
* 7. 发布:
* 7.1 查询系统当前npm地址,并记录
* 7.2 用户选择向npm公服发布:如果当前npm地址不是公服,则将npm地址切换到公服;发布包
* 7.3 用户选择向npm私服发布:修改包名(将@publicScope@privateScope);如果当前npm地址不是用户需要发布的私服地址,则切换npm地址;发布包;还原包名
* 7.4 如果切换了npm地址,还原系统npm地址
* @param {PackageInfo} selectedPackage
* @param {string} versionType
* @param {string[]} publishTargets
*/
async performPublishWorkflow(selectedPackage, versionType, publishTargets) {
// 5. 版本更新:使用用户选择的版本更新类型,对用户选择的需要发布的包进行版本号更新
await this.versionManager.updateVersion(selectedPackage, versionType)
// 7.1 查询系统当前npm地址,并记录
this.registryManager.saveOriginalRegistry()
// 7.2 用户选择向npm公服发布:如果当前npm地址不是公服,则将npm地址切换到公服;发布包
if (publishTargets.includes('public')) {
// 只有在发布公服时才构建
console.log(chalk.blue('🏗️ 正在构建项目...'))
this.buildManager.buildProject(selectedPackage)
console.log(chalk.green('✅ 项目构建完成'))
console.log(chalk.blue('📦 发布到公服...'))
const publicStrategy = PublishStrategyFactory.createStrategy('public')
await publicStrategy.execute(selectedPackage, this.registryManager)
console.log(chalk.green('✅ 公服发布完成'))
}
// 7.3 用户选择向npm私服发布:修改包名(将@publicScope@privateScope);如果当前npm地址不是用户需要发布的私服地址,则切换npm地址;发布包;还原包名
if (publishTargets.includes('private')) {
console.log(chalk.blue('📦 发布到私服...'))
const privateStrategy = PublishStrategyFactory.createStrategy('private')
await privateStrategy.execute(selectedPackage, this.registryManager)
console.log(chalk.green('✅ 私服发布完成'))
}
// 7.4 如果切换了npm地址,还原系统npm地址
this.registryManager.restoreOriginalRegistry()
}
/**
* 完成发布流程
* @param {PackageInfo} packageInfo
* @param {string} versionType
* @param {string[]} publishTargets
*/
completePublication(packageInfo, versionType, publishTargets) {
console.log(chalk.green('\n🎉 npm发布完成!'))
console.log(chalk.green(`📦 包名: ${packageInfo.name}`))
console.log(chalk.green(`📦 版本类型: ${versionType}`))
if (publishTargets.includes('public')) {
console.log(chalk.green('📦 公服发布: ✅ 完成'))
}
if (publishTargets.includes('private')) {
console.log(chalk.green('📦 私服发布: ✅ 完成'))
}
}
/**
* 优雅退出
* @param {string} message
*/
exitGracefully(message) {
console.log(chalk.yellow(`👋 ${message},退出流程`))
process.exit(0)
}
}
// ==================== 主入口 ====================
/**
* 主函数 - 使用工厂模式创建编排器
*/
function main() {
const config = Config.getDefaultConfig()
const orchestrator = new DualPublishOrchestrator(config)
orchestrator.execute()
}
// 启动应用
main()