Files
TankIC/tools/split.js
T
2026-07-28 01:16:15 +08:00

251 lines
9.1 KiB
JavaScript

// split.js - 将 main.b7114b43.js 切割为多文件加载结构
// 优化: 资源模块(标准 t.exports=i.p+"..." 格式)用 JSON + 模板替代,不生成独立文件
// 输出:
// ./3dtank.com/play/static/js/main.b7114b43.js (主文件: 加载器 + runtime)
// ./3dtank.com/play/static/js/unpacked/resources.json (资源模块 ID→路径映射)
// ./3dtank.com/play/static/js/unpacked/module_<id>.js (仅代码模块,约 126 个)
const fs = require('fs');
const path = require('path');
const acorn = require('acorn');
const walk = require('acorn-walk');
const prettier = require('prettier');
const BUNDLE_PATH = path.resolve(__dirname, '..', 'main.b7114b43.js');
const OUT_DIR = path.resolve(__dirname, '..', '3dtank.com', 'play', 'static', 'js');
const UNPACKED_DIR = path.join(OUT_DIR, 'unpacked');
const MAIN_PATH = path.join(OUT_DIR, 'main.b7114b43.js');
const RESOURCES_JSON_PATH = path.join(UNPACKED_DIR, 'resources.json');
// UMD 包装模板
const UMD_PREFIX = `var __script_base_url__=(typeof document!=="undefined"&&document.currentScript&&document.currentScript.src)?document.currentScript.src.replace(/[^\\/]+$/,""):"./";!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?exports.Launcher=n():t.Launcher=n()}(globalThis,async()=>{`;
const UMD_SUFFIX = `})//# sourceMappingURL=main.b7114b43.js.map\n`;
// 资源模块识别:标准格式 (t,n,i)=>{"use strict";t.exports=i.p+"path"}
// 提取 path
function tryExtractResource(valueSrc) {
// 标准格式: t.exports = i.p + "..."
const m = valueSrc.match(
/\{\s*"use strict";\s*(?:t|n|i)\.exports\s*=\s*(?:t|n|i)\.p\+"([^"]+)"\s*;?\s*\}/
);
if (m) return { type: 'std', path: m[1] };
// 变体: t.exports = i.p + "..." (无 use strict 或无大括号)
const m2 = valueSrc.match(
/(?:t|n|i)\.exports\s*=\s*(?:t|n|i)\.p\+"([^"]+)"\s*;?/
);
if (m2 && valueSrc.length < 200) return { type: 'std', path: m2[1] };
return null;
}
(async () => {
console.log('[1/7] 读取原 bundle...');
const source = fs.readFileSync(BUNDLE_PATH, 'utf8');
console.log(` 大小: ${(source.length / 1024 / 1024).toFixed(2)} MB`);
console.log('[2/7] 解析 AST...');
const ast = acorn.parse(source, {
ecmaVersion: 'latest',
sourceType: 'script',
allowReturnOutsideFunction: true,
});
let modulesDecl = null;
walk.simple(ast, {
VariableDeclarator(node) {
if (node.id && node.id.name === '__webpack_modules__' && node.init && node.init.type === 'ObjectExpression') {
modulesDecl = node;
}
},
});
if (!modulesDecl) throw new Error('未找到 __webpack_modules__');
const objStart = modulesDecl.init.start;
const objEnd = modulesDecl.init.end;
console.log('[3/7] 分类提取模块 (资源模块 vs 代码模块)...');
const props = modulesDecl.init.properties;
console.log(` 共 ${props.length} 个模块`);
const resourceMap = {}; // id -> path (资源模块)
const codeModuleIds = []; // 代码模块 ID 列表
let idx = 0;
let failCount = 0;
let resourceCount = 0;
let codeCount = 0;
for (const prop of props) {
idx++;
// 计算 ID
let keyRaw;
let keyNum;
if (prop.key.type === 'Identifier') {
keyRaw = prop.key.name;
keyNum = Number(keyRaw);
if (isNaN(keyNum)) keyNum = keyRaw;
} else if (prop.key.type === 'Literal') {
keyRaw = String(prop.key.value);
keyNum = typeof prop.key.value === 'number' ? prop.key.value : keyRaw;
} else {
keyRaw = `unknown_${idx}`;
keyNum = keyRaw;
}
const valueSrc = source.slice(prop.value.start, prop.value.end);
// 尝试识别为资源模块
const resource = tryExtractResource(valueSrc);
if (resource) {
// 资源模块:存入 resourceMap,不生成独立文件
const idKey = typeof keyNum === 'number' ? keyNum : keyRaw;
resourceMap[idKey] = resource.path;
resourceCount++;
continue;
}
// 代码模块:美化并写入独立文件
const paddedId = typeof keyNum === 'number' ? String(keyNum).padStart(5, '0') : keyRaw;
const idLiteral = typeof keyNum === 'number' ? keyNum : JSON.stringify(keyRaw);
let beautified = valueSrc;
try {
beautified = await prettier.format('(' + valueSrc + ')', {
parser: 'babel',
semi: true,
singleQuote: false,
printWidth: 110,
tabWidth: 2,
});
beautified = beautified.trim();
if (beautified.startsWith('(') && beautified.endsWith(')')) {
beautified = beautified.slice(1, -1).trim();
}
while (beautified.endsWith(';')) beautified = beautified.slice(0, -1).trimEnd();
} catch (e) {
failCount++;
}
const filePath = path.join(UNPACKED_DIR, `module_${paddedId}.js`);
const fileContent =
`// ============================================================\n` +
`// Module ID: ${keyRaw}\n` +
`// ============================================================\n` +
`;(window.__modules__ = window.__modules__ || {})[${idLiteral}] = ${beautified};\n`;
fs.writeFileSync(filePath, fileContent, 'utf8');
codeModuleIds.push(keyNum);
codeCount++;
if (idx % 200 === 0) {
console.log(` 已处理 ${idx}/${props.length} (资源 ${resourceCount}, 代码 ${codeCount})`);
}
}
console.log(` 完成。资源模块: ${resourceCount}, 代码模块: ${codeCount}, 美化失败: ${failCount}`);
console.log('[4/7] 写入 resources.json...');
fs.writeFileSync(
RESOURCES_JSON_PATH,
JSON.stringify(
{
type: 'webpack-resource-map',
generatedAt: new Date().toISOString(),
totalResources: resourceCount,
resourceMap,
},
null,
0
),
'utf8'
);
console.log(` resources.json 大小: ${(fs.statSync(RESOURCES_JSON_PATH).size / 1024).toFixed(1)} KB`);
console.log('[5/7] 提取原 webpack runtime...');
const runtimeStart = objEnd;
const returnIdx = source.lastIndexOf('return __webpack_exports__');
if (returnIdx === -1) throw new Error('找不到 return __webpack_exports__');
const returnEnd = returnIdx + 'return __webpack_exports__'.length;
let runtimeCode = source.slice(runtimeStart, returnEnd);
if (runtimeCode.startsWith(',')) {
runtimeCode = ';var ' + runtimeCode.slice(1);
}
console.log(` runtime 代码长度: ${runtimeCode.length} 字符`);
console.log('[6/7] 生成主文件 main.b7114b43.js...');
const codeIdsArrayLiteral = codeModuleIds
.map((id) => (typeof id === 'number' ? id : JSON.stringify(id)))
.join(',');
const loaderCode = `
// ===== 切割版模块加载器 =====
window.__modules__ = {};
const __base_url__ = window.__script_base_url__ || './';
// 6.1 加载资源模块 (从 resources.json 批量注册)
const __resource_resp__ = await fetch(__base_url__ + 'unpacked/resources.json');
if (!__resource_resp__.ok) throw new Error('resources.json load failed: HTTP ' + __resource_resp__.status);
const __resource_data__ = await __resource_resp__.json();
const __resource_map__ = __resource_data__.resourceMap;
for (const __id__ in __resource_map__) {
if (!Object.prototype.hasOwnProperty.call(__resource_map__, __id__)) continue;
const __path__ = __resource_map__[__id__];
const __id_num__ = Number(__id__);
window.__modules__[__id_num__] = (function(__p__) {
return (t, n, i) => {
"use strict";
t.exports = i.p + __p__;
};
})(__path__);
}
// 6.2 加载代码模块 (fetch + eval 独立文件)
const __code_ids__ = [${codeIdsArrayLiteral}];
await Promise.all(__code_ids__.map(async (__id__) => {
const __padded__ = typeof __id__ === 'number' ? String(__id__).padStart(5, '0') : __id__;
const __url__ = __base_url__ + 'unpacked/module_' + __padded__ + '.js';
const __resp__ = await fetch(__url__);
if (!__resp__.ok) throw new Error('Module ' + __id__ + ' load failed: HTTP ' + __resp__.status + ' @ ' + __url__);
const __code__ = await __resp__.text();
(0, eval)(__code__);
}));
var __webpack_modules__ = window.__modules__;
`;
const newBundle =
UMD_PREFIX +
'\n' +
loaderCode +
'\n// ===== 原 webpack runtime (一字未改) =====\n' +
runtimeCode +
';\n' +
UMD_SUFFIX;
fs.writeFileSync(MAIN_PATH, newBundle, 'utf8');
console.log(` 主文件大小: ${(newBundle.length / 1024).toFixed(1)} KB`);
console.log('[7/7] 验证语法...');
try {
acorn.parse(newBundle, {
ecmaVersion: 'latest',
sourceType: 'script',
allowReturnOutsideFunction: true,
});
console.log(' ✓ 主文件语法正确');
} catch (e) {
console.error(` ✗ 语法错误: ${e.message}`);
process.exit(1);
}
console.log('\n=== 完成 ===');
console.log(` 总模块数: ${props.length}`);
console.log(` 资源模块: ${resourceCount} (存入 resources.json)`);
console.log(` 代码模块: ${codeCount} (独立文件)`);
console.log(` 美化失败: ${failCount}`);
console.log(` 主文件: ${MAIN_PATH}`);
console.log(` 资源 JSON: ${RESOURCES_JSON_PATH}`);
console.log(` 代码模块目录: ${UNPACKED_DIR}\\`);
})().catch((err) => {
console.error('Fatal:', err);
process.exit(1);
});