276 lines
12 KiB
JavaScript
276 lines
12 KiB
JavaScript
#!/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.json(Cocos 关卡库)');
|
||
} 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);
|