Files
TankIC/tools/test_loader.js
T
TermiNexus 90c377ad79 feat(3dtank): 本地镜像与 hornet 工具链迁到新版 bundle main.6469b966.js
旧 main.b7114b43.js 已被新构建替换(10,011,039 字节 / 1335 模块),本地镜像整体重切到新版并清掉旧产物;hornet 工具链按 tools/map_symbols.js 生成的新旧符号映射改名后重新打包(3dtank_bundle.js 与 unpacked/hornet_bundle.js 同步)。tools/symbol_map.json(8.9MB 映射产物)与切割解包目录不进库,可用 map_symbols.js / split.js 重新生成。
2026-09-17 19:59:40 +08:00

231 lines
8.0 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.
// test_loader.js - 模拟浏览器加载流程,验证切割版 bundle 能正确加载所有模块
// 新版: 资源模块从 resources.json 加载,代码模块从独立文件加载
const fs = require('fs');
const path = require('path');
const http = require('http');
const ROOT = path.resolve(__dirname, '..', '3dtank.com');
const JS_DIR = path.join(ROOT, 'play', 'static', 'js');
// 自动定位切割版主文件 main.<hash>.js
const MAIN_PATH = path.join(JS_DIR, fs.readdirSync(JS_DIR).filter((f) => /^main\.[0-9a-f]+\.js$/.test(f)).sort().pop() || '');
const UNPACKED_DIR = path.join(JS_DIR, 'unpacked');
const RESOURCES_JSON_PATH = path.join(UNPACKED_DIR, 'resources.json');
// 简单的 HTTP 服务器,提供静态文件
const server = http.createServer((req, res) => {
let filePath = path.join(ROOT, req.url.split('?')[0]);
if (req.url === '/' || req.url === '') filePath = path.join(ROOT, 'index.html');
if (!fs.existsSync(filePath)) {
res.writeHead(404);
res.end('Not Found');
return;
}
const ext = path.extname(filePath).toLowerCase();
const ct = {
'.js': 'application/javascript',
'.html': 'text/html',
'.json': 'application/json',
}[ext] || 'application/octet-stream';
res.writeHead(200, { 'Content-Type': ct });
fs.createReadStream(filePath).pipe(res);
});
// 模拟 fetch
function makeFetch() {
return (url) =>
new Promise((resolve, reject) => {
http
.get(url, (res) => {
let body = '';
res.on('data', (d) => (body += d));
res.on('end', () => {
resolve({
ok: res.statusCode === 200,
status: res.statusCode,
text: () => Promise.resolve(body),
json: () => Promise.resolve(JSON.parse(body)),
});
});
})
.on('error', reject);
});
}
(async () => {
// 1. 检查文件结构
console.log('[1/5] 检查文件结构...');
const codeFiles = fs.readdirSync(UNPACKED_DIR).filter((f) => /^module_\d+\.js$/.test(f));
console.log(` unpacked/ 中代码模块文件: ${codeFiles.length}`);
if (!fs.existsSync(RESOURCES_JSON_PATH)) {
console.error(' ✗ resources.json 不存在');
process.exit(1);
}
const resourcesJson = JSON.parse(fs.readFileSync(RESOURCES_JSON_PATH, 'utf8'));
const resourceMap = resourcesJson.resourceMap;
const resourceCount = Object.keys(resourceMap).length;
console.log(` resources.json 资源模块: ${resourceCount}`);
console.log(` 合计: ${codeFiles.length + resourceCount}`);
// 2. 启动 HTTP 服务器
console.log('[2/5] 启动测试 HTTP 服务器...');
await new Promise((resolve) => server.listen(18888, resolve));
console.log(' 服务器监听 http://localhost:18888/');
const baseUrl = 'http://localhost:18888/play/static/js/';
const fetch = makeFetch();
// 创建模拟的 window 对象
const sandbox = {
console: console,
fetch,
globalThis: null,
};
sandbox.window = sandbox;
sandbox.globalThis = sandbox;
sandbox.window.__modules__ = {};
// 3. 阶段 1: 加载 resources.json,注册资源模块
console.log('[3/5] 阶段 1: 从 resources.json 注册资源模块...');
for (const __id__ in resourceMap) {
if (!Object.prototype.hasOwnProperty.call(resourceMap, __id__)) continue;
const __path__ = resourceMap[__id__];
const __id_num__ = Number(__id__);
sandbox.window.__modules__[__id_num__] = (function (__p__) {
return (t, n, i) => {
'use strict';
t.exports = i.p + __p__;
};
})(__path__);
}
console.log(` 已注册资源模块: ${Object.keys(sandbox.window.__modules__).length}`);
// 4. 阶段 2: 加载代码模块(fetch + eval)
console.log('[4/5] 阶段 2: 从独立文件加载代码模块 (fetch + eval)...');
const mainCode = fs.readFileSync(MAIN_PATH, 'utf8');
// 提取 __code_ids__ 数组
const m = mainCode.match(/const __code_ids__ = \[([^\]]+)\]/);
if (!m) {
console.error(' ✗ 主文件中找不到 __code_ids__');
process.exit(1);
}
const codeIds = m[1].split(',').map((x) => parseInt(x.trim(), 10));
console.log(` 待加载代码模块: ${codeIds.length}`);
let loadedCount = 0;
let failedCount = 0;
const failedModules = [];
const BATCH_SIZE = 50;
for (let i = 0; i < codeIds.length; i += BATCH_SIZE) {
const batch = codeIds.slice(i, i + BATCH_SIZE);
await Promise.all(
batch.map(async (id) => {
const paddedId = String(id).padStart(5, '0');
const url = baseUrl + 'unpacked/module_' + paddedId + '.js';
try {
const resp = await fetch(url);
if (!resp.ok) throw new Error('HTTP ' + resp.status);
const code = await resp.text();
// 在 sandbox 中 eval 执行
const fn = new Function('window', 'console', 'globalThis', code);
fn(sandbox.window, sandbox.console, sandbox);
loadedCount++;
} catch (e) {
failedCount++;
failedModules.push({ id, error: e.message });
}
})
);
if ((i / BATCH_SIZE) % 2 === 0) {
process.stdout.write(` 进度: ${loadedCount}/${codeIds.length} (失败 ${failedCount})\r`);
}
}
console.log(` 完成: ${loadedCount}/${codeIds.length} 加载成功, ${failedCount} 失败 `);
if (failedCount > 0) {
console.log(' 失败模块 (前 10):');
failedModules.slice(0, 10).forEach((f) => console.log(` ID ${f.id}: ${f.error}`));
}
// 5. 验证 window.__modules__ 注册情况
console.log('[5/5] 验证 window.__modules__ 注册...');
const modules = sandbox.window.__modules__;
if (!modules) {
console.error(' ✗ window.__modules__ 未定义');
server.close();
process.exit(1);
}
const registeredIds = Object.keys(modules).map(Number);
const totalExpected = resourceCount + codeIds.length;
console.log(` 已注册模块总数: ${registeredIds.length} (预期 ${totalExpected})`);
// 关键入口模块验证
const keyIds = [17, 76074, 13431, 26482, 78231, 82347, 87072];
keyIds.forEach((id) => {
const exists = typeof modules[id] === 'function';
const isResource = Object.prototype.hasOwnProperty.call(resourceMap, id);
const tag = isResource ? '资源' : '代码';
console.log(` 模块 ${id} [${tag}]: ${exists ? '✓' : '✗'}`);
});
// 检查资源模块是否正确返回路径
let resourceOkCount = 0;
let resourceFailCount = 0;
for (const id in resourceMap) {
const idNum = Number(id);
const expectedPath = resourceMap[id];
const mod = modules[idNum];
if (typeof mod !== 'function') {
resourceFailCount++;
continue;
}
// 模拟调用: i.p = "/play/"
const fakeModule = { exports: {} };
const fakeRequire = { p: '/play/' };
try {
mod(fakeModule, {}, fakeRequire);
const actualPath = fakeModule.exports;
if (actualPath === '/play/' + expectedPath) {
resourceOkCount++;
} else {
resourceFailCount++;
if (resourceFailCount <= 3) {
console.log(` 路径不匹配 ID ${id}: 期望 "/play/${expectedPath}", 实际 "${actualPath}"`);
}
}
} catch (e) {
resourceFailCount++;
}
}
console.log(` 资源模块路径验证: ${resourceOkCount}/${resourceCount} 正确`);
// 检查代码模块是否都注册为函数
const codeMissing = codeIds.filter((id) => typeof modules[id] !== 'function');
console.log(` 代码模块全部注册: ${codeMissing.length === 0 ? '✓' : '✗ 缺失 ' + codeMissing.length + ' 个'}`);
if (codeMissing.length > 0) {
console.log(` 缺失 ID (前 10): ${codeMissing.slice(0, 10).join(', ')}`);
}
server.close();
console.log('\n=== 测试完成 ===');
const allOk =
failedCount === 0 &&
registeredIds.length === totalExpected &&
resourceOkCount === resourceCount &&
codeMissing.length === 0;
if (allOk) {
console.log('✓ 所有模块加载并注册成功 (资源从 JSON + 代码从文件)');
} else {
console.log('✗ 存在加载或注册失败');
process.exit(1);
}
})().catch((err) => {
console.error('Fatal:', err);
server.close();
process.exit(1);
});