@honor-minigame/cli
Version:
honor minigame pack cli
486 lines (432 loc) • 18.6 kB
JavaScript
import fs from 'fs-extra'
import path from 'path'
import { check, get, add } from './lib/manifest.js'
import * as paths from './lib/paths.js'
import { filter, toConfigs } from './lib/subpackage.js'
import { done, error, info } from '../utils/logger.js'
import { checkError, multiConfig, singleConfig, base } from './config/process.js'
import { LOG_TITLE, RPK, RPK_SIGNED } from './lib/constanst.js'
import { fsExt } from '../utils/fsUtils.js'
import zipPack from './lib/zipPack.js'
import webpack from './webpack/lib/index.js'
let isIde = false
let packageType = 'build'
let targetPath = paths.DIST
/**
* 发布构建
*
* @param {Object} args - 构建参数
* @returns {Promise<Object>} 构建结果
*/
export const release = async (args) => {
return build({
...args,
isRelease: true,
buildType: 'release'
})
}
/**
* 构建项目包
*
* @param {Object} options - 构建选项
* @param {boolean} [options.isRelease=false] - 是否为发布版本
* @param {boolean} [options.inject=true] - 是否注入依赖
* @param {string} [options.buildType] - 构建类型
* @param {function} [options.complete=() => {}] - 构建完成后的回调函数
* @param {boolean} [options.amd=true] - 是否使用AMD模块
* @param {boolean} [options.rpk=false] - 是否使用RPK打包
* @returns {Promise<void>} 无返回值
*/
export const build = async ({ isRelease = false, inject = true, buildType, complete = () => { }, amd = true, rpk = false, ide = false, dist = '' }) => {
packageType = isRelease ? 'release' : 'build'
// 设置全局是否ide调用
targetPath = dist || paths.DIST
// 校验manifest
check()
try {
// 校验通过后,可以清空编译目录
fs.removeSync(paths.BUILD)
// IDE支持修改dist路径,如果路径为项目父级,会出现报错,所以在IDE中使用时,不删除dist目录
if (!ide) {
fs.removeSync(targetPath)
}
// TODO 开发需要看temp里面的包,方便测试,上线后去掉
fs.removeSync(paths.TEMP)
} catch (err) {
console.error('removeSync failed', err)
}
// 读取manifest配置
const manifest = get()
// 添加manifest配置 buildType
if (buildType) {
add('buildType', buildType)
} else {
add('buildType', 'dev')
}
// 过滤出合法的分包配置
const subpackages = filter(manifest.subpackages)
// 将分包配置转化为对应的webpack的打包入口
// 将分包配置转化为对应的externals用于打出兼容的原整包
const { entry, externals, isMain } = toConfigs(subpackages)
// 对分包名称判断 不能为main
if (isMain) {
error('请检查分包的配置,分包名称不能为main,请修改分包名称')
return
}
// 取出包名
const packageName = manifest.package || 'Bundle'
const rpkStartTime = (new Date()).valueOf()
// 如果配置了rpk,则直接将src目录copy到build目录并压缩
if (rpk) {
rpkZip(isRelease, packageName, subpackages, manifest, buildType, rpkStartTime, complete)
return
}
// 先打原整包
const singleCompiler = webpack(singleConfig(entry, isRelease, externals, inject, amd))
/**
* 编译原整包的完成处理函数
* @param {Object} err
* @param {Object} stats
*/
const singleDone = (err, stats) => {
if (checkError(err, stats)) {
return
}
// 如果存在未使用的模块,则将所有未使用的模块作为入口重新webpack打包
if (paths.unUsedFiles.length !== 0) {
info(`paths.unUsedFiles 存在未引入的文件重新打包,module: ${paths.unUsedFiles}`)
const entry = Object.create(null)
paths.unUsedFiles.forEach((unUsedFile) => {
const filePath = path.resolve(paths.BUILD, unUsedFile)
if (fs.existsSync(filePath)) {
const parsedPath = path.parse(unUsedFile)
const entryKey = path.join(parsedPath.dir, parsedPath.name)
entry[entryKey] = filePath
}
})
paths.unUsedFiles = []
const compiler = webpack(base(isRelease, entry, amd))
compiler.run(singleDone)
return
}
const compileStartTime = (new Date()).valueOf()
const promise = zipPack(isRelease, packageName, true, subpackages, manifest, buildType, compileStartTime)
promise.then((resolve) => {
doneNext()
})
}
// 等待原整包打包完成再继续编译
const doneNext = () => {
info(`${LOG_TITLE}原整包打包完成! 耗时: ${((new Date()).valueOf() - rpkStartTime) / 1000}s`)
// 原整包打包完成后,判断是否存在分包配置,如果有就编译分包,如果没有,拷贝原整包作为最终的输出包
if (subpackages.length) {
// 原整包打包完成后,开始打分包
info(`${LOG_TITLE}开始编译分包...`)
webpack(multiConfig(entry, isRelease, packageName, externals, subpackages, manifest, buildType, amd), (err, stats) => {
if (checkError(err, stats)) {
return
}
info(`${LOG_TITLE}总包打包完成~~~! 总耗时: ${((new Date()).valueOf() - rpkStartTime) / 1000}s`)
const qgGameName = path.basename(projectPath) + '-qg-build'
const dist = targetPath ? path.join(targetPath, qgGameName) : path.join(path.dirname(projectPath), qgGameName)
const isDistParent = path.dirname(dist).startsWith(paths.DIST) || (path.resolve(path.dirname(dist)).toLowerCase() === path.resolve(paths.DIST).toLowerCase())
const isTempParent = path.dirname(dist).startsWith(paths.TEMP) || (path.resolve(path.dirname(dist)).toLowerCase() === path.resolve(paths.TEMP).toLowerCase())
if (!isDistParent && !isTempParent) {
try {
// 创建A和B文件夹
const folderDist = path.join(dist, 'dist')
const folderDistTemp = path.join(dist, 'dist_temp')
// 检查目标路径是否存在且有写入权限
if (!fs.existsSync(folderDist)) {
fs.mkdirSync(folderDist, { recursive: true })
}
if (!fs.existsSync(folderDistTemp)) {
fs.mkdirSync(folderDistTemp, { recursive: true })
}
// 确保文件夹存在
try {
fs.ensureDirSync(folderDistTemp)
fs.ensureDirSync(folderDist)
} catch (mkdirErr) {
error(`创建文件夹失败: ${mkdirErr.message}`)
throw mkdirErr
}
// 复制DIST内容到folderDist文件夹
if (fs.existsSync(paths.DIST)) {
try {
fs.copySync(paths.DIST, folderDist)
info(`已将 ${paths.DIST} 内容复制到 ${folderDist}`)
} catch (copyErr) {
error(`复制DIST内容失败: ${copyErr.message}`)
throw copyErr
}
}
// 复制TEMP内容到folderDist_temp文件夹
if (fs.existsSync(paths.TEMP)) {
try {
fs.copySync(paths.TEMP, folderDistTemp)
info(`已将 ${paths.TEMP} 内容复制到 ${folderDistTemp}`)
} catch (copyErr) {
error(`复制TEMP内容失败: ${copyErr.message}`)
throw copyErr
}
}
} catch (err) {
error(`处理文件夹操作失败: ${err.message}`)
return
}
}
final(packageName, complete, rpkStartTime)
})
} else {
// 不分包不带插件包的 才走这个逻辑
// 不分包的带插件包的打包已经在webpack插件里面做了
const temp = path.join(paths.TEMP, `${packageName}${RPK}`)
const dist = path.join(targetPath, `${packageName}${isRelease ? RPK_SIGNED : RPK}`)
fs.copy(temp, dist).then(() => {
final(packageName, complete, rpkStartTime)
}).catch(err => {
error(err)
})
}
}
info(`${LOG_TITLE}开始编译原整包...`)
singleCompiler.run(singleDone)
}
// 全部结束后的处理函数
const final = (packageName, complete, rpkStartTime) => {
complete()
done(`${LOG_TITLE}【统一rpk】压缩包已生成:${targetPath} ${((new Date()).valueOf() - rpkStartTime) / 1000}s`)
// 打印包体信息
logRpk(packageName)
// 通知引擎自动更新包
// notify()
}
function logRpk(packageName) {
// 打印包体信息
try {
done(`【统一rpk】打包完成,包体信息如下:\n`)
const files = fs.readdirSync(paths.TEMP)
let subAllSize = 0
let subSize = 0
let mainSize = 0
let fileInfoArray = files.map(file => {
if (file.endsWith('.rpk')) {
const filePath = path.resolve(paths.TEMP, file)
const stats = fs.statSync(filePath)
let size = (Number(stats.size / 1024 / 1024)).toFixed(3)
size = size > 1 ? (Number(size)).toFixed(1) : size
const fileName = file.split('.rpk')[0]
// 包体名称
let name = fileName === 'main' ? '主包' : `分包/${fileName}`
name = fileName === packageName ? '原整包' : name
name = fileName === 'cocos-library' || fileName === 'laya-library' || fileName === 'egret-library' ? `插件包` : name
// 分包size加到一起
if (name.startsWith('分包')) {
subAllSize += parseFloat(size)
subSize++
}
if (name === '主包') {
mainSize = size
}
return { name: name, file, size: `${size} MB` }
}
}).filter(fileInfo => fileInfo !== undefined)
if (fileInfoArray.length === 1) {
const files = fs.readdirSync(targetPath)
fileInfoArray = files.map(file => {
if (file.endsWith('.rpk')) {
const fileName = file.split('.rpk')[0]
if (fileName.includes(packageName)) {
const filePath = path.resolve(targetPath, file)
const stats = fs.statSync(filePath)
let size = (Number(stats.size / 1024 / 1024)).toFixed(3)
size = size > 1 ? (Number(size)).toFixed(1) : size
return { name: 'dist/整包', file, size: `${size} MB` }
}
}
}).filter(fileInfo => fileInfo !== undefined)
} else {
const files = fs.readdirSync(targetPath)
const fileDistArray = files.map(file => {
if (file.endsWith('.rpk')) {
const fileName = file.split('.rpk')[0]
if (fileName.includes(packageName)) {
const filePath = path.resolve(targetPath, file)
const stats = fs.statSync(filePath)
let size = (Number(stats.size / 1024 / 1024)).toFixed(3)
size = size > 1 ? (Number(size)).toFixed(1) : size
return { name: 'dist/整包', file, size: `${size} MB` }
}
}
}).filter(fileInfo => fileInfo !== undefined)
if (subAllSize !== 0) {
fileInfoArray.push({ name: '所有分包', file: `${subSize}个`, size: `${(Number(subAllSize)).toFixed(3)} MB` })
}
const sortOrder = new Map([
['原整包', 1],
['主包', 2],
['插件包', 3],
['所有分包', 4]
])
fileInfoArray.sort((a, b) => {
if (sortOrder.get(a.name) && sortOrder.get(b.name)) {
return sortOrder.get(a.name) - sortOrder.get(b.name)
}
if (sortOrder.get(a.name)) {
return -1
}
if (sortOrder.get(b.name)) {
return 1
}
const sizeA = parseFloat(a.size.split('MB')[0])
const sizeB = parseFloat(b.size.split('MB')[0])
// 先按文件大小排序
if (sizeA !== sizeB) {
return sizeB - sizeA
} else {
return b.file.localeCompare(a.file)
}
})
fileInfoArray = fileDistArray.concat(fileInfoArray)
}
console.log(fileInfoArray.map(obj => Object.values(obj)))
if (fileInfoArray.length === 1) {
console.info(`\n### 开平会校验单个整包大小:${fileInfoArray[0].size}`)
} else if (mainSize !== 0 && subAllSize !== 0) {
console.info(`\n### 开平只会校验 主包main.rpk大小:${mainSize} MB, 所有分包大小:${(Number(subAllSize)).toFixed(3)} MB`)
}
console.log('\n')
} catch (err) {
}
}
function notify() {
try {
info(`### App Server ### 发送更新通知请求`)
const json = fs.readJsonSync(path.resolve(projectPath, 'client.json'))
console.log(json)
// 给游戏前台发送更新通知
const gameRequrl = `http://${json.client}:49517/update`
const reqGame = http.request({
host: json.client,
port: 49517,
path: '/update',
timeout: 2000
}, (res) => {
info(`### App Server ### 给引擎发送更新通知请求: ${gameRequrl} 成功`)
}).on('error', () => {
info(`### App Server ### 给引擎发送更新通知请求: ${gameRequrl} 失败`)
// 给调试器发送
const requrl = `http://${json.client}:39517/update`
const req = http.request({
host: json.client,
port: 39517,
path: '/update',
timeout: 2000
}, (res) => {
info(`### App Server ### 发送自动更新调试器器请求: ${requrl} 成功`)
}).on('error', () => {
// error(`### App Server ### 发送自动更新调试器请求: ${requrl} 失败: ${err.message}`)
}).on('timeout', function () {
// error(`### App Server ### 发送自动更新调试器请求: ${requrl} 超时: 3000ms`)
req.abort()
})
req.end()
}).on('timeout', function () {
info(`### App Server ### 给引擎发送更新通知请求: ${gameRequrl} 超时: 3000ms`)
reqGame.abort()
})
reqGame.end()
} catch (error) {
info(`### App Server ### 没找到client信息`)
}
}
/**
* 不使用webpack直接压缩打包联盟RPK文件
*
* @param {boolean} isRelease - 是否为发布版本
* @param {string} packageName - 包的名称
* @param {Array} subpackages - 分包配置
* @param {Object} manifest - 包的清单文件
* @param {string} buildType - 构建类型
* @param {number} rpkStartTime - RPK打包开始时间
* @param {function} complete - 打包完成后的回调函数
* @returns {Promise} 返回一个Promise,表示打包过程的状态
*/
const rpkZip = (isRelease, packageName, subpackages, manifest, buildType, rpkStartTime, complete) => {
// copy src目录下文件到build目录
fsExt.overwrite(paths.SRC, paths.BUILD)
// 联盟包体处理
// 联盟适配转换代码
let needTransferAlliance = true
const vivoAllianceAdapterJS = 'vivo_alliance_adapter'
const gameContent = fs.readFileSync(path.resolve(paths.BUILD, './game.js'), {
encoding: 'utf-8'
})
if (gameContent.includes(vivoAllianceAdapterJS)) {
needTransferAlliance = false
info('game.js 包含联盟 vivo_alliance_adapter.js 转换代码,不进行二次转换...')
}
if (needTransferAlliance) {
info('开始写入联盟转换代码...')
// 生成 main.js文件,引入 game.js,作为适配联盟api的入口
if (fs.existsSync(path.resolve(paths.BUILD, './main.js'))) {
info('main.js为入口文件,不允许作为业务文件引入进来,如果游戏有报错,请修改文件名,重新打包')
const content = fs.readFileSync(path.resolve(paths.BUILD, './main.js'), {
encoding: 'utf-8'
})
// 判断build是否包含适配文件
const allianceJs = path.resolve(paths.BUILD, './vivo_alliance_adapter.js')
if (!fs.existsSync(allianceJs)) {
const allianceJs = path.join(__dirname, './plugins/alliance_adapter/vivo_alliance_adapter.js')
fs.copyFileSync(allianceJs, path.resolve(paths.BUILD, './vivo_alliance_adapter.js'))
}
// 判断main.js是否已引入适配js
if (!content.includes(vivoAllianceAdapterJS)) {
// 重新写入vivo_alliance_adapter
const allianceAdapterContent = 'require(\'vivo_alliance_adapter.js\');\n'
fs.outputFileSync(path.resolve(paths.BUILD, './main.js'), allianceAdapterContent + content, 'utf8')
}
} else {
const allianceJs = path.join(__dirname, './plugins/alliance_adapter/vivo_alliance_adapter.js')
fs.copyFileSync(allianceJs, path.resolve(paths.BUILD, './vivo_alliance_adapter.js'))
fs.outputFileSync(path.resolve(paths.BUILD, './main.js'), 'require(\'vivo_alliance_adapter.js\');\nrequire("game.js");', 'utf8')
}
} else {
// 生成 main.js文件,引入 game.js,作为适配联盟api的入口
if (fs.existsSync(path.resolve(paths.BUILD, './main.js'))) {
info('main.js为入口文件,不允许作为业务文件引入进来,如果游戏有报错,请修改文件名,重新打包')
// throw new Error()
} else {
fs.outputFileSync(path.resolve(paths.BUILD, './main.js'), 'require("game.js");', 'utf8')
}
}
// 打包原整包
const compileStartTime = (new Date()).valueOf()
const promise = zipPack(isRelease, packageName, true, subpackages, manifest, buildType, compileStartTime)
promise.then((resolve) => {
doneNext()
})
// 等待原整包打包完成再继续编译
const doneNext = () => {
info(`${LOG_TITLE}原整包打包完成! 耗时: ${((new Date()).valueOf() - rpkStartTime) / 1000}s`)
// 原整包打包完成后,判断是否存在分包配置,如果有就编译分包,如果没有,拷贝原整包作为最终的输出包
if (subpackages.length) {
// 原整包打包完成后,开始打分包
info(`${LOG_TITLE}开始编译分包...`)
const compileStartTime = (new Date()).valueOf()
const promise = zipPack(isRelease, packageName, false, subpackages, manifest, buildType, compileStartTime)
promise.then((resolve) => {
final(packageName, complete, rpkStartTime)
})
} else {
const temp = path.join(paths.TEMP, `${packageName}${RPK}`)
const dist = path.join(targetPath, `${packageName}${isRelease ? RPK_SIGNED : RPK}`)
fs.copy(temp, dist).then(() => {
final(packageName, complete, rpkStartTime)
}).catch(err => {
error(err)
})
}
}
}