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

448 lines
20 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
/**
* 将 Cocos Web 构建产物整理为与 Unity 参考包完全一致的目录/文件名:
* /Users/liuyufei/tfrh/竞赛/mstest5
*
* node tools/package-for-cdn.js <cocosBuildDir> <outDir>
*/
const crypto = require('crypto');
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const { execSync } = require('child_process');
const {
patchPreloadSettings,
patchSplashSettings,
printPackageReport,
minifyLevelsDatabase,
brotliCompressFile,
brotliCompressWebglBundles,
formatBytes,
} = require('./package-optimize');
const { listRuntimeFiles, assertRuntimePack } = require('./runtime-pack');
const { splitLevelBundles } = require('./split-level-bundles');
const { splitLevelsDatabase } = require('./split-levels-database');
const buildDir = path.resolve(process.argv[2]);
const outDir = path.resolve(process.argv[3]);
const unityRef = path.resolve(process.argv[4] || '/Users/liuyufei/tfrh/竞赛/mstest5');
const PRODUCT = 'mstest5';
function parseCatalogBundles(catalogPath) {
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
const names = [...new Set(
(catalog.m_InternalIds || [])
.filter((id) => String(id).includes('.bundle'))
.map((id) => String(id).split('/').pop()),
)];
const scenesAll = names.find((n) => n.includes('scenes_all'));
const assetsAll = names.find((n) => n.includes('assets_all'));
const levelsAll = names.find((n) => n.includes('levels_all'));
const shaders = names.find((n) => n.includes('unitybuiltinshaders'));
if (!scenesAll || !assetsAll) {
throw new Error(`catalog.json missing scenes/assets bundle: ${names.join(', ')}`);
}
return { shaders, scenesAll, assetsAll, levelsAll, all: names };
}
/** 复制 catalog 后剥离历史 levels_all 条目,再写入本次 bundle */
function patchCatalogAddLevelsBundle(catalogPath, bundleFileName) {
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
const entry = `{UnityEngine.AddressableAssets.Addressables.RuntimePath}/WebGL/${bundleFileName}`;
const ids = (catalog.m_InternalIds || []).filter((id) => !String(id).includes('levels_all'));
ids.push(entry);
catalog.m_InternalIds = ids;
fs.writeFileSync(catalogPath, JSON.stringify(catalog), 'utf8');
}
/** 从 catalog 移除 levels_all 条目(仅 MERGE_LEVELS=1 合并包时使用) */
function patchCatalogRemoveLevelsBundle(catalogPath) {
const catalog = JSON.parse(fs.readFileSync(catalogPath, 'utf8'));
catalog.m_InternalIds = (catalog.m_InternalIds || []).filter((id) => !String(id).includes('levels_all'));
fs.writeFileSync(catalogPath, JSON.stringify(catalog), 'utf8');
}
function copyAssetsDirExcept(srcAssetsDir, dstAssetsDir, skipNames) {
const skip = new Set(skipNames || []);
if (!fs.existsSync(srcAssetsDir)) return;
mkdirp(dstAssetsDir);
for (const ent of fs.readdirSync(srcAssetsDir, { withFileTypes: true })) {
if (skip.has(ent.name)) continue;
const sp = path.join(srcAssetsDir, ent.name);
const dp = path.join(dstAssetsDir, ent.name);
if (ent.isDirectory()) copyDir(sp, dp);
else copyFile(sp, dp);
}
}
function hashFileMd5(filePath) {
return crypto.createHash('md5').update(fs.readFileSync(filePath)).digest('hex');
}
function attachLevelsDatabase(outDir) {
const levelsDbSrc = path.join(__dirname, '../assets/level-data/levels-database.json');
if (!fs.existsSync(levelsDbSrc)) {
console.warn('>>> 警告: 未找到 levels-database.json');
return;
}
splitLevelsDatabase(levelsDbSrc, outDir);
const levelsDbDst = path.join(outDir, 'levels-database.json');
const { before, after } = minifyLevelsDatabase(levelsDbSrc, levelsDbDst);
console.log(`>>> levels-database.json (legacy 回退): ${formatBytes(before)}${formatBytes(after)}`);
const { raw, br } = brotliCompressFile(levelsDbDst, path.join(outDir, 'levels-database.json.br'));
console.log(`>>> levels-database.json.br: ${formatBytes(raw)}${formatBytes(br)}`);
}
if (!buildDir || !outDir) {
console.error('Usage: package-for-cdn.js <cocosBuildDir> <outDir> [unityRefDir]');
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 zipDir(srcDir, outFile) {
const absOut = path.resolve(outFile);
mkdirp(path.dirname(absOut));
if (fs.existsSync(absOut)) fs.unlinkSync(absOut);
execSync(`cd "${path.resolve(srcDir)}" && zip -0 -q -r "${absOut}" .`, { stdio: 'pipe' });
}
function patchText(file, fn) {
const t = fs.readFileSync(file, 'utf8');
fs.writeFileSync(file, fn(t), 'utf8');
}
function brotliStub(dst, bytes) {
const buf = zlib.brotliCompressSync(bytes || Buffer.from([0]));
fs.writeFileSync(dst, buf);
}
/**
* 分包模式下:须先 loadLevelPrefab 再 VisualAssets.preload否则 resources 占满后 level-prefabs 加载失败。
* 对 Cocos 构建产物 assets/main/index.js 做稳定字符串 patch无需立即重编工程
*/
function patchMainIndexForSplitLoad(mainIndexPath) {
if (!fs.existsSync(mainIndexPath)) {
console.warn('>>> 跳过 main/index 分包 patch无 assets/main/index.js');
return;
}
let s = fs.readFileSync(mainIndexPath, 'utf8');
const rules = [
{
desc: 'AppBootstrap: 首屏只 loadLevelDatabase + bounds',
old: 'yield I(),yield _(),yield E(),yield A(),L(),yield D.preload();',
neu: 'yield A(),L();',
},
{
desc: 'GameController: 进关前按需拉 assets / 关卡库分片',
old: 'if(!this.creating){this.creating=!0;var t=R(e);',
neu: 'if(!this.creating){this.creating=!0;try{if(globalThis.__tfrhEnsureAssetsCore)yield globalThis.__tfrhEnsureAssetsCore();yield I(),yield _(),yield E();if(globalThis.__tfrhEnsureLevelDbShard)yield globalThis.__tfrhEnsureLevelDbShard(e);}catch(err){console.error("[GameController] 运行时资源加载失败",err),this.creating=!1;return}var t=R(e);',
},
{
desc: 'LevelDatabase: 分片索引模式 isReady',
old: 'isLevelDatabaseReady:function(){return null!==s}',
neu: 'isLevelDatabaseReady:function(){return null!==s||("undefined"!=typeof window&&!!window.__tfrhLevelsDbIndex)}',
},
{
desc: 'LevelDatabase: 启动读 index 而非全量库',
old: 'try{if("undefined"!=typeof window&&window.__tfrhLevelsDatabaseJson)',
neu: 'try{if("undefined"!=typeof window&&window.__tfrhLevelsDbIndex){s={version:window.__tfrhLevelsDbIndex.version||2,levels:{}},console.log("[LevelDatabase] 已加载索引 "+window.__tfrhLevelsDbIndex.total+" 关");return}if("undefined"!=typeof window&&window.__tfrhLevelsDatabaseJson)',
},
{
desc: 'LevelDatabase: validate 接受 index',
old: 'function w(){var e=c.length,n=null!=(t=c[0])?t:0;if(e<100||n<a)throw new Error',
neu: 'function w(){if("undefined"!=typeof window&&window.__tfrhLevelsDbIndex){var e=window.__tfrhLevelsDbIndex;if(e.total<100||e.min<a)throw new Error("关卡库索引过旧 ("+e.total+" 关)");return}var e=c.length,n=null!=(t=c[0])?t:0;if(e<100||n<a)throw new Error',
},
{
desc: 'LevelRegistry: min/max 读 index',
old: 'function f(){return c()?d():t}function E(){return c()?I():t}',
neu: 'function f(){var e="undefined"!=typeof window&&window.__tfrhLevelsDbIndex;return e?e.min:c()?d():t}function E(){var e="undefined"!=typeof window&&window.__tfrhLevelsDbIndex;return e?e.max:c()?I():t}',
},
{
desc: 'LevelRegistry: hasLevel 读 index 范围',
old: 'hasLevel:function(e){return v(e)}',
neu: 'hasLevel:function(e){if(v(e))return!0;var n="undefined"!=typeof window&&window.__tfrhLevelsDbIndex;return!!n&&e>=n.min&&e<=n.max}',
},
{
desc: 'LevelDatabase: getLevelConfig 读 loader 分片',
old: 'getLevelConfig:function(e){var n;return null!=(n=f[e])?n:null}',
neu: 'getLevelConfig:function(e){var n;if(null!=(n=f[e]))return n;var t="undefined"!=typeof window&&window.__tfrhLevelsDatabaseJson&&window.__tfrhLevelsDatabaseJson.levels;return t&&t[String(e)]||null}',
},
{
desc: 'GameController: 使用 loader 全局 loadLevelPrefab hook (新构建 K)',
old: 'var l,r=yield K(i);',
neu: 'var l,r=yield(globalThis.__tfrhLoadLevelPrefab||K)(i);',
},
{
desc: 'GameController: 使用 loader 全局 loadLevelPrefab hook (旧构建 z)',
old: 'var l,r=yield z(i);',
neu: 'var l,r=yield(globalThis.__tfrhLoadLevelPrefab||z)(i);',
},
{
desc: 'AppBootstrap: 首关前不 preload UI 贴图',
old: 'yield h.preload(),yield O.preload()',
neu: 'yield O.preload()',
},
{
desc: 'AppBootstrap: 进关再 loadLevel不在 bootstrap 拉首关)',
old: 'yield F.createNewLevel(F.initialLevelID),S.purgeScene(c),F.scheduleOnce((function(){return S.purgeScene(c)}),0),F.scheduleOnce((function(){return S.purgeScene(c)}),.15),F.markReady()',
neu: 'F.markReady()',
},
{
desc: 'GameController: 使用 loader 全局 loadLevelPrefab hook',
old: 'var l,r=yield z(i);yield H.preload(this.uiStyle)',
neu: 'var l,r=yield(globalThis.__tfrhLoadLevelPrefab||z)(i);yield H.preload(this.uiStyle)',
},
{
desc: 'LevelPrefabLoader: 去掉裸 LevelN 路径',
old: 'return[].concat(new Set([r,n,"level-prefabs/"+n]))',
neu: 'return[].concat(new Set([r,"level-prefabs/"+n]))',
},
{
desc: 'LevelPrefabLoader: 进关前 ensureLevelPack',
old: 'function*(e){yield a();var r=v(e);',
neu: 'function*(e){yield a();var _h=globalThis.__tfrhEnsureLevelPack,_m=/Level(\\d+)\\s*$/.exec(e.trim()),_lid=_m?parseInt(_m[1],10):void 0;if(_lid!==void 0&&typeof _h==="function")yield _h(_lid);var r=v(e);',
},
{
desc: 'LevelPrefabLoader: ensureLevelPack 后清空 bundle 缓存 (新构建 w/L/s/f)',
old: 'yield u(),yield L(e);var r=v(e),n=null;try{var l=yield c();return yield d((function(e){return p(l,e)}),r)}catch(e){n=e,console.warn("[LevelPrefabLoader] level-prefabs bundle 加载失败",e)}var t=yield y(e);if(null!=t&&t.isValid)return t;throw console.error(\'[LevelPrefabLoader] 关卡预制体未找到。请确认 bundle-level-prefabs 已标记为 Asset Bundle "level-prefabs",并运行: python3 tools/bake_cocos_level_prefabs.py\',n),n instanceof Error?n:new Error("missing prefab: "+e)',
neu: 'yield u(),yield L(e);s=null,f=null;var r=v(e);try{var l=yield c();return yield d((function(e){return p(l,e)}),r)}catch(e){throw console.error("[LevelPrefabLoader] 关卡预制体未找到",e),e||new Error("missing prefab: "+e)}',
},
{
desc: 'LevelPrefabLoader: ensureLevelPack 后清空 bundle 缓存 (旧构建 g/w/c/f)',
old: 'yield a(),yield w(e);var r=b(e);try{var n=yield p();return yield y((function(e){return d(n,e)}),r)}catch(e){throw console.error("[LevelPrefabLoader] 关卡预制体未找到",e),e||new Error("missing prefab")}',
neu: 'yield a(),yield w(e);c=null,f=null;var r=b(e);try{var n=yield p();return yield y((function(e){return d(n,e)}),r)}catch(e){throw console.error("[LevelPrefabLoader] 关卡预制体未找到",e),e||new Error("missing prefab")}',
},
{
desc: 'LevelPrefabLoader: 禁用 resources 回退',
old: '}catch(e){console.warn("[LevelPrefabLoader] level-prefabs bundle 不可用,尝试 resources",e)}var l=yield L(e);if(null!=l&&l.isValid)return l;try{return yield y(v,r)}catch(e){throw console.error(\'[LevelPrefabLoader] 关卡预制体未找到。请确认 bundle-level-prefabs 已标记为 Asset Bundle "level-prefabs",并运行: python3 tools/bake_cocos_level_prefabs.py\',e),e}',
neu: '}catch(e){throw console.error("[LevelPrefabLoader] 关卡预制体未找到",e),e||new Error("missing prefab")}',
},
];
let n = 0;
for (const { desc, old, neu } of rules) {
if (!s.includes(old)) continue;
s = s.replace(old, neu);
n += 1;
console.log(`>>> main/index.js patch: ${desc}`);
}
if (n === 0) {
console.warn('>>> 警告: main/index.js 未匹配分包 patch请确认 Cocos 已构建或更新 patch 规则)');
}
fs.writeFileSync(mainIndexPath, s, 'utf8');
}
if (!fs.existsSync(path.join(buildDir, 'index.html'))) {
console.error('Missing Cocos build:', buildDir);
process.exit(1);
}
if (!fs.existsSync(path.join(unityRef, 'index.html'))) {
console.error('Missing Unity reference index.html:', unityRef);
console.error('需要完整 Unity 参考包(如 ~/tfrh/竞赛/mstest5static/unity 扁平包不可用。');
process.exit(1);
}
if (!fs.existsSync(path.join(unityRef, 'StreamingAssets/aa/catalog.json'))) {
console.error('Missing Unity reference catalog:', unityRef);
process.exit(1);
}
const tmp = path.join(outDir, '..', '.pack-tmp-' + Date.now());
rmrf(outDir);
mkdirp(tmp);
console.log('>>> Unity 参考:', unityRef);
console.log('>>> Cocos 构建:', buildDir);
console.log('>>> 输出(运行时包):', outDir);
// —— 1. StreamingAssets 元数据(不含 index.html / TemplateData与 CDN 上传一致)——
mkdirp(outDir);
const aaDir = path.join(outDir, 'StreamingAssets', 'aa');
const webglDir = path.join(aaDir, 'WebGL');
mkdirp(webglDir);
copyFile(path.join(unityRef, 'StreamingAssets/aa/settings.json'), path.join(aaDir, 'settings.json'));
copyFile(path.join(unityRef, 'StreamingAssets/aa/catalog.json'), path.join(aaDir, 'catalog.json'));
copyDir(path.join(unityRef, 'StreamingAssets/aa/AddressablesLink'), path.join(aaDir, 'AddressablesLink'));
const BUNDLE = parseCatalogBundles(path.join(aaDir, 'catalog.json'));
console.log('>>> catalog bundles:', BUNDLE.all.join(', '));
if (BUNDLE.shaders) {
copyFile(
path.join(unityRef, 'StreamingAssets/aa/WebGL', BUNDLE.shaders),
path.join(webglDir, BUNDLE.shaders),
);
}
// —— 2. 打 scenes bundleCocos 运行时 JS——
const scenesStage = path.join(tmp, 'scenes');
mkdirp(scenesStage);
for (const f of ['index.js', 'application.js']) {
copyFile(path.join(buildDir, f), path.join(scenesStage, f));
}
copyDir(path.join(buildDir, 'src'), path.join(scenesStage, 'src'));
copyDir(path.join(buildDir, 'cocos-js'), path.join(scenesStage, 'cocos-js'));
copyFile(
path.join(path.dirname(__filename), '../web-template/cocos-bridge.js'),
path.join(scenesStage, 'cocos-bridge.js'),
);
patchText(path.join(scenesStage, 'application.js'), (t) =>
t.replace(/this\.settingsPath\s*=\s*'[^']*'/, "this.settingsPath = 'src/settings.json'"),
);
// 保持 assets.server 为空,由 loader fetch shim / 本地解压目录提供资源
patchText(path.join(scenesStage, 'src', 'settings.json'), (t) => {
const j = JSON.parse(t);
j.assets = j.assets || {};
j.assets.server = '';
patchPreloadSettings(j, { preloadResources: false, preloadLevelPrefabs: false });
patchSplashSettings(j, { stripSplashAssets: true });
return JSON.stringify(j);
});
zipDir(scenesStage, path.join(webglDir, BUNDLE.scenesAll));
const scenesZip = path.join(webglDir, BUNDLE.scenesAll);
const scenesBr = brotliCompressFile(scenesZip, scenesZip + '.br');
console.log(`>>> ${BUNDLE.scenesAll}.br: ${formatBytes(scenesBr.raw)}${formatBytes(scenesBr.br)}`);
// —— 3a. assets_all核心资源不含 level-prefabs——
// 默认分包MERGE_LEVELS=1 时合并 level-prefabs 进 assets_all不推荐
const MERGE_LEVELS = process.env.MERGE_LEVELS === '1';
const assetsCoreStage = path.join(tmp, 'assets-core');
mkdirp(assetsCoreStage);
if (MERGE_LEVELS) {
copyDir(path.join(buildDir, 'assets'), path.join(assetsCoreStage, 'assets'));
patchCatalogRemoveLevelsBundle(path.join(aaDir, 'catalog.json'));
console.log('>>> MERGE_LEVELS=1: level-prefabs 已并入 assets_all');
} else {
copyAssetsDirExcept(
path.join(buildDir, 'assets'),
path.join(assetsCoreStage, 'assets'),
['level-prefabs'],
);
// level-prefabs 壳config + index进首屏各关 import 单独按需包
const lpShellSrc = path.join(buildDir, 'assets', 'level-prefabs');
const lpShellDst = path.join(assetsCoreStage, 'assets', 'level-prefabs');
mkdirp(lpShellDst);
for (const f of ['config.json', 'index.js']) {
const sp = path.join(lpShellSrc, f);
if (fs.existsSync(sp)) copyFile(sp, path.join(lpShellDst, f));
}
patchMainIndexForSplitLoad(path.join(assetsCoreStage, 'assets', 'main', 'index.js'));
console.log('>>> 分包模式: assets_all 含 level-prefabs 壳,不含关卡 import');
}
const nativeWasm = path.join(buildDir, 'cocos-js', 'assets');
if (fs.existsSync(nativeWasm)) {
copyDir(nativeWasm, path.join(assetsCoreStage, 'assets'));
}
copyFile(path.join(scenesStage, 'src', 'settings.json'), path.join(assetsCoreStage, 'src', 'settings.json'));
if (fs.existsSync(path.join(scenesStage, 'src', 'effect.bin'))) {
copyFile(path.join(scenesStage, 'src', 'effect.bin'), path.join(assetsCoreStage, 'src', 'effect.bin'));
}
const assetsCoreZip = path.join(webglDir, BUNDLE.assetsAll);
zipDir(assetsCoreStage, assetsCoreZip);
console.log(`>>> assets_core (assets_all): ${formatBytes(fs.statSync(assetsCoreZip).size)}`);
const assetsBr = brotliCompressFile(assetsCoreZip, assetsCoreZip + '.br');
console.log(`>>> ${BUNDLE.assetsAll}.br: ${formatBytes(assetsBr.raw)}${formatBytes(assetsBr.br)}`);
// —— 3b. 每关独立 bundle + levels-manifest.json ——
const levelPrefabsSrc = path.join(buildDir, 'assets', 'level-prefabs');
let levelPackStats = null;
if (!MERGE_LEVELS) {
if (!fs.existsSync(levelPrefabsSrc)) {
console.error('>>> 错误: 分包模式需要 build/assets/level-prefabs');
console.error('>>> 请确认 bundle-level-prefabs 已配置并在 Cocos 中重新构建 Web Desktop');
process.exit(1);
}
patchCatalogRemoveLevelsBundle(path.join(aaDir, 'catalog.json'));
const manifestPath = path.join(aaDir, 'levels-manifest.json');
levelPackStats = splitLevelBundles(levelPrefabsSrc, webglDir, manifestPath, tmp);
const brStats = brotliCompressWebglBundles(webglDir);
console.log(`>>> bundle Brotli: ${brStats.count} 个, 节省 ${formatBytes(brStats.saved)}`);
}
attachLevelsDatabase(outDir);
// —— 4. Build/ 仅 4 个文件(与 Unity 同名)——
const buildOut = path.join(outDir, 'Build');
mkdirp(buildOut);
const loaderTpl = fs.readFileSync(
path.join(path.dirname(__filename), '../web-template/mstest5-cocos-loader.js'),
'utf8',
);
fs.writeFileSync(
path.join(buildOut, `${PRODUCT}.loader.js`),
loaderTpl.replace(/__PRODUCT_NAME__/g, PRODUCT),
'utf8',
);
// .br 占位index.html config 需要文件存在;实际由自定义 loader 启动 Cocos
brotliStub(path.join(buildOut, `${PRODUCT}.data.br`));
brotliStub(path.join(buildOut, `${PRODUCT}.framework.js.br`));
brotliStub(path.join(buildOut, `${PRODUCT}.wasm.br`));
rmrf(tmp);
// —— 5. 独立调试页(可选,不进 static/unity 也不上传 OSS——
const standaloneDir = path.join(path.dirname(outDir), 'standalone-player');
rmrf(standaloneDir);
mkdirp(standaloneDir);
copyFile(path.join(unityRef, 'index.html'), path.join(standaloneDir, 'index.html'));
copyDir(path.join(unityRef, 'TemplateData'), path.join(standaloneDir, 'TemplateData'));
for (const item of [
'Build',
'StreamingAssets',
'levels-database.json',
'levels-database.json.br',
'levels-db-index.json',
'levels-db-index.json.br',
]) {
const src = path.join(outDir, item);
const dst = path.join(standaloneDir, item);
if (!fs.existsSync(src)) continue;
if (fs.statSync(src).isDirectory()) copyDir(src, dst);
else copyFile(src, dst);
}
console.log('>>> 独立调试页:', standaloneDir);
assertRuntimePack(outDir, { requireLevelsBundle: false, requireLevelsManifest: !MERGE_LEVELS });
const runtimeFiles = listRuntimeFiles(outDir);
console.log('\n>>> 运行时包文件(本地 static/unity = OSS unitycdndir:');
for (const { rel } of runtimeFiles) console.log(' ', rel);
const requiredBundles = [
path.join(webglDir, BUNDLE.scenesAll),
path.join(webglDir, BUNDLE.assetsAll),
];
if (BUNDLE.shaders) requiredBundles.push(path.join(webglDir, BUNDLE.shaders));
for (const p of requiredBundles) {
if (!fs.existsSync(p) || fs.statSync(p).size < 100) {
console.error('>>> bundle 无效:', p);
process.exit(1);
}
}
const preloadNote = MERGE_LEVELS
? 'scenes ∥ assets_all 合并包 (MERGE_LEVELS=1)'
: `scenes ∥ assets_core 首屏(引擎启动必需);关卡库分片 + 关卡 bundle 进关按需 (${levelPackStats ? levelPackStats.packed : '?'} 关)`;
console.log('\n完成。运行时包 → 本地 import-to-unity.sh / OSS unitycdndir同一目录');
printPackageReport(outDir, { preloadNote });