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

276 lines
12 KiB
JavaScript
Raw Permalink 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.
#!/usr/bin/env node
/**
* 项目运行包:扁平结构,直接供 scratch-gui static/unity 本地加载。
*
* node tools/package-for-project.js <cocosBuildDir> <outDir> [options]
*
* 选项:
* --minify-db 压缩 levels-database.json默认开启
* --no-minify-db 保留格式化 JSON
* --brotli-db 额外生成 levels-database.json.br
* --preload-resources 启动时预加载 resources 包(默认关闭,按需加载)
* --preload-levels 启动时预加载 level-prefabs 包(默认关闭)
* --report 打印体积报告(默认开启)
* --no-report
*
* 输出结构:
* Build/mstest5.loader.js + 占位 .br
* index.js, application.js, cocos-bridge.js
* cocos-js/, src/, assets/
*/
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const {
minifyLevelsDatabase,
brotliCompressFile,
patchPreloadSettings,
patchSplashSettings,
printPackageReport,
formatBytes,
} = require('./package-optimize');
const argv = process.argv.slice(2);
const positional = [];
const flags = new Set();
for (const a of argv) {
if (a.startsWith('--')) flags.add(a);
else positional.push(a);
}
const buildDir = path.resolve(positional[0] || '');
const outDir = path.resolve(positional[1] || '');
const opts = {
minifyDb: !flags.has('--no-minify-db'),
brotliDb: flags.has('--brotli-db'),
preloadResources: flags.has('--preload-resources'),
preloadLevelPrefabs: flags.has('--preload-levels'),
report: !flags.has('--no-report'),
};
const PRODUCT = 'mstest5';
const UNITY_BASE = '/unity/';
const templateDir = path.join(__dirname, '../web-template');
if (!buildDir || !outDir) {
console.error('Usage: package-for-project.js <cocosBuildDir> <outDir> [--minify-db] [--brotli-db] ...');
process.exit(1);
}
function rmrf(p) {
if (fs.existsSync(p)) fs.rmSync(p, { recursive: true, force: true });
}
function mkdirp(p) { fs.mkdirSync(p, { recursive: true }); }
function copyFile(s, d) { mkdirp(path.dirname(d)); fs.copyFileSync(s, d); }
function copyDir(s, d) {
if (!fs.existsSync(s)) return;
mkdirp(d);
for (const ent of fs.readdirSync(s, { withFileTypes: true })) {
const sp = path.join(s, ent.name);
const dp = path.join(d, ent.name);
if (ent.isDirectory()) copyDir(sp, dp);
else copyFile(sp, dp);
}
}
function patchText(file, fn) {
const t = fs.readFileSync(file, 'utf8');
fs.writeFileSync(file, fn(t), 'utf8');
}
function brotliStub(dst) {
fs.writeFileSync(dst, zlib.brotliCompressSync(Buffer.from([0])));
}
const required = ['index.js', 'application.js', 'src', 'cocos-js', 'assets'];
for (const r of required) {
if (!fs.existsSync(path.join(buildDir, r))) {
console.error('Missing in Cocos build:', r, 'at', buildDir);
process.exit(1);
}
}
console.log('>>> Cocos 构建:', buildDir);
console.log('>>> 输出(项目运行包):', outDir);
console.log('>>> 优化:', JSON.stringify({
minifyDb: opts.minifyDb,
brotliDb: opts.brotliDb,
preloadResources: opts.preloadResources,
preloadLevelPrefabs: opts.preloadLevelPrefabs,
}));
mkdirp(outDir);
for (const item of [
'Build', 'index.js', 'application.js', 'cocos-bridge.js',
'cocos-js', 'src', 'assets',
'StreamingAssets', 'TemplateData', 'index.html',
'levels-database.json', 'levels-database.json.br',
]) {
rmrf(path.join(outDir, item));
}
for (const item of ['index.js', 'application.js']) {
copyFile(path.join(buildDir, item), path.join(outDir, item));
}
copyDir(path.join(buildDir, 'src'), path.join(outDir, 'src'));
copyDir(path.join(buildDir, 'cocos-js'), path.join(outDir, 'cocos-js'));
copyDir(path.join(buildDir, 'assets'), path.join(outDir, 'assets'));
const EMBEDDED_RESOLUTION_PATCH =
'!(function(){v.resizeWithBrowserSize(!0);v.setDesignResolutionSize(G,H,C.FIXED_WIDTH)})()';
function patchMainIndexRuntime(t) {
return t
.replace(/return\[\]\.concat\(l\)\},entityFlipX/g, 'return Array.from(l)},entityFlipX')
.replace(/return\[\]\.concat\(n\)\},e\.ensureUILayerTree/g, 'return Array.from(n)},e.ensureUILayerTree')
.replace(/e\("applyEmbeddedDesignResolution",\(function\(\)\{[^}]+\}\)\)/g, 'e("applyEmbeddedDesignResolution",(function(){t.resizeWithBrowserSize(!0),t.setDesignResolutionSize(s,o,n.FIXED_WIDTH)}))')
.replace(/v\.setDesignResolutionSize\(G,H,C\.(?:SHOW_ALL|FIXED_WIDTH|FIXED_HEIGHT)\)/g, EMBEDDED_RESOLUTION_PATCH)
.replace(/setDesignResolutionSize\(G,H,C\.(?:SHOW_ALL|FIXED_WIDTH|FIXED_HEIGHT)\)/g, EMBEDDED_RESOLUTION_PATCH)
.replace(
/e\.syncMapDataComponent=function\(e,n\)\{var r,t,o=e\.getComponent\(p\);o&&\(o\.levelID=n\.levelID,o\.theme=T\(n\),o\.groundJson=JSON\.stringify\(null!=\(r=n\.ground\)\?r:\{\}\),o\.borderJson=JSON\.stringify\(null!=\(t=n\.border\)\?t:\{\}\)\)\}/,
'e.syncMapDataComponent=function(e,n){var r,t,o=e.getComponent(p);if(!o)return;o.levelID=n.levelID;var i=n.theme&&String(n.theme).trim();o.theme=i?T(n):o.theme||T(n);r=n.ground;r&&Object.keys(r).length&&(o.groundJson=JSON.stringify(r));t=n.border;t&&Object.keys(t).length&&(o.borderJson=JSON.stringify(t))}',
)
.replace(
/e\.prepare=function\(\)\{var e=r\(\(function\*\(e,n\)\{e\.name="Level_"\+n\.levelID,f\.purgeRuntimeGrids\(e\),this\.ensureUILayerTree\(e\);var r=T\(n\),t=m\(e,n\);/,
'e.prepare=function(){var e=r((function*(e,n){e.name="Level_"+n.levelID,f.purgeRuntimeGrids(e),this.ensureUILayerTree(e);var _md=e.getComponent(p);if(_md){try{var _g=JSON.parse(_md.groundJson||"{}");(!n.ground||!Object.keys(n.ground).length)&&Object.keys(_g).length&&(n.ground=_g);var _b=JSON.parse(_md.borderJson||"{}");(!n.border||!Object.keys(n.border).length)&&Object.keys(_b).length&&(n.border=_b)}catch(_e){}(!n.theme||!String(n.theme).trim())&&_md.theme&&(n.theme=_md.theme)}var r=T(n),t=m(e,n);',
)
.replace(
/function p\(e\)\{var r=e\.children\.filter\(\(function\(e\)\{return e\.isValid&&y\.test\(e\.name\)\}\)\);/,
'function p(e){if(!e||!e.isValid||!e.children)return;var r=e.children.filter((function(e){return e.isValid&&y.test(e.name)}));',
)
.replace(
/this\.applyMapDataFromConfig\(n\),yield J\.prepare\(o,n\);var s=n\.theme\|\|"silu";/,
'yield J.prepare(o,n),this.applyMapDataFromConfig(n),this.curConfig=n;var s=n.theme||"silu";',
)
.replace(
/c\.moveTowards\(i,e,this\.targetPosition,this\.moveSpeed\*t\)/g,
'c.moveTowards(i,e,this.targetPosition,this.moveSpeed*t*((null!=(S=globalThis.__tfrhGameSpeed)&&S>0)?S:1))',
)
.replace(
/if\(!\(t<=0\)\)if\(this\.frameTimer\+=e,!\(this\.frameTimer<t\)\)/,
'if(!(t<=0))if(this.frameTimer+=e*((null!=(z=globalThis.__tfrhGameSpeed)&&z>0)?z:1),!(this.frameTimer<t))',
)
.replace(
/a\.onPlaySpeedChange=function\(\)\{this\.speedIndex=\(this\.speedIndex\+1\)%l\.SPEEDS\.length,d\.globalGameTimeScale=l\.SPEEDS\[this\.speedIndex\],this\.setPlaySpeedSprite\(this\.resolveHudTheme\(\)\)\}/,
'a.onPlaySpeedChange=function(){this.speedIndex=(this.speedIndex+1)%l.SPEEDS.length;var e=l.SPEEDS[this.speedIndex];d.globalGameTimeScale=e,globalThis.__tfrhGameSpeed=e,console.log("[UIMain] 倍速 x"+e),this.setPlaySpeedSprite(this.resolveHudTheme())}',
)
.replace(
/a\.onLoad=function\(\)\{d\.globalGameTimeScale=l\.SPEEDS\[this\.speedIndex\],this\.buildUI\(\)/,
'a.onLoad=function(){d.globalGameTimeScale=l.SPEEDS[this.speedIndex],globalThis.__tfrhGameSpeed=l.SPEEDS[this.speedIndex],this.buildUI()',
)
.replace(
/a\.onRevertGame=function\(\)\{var e;this\.speedIndex=0,d\.globalGameTimeScale=l\.SPEEDS\[this\.speedIndex\],null==\(e=w\.instance\)\|\|e\.resetLevel\(\)\}/,
'a.onRevertGame=function(){var e;this.speedIndex=0,d.globalGameTimeScale=l.SPEEDS[this.speedIndex],globalThis.__tfrhGameSpeed=l.SPEEDS[this.speedIndex],null==(e=w.instance)||e.resetLevel()}',
);
}
const mainIndex = path.join(outDir, 'assets', 'main', 'index.js');
if (fs.existsSync(mainIndex)) {
patchText(mainIndex, (t) => patchMainIndexRuntime(t));
}
copyFile(
path.join(templateDir, 'cocos-bridge.js'),
path.join(outDir, 'cocos-bridge.js'),
);
const levelsDbSrc = path.join(__dirname, '../assets/level-data/levels-database.json');
const levelsDbDst = path.join(outDir, 'levels-database.json');
if (fs.existsSync(levelsDbSrc)) {
if (opts.minifyDb) {
const { before, after } = minifyLevelsDatabase(levelsDbSrc, levelsDbDst);
console.log(`>>> levels-database.json 压缩: ${formatBytes(before)}${formatBytes(after)}`);
} else {
copyFile(levelsDbSrc, levelsDbDst);
}
if (opts.brotliDb) {
const { raw, br } = brotliCompressFile(levelsDbDst, path.join(outDir, 'levels-database.json.br'));
console.log(`>>> levels-database.json.br: ${formatBytes(raw)}${formatBytes(br)}`);
}
console.log('>>> 已附带 levels-database.jsonCocos 关卡库)');
} else {
console.warn('>>> 警告: 未找到 levels-database.json请先运行 tools/sync-level-db.sh');
}
patchText(path.join(outDir, 'application.js'), (t) =>
t.replace(/this\.settingsPath\s*=\s*'[^']*'/, `this.settingsPath = '${UNITY_BASE}src/settings.json'`),
);
function patchSettingsForFrontend(settingsFile) {
patchText(settingsFile, (t) => {
const j = JSON.parse(t);
j.assets = j.assets || {};
j.assets.server = UNITY_BASE;
patchPreloadSettings(j, {
preloadResources: opts.preloadResources,
preloadLevelPrefabs: opts.preloadLevelPrefabs,
});
patchSplashSettings(j, { stripSplashAssets: true });
if (j.rendering) {
j.rendering.effectSettingsPath = `${UNITY_BASE}src/effect.bin`;
}
if (j.scripting && Array.isArray(j.scripting.scriptPackages)) {
j.scripting.scriptPackages = j.scripting.scriptPackages.map((p) => {
const rel = String(p).replace(/^\.\.\//, '').replace(/^\//, '');
return `${UNITY_BASE}${rel}`;
});
}
return JSON.stringify(j);
});
}
const settingsPath = path.join(outDir, 'src', 'settings.json');
if (fs.existsSync(settingsPath)) {
patchSettingsForFrontend(settingsPath);
const preload = JSON.parse(fs.readFileSync(settingsPath, 'utf8')).assets?.preloadBundles || [];
console.log('>>> 预加载 bundles:', preload.map((b) => b.bundle).join(', ') || '(none)');
}
const importMapPath = path.join(outDir, 'src', 'import-map.json');
if (fs.existsSync(importMapPath)) {
patchText(importMapPath, (t) => {
const j = JSON.parse(t);
j.imports = j.imports || {};
if (j.imports.cc) j.imports.cc = `${UNITY_BASE}cocos-js/cc.js`;
return JSON.stringify(j);
});
}
const buildOut = path.join(outDir, 'Build');
mkdirp(buildOut);
const loaderTpl = fs.readFileSync(path.join(templateDir, 'mstest5-cocos-loader.js'), 'utf8');
fs.writeFileSync(
path.join(buildOut, `${PRODUCT}.loader.js`),
loaderTpl.replace(/__PRODUCT_NAME__/g, PRODUCT),
'utf8',
);
for (const stub of ['data', 'framework.js', 'wasm']) {
brotliStub(path.join(buildOut, `${PRODUCT}.${stub}.br`));
}
if (opts.report) {
const preloadNote = [
'main',
opts.preloadResources ? 'resources' : null,
opts.preloadLevelPrefabs ? 'level-prefabs' : null,
].filter(Boolean).join(', ');
printPackageReport(outDir, { preloadNote });
const hasLevelBundle = fs.existsSync(path.join(outDir, 'assets', 'level-prefabs'));
if (!hasLevelBundle) {
console.log('\n>>> 提示: 尚未拆分 level-prefabs 分包。运行 migrate-level-prefab-bundle.sh 后重建可显著减小 resources 体积。');
}
}
console.log('\n>>> 项目运行包文件:');
for (const rel of [
'Build/mstest5.loader.js',
'index.js',
'application.js',
'cocos-bridge.js',
'cocos-js/',
'src/',
'assets/',
'levels-database.json',
]) {
console.log(' ', rel);
}
console.log('\n完成。flat 包与 CDN 不一致,生产请用: bash tools/package-for-project.sh');
console.log(' ~/tfrh/001code/001code-html/scratch-gui/static/unity/import-to-unity.sh', outDir);