Files
cocos/tools/package-optimize.js
2026-06-18 14:07:38 +08:00

173 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 打包优化关卡库压缩、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默认只预加载 mainresources / 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 storeloader 不支持 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,
};