// extract.js - 把 webpack bundle 拆成可读源码工程 // 用法: node extract.js [bundle路径] [输出目录] // 默认 bundle = ../../main.6469b966.js(TankIC 根目录) // 默认输出 = ../src // 输出: // src/module_.js 每个代码模块(prettier 美化) // src/resources.json 资源模块 ID → 路径 // src/index.json 模块索引(id / 文件 / 是否资源) const fs = require('fs'); const path = require('path'); const acorn = require('acorn'); const walk = require('acorn-walk'); const prettier = require('prettier'); const PROJECT = path.resolve(__dirname, '..'); const argv = process.argv.slice(2).filter((a) => !a.startsWith('--')); const BUNDLE_PATH = path.resolve(argv[0] || path.join(PROJECT, '..', 'main.6469b966.js')); const SRC_DIR = path.resolve(argv[1] || path.join(PROJECT, 'src')); const RESOURCES_JSON = path.join(SRC_DIR, 'resources.json'); const INDEX_JSON = path.join(SRC_DIR, 'index.json'); function tryExtractResource(valueSrc) { let m = valueSrc.match(/\{\s*"use strict";\s*(?:t|n|i)\.exports\s*=\s*(?:t|n|i)\.p\+"([^"]+)"\s*;?\s*\}/); if (m) return m[1]; m = valueSrc.match(/(?:t|n|i)\.exports\s*=\s*(?:t|n|i)\.p\+"([^"]+)"\s*;?/); if (m && valueSrc.length < 200) return m[1]; return null; } (async () => { if (!fs.existsSync(BUNDLE_PATH)) { console.error('找不到 bundle: ' + BUNDLE_PATH); process.exit(1); } if (!fs.existsSync(SRC_DIR)) fs.mkdirSync(SRC_DIR, { recursive: true }); // 清理旧产物 for (const f of fs.readdirSync(SRC_DIR)) { if (/^module_\d+\.js$/.test(f) || f === 'resources.json' || f === 'index.json') { fs.unlinkSync(path.join(SRC_DIR, f)); } } console.log('[1/4] 读取 bundle: ' + BUNDLE_PATH); const source = fs.readFileSync(BUNDLE_PATH, 'utf8'); console.log(' 大小: ' + (source.length / 1024 / 1024).toFixed(2) + ' MB'); console.log('[2/4] 解析 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__'); console.log('[3/4] 拆分模块...'); const props = modulesDecl.init.properties; const resourceMap = {}; const index = []; let codeCount = 0, resourceCount = 0, failCount = 0; for (const prop of props) { let keyRaw, keyNum; if (prop.key.type === 'Identifier') { keyRaw = prop.key.name; keyNum = Number(keyRaw); } else { keyRaw = String(prop.key.value); keyNum = typeof prop.key.value === 'number' ? prop.key.value : keyRaw; } const valueSrc = source.slice(prop.value.start, prop.value.end); const resourcePath = tryExtractResource(valueSrc); if (resourcePath) { resourceMap[keyNum] = resourcePath; resourceCount++; index.push({ id: keyNum, type: 'resource', path: resourcePath }); continue; } 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 padded = String(keyNum).padStart(5, '0'); const file = 'module_' + padded + '.js'; const header = '// ============================================================\n' + '// Module ID: ' + keyRaw + '\n' + '// ============================================================\n'; fs.writeFileSync(path.join(SRC_DIR, file), header + ';(window.__modules__ = window.__modules__ || {})[' + keyNum + '] = ' + beautified + ';\n', 'utf8'); codeCount++; index.push({ id: keyNum, type: 'code', file }); } console.log('[4/4] 写 resources.json / index.json...'); fs.writeFileSync(RESOURCES_JSON, JSON.stringify({ type: 'webpack-resource-map', resourceMap }, null, 0), 'utf8'); fs.writeFileSync(INDEX_JSON, JSON.stringify({ bundle: path.basename(BUNDLE_PATH), modules: index }, null, 1), 'utf8'); console.log('代码模块 ' + codeCount + ' / 资源模块 ' + resourceCount + ' / 美化失败 ' + failCount); console.log('输出目录: ' + SRC_DIR); })().catch((err) => { console.error('Fatal:', err); process.exit(1); });