@flun/node-mobile-app 4.0.7 → 4.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Android /346/236/204/345/273/272/346/214/207/345/215/227.md" +354 -354
- package/CHANGELOG.md +20 -13
- package/LICENSE +33 -33
- package/README.md +661 -652
- package/bin/cli.js +20 -15
- package/mobileAppConfig.js +165 -165
- package/package.json +8 -8
- package/src/commands/build.js +119 -119
- package/src/commands/test.js +169 -169
- package/src/config.js +112 -112
- package/src/copy.js +320 -320
- package/src/fingerprint.js +129 -129
- package/src/generate.js +103 -103
- package/src/install.js +327 -327
- package/src/optimize-node-modules.js +711 -711
- package/src/patcher.js +393 -393
- package/src/platform-ios.js +140 -140
- package/src/utils.js +280 -280
- package/template/android/gradle.properties +48 -48
- package/template/eslint.config.js +5 -5
- package/template/ios/MyApp/Info.plist +73 -73
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +477 -477
|
@@ -1,712 +1,712 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { createRequire } from 'module';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
|
|
6
|
-
const require = createRequire(import.meta.url), subpath = (...parts) => './' + path.posix.join(...parts),
|
|
7
|
-
BUNDLED_DIR = 'bundled', NM_DIR = 'node_modules', PKG_JSON = 'package.json',
|
|
8
|
-
ENTRY_EXTS = ['.js', '.cjs', '.mjs', '/index.js', '/index.cjs', '/index.mjs'],
|
|
9
|
-
JS_RE = /\.(js|mjs|cjs)$/, DOT_SLASH_RE = /^\.\//, POSIX_RE = /\\/g, PKG_SUBPATH = subpath(PKG_JSON),
|
|
10
|
-
SCRIPT_FILE_RE = /([\p{L}\p{N}_.\-/\\]+\.(?:js|cjs|mjs))/u,
|
|
11
|
-
SNAPSHOT_FILE = '.deps-snapshot.json', SKIP_PKGS = new Set(['esbuild', '@esbuild', 'node-gyp-build',
|
|
12
|
-
'node-gyp-build-optional-packages', 'node-addon-api', 'node']),
|
|
13
|
-
EXCLUDE_NAMES = new Set([PKG_JSON, 'package-lock.json', NM_DIR, '.git', '.gitignore', '.npmignore']),
|
|
14
|
-
TARGET_PLAT_MAP = { android: ['linux'], ios: ['darwin'] },
|
|
15
|
-
ABI_ARCH_MAP = { 'arm64-v8a': 'arm64', 'armeabi-v7a': 'arm', 'x86_64': 'x64', 'x86': 'ia32' },
|
|
16
|
-
IOS_ARCHES = ['arm64', 'x64'], INDEX_FILES = ['index.js', 'index.cjs', 'index.mjs'],
|
|
17
|
-
// ==================== 通用工具 ====================
|
|
18
|
-
toPosix = p => p.replace(POSIX_RE, '/'),
|
|
19
|
-
pickCond = (v, prefer) => {
|
|
20
|
-
if (typeof v === 'string') return v;
|
|
21
|
-
if (v && typeof v === 'object') {
|
|
22
|
-
for (const k of prefer) {
|
|
23
|
-
const r = pickCond(v[k], prefer);
|
|
24
|
-
if (r) return r;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return null;
|
|
28
|
-
},
|
|
29
|
-
// ==================== 快照 ====================
|
|
30
|
-
writeSnapshotFlags = async (snapshotDir, targetPlatform, abiFilters, runtimeMajor) => {
|
|
31
|
-
const p = path.join(snapshotDir, SNAPSHOT_FILE);
|
|
32
|
-
if (!fs.existsSync(p)) return;
|
|
33
|
-
const old = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
34
|
-
old.platform = targetPlatform, old.abiFilters = abiFilters, old.runtimeMajor = runtimeMajor;
|
|
35
|
-
old.optimizeHash = SELF_HASH, old.verified = false, old.optimized = true;
|
|
36
|
-
fs.writeFileSync(p, JSON.stringify(old, null, 2));
|
|
37
|
-
},
|
|
38
|
-
// ==================== 扫描工具 ====================
|
|
39
|
-
findPackages = dir => {
|
|
40
|
-
const result = [];
|
|
41
|
-
if (!fs.existsSync(dir)) return result;
|
|
42
|
-
for (const entry of fs.readdirSync(dir)) {
|
|
43
|
-
if (entry === '.bin' || entry.startsWith('.')) continue;
|
|
44
|
-
const full = path.join(dir, entry);
|
|
45
|
-
if (!fs.statSync(full).isDirectory()) continue;
|
|
46
|
-
if (entry === NM_DIR) { result.push(...findPackages(full)); continue; }
|
|
47
|
-
if (entry.startsWith('@')) {
|
|
48
|
-
for (const sub of fs.readdirSync(full)) {
|
|
49
|
-
const pkgDir = path.join(full, sub);
|
|
50
|
-
if (fs.statSync(pkgDir).isDirectory()) {
|
|
51
|
-
result.push(pkgDir);
|
|
52
|
-
const nested = path.join(pkgDir, NM_DIR);
|
|
53
|
-
if (fs.existsSync(nested)) result.push(...findPackages(nested));
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
} else {
|
|
57
|
-
result.push(full);
|
|
58
|
-
const nested = path.join(full, NM_DIR);
|
|
59
|
-
if (fs.existsSync(nested)) result.push(...findPackages(nested));
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
return result;
|
|
63
|
-
},
|
|
64
|
-
hasNativeModule = dir => {
|
|
65
|
-
let found = false;
|
|
66
|
-
const walk = d => {
|
|
67
|
-
if (found) return;
|
|
68
|
-
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
69
|
-
if (f.isDirectory()) walk(path.join(d, f.name));
|
|
70
|
-
else if (f.name.endsWith('.node') || f.name === 'binding.gyp') { found = true; return; }
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
try { walk(dir); } catch { }
|
|
74
|
-
return found;
|
|
75
|
-
},
|
|
76
|
-
countJS = dir => {
|
|
77
|
-
let n = 0;
|
|
78
|
-
const walk = d => {
|
|
79
|
-
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
80
|
-
if (f.isDirectory()) walk(path.join(d, f.name));
|
|
81
|
-
else if (JS_RE.test(f.name)) n++;
|
|
82
|
-
}
|
|
83
|
-
};
|
|
84
|
-
walk(dir);
|
|
85
|
-
return n;
|
|
86
|
-
},
|
|
87
|
-
findPkgJsons = dir => {
|
|
88
|
-
const out = [],
|
|
89
|
-
walk = d => {
|
|
90
|
-
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
91
|
-
const full = path.join(d, f.name);
|
|
92
|
-
if (f.isDirectory()) {
|
|
93
|
-
if (f.name === '.bin' || f.name === NM_DIR) continue;
|
|
94
|
-
walk(full);
|
|
95
|
-
} else if (f.name === PKG_JSON) out.push(full);
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
walk(dir);
|
|
99
|
-
return out;
|
|
100
|
-
},
|
|
101
|
-
// ==================== 合并 ====================
|
|
102
|
-
keyToFilename = key => {
|
|
103
|
-
if (key === '.') return 'index';
|
|
104
|
-
return key.replace(DOT_SLASH_RE, '').replace(/\//g, '__');
|
|
105
|
-
},
|
|
106
|
-
parseEntriesAll = (pkgDir, j) => {
|
|
107
|
-
const isESM = (j.type === 'module'), prefer = isESM ? ['import', 'module', 'node', 'default', 'require']
|
|
108
|
-
: ['require', 'node', 'default', 'import', 'module'], entries = [], wildcards = [],
|
|
109
|
-
tryPath = rel => {
|
|
110
|
-
const p = path.join(pkgDir, rel);
|
|
111
|
-
if (fs.existsSync(p)) return p;
|
|
112
|
-
const exts = ENTRY_EXTS;
|
|
113
|
-
for (const ext of exts) if (fs.existsSync(p + ext)) return p + ext;
|
|
114
|
-
return null;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
if (j.exports && typeof j.exports === 'object') {
|
|
118
|
-
const keys = Object.keys(j.exports), allConditional = keys.every(k => !k.startsWith('.'));
|
|
119
|
-
if (allConditional) {
|
|
120
|
-
const picked = pickCond(j.exports, prefer);
|
|
121
|
-
if (!picked) return { ok: false };
|
|
122
|
-
const abs = tryPath(picked);
|
|
123
|
-
if (!abs) return { ok: false };
|
|
124
|
-
entries.push({ key: '.', entryFile: abs });
|
|
125
|
-
return { ok: true, entries, wildcards };
|
|
126
|
-
}
|
|
127
|
-
for (const key of keys) {
|
|
128
|
-
if (key === PKG_SUBPATH) continue;
|
|
129
|
-
if (key.includes('*')) { wildcards.push(key); continue; }
|
|
130
|
-
const val = j.exports[key], picked = pickCond(Array.isArray(val) ? val[0] : val, prefer);
|
|
131
|
-
if (!picked) continue;
|
|
132
|
-
if (!JS_RE.test(picked)) continue;
|
|
133
|
-
const abs = path.join(pkgDir, picked);
|
|
134
|
-
if (!fs.existsSync(abs)) continue;
|
|
135
|
-
entries.push({ key, entryFile: abs });
|
|
136
|
-
}
|
|
137
|
-
if (entries.length === 0) return { ok: false };
|
|
138
|
-
return { ok: true, entries, wildcards };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// 无 exports,用 main/module
|
|
142
|
-
let entryFile = null;
|
|
143
|
-
if (j.module) entryFile = tryPath(j.module);
|
|
144
|
-
if (!entryFile && j.main) entryFile = tryPath(j.main);
|
|
145
|
-
if (!entryFile) {
|
|
146
|
-
for (const c of INDEX_FILES) {
|
|
147
|
-
const p = path.join(pkgDir, c);
|
|
148
|
-
if (fs.existsSync(p)) { entryFile = p; break; }
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (!entryFile || !fs.existsSync(entryFile)) return { ok: false };
|
|
152
|
-
entries.push({ key: '.', entryFile });
|
|
153
|
-
return { ok: true, entries, wildcards };
|
|
154
|
-
},
|
|
155
|
-
mergeAll = async (nmDir, excludePkgs, runtimeMajor) => {
|
|
156
|
-
let esbuild;
|
|
157
|
-
try { esbuild = require('esbuild'); }
|
|
158
|
-
catch { throw new Error('esbuild 未找到,请确认 @flun/node-mobile-app 依赖已安装'); }
|
|
159
|
-
|
|
160
|
-
const packages = findPackages(nmDir);
|
|
161
|
-
let merged = 0, skipped = 0, totalDeleted = 0, resMap = null, resLog = null;
|
|
162
|
-
// 调试模式:预扫各包的 postinstall 资源目录,用于统计被阶段1 删除的资源 JS
|
|
163
|
-
if (process.env.OPTIMIZE_DEBUG) {
|
|
164
|
-
resMap = new Map(), resLog = [];
|
|
165
|
-
for (const pkgDir of packages) {
|
|
166
|
-
const pj = path.join(pkgDir, PKG_JSON);
|
|
167
|
-
if (!fs.existsSync(pj)) continue;
|
|
168
|
-
let j;
|
|
169
|
-
try { j = JSON.parse(fs.readFileSync(pj, 'utf8')); } catch { continue; }
|
|
170
|
-
if (!j.scripts) continue;
|
|
171
|
-
const items = new Set();
|
|
172
|
-
for (const k of ['postinstall', 'preinstall', 'install', 'prepare']) {
|
|
173
|
-
const cmd = j.scripts[k];
|
|
174
|
-
if (!cmd) continue;
|
|
175
|
-
const sr = extractScript(cmd);
|
|
176
|
-
if (!sr) continue;
|
|
177
|
-
const sa = path.resolve(pkgDir, sr);
|
|
178
|
-
if (!fs.existsSync(sa)) continue;
|
|
179
|
-
for (const item of extractResources(pkgDir, fs.readFileSync(sa, 'utf8'))) items.add(item);
|
|
180
|
-
}
|
|
181
|
-
if (items.size > 0) resMap.set(pkgDir, items);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
for (const pkgDir of packages) {
|
|
186
|
-
const pj = path.join(pkgDir, PKG_JSON);
|
|
187
|
-
if (!fs.existsSync(pj)) { skipped++; continue; }
|
|
188
|
-
let j;
|
|
189
|
-
try { j = JSON.parse(fs.readFileSync(pj, 'utf8')); } catch { skipped++; continue; }
|
|
190
|
-
const relName = toPosix(path.relative(nmDir, pkgDir));
|
|
191
|
-
if (excludePkgs.has(relName)) { skipped++; continue; }
|
|
192
|
-
if (SKIP_PKGS.has(relName)) { skipped++; continue; }
|
|
193
|
-
if (hasNativeModule(pkgDir)) { skipped++; continue; }
|
|
194
|
-
// 删除安装期脚本字段:运行时无安装期,且 Gradle npm rebuild 会触发脚本导致失败
|
|
195
|
-
// 原生模块包已在上一步跳过(其 scripts 是编译必需,必须保留)
|
|
196
|
-
if (j.scripts) { delete j.scripts; fs.writeFileSync(pj, JSON.stringify(j, null, 2)); }
|
|
197
|
-
if (countJS(pkgDir) <= 1) { skipped++; continue; }
|
|
198
|
-
// 已优化过的包跳过
|
|
199
|
-
const mainStr = String(j.main || ''), exportsStr = JSON.stringify(j.exports || '');
|
|
200
|
-
if (mainStr.includes(BUNDLED_DIR) || exportsStr.includes(BUNDLED_DIR)) { skipped++; continue; }
|
|
201
|
-
|
|
202
|
-
const parse = parseEntriesAll(pkgDir, j);
|
|
203
|
-
if (!parse.ok) { skipped++; continue; }
|
|
204
|
-
|
|
205
|
-
const isESM = (j.type === 'module'), bundledDir = path.join(pkgDir, BUNDLED_DIR), newExports = {},
|
|
206
|
-
banner = "import { createRequire as __ebCR } from 'module'; var require = __ebCR(import.meta.url);",
|
|
207
|
-
artifacts = new Set();
|
|
208
|
-
|
|
209
|
-
fs.mkdirSync(bundledDir, { recursive: true });
|
|
210
|
-
let entryOk = true;
|
|
211
|
-
for (const e of parse.entries) {
|
|
212
|
-
const baseName = keyToFilename(e.key), outName = baseName + (isESM ? '.mjs' : '.cjs'),
|
|
213
|
-
outFile = path.join(bundledDir, outName);
|
|
214
|
-
try {
|
|
215
|
-
await esbuild.build({
|
|
216
|
-
entryPoints: [e.entryFile],
|
|
217
|
-
bundle: true,
|
|
218
|
-
platform: 'node',
|
|
219
|
-
format: isESM ? 'esm' : 'cjs',
|
|
220
|
-
target: 'node' + runtimeMajor,
|
|
221
|
-
outfile: outFile,
|
|
222
|
-
packages: 'external',
|
|
223
|
-
banner: isESM ? { js: banner } : undefined,
|
|
224
|
-
logLevel: 'silent'
|
|
225
|
-
});
|
|
226
|
-
artifacts.add(outFile);
|
|
227
|
-
} catch {
|
|
228
|
-
try { fs.unlinkSync(outFile) } catch { }
|
|
229
|
-
entryOk = false;
|
|
230
|
-
break;
|
|
231
|
-
}
|
|
232
|
-
if (isESM) newExports[e.key] = subpath(BUNDLED_DIR, outName);
|
|
233
|
-
else {
|
|
234
|
-
// CJS 包:若 bundle 末尾是 module.exports = require_xxx()(整体赋值),
|
|
235
|
-
// cjs-module-lexer 静态分析拿不到命名导出,此时定位主入口的 __commonJS 块,
|
|
236
|
-
// 块内抓 exports.X = 形式,追加显式声明让 lexer 识别;
|
|
237
|
-
try {
|
|
238
|
-
const code = fs.readFileSync(outFile, 'utf8'),
|
|
239
|
-
m = code.match(/module\.exports\s*=\s*require_(\w+)\s*\(\s*\)\s*;?\s*$/m);
|
|
240
|
-
if (m) {
|
|
241
|
-
const fnName = m[1], startPat = 'var require_' + fnName + ' = __commonJS({',
|
|
242
|
-
start = code.indexOf(startPat);
|
|
243
|
-
if (start >= 0) {
|
|
244
|
-
let depth = 0, i = start + startPat.length - 1, end = -1;
|
|
245
|
-
while (i < code.length) {
|
|
246
|
-
const ch = code[i];
|
|
247
|
-
if (ch === '{') depth++;
|
|
248
|
-
else if (ch === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
249
|
-
i++;
|
|
250
|
-
}
|
|
251
|
-
if (end >= 0) {
|
|
252
|
-
const block = code.substring(start, end + 1), keys = new Set(),
|
|
253
|
-
re = /\bexports\d*\.([\p{ID_Start}$_][\p{ID_Continue}$]*)\s*=/gu;
|
|
254
|
-
let mm;
|
|
255
|
-
while ((mm = re.exec(block)) !== null) keys.add(mm[1]);
|
|
256
|
-
if (keys.size > 0) {
|
|
257
|
-
const decls = [...keys].map(k => `try { module.exports[${JSON.stringify(k)}] =
|
|
258
|
-
module.exports[${JSON.stringify(k)}]; } catch {}`);
|
|
259
|
-
fs.appendFileSync(outFile, '\n' + decls.join('\n') + '\n');
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
} catch { }
|
|
265
|
-
newExports[e.key] = {
|
|
266
|
-
import: subpath(BUNDLED_DIR, outName), require: subpath(BUNDLED_DIR, outName),
|
|
267
|
-
default: subpath(BUNDLED_DIR, outName)
|
|
268
|
-
};
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
if (!entryOk) {
|
|
273
|
-
fs.rmSync(bundledDir, { recursive: true, force: true }), skipped++;
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// 保留通配符键和 package.json
|
|
278
|
-
for (const w of parse.wildcards) if (j.exports && j.exports[w]) newExports[w] = j.exports[w];
|
|
279
|
-
if (j.exports && j.exports[PKG_SUBPATH]) newExports[PKG_SUBPATH] = j.exports[PKG_SUBPATH];
|
|
280
|
-
|
|
281
|
-
fs.writeFileSync(pj + '.orig', JSON.stringify(j));
|
|
282
|
-
|
|
283
|
-
// 通配符覆盖路径保护
|
|
284
|
-
const wildcardPrefixes = parse.wildcards.map(w => w.replace(DOT_SLASH_RE, '').replace(/\/\*.*$/, '/'))
|
|
285
|
-
.filter(Boolean);
|
|
286
|
-
|
|
287
|
-
j.exports = newExports;
|
|
288
|
-
if (newExports['.'])
|
|
289
|
-
j.main = isESM ? newExports['.'] : (newExports['.'].require || newExports['.'].default || newExports['.']);
|
|
290
|
-
else delete j.main;
|
|
291
|
-
if (isESM && j.main) j.module = j.main;
|
|
292
|
-
fs.writeFileSync(pj, JSON.stringify(j, null, 2));
|
|
293
|
-
|
|
294
|
-
// 删除原 JS
|
|
295
|
-
let deleted = 0;
|
|
296
|
-
const resItems = resMap ? (resMap.get(pkgDir) || null) : null, resJsHits = resItems ? new Map() : null,
|
|
297
|
-
walkDel = d => {
|
|
298
|
-
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
299
|
-
if (f.isDirectory()) {
|
|
300
|
-
if (f.name === NM_DIR || f.name === BUNDLED_DIR) continue;
|
|
301
|
-
walkDel(path.join(d, f.name));
|
|
302
|
-
} else if (JS_RE.test(f.name)) {
|
|
303
|
-
const full = path.join(d, f.name), rel = toPosix(path.relative(pkgDir, full));
|
|
304
|
-
if (wildcardPrefixes.some(p => rel.startsWith(p))) continue;
|
|
305
|
-
if (resItems) {
|
|
306
|
-
const top = rel.split('/')[0];
|
|
307
|
-
if (resItems.has(top)) resJsHits.set(top, (resJsHits.get(top) || 0) + 1);
|
|
308
|
-
}
|
|
309
|
-
fs.unlinkSync(full); deleted++;
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
walkDel(pkgDir), totalDeleted += deleted, merged++;
|
|
314
|
-
if (resJsHits && resJsHits.size > 0 && resLog)
|
|
315
|
-
for (const [k, v] of resJsHits) resLog.push(` ${relName}: ${k} (${v} 个 JS)`);
|
|
316
|
-
if (process.env.OPTIMIZE_DEBUG) console.log(chalk.gray(` ✓ ${relName} (${parse.entries.length} 入口)`));
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (resLog && resLog.length > 0) {
|
|
320
|
-
console.log(chalk.gray(' 处理保留的资源 JS(被引用则合并,否则删除):'));
|
|
321
|
-
for (const line of resLog) console.log(chalk.gray(line));
|
|
322
|
-
}
|
|
323
|
-
return { merged, skipped, totalDeleted };
|
|
324
|
-
},
|
|
325
|
-
// ==================== 清空资源 ====================
|
|
326
|
-
collectReferencedFiles = nmRoot => {
|
|
327
|
-
const refs = new Set(),
|
|
328
|
-
walk = d => {
|
|
329
|
-
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
330
|
-
const full = path.join(d, f.name);
|
|
331
|
-
if (f.isDirectory()) {
|
|
332
|
-
if (f.name === NM_DIR && d !== nmRoot) continue;
|
|
333
|
-
walk(full);
|
|
334
|
-
} else if (JS_RE.test(f.name)) {
|
|
335
|
-
let code;
|
|
336
|
-
try { code = fs.readFileSync(full, 'utf8'); } catch { continue; }
|
|
337
|
-
const re = /(?:from|require|import)\s*\(?\s*['"](\.[^'"]+)['"]/g;
|
|
338
|
-
let m;
|
|
339
|
-
while ((m = re.exec(code)) !== null) {
|
|
340
|
-
const abs = path.resolve(path.dirname(full), m[1]);
|
|
341
|
-
refs.add(abs);
|
|
342
|
-
if (!path.extname(abs))
|
|
343
|
-
for (const ext of ENTRY_EXTS) if (fs.existsSync(abs + ext)) refs.add(abs + ext);
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
};
|
|
348
|
-
walk(nmRoot);
|
|
349
|
-
return refs;
|
|
350
|
-
},
|
|
351
|
-
extractScript = cmd => {
|
|
352
|
-
const m = cmd.match(SCRIPT_FILE_RE);
|
|
353
|
-
return m ? m[1] : null;
|
|
354
|
-
},
|
|
355
|
-
extractResources = (pkgDir, scriptCode) => {
|
|
356
|
-
const strings = new Set(), re = /['"`]([^\s'"`]{2,})['"`]/g;
|
|
357
|
-
let m;
|
|
358
|
-
while ((m = re.exec(scriptCode)) !== null) strings.add(m[1]);
|
|
359
|
-
const items = [];
|
|
360
|
-
for (const s of strings) {
|
|
361
|
-
const first = s.split(/[\\/]/)[0];
|
|
362
|
-
if (EXCLUDE_NAMES.has(first) || !first || first === '.' || first === '..') continue;
|
|
363
|
-
if (fs.existsSync(path.join(pkgDir, first))) items.push(first);
|
|
364
|
-
}
|
|
365
|
-
return [...new Set(items)];
|
|
366
|
-
},
|
|
367
|
-
clearItem = (p, refs) => {
|
|
368
|
-
let n = 0;
|
|
369
|
-
const st = fs.statSync(p);
|
|
370
|
-
if (st.isDirectory()) {
|
|
371
|
-
for (const f of fs.readdirSync(p)) {
|
|
372
|
-
const full = path.join(p, f), sub = fs.statSync(full);
|
|
373
|
-
if (sub.isDirectory()) n += clearItem(full, refs);
|
|
374
|
-
else if (!JS_RE.test(f) && !refs.has(full)) try { fs.rmSync(full, { force: true }), n++; } catch { }
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
else if (!JS_RE.test(path.basename(p)) && !refs.has(p)) fs.writeFileSync(p, ''), n++;
|
|
378
|
-
return n;
|
|
379
|
-
},
|
|
380
|
-
clearResources = async (nmDir, excludePkgs) => {
|
|
381
|
-
const refs = collectReferencedFiles(nmDir);
|
|
382
|
-
let cleared = 0;
|
|
383
|
-
for (const pj of findPkgJsons(nmDir)) {
|
|
384
|
-
let j;
|
|
385
|
-
try { j = JSON.parse(fs.readFileSync(pj, 'utf8')); } catch { continue; }
|
|
386
|
-
if (!j.scripts) continue;
|
|
387
|
-
const pkgDir = path.dirname(pj), pkgRel = toPosix(path.relative(nmDir, pkgDir));
|
|
388
|
-
if (SKIP_PKGS.has(pkgRel) || excludePkgs.has(pkgRel)) continue;
|
|
389
|
-
const pkgItems = [];
|
|
390
|
-
for (const k of ['postinstall', 'preinstall', 'install', 'prepare']) {
|
|
391
|
-
const cmd = j.scripts[k];
|
|
392
|
-
if (!cmd) continue;
|
|
393
|
-
const scriptRel = extractScript(cmd);
|
|
394
|
-
if (!scriptRel) continue;
|
|
395
|
-
const scriptAbs = path.resolve(pkgDir, scriptRel);
|
|
396
|
-
if (!fs.existsSync(scriptAbs)) continue;
|
|
397
|
-
const code = fs.readFileSync(scriptAbs, 'utf8'), items = extractResources(pkgDir, code);
|
|
398
|
-
for (const item of items) {
|
|
399
|
-
const cnt = clearItem(path.join(pkgDir, item), refs);
|
|
400
|
-
if (cnt > 0) cleared += cnt, pkgItems.push(`${item}(${cnt})`);
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (process.env.OPTIMIZE_DEBUG && pkgItems.length > 0)
|
|
404
|
-
console.log(chalk.gray(` ✓ ${pkgRel}: ${pkgItems.join(', ')}`));
|
|
405
|
-
}
|
|
406
|
-
return cleared;
|
|
407
|
-
},
|
|
408
|
-
// ==================== 清平台 ====================
|
|
409
|
-
prunePlatform = async (nmDir, myPlat, myArch, targetPlatform) => {
|
|
410
|
-
let removed = 0;
|
|
411
|
-
const readHead = file => {
|
|
412
|
-
let fd;
|
|
413
|
-
try {
|
|
414
|
-
fd = fs.openSync(file, 'r');
|
|
415
|
-
const buf = Buffer.alloc(512), n = fs.readSync(fd, buf, 0, 512, 0);
|
|
416
|
-
return n > 0 ? buf.subarray(0, n) : null;
|
|
417
|
-
} catch {
|
|
418
|
-
return null;
|
|
419
|
-
} finally {
|
|
420
|
-
if (fd !== undefined) fs.closeSync(fd);
|
|
421
|
-
}
|
|
422
|
-
},
|
|
423
|
-
sniffPlat = buf => {
|
|
424
|
-
if (!buf || buf.length < 4) return null;
|
|
425
|
-
const hex = buf.toString('hex', 0, 4).toUpperCase();
|
|
426
|
-
if (hex.startsWith('4D5A')) return 'win32';
|
|
427
|
-
if (hex === '7F454C46') return 'linux';
|
|
428
|
-
if (['CFFAEDFE', 'CEFAEDFE', 'FEEDFACE', 'FEEDFACF'].includes(hex)) return 'darwin';
|
|
429
|
-
return null;
|
|
430
|
-
},
|
|
431
|
-
sniffArch = buf => {
|
|
432
|
-
if (!buf || buf.length < 64) return null;
|
|
433
|
-
const hex = buf.toString('hex', 0, 4).toUpperCase();
|
|
434
|
-
try {
|
|
435
|
-
if (hex.startsWith('4D5A')) {
|
|
436
|
-
const off = buf.readUInt32LE(0x3C);
|
|
437
|
-
if (off + 6 > buf.length) return null;
|
|
438
|
-
const m = buf.readUInt16LE(off + 4);
|
|
439
|
-
return m === 0x8664 ? 'x64' : m === 0xAA64 ? 'arm64' : m === 0x14C ?
|
|
440
|
-
'ia32' : (m === 0x1C0 || m === 0x1C4) ? 'arm' : null;
|
|
441
|
-
}
|
|
442
|
-
if (hex === '7F454C46') {
|
|
443
|
-
const m = buf.readUInt16LE(0x12);
|
|
444
|
-
return m === 0x3E ? 'x64' : m === 0xB7 ? 'arm64' : m === 0x28 ? 'arm' : m === 0x03 ?
|
|
445
|
-
'ia32' : null;
|
|
446
|
-
}
|
|
447
|
-
if (hex === 'CFFAEDFE' || hex === 'CEFAEDFE') {
|
|
448
|
-
const c = buf.readUInt32LE(4);
|
|
449
|
-
return c === 0x01000007 ? 'x64' : c === 0x0100000C ? 'arm64' : c === 0x7 ? 'ia32' : c === 0xC ?
|
|
450
|
-
'arm' : null;
|
|
451
|
-
}
|
|
452
|
-
if (hex === 'FEEDFACF' || hex === 'FEEDFACE') {
|
|
453
|
-
const c = buf.readUInt32BE(4);
|
|
454
|
-
return c === 0x01000007 ? 'x64' : c === 0x0100000C ? 'arm64' : c === 0x7 ? 'ia32' : c === 0xC ?
|
|
455
|
-
'arm' : null;
|
|
456
|
-
}
|
|
457
|
-
} catch { }
|
|
458
|
-
return null;
|
|
459
|
-
},
|
|
460
|
-
suffixPlat = name => {
|
|
461
|
-
const ext = path.extname(name).toLowerCase();
|
|
462
|
-
if (ext === '.exe' || ext === '.dll') return 'win32';
|
|
463
|
-
if (ext === '.dylib') return 'darwin';
|
|
464
|
-
if (ext === '.so') return 'linux';
|
|
465
|
-
return null;
|
|
466
|
-
},
|
|
467
|
-
delFile = p => {
|
|
468
|
-
try {
|
|
469
|
-
fs.statSync(p).isDirectory() ? fs.rmSync(p, { recursive: true, force: true })
|
|
470
|
-
: fs.unlinkSync(p), removed++;
|
|
471
|
-
} catch { }
|
|
472
|
-
},
|
|
473
|
-
processDir = d => {
|
|
474
|
-
let entries;
|
|
475
|
-
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
476
|
-
for (const e of entries) {
|
|
477
|
-
const full = path.join(d, e.name),
|
|
478
|
-
expectedPrebuilds = targetPlatform === 'ios' ? ['darwin'] : ['android'],
|
|
479
|
-
prebuildMatch = full.match(/[\\/]prebuilds[\\/](linux|darwin|win32|android)-/i);
|
|
480
|
-
|
|
481
|
-
if (prebuildMatch && !expectedPrebuilds.includes(prebuildMatch[1].toLowerCase()))
|
|
482
|
-
{ delFile(full); continue; }
|
|
483
|
-
if (e.isDirectory()) { processDir(full); continue; }
|
|
484
|
-
if (JS_RE.test(e.name)) continue;
|
|
485
|
-
let plat = suffixPlat(e.name), arch = null;
|
|
486
|
-
const head = readHead(full);
|
|
487
|
-
if (head) {
|
|
488
|
-
if (!plat) plat = sniffPlat(head);
|
|
489
|
-
arch = sniffArch(head);
|
|
490
|
-
}
|
|
491
|
-
const platBad = plat && !myPlat.includes(plat), archBad = plat && arch && !myArch.includes(arch);
|
|
492
|
-
if (platBad || archBad) {
|
|
493
|
-
if (process.env.OPTIMIZE_DEBUG) {
|
|
494
|
-
const rel = toPosix(path.relative(nmDir, full));
|
|
495
|
-
console.log(chalk.gray(` ✓ ${rel} (${platBad ? '平台不符: ' + plat : '架构不符: ' + arch})`));
|
|
496
|
-
}
|
|
497
|
-
delFile(full);
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
};
|
|
501
|
-
processDir(nmDir);
|
|
502
|
-
return removed;
|
|
503
|
-
},
|
|
504
|
-
// ==================== 清开发文件 ====================
|
|
505
|
-
pruneDevFiles = async nmDir => {
|
|
506
|
-
let deleted = 0;
|
|
507
|
-
const PATTERNS = [
|
|
508
|
-
/(^|\/)tsconfig\.json$/,
|
|
509
|
-
/(^|\/)\.nycrc(\.(json|yml))?$/,
|
|
510
|
-
/(^|\/)\.editorconfig$/, /(^|\/)\.prettierignore$/, /(^|\/)\.eslintignore$/,
|
|
511
|
-
/(^|\/)\.dockerignore$/, /(^|\/)\.gitignore$/, /(^|\/)\.gitattributes$/, /(^|\/)\.npmignore$/,
|
|
512
|
-
/\.(md|markdown)$/i, /\.(yml|yaml)$/i, /\.(gyp|gypi|h|hh|hpp|c|cc|cpp|cxx|m|mm)$/i,
|
|
513
|
-
/\.map$/, /metafile-.*\.json$/,
|
|
514
|
-
/(^|\/)\.eslintrc(\.(json|yml|js|cjs))?$/, /(^|\/)\.babelrc(\.(json|js))?$/,
|
|
515
|
-
/(^|\/)package-lock\.json$/, /(^|\/)\.package-lock\.json$/, /(^|\/)yarn\.lock$/,
|
|
516
|
-
/(^|\/)package-support\.json$/,
|
|
517
|
-
], DIR_SKIP = new Set([BUNDLED_DIR]),
|
|
518
|
-
walk = d => {
|
|
519
|
-
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
520
|
-
const full = path.join(d, f.name);
|
|
521
|
-
if (f.isDirectory()) {
|
|
522
|
-
if (DIR_SKIP.has(f.name)) continue;
|
|
523
|
-
if (f.name === '.github' || f.name === '.circleci') {
|
|
524
|
-
fs.rmSync(full, { recursive: true, force: true }), deleted++;
|
|
525
|
-
continue;
|
|
526
|
-
}
|
|
527
|
-
walk(full);
|
|
528
|
-
} else {
|
|
529
|
-
const rel = toPosix(path.relative(nmDir, full));
|
|
530
|
-
if (PATTERNS.some(re => re.test(rel))) try { fs.unlinkSync(full), deleted++; } catch { }
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
};
|
|
534
|
-
walk(nmDir);
|
|
535
|
-
return deleted;
|
|
536
|
-
},
|
|
537
|
-
pruneIntermediates = async nmDir => {
|
|
538
|
-
let deleted = 0;
|
|
539
|
-
const MID_RE =
|
|
540
|
-
/\.(o|d|mk|stamp|Makefile|iobj|ipdb|pdb|exp|lib|obj|vcxproj|tlog|recipe|lastbuildstate|nativecodeanalysis\.xml)$/i,
|
|
541
|
-
walk = d => {
|
|
542
|
-
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
543
|
-
const full = path.join(d, f.name);
|
|
544
|
-
if (f.isDirectory()) walk(full);
|
|
545
|
-
else if (full.includes(path.sep + 'obj.target' + path.sep) || f.name === 'Makefile' ||
|
|
546
|
-
f.name.endsWith('.orig') || MID_RE.test(f.name)) try { fs.unlinkSync(full), deleted++; } catch { }
|
|
547
|
-
}
|
|
548
|
-
};
|
|
549
|
-
walk(nmDir);
|
|
550
|
-
return deleted;
|
|
551
|
-
},
|
|
552
|
-
applyFilesRules = async nmDir => {
|
|
553
|
-
let deleted = 0;
|
|
554
|
-
// 1) 删顶层 node 假包(npm 上名为 node 的包,仅提供 node.exe,运行时不需要)
|
|
555
|
-
const LICENSE_RE = /^licen[sc]e/i, TS_RE = /\.(ts|cts|mts)$/, nodePkg = path.join(nmDir, 'node');
|
|
556
|
-
if (fs.existsSync(nodePkg)) try { fs.rmSync(nodePkg, { recursive: true, force: true }), deleted++; } catch { }
|
|
557
|
-
|
|
558
|
-
// 2) 递归删 node-<platform>* 假包
|
|
559
|
-
const walkForFakeNode = d => {
|
|
560
|
-
let entries;
|
|
561
|
-
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
562
|
-
for (const f of entries) {
|
|
563
|
-
if (!f.isDirectory()) continue;
|
|
564
|
-
const full = path.join(d, f.name);
|
|
565
|
-
if (/^node-(win|darwin|linux|freebsd|sunos|aix)/.test(f.name)) {
|
|
566
|
-
try { fs.rmSync(full, { recursive: true, force: true }), deleted++; } catch { }
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
walkForFakeNode(full);
|
|
570
|
-
}
|
|
571
|
-
};
|
|
572
|
-
walkForFakeNode(nmDir);
|
|
573
|
-
|
|
574
|
-
// 3) 删 license/licence 文件或目录 + .ts/.cts/.mts 文件
|
|
575
|
-
const walk = d => {
|
|
576
|
-
let entries;
|
|
577
|
-
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return }
|
|
578
|
-
for (const f of entries) {
|
|
579
|
-
const full = path.join(d, f.name);
|
|
580
|
-
if (f.isDirectory()) {
|
|
581
|
-
if (f.name === BUNDLED_DIR) continue;
|
|
582
|
-
if (LICENSE_RE.test(f.name)) {
|
|
583
|
-
try { fs.rmSync(full, { recursive: true, force: true }), deleted++; } catch { }
|
|
584
|
-
continue;
|
|
585
|
-
}
|
|
586
|
-
walk(full);
|
|
587
|
-
}
|
|
588
|
-
else if (LICENSE_RE.test(f.name) || TS_RE.test(f.name)) try { fs.unlinkSync(full), deleted++; } catch { }
|
|
589
|
-
}
|
|
590
|
-
};
|
|
591
|
-
walk(nmDir);
|
|
592
|
-
|
|
593
|
-
const cleanEmpty = d => {
|
|
594
|
-
for (const e of fs.readdirSync(d, { withFileTypes: true }))
|
|
595
|
-
if (e.isDirectory()) cleanEmpty(path.join(d, e.name));
|
|
596
|
-
if (d !== nmDir) try { if (fs.readdirSync(d).length === 0) fs.rmdirSync(d), deleted++; } catch { }
|
|
597
|
-
};
|
|
598
|
-
cleanEmpty(nmDir);
|
|
599
|
-
return deleted;
|
|
600
|
-
};
|
|
601
|
-
|
|
602
|
-
// ==================== 导出函数 ====================
|
|
603
|
-
/**
|
|
604
|
-
* 清除快照中的 verified 标记(优化开始或构建失败时调用)
|
|
605
|
-
* >查看定义:@see {@link clearVerified}
|
|
606
|
-
* @param {string} snapshotDir 快照目录
|
|
607
|
-
* @returns {Promise<void>}
|
|
608
|
-
*/
|
|
609
|
-
const clearVerified = async snapshotDir => {
|
|
610
|
-
const p = path.join(snapshotDir, SNAPSHOT_FILE);
|
|
611
|
-
if (!fs.existsSync(p)) return;
|
|
612
|
-
try {
|
|
613
|
-
const old = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
614
|
-
if (old.verified !== false) old.verified = false, fs.writeFileSync(p, JSON.stringify(old, null, 2));
|
|
615
|
-
} catch { }
|
|
616
|
-
};
|
|
617
|
-
/**
|
|
618
|
-
* 标记快照 verified=true(构建全流程成功后由 build.js / test.js 调用)
|
|
619
|
-
* >查看定义:@see {@link markVerified}
|
|
620
|
-
* @param {string} snapshotDir 快照目录
|
|
621
|
-
* @returns {Promise<void>}
|
|
622
|
-
*/
|
|
623
|
-
const markVerified = async snapshotDir => {
|
|
624
|
-
const p = path.join(snapshotDir, SNAPSHOT_FILE);
|
|
625
|
-
if (!fs.existsSync(p)) return;
|
|
626
|
-
try {
|
|
627
|
-
const old = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
628
|
-
old.verified = true, fs.writeFileSync(p, JSON.stringify(old, null, 2));
|
|
629
|
-
} catch { }
|
|
630
|
-
};
|
|
631
|
-
/**
|
|
632
|
-
* 执行 node_modules 六阶段优化
|
|
633
|
-
* >查看定义:@see {@link optimizeNodeModules}
|
|
634
|
-
* @param {string} tempDir node_modules 所在目录(nodejs-assets/nodejs-project)
|
|
635
|
-
* @param {object} [config] 优化配置
|
|
636
|
-
* @param {string} [config.snapshotDir] 快照目录(默认同 tempDir)
|
|
637
|
-
* @param {string} [config.targetPlatform] 目标平台:'android'(默认)或 'ios'
|
|
638
|
-
* @param {string[]} [config.abiFilters] Android 目标架构(默认 ['arm64-v8a']);iOS 固定保留 arm64 + x64
|
|
639
|
-
* @param {string} config.runtimeMajor 运行时 Node 主版本号(必填,从 libnode/node_version.h 读取)
|
|
640
|
-
* @param {string[]} [config.exclude] 排除的包名列表
|
|
641
|
-
* @returns {Promise<object>} 各阶段统计(merged / cleared / platform / devFiles / intermediates / filesRules)
|
|
642
|
-
*/
|
|
643
|
-
const optimizeNodeModules = async (tempDir, config = {}) => {
|
|
644
|
-
const targetPlatform = config.targetPlatform || 'android', abiFilters = config.abiFilters || ['arm64-v8a'],
|
|
645
|
-
runtimeMajor = config.runtimeMajor, excludePkgs = new Set(config.exclude || []),
|
|
646
|
-
nmDir = path.join(tempDir, NM_DIR), snapshotDir = config.snapshotDir || tempDir,
|
|
647
|
-
snapPath = path.join(snapshotDir, SNAPSHOT_FILE), myPlat = TARGET_PLAT_MAP[targetPlatform] || [],
|
|
648
|
-
myArch = targetPlatform === 'ios' ? IOS_ARCHES : (abiFilters || []).map(a => ABI_ARCH_MAP[a]).filter(Boolean);
|
|
649
|
-
|
|
650
|
-
if (!runtimeMajor) throw new Error('optimizeNodeModules 缺少 config.runtimeMajor(应从 libnode/node_version.h 读取)');
|
|
651
|
-
if (!fs.existsSync(nmDir)) {
|
|
652
|
-
console.log(chalk.gray('[优化] node_modules 不存在,跳过'));
|
|
653
|
-
return { skipped: true };
|
|
654
|
-
}
|
|
655
|
-
|
|
656
|
-
await clearVerified(snapshotDir); // 执行优化前清 verified,构建成功后再由 build.js / test.js 写回 true
|
|
657
|
-
const t0 = Date.now();
|
|
658
|
-
try {
|
|
659
|
-
// 阶段1: 清空资源(跳过 js,留待合并阶段处理)
|
|
660
|
-
console.log(chalk.cyan(' [1/6] 清空复制到项目根的资源...'));
|
|
661
|
-
const c = await clearResources(nmDir, excludePkgs);
|
|
662
|
-
console.log(chalk.green(` 清空 ${c} 个文件(js 文件保留,留待合并阶段处理)`));
|
|
663
|
-
|
|
664
|
-
// 阶段2: 合并
|
|
665
|
-
console.log(chalk.cyan(' [2/6] 合并 JS...'));
|
|
666
|
-
const m = await mergeAll(nmDir, excludePkgs, runtimeMajor);
|
|
667
|
-
console.log(chalk.green(` 合并 ${m.merged} 包,跳过 ${m.skipped},删 ${m.totalDeleted} 个 JS`));
|
|
668
|
-
|
|
669
|
-
// 阶段3: 清平台二进制
|
|
670
|
-
console.log(chalk.cyan(' [3/6] 清理非当前平台二进制...'));
|
|
671
|
-
const p = await prunePlatform(nmDir, myPlat, myArch, targetPlatform);
|
|
672
|
-
console.log(chalk.green(` 清理 ${p} 项`));
|
|
673
|
-
|
|
674
|
-
// 阶段4: 清开发文件
|
|
675
|
-
console.log(chalk.cyan(' [4/6] 清理开发文件...'));
|
|
676
|
-
const d = await pruneDevFiles(nmDir);
|
|
677
|
-
console.log(chalk.green(` 清理 ${d} 个文件`));
|
|
678
|
-
|
|
679
|
-
// 阶段5: 清中间产物
|
|
680
|
-
console.log(chalk.cyan(' [5/6] 清理中间产物...'));
|
|
681
|
-
const i = await pruneIntermediates(nmDir);
|
|
682
|
-
console.log(chalk.green(` 清理 ${i} 个文件`));
|
|
683
|
-
|
|
684
|
-
// 阶段6: 清除不参与运行的文件(与 build.js 的 files 数组对齐)
|
|
685
|
-
console.log(chalk.cyan(' [6/6] 清除不参与运行的文件和空目录...'));
|
|
686
|
-
const fr = await applyFilesRules(nmDir);
|
|
687
|
-
console.log(chalk.green(` 清理 ${fr} 项`));
|
|
688
|
-
|
|
689
|
-
// 写 platform/abiFilters/runtimeMajor,同时 verified=false
|
|
690
|
-
await writeSnapshotFlags(snapshotDir, targetPlatform, abiFilters, runtimeMajor);
|
|
691
|
-
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
692
|
-
console.log(chalk.green(` [信息] 优化完成,耗时 ${elapsed}s`));
|
|
693
|
-
|
|
694
|
-
return { merged: m.merged, cleared: c, platform: p, devFiles: d, intermediates: i, filesRules: fr };
|
|
695
|
-
} catch (err) {
|
|
696
|
-
// 失败时删除依赖快照,下次强制重装 + 重优化
|
|
697
|
-
try { fs.rmSync(snapPath, { recursive: true, force: true }); } catch { }
|
|
698
|
-
throw err;
|
|
699
|
-
}
|
|
700
|
-
};
|
|
701
|
-
/**
|
|
702
|
-
* 当前优化文件的 SHA256 前 16 位(缓存指纹:优化代码一改,缓存自动失效)
|
|
703
|
-
* >查看定义:@see {@link SELF_HASH}
|
|
704
|
-
*/
|
|
705
|
-
const SELF_HASH = (() => {
|
|
706
|
-
try {
|
|
707
|
-
const src = fs.readFileSync(new URL(import.meta.url), 'utf8');
|
|
708
|
-
return require('crypto').createHash('sha256').update(src).digest('hex').slice(0, 16);
|
|
709
|
-
} catch { return 'unknown'; }
|
|
710
|
-
})();
|
|
711
|
-
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
const require = createRequire(import.meta.url), subpath = (...parts) => './' + path.posix.join(...parts),
|
|
7
|
+
BUNDLED_DIR = 'bundled', NM_DIR = 'node_modules', PKG_JSON = 'package.json',
|
|
8
|
+
ENTRY_EXTS = ['.js', '.cjs', '.mjs', '/index.js', '/index.cjs', '/index.mjs'],
|
|
9
|
+
JS_RE = /\.(js|mjs|cjs)$/, DOT_SLASH_RE = /^\.\//, POSIX_RE = /\\/g, PKG_SUBPATH = subpath(PKG_JSON),
|
|
10
|
+
SCRIPT_FILE_RE = /([\p{L}\p{N}_.\-/\\]+\.(?:js|cjs|mjs))/u,
|
|
11
|
+
SNAPSHOT_FILE = '.deps-snapshot.json', SKIP_PKGS = new Set(['esbuild', '@esbuild', 'node-gyp-build',
|
|
12
|
+
'node-gyp-build-optional-packages', 'node-addon-api', 'node']),
|
|
13
|
+
EXCLUDE_NAMES = new Set([PKG_JSON, 'package-lock.json', NM_DIR, '.git', '.gitignore', '.npmignore']),
|
|
14
|
+
TARGET_PLAT_MAP = { android: ['linux'], ios: ['darwin'] },
|
|
15
|
+
ABI_ARCH_MAP = { 'arm64-v8a': 'arm64', 'armeabi-v7a': 'arm', 'x86_64': 'x64', 'x86': 'ia32' },
|
|
16
|
+
IOS_ARCHES = ['arm64', 'x64'], INDEX_FILES = ['index.js', 'index.cjs', 'index.mjs'],
|
|
17
|
+
// ==================== 通用工具 ====================
|
|
18
|
+
toPosix = p => p.replace(POSIX_RE, '/'),
|
|
19
|
+
pickCond = (v, prefer) => {
|
|
20
|
+
if (typeof v === 'string') return v;
|
|
21
|
+
if (v && typeof v === 'object') {
|
|
22
|
+
for (const k of prefer) {
|
|
23
|
+
const r = pickCond(v[k], prefer);
|
|
24
|
+
if (r) return r;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return null;
|
|
28
|
+
},
|
|
29
|
+
// ==================== 快照 ====================
|
|
30
|
+
writeSnapshotFlags = async (snapshotDir, targetPlatform, abiFilters, runtimeMajor) => {
|
|
31
|
+
const p = path.join(snapshotDir, SNAPSHOT_FILE);
|
|
32
|
+
if (!fs.existsSync(p)) return;
|
|
33
|
+
const old = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
34
|
+
old.platform = targetPlatform, old.abiFilters = abiFilters, old.runtimeMajor = runtimeMajor;
|
|
35
|
+
old.optimizeHash = SELF_HASH, old.verified = false, old.optimized = true;
|
|
36
|
+
fs.writeFileSync(p, JSON.stringify(old, null, 2));
|
|
37
|
+
},
|
|
38
|
+
// ==================== 扫描工具 ====================
|
|
39
|
+
findPackages = dir => {
|
|
40
|
+
const result = [];
|
|
41
|
+
if (!fs.existsSync(dir)) return result;
|
|
42
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
43
|
+
if (entry === '.bin' || entry.startsWith('.')) continue;
|
|
44
|
+
const full = path.join(dir, entry);
|
|
45
|
+
if (!fs.statSync(full).isDirectory()) continue;
|
|
46
|
+
if (entry === NM_DIR) { result.push(...findPackages(full)); continue; }
|
|
47
|
+
if (entry.startsWith('@')) {
|
|
48
|
+
for (const sub of fs.readdirSync(full)) {
|
|
49
|
+
const pkgDir = path.join(full, sub);
|
|
50
|
+
if (fs.statSync(pkgDir).isDirectory()) {
|
|
51
|
+
result.push(pkgDir);
|
|
52
|
+
const nested = path.join(pkgDir, NM_DIR);
|
|
53
|
+
if (fs.existsSync(nested)) result.push(...findPackages(nested));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
result.push(full);
|
|
58
|
+
const nested = path.join(full, NM_DIR);
|
|
59
|
+
if (fs.existsSync(nested)) result.push(...findPackages(nested));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
},
|
|
64
|
+
hasNativeModule = dir => {
|
|
65
|
+
let found = false;
|
|
66
|
+
const walk = d => {
|
|
67
|
+
if (found) return;
|
|
68
|
+
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
69
|
+
if (f.isDirectory()) walk(path.join(d, f.name));
|
|
70
|
+
else if (f.name.endsWith('.node') || f.name === 'binding.gyp') { found = true; return; }
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
try { walk(dir); } catch { }
|
|
74
|
+
return found;
|
|
75
|
+
},
|
|
76
|
+
countJS = dir => {
|
|
77
|
+
let n = 0;
|
|
78
|
+
const walk = d => {
|
|
79
|
+
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
80
|
+
if (f.isDirectory()) walk(path.join(d, f.name));
|
|
81
|
+
else if (JS_RE.test(f.name)) n++;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
walk(dir);
|
|
85
|
+
return n;
|
|
86
|
+
},
|
|
87
|
+
findPkgJsons = dir => {
|
|
88
|
+
const out = [],
|
|
89
|
+
walk = d => {
|
|
90
|
+
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
91
|
+
const full = path.join(d, f.name);
|
|
92
|
+
if (f.isDirectory()) {
|
|
93
|
+
if (f.name === '.bin' || f.name === NM_DIR) continue;
|
|
94
|
+
walk(full);
|
|
95
|
+
} else if (f.name === PKG_JSON) out.push(full);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
walk(dir);
|
|
99
|
+
return out;
|
|
100
|
+
},
|
|
101
|
+
// ==================== 合并 ====================
|
|
102
|
+
keyToFilename = key => {
|
|
103
|
+
if (key === '.') return 'index';
|
|
104
|
+
return key.replace(DOT_SLASH_RE, '').replace(/\//g, '__');
|
|
105
|
+
},
|
|
106
|
+
parseEntriesAll = (pkgDir, j) => {
|
|
107
|
+
const isESM = (j.type === 'module'), prefer = isESM ? ['import', 'module', 'node', 'default', 'require']
|
|
108
|
+
: ['require', 'node', 'default', 'import', 'module'], entries = [], wildcards = [],
|
|
109
|
+
tryPath = rel => {
|
|
110
|
+
const p = path.join(pkgDir, rel);
|
|
111
|
+
if (fs.existsSync(p)) return p;
|
|
112
|
+
const exts = ENTRY_EXTS;
|
|
113
|
+
for (const ext of exts) if (fs.existsSync(p + ext)) return p + ext;
|
|
114
|
+
return null;
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (j.exports && typeof j.exports === 'object') {
|
|
118
|
+
const keys = Object.keys(j.exports), allConditional = keys.every(k => !k.startsWith('.'));
|
|
119
|
+
if (allConditional) {
|
|
120
|
+
const picked = pickCond(j.exports, prefer);
|
|
121
|
+
if (!picked) return { ok: false };
|
|
122
|
+
const abs = tryPath(picked);
|
|
123
|
+
if (!abs) return { ok: false };
|
|
124
|
+
entries.push({ key: '.', entryFile: abs });
|
|
125
|
+
return { ok: true, entries, wildcards };
|
|
126
|
+
}
|
|
127
|
+
for (const key of keys) {
|
|
128
|
+
if (key === PKG_SUBPATH) continue;
|
|
129
|
+
if (key.includes('*')) { wildcards.push(key); continue; }
|
|
130
|
+
const val = j.exports[key], picked = pickCond(Array.isArray(val) ? val[0] : val, prefer);
|
|
131
|
+
if (!picked) continue;
|
|
132
|
+
if (!JS_RE.test(picked)) continue;
|
|
133
|
+
const abs = path.join(pkgDir, picked);
|
|
134
|
+
if (!fs.existsSync(abs)) continue;
|
|
135
|
+
entries.push({ key, entryFile: abs });
|
|
136
|
+
}
|
|
137
|
+
if (entries.length === 0) return { ok: false };
|
|
138
|
+
return { ok: true, entries, wildcards };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 无 exports,用 main/module
|
|
142
|
+
let entryFile = null;
|
|
143
|
+
if (j.module) entryFile = tryPath(j.module);
|
|
144
|
+
if (!entryFile && j.main) entryFile = tryPath(j.main);
|
|
145
|
+
if (!entryFile) {
|
|
146
|
+
for (const c of INDEX_FILES) {
|
|
147
|
+
const p = path.join(pkgDir, c);
|
|
148
|
+
if (fs.existsSync(p)) { entryFile = p; break; }
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (!entryFile || !fs.existsSync(entryFile)) return { ok: false };
|
|
152
|
+
entries.push({ key: '.', entryFile });
|
|
153
|
+
return { ok: true, entries, wildcards };
|
|
154
|
+
},
|
|
155
|
+
mergeAll = async (nmDir, excludePkgs, runtimeMajor) => {
|
|
156
|
+
let esbuild;
|
|
157
|
+
try { esbuild = require('esbuild'); }
|
|
158
|
+
catch { throw new Error('esbuild 未找到,请确认 @flun/node-mobile-app 依赖已安装'); }
|
|
159
|
+
|
|
160
|
+
const packages = findPackages(nmDir);
|
|
161
|
+
let merged = 0, skipped = 0, totalDeleted = 0, resMap = null, resLog = null;
|
|
162
|
+
// 调试模式:预扫各包的 postinstall 资源目录,用于统计被阶段1 删除的资源 JS
|
|
163
|
+
if (process.env.OPTIMIZE_DEBUG) {
|
|
164
|
+
resMap = new Map(), resLog = [];
|
|
165
|
+
for (const pkgDir of packages) {
|
|
166
|
+
const pj = path.join(pkgDir, PKG_JSON);
|
|
167
|
+
if (!fs.existsSync(pj)) continue;
|
|
168
|
+
let j;
|
|
169
|
+
try { j = JSON.parse(fs.readFileSync(pj, 'utf8')); } catch { continue; }
|
|
170
|
+
if (!j.scripts) continue;
|
|
171
|
+
const items = new Set();
|
|
172
|
+
for (const k of ['postinstall', 'preinstall', 'install', 'prepare']) {
|
|
173
|
+
const cmd = j.scripts[k];
|
|
174
|
+
if (!cmd) continue;
|
|
175
|
+
const sr = extractScript(cmd);
|
|
176
|
+
if (!sr) continue;
|
|
177
|
+
const sa = path.resolve(pkgDir, sr);
|
|
178
|
+
if (!fs.existsSync(sa)) continue;
|
|
179
|
+
for (const item of extractResources(pkgDir, fs.readFileSync(sa, 'utf8'))) items.add(item);
|
|
180
|
+
}
|
|
181
|
+
if (items.size > 0) resMap.set(pkgDir, items);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
for (const pkgDir of packages) {
|
|
186
|
+
const pj = path.join(pkgDir, PKG_JSON);
|
|
187
|
+
if (!fs.existsSync(pj)) { skipped++; continue; }
|
|
188
|
+
let j;
|
|
189
|
+
try { j = JSON.parse(fs.readFileSync(pj, 'utf8')); } catch { skipped++; continue; }
|
|
190
|
+
const relName = toPosix(path.relative(nmDir, pkgDir));
|
|
191
|
+
if (excludePkgs.has(relName)) { skipped++; continue; }
|
|
192
|
+
if (SKIP_PKGS.has(relName)) { skipped++; continue; }
|
|
193
|
+
if (hasNativeModule(pkgDir)) { skipped++; continue; }
|
|
194
|
+
// 删除安装期脚本字段:运行时无安装期,且 Gradle npm rebuild 会触发脚本导致失败
|
|
195
|
+
// 原生模块包已在上一步跳过(其 scripts 是编译必需,必须保留)
|
|
196
|
+
if (j.scripts) { delete j.scripts; fs.writeFileSync(pj, JSON.stringify(j, null, 2)); }
|
|
197
|
+
if (countJS(pkgDir) <= 1) { skipped++; continue; }
|
|
198
|
+
// 已优化过的包跳过
|
|
199
|
+
const mainStr = String(j.main || ''), exportsStr = JSON.stringify(j.exports || '');
|
|
200
|
+
if (mainStr.includes(BUNDLED_DIR) || exportsStr.includes(BUNDLED_DIR)) { skipped++; continue; }
|
|
201
|
+
|
|
202
|
+
const parse = parseEntriesAll(pkgDir, j);
|
|
203
|
+
if (!parse.ok) { skipped++; continue; }
|
|
204
|
+
|
|
205
|
+
const isESM = (j.type === 'module'), bundledDir = path.join(pkgDir, BUNDLED_DIR), newExports = {},
|
|
206
|
+
banner = "import { createRequire as __ebCR } from 'module'; var require = __ebCR(import.meta.url);",
|
|
207
|
+
artifacts = new Set();
|
|
208
|
+
|
|
209
|
+
fs.mkdirSync(bundledDir, { recursive: true });
|
|
210
|
+
let entryOk = true;
|
|
211
|
+
for (const e of parse.entries) {
|
|
212
|
+
const baseName = keyToFilename(e.key), outName = baseName + (isESM ? '.mjs' : '.cjs'),
|
|
213
|
+
outFile = path.join(bundledDir, outName);
|
|
214
|
+
try {
|
|
215
|
+
await esbuild.build({
|
|
216
|
+
entryPoints: [e.entryFile],
|
|
217
|
+
bundle: true,
|
|
218
|
+
platform: 'node',
|
|
219
|
+
format: isESM ? 'esm' : 'cjs',
|
|
220
|
+
target: 'node' + runtimeMajor,
|
|
221
|
+
outfile: outFile,
|
|
222
|
+
packages: 'external',
|
|
223
|
+
banner: isESM ? { js: banner } : undefined,
|
|
224
|
+
logLevel: 'silent'
|
|
225
|
+
});
|
|
226
|
+
artifacts.add(outFile);
|
|
227
|
+
} catch {
|
|
228
|
+
try { fs.unlinkSync(outFile) } catch { }
|
|
229
|
+
entryOk = false;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
if (isESM) newExports[e.key] = subpath(BUNDLED_DIR, outName);
|
|
233
|
+
else {
|
|
234
|
+
// CJS 包:若 bundle 末尾是 module.exports = require_xxx()(整体赋值),
|
|
235
|
+
// cjs-module-lexer 静态分析拿不到命名导出,此时定位主入口的 __commonJS 块,
|
|
236
|
+
// 块内抓 exports.X = 形式,追加显式声明让 lexer 识别;
|
|
237
|
+
try {
|
|
238
|
+
const code = fs.readFileSync(outFile, 'utf8'),
|
|
239
|
+
m = code.match(/module\.exports\s*=\s*require_(\w+)\s*\(\s*\)\s*;?\s*$/m);
|
|
240
|
+
if (m) {
|
|
241
|
+
const fnName = m[1], startPat = 'var require_' + fnName + ' = __commonJS({',
|
|
242
|
+
start = code.indexOf(startPat);
|
|
243
|
+
if (start >= 0) {
|
|
244
|
+
let depth = 0, i = start + startPat.length - 1, end = -1;
|
|
245
|
+
while (i < code.length) {
|
|
246
|
+
const ch = code[i];
|
|
247
|
+
if (ch === '{') depth++;
|
|
248
|
+
else if (ch === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
249
|
+
i++;
|
|
250
|
+
}
|
|
251
|
+
if (end >= 0) {
|
|
252
|
+
const block = code.substring(start, end + 1), keys = new Set(),
|
|
253
|
+
re = /\bexports\d*\.([\p{ID_Start}$_][\p{ID_Continue}$]*)\s*=/gu;
|
|
254
|
+
let mm;
|
|
255
|
+
while ((mm = re.exec(block)) !== null) keys.add(mm[1]);
|
|
256
|
+
if (keys.size > 0) {
|
|
257
|
+
const decls = [...keys].map(k => `try { module.exports[${JSON.stringify(k)}] =
|
|
258
|
+
module.exports[${JSON.stringify(k)}]; } catch {}`);
|
|
259
|
+
fs.appendFileSync(outFile, '\n' + decls.join('\n') + '\n');
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} catch { }
|
|
265
|
+
newExports[e.key] = {
|
|
266
|
+
import: subpath(BUNDLED_DIR, outName), require: subpath(BUNDLED_DIR, outName),
|
|
267
|
+
default: subpath(BUNDLED_DIR, outName)
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!entryOk) {
|
|
273
|
+
fs.rmSync(bundledDir, { recursive: true, force: true }), skipped++;
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 保留通配符键和 package.json
|
|
278
|
+
for (const w of parse.wildcards) if (j.exports && j.exports[w]) newExports[w] = j.exports[w];
|
|
279
|
+
if (j.exports && j.exports[PKG_SUBPATH]) newExports[PKG_SUBPATH] = j.exports[PKG_SUBPATH];
|
|
280
|
+
|
|
281
|
+
fs.writeFileSync(pj + '.orig', JSON.stringify(j));
|
|
282
|
+
|
|
283
|
+
// 通配符覆盖路径保护
|
|
284
|
+
const wildcardPrefixes = parse.wildcards.map(w => w.replace(DOT_SLASH_RE, '').replace(/\/\*.*$/, '/'))
|
|
285
|
+
.filter(Boolean);
|
|
286
|
+
|
|
287
|
+
j.exports = newExports;
|
|
288
|
+
if (newExports['.'])
|
|
289
|
+
j.main = isESM ? newExports['.'] : (newExports['.'].require || newExports['.'].default || newExports['.']);
|
|
290
|
+
else delete j.main;
|
|
291
|
+
if (isESM && j.main) j.module = j.main;
|
|
292
|
+
fs.writeFileSync(pj, JSON.stringify(j, null, 2));
|
|
293
|
+
|
|
294
|
+
// 删除原 JS
|
|
295
|
+
let deleted = 0;
|
|
296
|
+
const resItems = resMap ? (resMap.get(pkgDir) || null) : null, resJsHits = resItems ? new Map() : null,
|
|
297
|
+
walkDel = d => {
|
|
298
|
+
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
299
|
+
if (f.isDirectory()) {
|
|
300
|
+
if (f.name === NM_DIR || f.name === BUNDLED_DIR) continue;
|
|
301
|
+
walkDel(path.join(d, f.name));
|
|
302
|
+
} else if (JS_RE.test(f.name)) {
|
|
303
|
+
const full = path.join(d, f.name), rel = toPosix(path.relative(pkgDir, full));
|
|
304
|
+
if (wildcardPrefixes.some(p => rel.startsWith(p))) continue;
|
|
305
|
+
if (resItems) {
|
|
306
|
+
const top = rel.split('/')[0];
|
|
307
|
+
if (resItems.has(top)) resJsHits.set(top, (resJsHits.get(top) || 0) + 1);
|
|
308
|
+
}
|
|
309
|
+
fs.unlinkSync(full); deleted++;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
walkDel(pkgDir), totalDeleted += deleted, merged++;
|
|
314
|
+
if (resJsHits && resJsHits.size > 0 && resLog)
|
|
315
|
+
for (const [k, v] of resJsHits) resLog.push(` ${relName}: ${k} (${v} 个 JS)`);
|
|
316
|
+
if (process.env.OPTIMIZE_DEBUG) console.log(chalk.gray(` ✓ ${relName} (${parse.entries.length} 入口)`));
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (resLog && resLog.length > 0) {
|
|
320
|
+
console.log(chalk.gray(' 处理保留的资源 JS(被引用则合并,否则删除):'));
|
|
321
|
+
for (const line of resLog) console.log(chalk.gray(line));
|
|
322
|
+
}
|
|
323
|
+
return { merged, skipped, totalDeleted };
|
|
324
|
+
},
|
|
325
|
+
// ==================== 清空资源 ====================
|
|
326
|
+
collectReferencedFiles = nmRoot => {
|
|
327
|
+
const refs = new Set(),
|
|
328
|
+
walk = d => {
|
|
329
|
+
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
330
|
+
const full = path.join(d, f.name);
|
|
331
|
+
if (f.isDirectory()) {
|
|
332
|
+
if (f.name === NM_DIR && d !== nmRoot) continue;
|
|
333
|
+
walk(full);
|
|
334
|
+
} else if (JS_RE.test(f.name)) {
|
|
335
|
+
let code;
|
|
336
|
+
try { code = fs.readFileSync(full, 'utf8'); } catch { continue; }
|
|
337
|
+
const re = /(?:from|require|import)\s*\(?\s*['"](\.[^'"]+)['"]/g;
|
|
338
|
+
let m;
|
|
339
|
+
while ((m = re.exec(code)) !== null) {
|
|
340
|
+
const abs = path.resolve(path.dirname(full), m[1]);
|
|
341
|
+
refs.add(abs);
|
|
342
|
+
if (!path.extname(abs))
|
|
343
|
+
for (const ext of ENTRY_EXTS) if (fs.existsSync(abs + ext)) refs.add(abs + ext);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
walk(nmRoot);
|
|
349
|
+
return refs;
|
|
350
|
+
},
|
|
351
|
+
extractScript = cmd => {
|
|
352
|
+
const m = cmd.match(SCRIPT_FILE_RE);
|
|
353
|
+
return m ? m[1] : null;
|
|
354
|
+
},
|
|
355
|
+
extractResources = (pkgDir, scriptCode) => {
|
|
356
|
+
const strings = new Set(), re = /['"`]([^\s'"`]{2,})['"`]/g;
|
|
357
|
+
let m;
|
|
358
|
+
while ((m = re.exec(scriptCode)) !== null) strings.add(m[1]);
|
|
359
|
+
const items = [];
|
|
360
|
+
for (const s of strings) {
|
|
361
|
+
const first = s.split(/[\\/]/)[0];
|
|
362
|
+
if (EXCLUDE_NAMES.has(first) || !first || first === '.' || first === '..') continue;
|
|
363
|
+
if (fs.existsSync(path.join(pkgDir, first))) items.push(first);
|
|
364
|
+
}
|
|
365
|
+
return [...new Set(items)];
|
|
366
|
+
},
|
|
367
|
+
clearItem = (p, refs) => {
|
|
368
|
+
let n = 0;
|
|
369
|
+
const st = fs.statSync(p);
|
|
370
|
+
if (st.isDirectory()) {
|
|
371
|
+
for (const f of fs.readdirSync(p)) {
|
|
372
|
+
const full = path.join(p, f), sub = fs.statSync(full);
|
|
373
|
+
if (sub.isDirectory()) n += clearItem(full, refs);
|
|
374
|
+
else if (!JS_RE.test(f) && !refs.has(full)) try { fs.rmSync(full, { force: true }), n++; } catch { }
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
else if (!JS_RE.test(path.basename(p)) && !refs.has(p)) fs.writeFileSync(p, ''), n++;
|
|
378
|
+
return n;
|
|
379
|
+
},
|
|
380
|
+
clearResources = async (nmDir, excludePkgs) => {
|
|
381
|
+
const refs = collectReferencedFiles(nmDir);
|
|
382
|
+
let cleared = 0;
|
|
383
|
+
for (const pj of findPkgJsons(nmDir)) {
|
|
384
|
+
let j;
|
|
385
|
+
try { j = JSON.parse(fs.readFileSync(pj, 'utf8')); } catch { continue; }
|
|
386
|
+
if (!j.scripts) continue;
|
|
387
|
+
const pkgDir = path.dirname(pj), pkgRel = toPosix(path.relative(nmDir, pkgDir));
|
|
388
|
+
if (SKIP_PKGS.has(pkgRel) || excludePkgs.has(pkgRel)) continue;
|
|
389
|
+
const pkgItems = [];
|
|
390
|
+
for (const k of ['postinstall', 'preinstall', 'install', 'prepare']) {
|
|
391
|
+
const cmd = j.scripts[k];
|
|
392
|
+
if (!cmd) continue;
|
|
393
|
+
const scriptRel = extractScript(cmd);
|
|
394
|
+
if (!scriptRel) continue;
|
|
395
|
+
const scriptAbs = path.resolve(pkgDir, scriptRel);
|
|
396
|
+
if (!fs.existsSync(scriptAbs)) continue;
|
|
397
|
+
const code = fs.readFileSync(scriptAbs, 'utf8'), items = extractResources(pkgDir, code);
|
|
398
|
+
for (const item of items) {
|
|
399
|
+
const cnt = clearItem(path.join(pkgDir, item), refs);
|
|
400
|
+
if (cnt > 0) cleared += cnt, pkgItems.push(`${item}(${cnt})`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
if (process.env.OPTIMIZE_DEBUG && pkgItems.length > 0)
|
|
404
|
+
console.log(chalk.gray(` ✓ ${pkgRel}: ${pkgItems.join(', ')}`));
|
|
405
|
+
}
|
|
406
|
+
return cleared;
|
|
407
|
+
},
|
|
408
|
+
// ==================== 清平台 ====================
|
|
409
|
+
prunePlatform = async (nmDir, myPlat, myArch, targetPlatform) => {
|
|
410
|
+
let removed = 0;
|
|
411
|
+
const readHead = file => {
|
|
412
|
+
let fd;
|
|
413
|
+
try {
|
|
414
|
+
fd = fs.openSync(file, 'r');
|
|
415
|
+
const buf = Buffer.alloc(512), n = fs.readSync(fd, buf, 0, 512, 0);
|
|
416
|
+
return n > 0 ? buf.subarray(0, n) : null;
|
|
417
|
+
} catch {
|
|
418
|
+
return null;
|
|
419
|
+
} finally {
|
|
420
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
421
|
+
}
|
|
422
|
+
},
|
|
423
|
+
sniffPlat = buf => {
|
|
424
|
+
if (!buf || buf.length < 4) return null;
|
|
425
|
+
const hex = buf.toString('hex', 0, 4).toUpperCase();
|
|
426
|
+
if (hex.startsWith('4D5A')) return 'win32';
|
|
427
|
+
if (hex === '7F454C46') return 'linux';
|
|
428
|
+
if (['CFFAEDFE', 'CEFAEDFE', 'FEEDFACE', 'FEEDFACF'].includes(hex)) return 'darwin';
|
|
429
|
+
return null;
|
|
430
|
+
},
|
|
431
|
+
sniffArch = buf => {
|
|
432
|
+
if (!buf || buf.length < 64) return null;
|
|
433
|
+
const hex = buf.toString('hex', 0, 4).toUpperCase();
|
|
434
|
+
try {
|
|
435
|
+
if (hex.startsWith('4D5A')) {
|
|
436
|
+
const off = buf.readUInt32LE(0x3C);
|
|
437
|
+
if (off + 6 > buf.length) return null;
|
|
438
|
+
const m = buf.readUInt16LE(off + 4);
|
|
439
|
+
return m === 0x8664 ? 'x64' : m === 0xAA64 ? 'arm64' : m === 0x14C ?
|
|
440
|
+
'ia32' : (m === 0x1C0 || m === 0x1C4) ? 'arm' : null;
|
|
441
|
+
}
|
|
442
|
+
if (hex === '7F454C46') {
|
|
443
|
+
const m = buf.readUInt16LE(0x12);
|
|
444
|
+
return m === 0x3E ? 'x64' : m === 0xB7 ? 'arm64' : m === 0x28 ? 'arm' : m === 0x03 ?
|
|
445
|
+
'ia32' : null;
|
|
446
|
+
}
|
|
447
|
+
if (hex === 'CFFAEDFE' || hex === 'CEFAEDFE') {
|
|
448
|
+
const c = buf.readUInt32LE(4);
|
|
449
|
+
return c === 0x01000007 ? 'x64' : c === 0x0100000C ? 'arm64' : c === 0x7 ? 'ia32' : c === 0xC ?
|
|
450
|
+
'arm' : null;
|
|
451
|
+
}
|
|
452
|
+
if (hex === 'FEEDFACF' || hex === 'FEEDFACE') {
|
|
453
|
+
const c = buf.readUInt32BE(4);
|
|
454
|
+
return c === 0x01000007 ? 'x64' : c === 0x0100000C ? 'arm64' : c === 0x7 ? 'ia32' : c === 0xC ?
|
|
455
|
+
'arm' : null;
|
|
456
|
+
}
|
|
457
|
+
} catch { }
|
|
458
|
+
return null;
|
|
459
|
+
},
|
|
460
|
+
suffixPlat = name => {
|
|
461
|
+
const ext = path.extname(name).toLowerCase();
|
|
462
|
+
if (ext === '.exe' || ext === '.dll') return 'win32';
|
|
463
|
+
if (ext === '.dylib') return 'darwin';
|
|
464
|
+
if (ext === '.so') return 'linux';
|
|
465
|
+
return null;
|
|
466
|
+
},
|
|
467
|
+
delFile = p => {
|
|
468
|
+
try {
|
|
469
|
+
fs.statSync(p).isDirectory() ? fs.rmSync(p, { recursive: true, force: true })
|
|
470
|
+
: fs.unlinkSync(p), removed++;
|
|
471
|
+
} catch { }
|
|
472
|
+
},
|
|
473
|
+
processDir = d => {
|
|
474
|
+
let entries;
|
|
475
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
476
|
+
for (const e of entries) {
|
|
477
|
+
const full = path.join(d, e.name),
|
|
478
|
+
expectedPrebuilds = targetPlatform === 'ios' ? ['darwin'] : ['android'],
|
|
479
|
+
prebuildMatch = full.match(/[\\/]prebuilds[\\/](linux|darwin|win32|android)-/i);
|
|
480
|
+
|
|
481
|
+
if (prebuildMatch && !expectedPrebuilds.includes(prebuildMatch[1].toLowerCase()))
|
|
482
|
+
{ delFile(full); continue; }
|
|
483
|
+
if (e.isDirectory()) { processDir(full); continue; }
|
|
484
|
+
if (JS_RE.test(e.name)) continue;
|
|
485
|
+
let plat = suffixPlat(e.name), arch = null;
|
|
486
|
+
const head = readHead(full);
|
|
487
|
+
if (head) {
|
|
488
|
+
if (!plat) plat = sniffPlat(head);
|
|
489
|
+
arch = sniffArch(head);
|
|
490
|
+
}
|
|
491
|
+
const platBad = plat && !myPlat.includes(plat), archBad = plat && arch && !myArch.includes(arch);
|
|
492
|
+
if (platBad || archBad) {
|
|
493
|
+
if (process.env.OPTIMIZE_DEBUG) {
|
|
494
|
+
const rel = toPosix(path.relative(nmDir, full));
|
|
495
|
+
console.log(chalk.gray(` ✓ ${rel} (${platBad ? '平台不符: ' + plat : '架构不符: ' + arch})`));
|
|
496
|
+
}
|
|
497
|
+
delFile(full);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
processDir(nmDir);
|
|
502
|
+
return removed;
|
|
503
|
+
},
|
|
504
|
+
// ==================== 清开发文件 ====================
|
|
505
|
+
pruneDevFiles = async nmDir => {
|
|
506
|
+
let deleted = 0;
|
|
507
|
+
const PATTERNS = [
|
|
508
|
+
/(^|\/)tsconfig\.json$/,
|
|
509
|
+
/(^|\/)\.nycrc(\.(json|yml))?$/,
|
|
510
|
+
/(^|\/)\.editorconfig$/, /(^|\/)\.prettierignore$/, /(^|\/)\.eslintignore$/,
|
|
511
|
+
/(^|\/)\.dockerignore$/, /(^|\/)\.gitignore$/, /(^|\/)\.gitattributes$/, /(^|\/)\.npmignore$/,
|
|
512
|
+
/\.(md|markdown)$/i, /\.(yml|yaml)$/i, /\.(gyp|gypi|h|hh|hpp|c|cc|cpp|cxx|m|mm)$/i,
|
|
513
|
+
/\.map$/, /metafile-.*\.json$/,
|
|
514
|
+
/(^|\/)\.eslintrc(\.(json|yml|js|cjs))?$/, /(^|\/)\.babelrc(\.(json|js))?$/,
|
|
515
|
+
/(^|\/)package-lock\.json$/, /(^|\/)\.package-lock\.json$/, /(^|\/)yarn\.lock$/,
|
|
516
|
+
/(^|\/)package-support\.json$/,
|
|
517
|
+
], DIR_SKIP = new Set([BUNDLED_DIR]),
|
|
518
|
+
walk = d => {
|
|
519
|
+
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
520
|
+
const full = path.join(d, f.name);
|
|
521
|
+
if (f.isDirectory()) {
|
|
522
|
+
if (DIR_SKIP.has(f.name)) continue;
|
|
523
|
+
if (f.name === '.github' || f.name === '.circleci') {
|
|
524
|
+
fs.rmSync(full, { recursive: true, force: true }), deleted++;
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
walk(full);
|
|
528
|
+
} else {
|
|
529
|
+
const rel = toPosix(path.relative(nmDir, full));
|
|
530
|
+
if (PATTERNS.some(re => re.test(rel))) try { fs.unlinkSync(full), deleted++; } catch { }
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
walk(nmDir);
|
|
535
|
+
return deleted;
|
|
536
|
+
},
|
|
537
|
+
pruneIntermediates = async nmDir => {
|
|
538
|
+
let deleted = 0;
|
|
539
|
+
const MID_RE =
|
|
540
|
+
/\.(o|d|mk|stamp|Makefile|iobj|ipdb|pdb|exp|lib|obj|vcxproj|tlog|recipe|lastbuildstate|nativecodeanalysis\.xml)$/i,
|
|
541
|
+
walk = d => {
|
|
542
|
+
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
543
|
+
const full = path.join(d, f.name);
|
|
544
|
+
if (f.isDirectory()) walk(full);
|
|
545
|
+
else if (full.includes(path.sep + 'obj.target' + path.sep) || f.name === 'Makefile' ||
|
|
546
|
+
f.name.endsWith('.orig') || MID_RE.test(f.name)) try { fs.unlinkSync(full), deleted++; } catch { }
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
walk(nmDir);
|
|
550
|
+
return deleted;
|
|
551
|
+
},
|
|
552
|
+
applyFilesRules = async nmDir => {
|
|
553
|
+
let deleted = 0;
|
|
554
|
+
// 1) 删顶层 node 假包(npm 上名为 node 的包,仅提供 node.exe,运行时不需要)
|
|
555
|
+
const LICENSE_RE = /^licen[sc]e/i, TS_RE = /\.(ts|cts|mts)$/, nodePkg = path.join(nmDir, 'node');
|
|
556
|
+
if (fs.existsSync(nodePkg)) try { fs.rmSync(nodePkg, { recursive: true, force: true }), deleted++; } catch { }
|
|
557
|
+
|
|
558
|
+
// 2) 递归删 node-<platform>* 假包
|
|
559
|
+
const walkForFakeNode = d => {
|
|
560
|
+
let entries;
|
|
561
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
562
|
+
for (const f of entries) {
|
|
563
|
+
if (!f.isDirectory()) continue;
|
|
564
|
+
const full = path.join(d, f.name);
|
|
565
|
+
if (/^node-(win|darwin|linux|freebsd|sunos|aix)/.test(f.name)) {
|
|
566
|
+
try { fs.rmSync(full, { recursive: true, force: true }), deleted++; } catch { }
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
walkForFakeNode(full);
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
walkForFakeNode(nmDir);
|
|
573
|
+
|
|
574
|
+
// 3) 删 license/licence 文件或目录 + .ts/.cts/.mts 文件
|
|
575
|
+
const walk = d => {
|
|
576
|
+
let entries;
|
|
577
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return }
|
|
578
|
+
for (const f of entries) {
|
|
579
|
+
const full = path.join(d, f.name);
|
|
580
|
+
if (f.isDirectory()) {
|
|
581
|
+
if (f.name === BUNDLED_DIR) continue;
|
|
582
|
+
if (LICENSE_RE.test(f.name)) {
|
|
583
|
+
try { fs.rmSync(full, { recursive: true, force: true }), deleted++; } catch { }
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
walk(full);
|
|
587
|
+
}
|
|
588
|
+
else if (LICENSE_RE.test(f.name) || TS_RE.test(f.name)) try { fs.unlinkSync(full), deleted++; } catch { }
|
|
589
|
+
}
|
|
590
|
+
};
|
|
591
|
+
walk(nmDir);
|
|
592
|
+
|
|
593
|
+
const cleanEmpty = d => {
|
|
594
|
+
for (const e of fs.readdirSync(d, { withFileTypes: true }))
|
|
595
|
+
if (e.isDirectory()) cleanEmpty(path.join(d, e.name));
|
|
596
|
+
if (d !== nmDir) try { if (fs.readdirSync(d).length === 0) fs.rmdirSync(d), deleted++; } catch { }
|
|
597
|
+
};
|
|
598
|
+
cleanEmpty(nmDir);
|
|
599
|
+
return deleted;
|
|
600
|
+
};
|
|
601
|
+
|
|
602
|
+
// ==================== 导出函数 ====================
|
|
603
|
+
/**
|
|
604
|
+
* 清除快照中的 verified 标记(优化开始或构建失败时调用)
|
|
605
|
+
* >查看定义:@see {@link clearVerified}
|
|
606
|
+
* @param {string} snapshotDir 快照目录
|
|
607
|
+
* @returns {Promise<void>}
|
|
608
|
+
*/
|
|
609
|
+
const clearVerified = async snapshotDir => {
|
|
610
|
+
const p = path.join(snapshotDir, SNAPSHOT_FILE);
|
|
611
|
+
if (!fs.existsSync(p)) return;
|
|
612
|
+
try {
|
|
613
|
+
const old = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
614
|
+
if (old.verified !== false) old.verified = false, fs.writeFileSync(p, JSON.stringify(old, null, 2));
|
|
615
|
+
} catch { }
|
|
616
|
+
};
|
|
617
|
+
/**
|
|
618
|
+
* 标记快照 verified=true(构建全流程成功后由 build.js / test.js 调用)
|
|
619
|
+
* >查看定义:@see {@link markVerified}
|
|
620
|
+
* @param {string} snapshotDir 快照目录
|
|
621
|
+
* @returns {Promise<void>}
|
|
622
|
+
*/
|
|
623
|
+
const markVerified = async snapshotDir => {
|
|
624
|
+
const p = path.join(snapshotDir, SNAPSHOT_FILE);
|
|
625
|
+
if (!fs.existsSync(p)) return;
|
|
626
|
+
try {
|
|
627
|
+
const old = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
628
|
+
old.verified = true, fs.writeFileSync(p, JSON.stringify(old, null, 2));
|
|
629
|
+
} catch { }
|
|
630
|
+
};
|
|
631
|
+
/**
|
|
632
|
+
* 执行 node_modules 六阶段优化
|
|
633
|
+
* >查看定义:@see {@link optimizeNodeModules}
|
|
634
|
+
* @param {string} tempDir node_modules 所在目录(nodejs-assets/nodejs-project)
|
|
635
|
+
* @param {object} [config] 优化配置
|
|
636
|
+
* @param {string} [config.snapshotDir] 快照目录(默认同 tempDir)
|
|
637
|
+
* @param {string} [config.targetPlatform] 目标平台:'android'(默认)或 'ios'
|
|
638
|
+
* @param {string[]} [config.abiFilters] Android 目标架构(默认 ['arm64-v8a']);iOS 固定保留 arm64 + x64
|
|
639
|
+
* @param {string} config.runtimeMajor 运行时 Node 主版本号(必填,从 libnode/node_version.h 读取)
|
|
640
|
+
* @param {string[]} [config.exclude] 排除的包名列表
|
|
641
|
+
* @returns {Promise<object>} 各阶段统计(merged / cleared / platform / devFiles / intermediates / filesRules)
|
|
642
|
+
*/
|
|
643
|
+
const optimizeNodeModules = async (tempDir, config = {}) => {
|
|
644
|
+
const targetPlatform = config.targetPlatform || 'android', abiFilters = config.abiFilters || ['arm64-v8a'],
|
|
645
|
+
runtimeMajor = config.runtimeMajor, excludePkgs = new Set(config.exclude || []),
|
|
646
|
+
nmDir = path.join(tempDir, NM_DIR), snapshotDir = config.snapshotDir || tempDir,
|
|
647
|
+
snapPath = path.join(snapshotDir, SNAPSHOT_FILE), myPlat = TARGET_PLAT_MAP[targetPlatform] || [],
|
|
648
|
+
myArch = targetPlatform === 'ios' ? IOS_ARCHES : (abiFilters || []).map(a => ABI_ARCH_MAP[a]).filter(Boolean);
|
|
649
|
+
|
|
650
|
+
if (!runtimeMajor) throw new Error('optimizeNodeModules 缺少 config.runtimeMajor(应从 libnode/node_version.h 读取)');
|
|
651
|
+
if (!fs.existsSync(nmDir)) {
|
|
652
|
+
console.log(chalk.gray('[优化] node_modules 不存在,跳过'));
|
|
653
|
+
return { skipped: true };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
await clearVerified(snapshotDir); // 执行优化前清 verified,构建成功后再由 build.js / test.js 写回 true
|
|
657
|
+
const t0 = Date.now();
|
|
658
|
+
try {
|
|
659
|
+
// 阶段1: 清空资源(跳过 js,留待合并阶段处理)
|
|
660
|
+
console.log(chalk.cyan(' [1/6] 清空复制到项目根的资源...'));
|
|
661
|
+
const c = await clearResources(nmDir, excludePkgs);
|
|
662
|
+
console.log(chalk.green(` 清空 ${c} 个文件(js 文件保留,留待合并阶段处理)`));
|
|
663
|
+
|
|
664
|
+
// 阶段2: 合并
|
|
665
|
+
console.log(chalk.cyan(' [2/6] 合并 JS...'));
|
|
666
|
+
const m = await mergeAll(nmDir, excludePkgs, runtimeMajor);
|
|
667
|
+
console.log(chalk.green(` 合并 ${m.merged} 包,跳过 ${m.skipped},删 ${m.totalDeleted} 个 JS`));
|
|
668
|
+
|
|
669
|
+
// 阶段3: 清平台二进制
|
|
670
|
+
console.log(chalk.cyan(' [3/6] 清理非当前平台二进制...'));
|
|
671
|
+
const p = await prunePlatform(nmDir, myPlat, myArch, targetPlatform);
|
|
672
|
+
console.log(chalk.green(` 清理 ${p} 项`));
|
|
673
|
+
|
|
674
|
+
// 阶段4: 清开发文件
|
|
675
|
+
console.log(chalk.cyan(' [4/6] 清理开发文件...'));
|
|
676
|
+
const d = await pruneDevFiles(nmDir);
|
|
677
|
+
console.log(chalk.green(` 清理 ${d} 个文件`));
|
|
678
|
+
|
|
679
|
+
// 阶段5: 清中间产物
|
|
680
|
+
console.log(chalk.cyan(' [5/6] 清理中间产物...'));
|
|
681
|
+
const i = await pruneIntermediates(nmDir);
|
|
682
|
+
console.log(chalk.green(` 清理 ${i} 个文件`));
|
|
683
|
+
|
|
684
|
+
// 阶段6: 清除不参与运行的文件(与 build.js 的 files 数组对齐)
|
|
685
|
+
console.log(chalk.cyan(' [6/6] 清除不参与运行的文件和空目录...'));
|
|
686
|
+
const fr = await applyFilesRules(nmDir);
|
|
687
|
+
console.log(chalk.green(` 清理 ${fr} 项`));
|
|
688
|
+
|
|
689
|
+
// 写 platform/abiFilters/runtimeMajor,同时 verified=false
|
|
690
|
+
await writeSnapshotFlags(snapshotDir, targetPlatform, abiFilters, runtimeMajor);
|
|
691
|
+
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
|
|
692
|
+
console.log(chalk.green(` [信息] 优化完成,耗时 ${elapsed}s`));
|
|
693
|
+
|
|
694
|
+
return { merged: m.merged, cleared: c, platform: p, devFiles: d, intermediates: i, filesRules: fr };
|
|
695
|
+
} catch (err) {
|
|
696
|
+
// 失败时删除依赖快照,下次强制重装 + 重优化
|
|
697
|
+
try { fs.rmSync(snapPath, { recursive: true, force: true }); } catch { }
|
|
698
|
+
throw err;
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
/**
|
|
702
|
+
* 当前优化文件的 SHA256 前 16 位(缓存指纹:优化代码一改,缓存自动失效)
|
|
703
|
+
* >查看定义:@see {@link SELF_HASH}
|
|
704
|
+
*/
|
|
705
|
+
const SELF_HASH = (() => {
|
|
706
|
+
try {
|
|
707
|
+
const src = fs.readFileSync(new URL(import.meta.url), 'utf8');
|
|
708
|
+
return require('crypto').createHash('sha256').update(src).digest('hex').slice(0, 16);
|
|
709
|
+
} catch { return 'unknown'; }
|
|
710
|
+
})();
|
|
711
|
+
|
|
712
712
|
export { clearVerified, markVerified, optimizeNodeModules, SELF_HASH };
|