/** * 打包优化:关卡库压缩、settings 预加载策略、体积报告。 * 供 package-for-project.js / package-for-cdn.js 共用。 */ const fs = require('fs'); const os = require('os'); const path = require('path'); const zlib = require('zlib'); const { execSync } = require('child_process'); function formatBytes(n) { if (n >= 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`; if (n >= 1024) return `${(n / 1024).toFixed(1)} KB`; return `${n} B`; } function dirSize(root) { if (!fs.existsSync(root)) return 0; let total = 0; for (const ent of fs.readdirSync(root, { withFileTypes: true })) { const p = path.join(root, ent.name); if (ent.isDirectory()) total += dirSize(p); else total += fs.statSync(p).size; } return total; } function fileSize(p) { return fs.existsSync(p) ? fs.statSync(p).size : 0; } /** 压缩关卡库 JSON(去掉空白,不改变语义) */ function minifyLevelsDatabase(srcPath, dstPath) { const raw = fs.readFileSync(srcPath, 'utf8'); const data = JSON.parse(raw); const compact = JSON.stringify(data); fs.writeFileSync(dstPath, compact, 'utf8'); return { before: Buffer.byteLength(raw, 'utf8'), after: Buffer.byteLength(compact, 'utf8') }; } /** 可选:生成 .br 供静态服务器直接返回 */ function brotliCompressFile(srcPath, dstPath, quality = 9) { const input = fs.readFileSync(srcPath); const out = zlib.brotliCompressSync(input, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: quality }, }); fs.writeFileSync(dstPath, out); return { raw: input.length, br: out.length }; } /** 为 WebGL 目录下所有 .bundle 生成 .bundle.br */ function brotliCompressWebglBundles(webglDir, opts = {}) { if (!fs.existsSync(webglDir)) return { count: 0, saved: 0 }; let count = 0; let saved = 0; for (const name of fs.readdirSync(webglDir)) { if (!name.endsWith('.bundle') || name.endsWith('.bundle.br')) continue; const src = path.join(webglDir, name); if (!fs.statSync(src).isFile()) continue; const { raw, br } = brotliCompressFile(src, src + '.br', opts.quality); count += 1; saved += raw - br; console.log(`>>> ${name}.br: ${formatBytes(raw)} → ${formatBytes(br)}`); } return { count, saved }; } /** * 调整预加载 bundle:默认只预加载 main,resources / level-prefabs 按需加载。 * @param {object} opts * @param {boolean} [opts.preloadResources=false] * @param {boolean} [opts.preloadLevelPrefabs=false] */ function patchPreloadSettings(settingsObj, opts = {}) { const preloadResources = opts.preloadResources === true; const preloadLevelPrefabs = opts.preloadLevelPrefabs === true; const bundles = settingsObj.assets?.projectBundles || []; const preload = [{ bundle: 'main' }]; if (preloadResources && bundles.includes('resources')) { preload.push({ bundle: 'resources' }); } if (preloadLevelPrefabs && bundles.includes('level-prefabs')) { preload.push({ bundle: 'level-prefabs' }); } settingsObj.assets = settingsObj.assets || {}; settingsObj.assets.preloadBundles = preload; return settingsObj; } /** * 关闭 Cocos 启动闪屏("Created with Cocos")。 * totalTime <= 0 时引擎跳过闪屏等待,首屏可快约 2s;不影响关卡切换与网络下载。 * @param {object} opts * @param {boolean} [opts.disableSplash=true] * @param {boolean} [opts.stripSplashAssets=false] 去掉内嵌 logo/background,略减 scenes 包体积 */ function patchSplashSettings(settingsObj, opts = {}) { const disableSplash = opts.disableSplash !== false; if (!disableSplash) return settingsObj; settingsObj.splashScreen = settingsObj.splashScreen || {}; settingsObj.splashScreen.totalTime = 0; if (opts.stripSplashAssets) { delete settingsObj.splashScreen.logo; delete settingsObj.splashScreen.background; } return settingsObj; } /** * 就地修改 zip bundle 内的 settings.json(必须用 zip -0 store,loader 不支持 deflate)。 */ function patchSplashInZipBundle(zipPath, opts = {}) { const absZip = path.resolve(zipPath); const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cocos-splash-')); try { execSync(`unzip -q -o "${absZip}" -d "${tmp}"`, { stdio: 'pipe' }); const settingsPath = path.join(tmp, 'src', 'settings.json'); if (!fs.existsSync(settingsPath)) return false; const j = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); patchSplashSettings(j, opts); fs.writeFileSync(settingsPath, JSON.stringify(j)); const newZip = absZip + '.new'; if (fs.existsSync(newZip)) fs.unlinkSync(newZip); execSync(`cd "${tmp}" && zip -0 -q -r "${newZip}" .`, { stdio: 'pipe' }); fs.renameSync(newZip, absZip); return true; } finally { fs.rmSync(tmp, { recursive: true, force: true }); } } function printPackageReport(outDir, opts = {}) { const lines = ['\n>>> 包体积报告:']; const entries = [ ['cocos-js', path.join(outDir, 'cocos-js')], ['src', path.join(outDir, 'src')], ['assets/main', path.join(outDir, 'assets', 'main')], ['assets/resources', path.join(outDir, 'assets', 'resources')], ['assets/level-prefabs', path.join(outDir, 'assets', 'level-prefabs')], ['assets/internal', path.join(outDir, 'assets', 'internal')], ['levels-database.json', path.join(outDir, 'levels-database.json')], ['levels-database.json.br', path.join(outDir, 'levels-database.json.br')], ['Build', path.join(outDir, 'Build')], ]; let total = 0; for (const [label, p] of entries) { if (!fs.existsSync(p)) continue; const sz = fs.statSync(p).isDirectory() ? dirSize(p) : fileSize(p); if (sz <= 0) continue; total += sz; lines.push(` ${label.padEnd(28)} ${formatBytes(sz)}`); } const grand = dirSize(outDir); lines.push(` ${'合计(顶层目录)'.padEnd(28)} ${formatBytes(grand)}`); if (opts.preloadNote) lines.push(` 预加载: ${opts.preloadNote}`); console.log(lines.join('\n')); } module.exports = { formatBytes, dirSize, fileSize, minifyLevelsDatabase, brotliCompressFile, brotliCompressWebglBundles, patchPreloadSettings, patchSplashSettings, patchSplashInZipBundle, printPackageReport, };