UNPKG

@honor-minigame/cli

Version:

honor minigame pack cli

486 lines (432 loc) 18.6 kB
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) }) } } }