@flun/node-mobile-app 3.0.0 → 3.1.1

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/CHANGELOG.md CHANGED
@@ -1,4 +1,17 @@
1
1
  # 变更日志
2
+ ## [3.1.1] - 2026-10-15 20:29
3
+ ### 更新
4
+ - 同步更新了一些依赖包;
5
+
6
+ ## [3.1.0] - 2026-10-15 20:15
7
+ ### 修复
8
+ - **优化阶段步骤顺序调整(重要)**:构建时原优化顺序是「先合并 JS、后清空资源」——合并会删除 postinstall 脚本文件,导致清空阶段读不到脚本、无法提取资源目录,报「清空 0 项」、资源目录残留;现改为**先清空资源、后合并**:
9
+ - 清空资源时**跳过 js 文件**,留待合并阶段处理;
10
+ - 日志如实反映:「清空 N 个文件(js 文件保留,留待合并阶段处理)」;
11
+ - **`clearItem` 单文件分支漏判 js(重要)**:目录分支已有「跳过 js」判断,单文件分支遗漏,导致资源目录中的**单个 js 文件**被清空为 0 字节,合并阶段读到空文件、产物损坏;现补充 js 判断,js 一律保留给合并阶段;
12
+ - **合并阶段日志标题**:由「处理 JS 资源(用到则合并,未用到则提前清除)」改为「处理保留的资源 JS(被引用则合并,否则删除)」,与新流程呼应;
13
+ - **Gradle 原生编译报 postinstall 失败(重要)**:Gradle 编译原生模块时执行 `npm rebuild`,会触发各包 postinstall;而合并阶段已删除脚本文件,导致 `Cannot find module ... copy-files.js`、构建失败;现合并阶段同步**删除 `package.json` 的 `scripts` 字段**(运行时无安装期,scripts 无用),rebuild 不再触发;
14
+ - **`--debug` 下 `quiet` 命令仍被静默**:`run` 的 quiet 分支未判断 debug,导致 `--debug` 时 `gradlew` / `xcodebuild` 等命令输出仍被吞;现改为「debug 时忽略 quiet,全量输出」(不影响默认模式下 test 的精简行为);
2
15
 
3
16
  ## [3.0.0] - 2026-10-13 21:19
4
17
  ### 新增
@@ -28,12 +41,4 @@
28
41
  - 若**构建或运行异常**,按以下顺序排查:
29
42
  1. 在 `mobileAppConfig.js` 中设置 `optimize: { enabled: false }` 关闭优化,验证是否为优化引起;
30
43
  2. 若确认是某个包合并后异常,用 `optimize: { exclude: ['包名'] }` 排除优化时异常或不想优化的包;
31
- 3. 若需彻底重来,删除 `node-mobile-app-build` 目录后重新构建。
32
-
33
- ## [2.1.2] - 2026-09-29 19:53
34
- ### 更改
35
- - 加固了拷贝文件的一些逻辑;
36
-
37
- ## [2.1.1] - 2026-09-29 13:55
38
- ### 优化
39
- - 优化了 'README.md' 文档;
44
+ 3. 若需彻底重来,删除 `node-mobile-app-build` 目录后重新构建。
package/README.md CHANGED
@@ -17,7 +17,7 @@
17
17
  - **增量构建**:文件指纹判断缓存,无变化时 10 秒级重跑
18
18
  - **跨平台**:Android(Windows / macOS / Linux)+ iOS(仅 macOS)
19
19
  - **离线可用**:`build` 打出的 APK / IPA 内置 JS,无需连电脑也能跑
20
- - **node_modules 优化(默认开启)**:构建时自动合并 JS、清平台二进制、清开发文件等,大幅减少打包文件数与体积、加速启动;无需配置
20
+ - **node_modules 优化(默认开启)**:构建时自动执行六阶段优化——先清空 postinstall 资源(保留 js 供合并)、再合并 JS、清平台二进制、清开发文件(原生模块编译必需文件保留)、清中间产物、清冗余与空目录;大幅减少打包文件数与体积、加速启动,无需配置
21
21
 
22
22
  ---
23
23
 
@@ -54,7 +54,7 @@
54
54
  │ ├── fingerprint.js # 缓存指纹(判断是否需要清理构建缓存)
55
55
  │ ├── generate.js # 生成 main.js(桥接)+ mobileApp.runtime.ts(运行时配置)
56
56
  │ ├── install.js # 依赖安装 + 插件 abiFilters patch + WebView SSL patch
57
- │ ├── optimize-node-modules.js # node_modules 优化(六阶段:合并 JS / 清资源 / 清平台二进制 / 清开发文件 / 清中间产物 / 清冗余)
57
+ │ ├── optimize-node-modules.js # node_modules 优化(六阶段:清资源 / 合并 JS / 清平台二进制 / 清开发文件 / 清中间产物 / 清冗余)
58
58
  │ ├── patcher.js # 应用信息 / Android / iOS 配置 patch
59
59
  │ ├── platform-ios.js # iOS 构建(pod install / xcodebuild archive / export)
60
60
  │ ├── utils.js # 通用工具(文件操作 / 进程调用 / adb / hermesc 定位)
@@ -350,8 +350,8 @@ signing: {
350
350
 
351
351
  **六阶段流程**:
352
352
 
353
- 1. **合并 JS**:按包入口 bundle,多个 JS 文件合并为单文件(`bundled/` 目录);
354
- 2. **清空资源**:扫描各包 `postinstall` 引用的目录/文件,保留目录结构、清空内容;被 import/require 的保留;
353
+ 1. **清空资源**:扫描各包 `postinstall` 引用的目录/文件,保留目录结构、清空内容;js 文件一律保留,留待合并阶段处理;
354
+ 2. **合并 JS**:按包入口 bundle,多个 JS 文件合并为单文件(`bundled/` 目录);合并后同步删除 `package.json` 的 `scripts` 字段(运行时无安装期,且 Gradle `npm rebuild` 会触发脚本导致失败);
355
355
  3. **清平台二进制**:按目标平台 + `abiFilters` 逐文件判定,删除非本机二进制;
356
356
  4. **清开发文件**:删除 `.md` / `.map` / `.d.ts` / `.h` / `.c` 等非运行文件;**原生模块包(含 `binding.gyp`)内的编译必需文件保留**;
357
357
  5. **清中间产物**;
@@ -363,7 +363,7 @@ signing: {
363
363
 
364
364
  - `optimize: { enabled: false }` 整体关闭优化;
365
365
  - `optimize: { exclude: ['包名'] }` 排除优化时异常或不想优化的包;
366
- - 加 `--debug` 查看优化各阶段明细。
366
+ - 加 `--debug` 查看优化各阶段明细与各命令完整输出。
367
367
 
368
368
  ## 服务地址说明
369
369
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flun/node-mobile-app",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "将 Node.js 项目一键打包为 Android / iOS 移动应用,运行时由 @flun/nodejs-mobile-react-native 提供,无需修改你的 Node 代码;",
5
5
  "author": "flun <open@flun.top>",
6
6
  "keywords": [
@@ -40,26 +40,26 @@
40
40
  "postinstall": "node scripts/copy-files.js"
41
41
  },
42
42
  "dependencies": {
43
- "@babel/core": "^8.0.6",
44
- "@eslint-react/eslint-plugin": "^5.21.3",
43
+ "@babel/core": "^8.0.7",
44
+ "@eslint-react/eslint-plugin": "^5.27.2",
45
45
  "@eslint/js": "^10.0.1",
46
46
  "@flun/nodejs-mobile-react-native": "^2.0.8",
47
- "@react-native/babel-preset": "^0.87.1",
47
+ "@react-native/babel-preset": "^0.88.0",
48
48
  "@react-native-community/cli": "^20.2.0",
49
49
  "@react-native-community/cli-platform-android": "^20.2.0",
50
50
  "@react-native-community/cli-platform-ios": "^20.2.0",
51
- "@react-native/metro-config": "^0.87.1",
51
+ "@react-native/metro-config": "^0.88.0",
52
52
  "@types/react": "^19.3.0",
53
- "@typescript-eslint/eslint-plugin": "^8.71.0",
54
- "@typescript-eslint/parser": "^8.71.0",
53
+ "@typescript-eslint/eslint-plugin": "^8.71.2",
54
+ "@typescript-eslint/parser": "^8.71.2",
55
55
  "esbuild": "^0.28.2",
56
- "eslint": "^10.11.0",
56
+ "eslint": "^10.12.0",
57
57
  "eslint-config-prettier": "^10.1.8",
58
58
  "eslint-plugin-eslint-comments": "^3.2.0",
59
59
  "eslint-plugin-react-hooks": "^7.1.1",
60
60
  "jimp": "^1.6.1",
61
- "prettier": "^3.9.8",
62
- "react": "^19.3.0",
61
+ "prettier": "^3.9.10",
62
+ "react": "^19.3.1",
63
63
  "react-native": "^0.87.1",
64
64
  "react-native-webview": "^14.0.1"
65
65
  },
package/src/install.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import path from 'path';
2
2
  import fs from 'fs';
3
3
  import { run } from './utils.js';
4
- import { optimizeNodeModules, clearVerified, SELF_HASH } from './optimize-node-modules.js';
4
+ import { clearVerified, optimizeNodeModules, SELF_HASH } from './optimize-node-modules.js';
5
5
  const
6
6
  /**
7
7
  * 内部:从 libnode 的 node_version.h 读取运行时 Node 主版本号
@@ -33,35 +33,33 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
33
33
  old.platform = targetPlatform, old.abiFilters = abiFilters, old.runtimeMajor = runtimeMajor;
34
34
  old.optimizeHash = SELF_HASH, old.verified = false, old.optimized = true;
35
35
  fs.writeFileSync(p, JSON.stringify(old, null, 2));
36
- };
37
-
38
-
39
- // ==================== 扫描工具 ====================
40
- const findPackages = dir => {
41
- const result = [];
42
- if (!fs.existsSync(dir)) return result;
43
- for (const entry of fs.readdirSync(dir)) {
44
- if (entry === '.bin' || entry.startsWith('.')) continue;
45
- const full = path.join(dir, entry);
46
- if (!fs.statSync(full).isDirectory()) continue;
47
- if (entry === NM_DIR) { result.push(...findPackages(full)); continue; }
48
- if (entry.startsWith('@')) {
49
- for (const sub of fs.readdirSync(full)) {
50
- const pkgDir = path.join(full, sub);
51
- if (fs.statSync(pkgDir).isDirectory()) {
52
- result.push(pkgDir);
53
- const nested = path.join(pkgDir, NM_DIR);
54
- if (fs.existsSync(nested)) result.push(...findPackages(nested));
36
+ },
37
+ // ==================== 扫描工具 ====================
38
+ findPackages = dir => {
39
+ const result = [];
40
+ if (!fs.existsSync(dir)) return result;
41
+ for (const entry of fs.readdirSync(dir)) {
42
+ if (entry === '.bin' || entry.startsWith('.')) continue;
43
+ const full = path.join(dir, entry);
44
+ if (!fs.statSync(full).isDirectory()) continue;
45
+ if (entry === NM_DIR) { result.push(...findPackages(full)); continue; }
46
+ if (entry.startsWith('@')) {
47
+ for (const sub of fs.readdirSync(full)) {
48
+ const pkgDir = path.join(full, sub);
49
+ if (fs.statSync(pkgDir).isDirectory()) {
50
+ result.push(pkgDir);
51
+ const nested = path.join(pkgDir, NM_DIR);
52
+ if (fs.existsSync(nested)) result.push(...findPackages(nested));
53
+ }
55
54
  }
55
+ } else {
56
+ result.push(full);
57
+ const nested = path.join(full, NM_DIR);
58
+ if (fs.existsSync(nested)) result.push(...findPackages(nested));
56
59
  }
57
- } else {
58
- result.push(full);
59
- const nested = path.join(full, NM_DIR);
60
- if (fs.existsSync(nested)) result.push(...findPackages(nested));
61
60
  }
62
- }
63
- return result;
64
- },
61
+ return result;
62
+ },
65
63
  hasNativeModule = dir => {
66
64
  let found = false;
67
65
  const walk = d => {
@@ -287,6 +285,8 @@ const findPackages = dir => {
287
285
  j.main = isESM ? newExports['.'] : (newExports['.'].require || newExports['.'].default || newExports['.']);
288
286
  else delete j.main;
289
287
  if (isESM && j.main) j.module = j.main;
288
+ // 删除安装期脚本字段:移动端 Gradle 会 npm rebuild,触发 postinstall;运行时无安装期,scripts 无用
289
+ delete j.scripts;
290
290
  fs.writeFileSync(pj, JSON.stringify(j, null, 2));
291
291
 
292
292
  // 删除原 JS
@@ -315,7 +315,7 @@ const findPackages = dir => {
315
315
  }
316
316
 
317
317
  if (resLog && resLog.length > 0) {
318
- console.log(' 处理 JS 资源(用到则合并,未用到则提前清除):');
318
+ console.log(' 处理保留的资源 JS(被引用则合并,否则删除):');
319
319
  for (const line of resLog) console.log(line);
320
320
  }
321
321
  return { merged, skipped, totalDeleted };
@@ -363,20 +363,17 @@ const findPackages = dir => {
363
363
  return [...new Set(items)];
364
364
  },
365
365
  clearItem = (p, refs) => {
366
+ let n = 0;
366
367
  const st = fs.statSync(p);
367
368
  if (st.isDirectory()) {
368
369
  for (const f of fs.readdirSync(p)) {
369
370
  const full = path.join(p, f), sub = fs.statSync(full);
370
- if (sub.isDirectory()) clearItem(full, refs);
371
- else {
372
- if (refs.has(full)) continue;
373
- fs.rmSync(full, { force: true });
374
- }
371
+ if (sub.isDirectory()) n += clearItem(full, refs);
372
+ else if (!JS_RE.test(f) && !refs.has(full)) try { fs.rmSync(full, { force: true }), n++; } catch { }
375
373
  }
376
- } else {
377
- if (refs.has(p)) return;
378
- fs.writeFileSync(p, '');
379
374
  }
375
+ else if (!JS_RE.test(path.basename(p)) && !refs.has(p)) fs.writeFileSync(p, ''), n++;
376
+ return n;
380
377
  },
381
378
  clearResources = async (nmDir, excludePkgs) => {
382
379
  const refs = collectReferencedFiles(nmDir);
@@ -396,8 +393,10 @@ const findPackages = dir => {
396
393
  const scriptAbs = path.resolve(pkgDir, scriptRel);
397
394
  if (!fs.existsSync(scriptAbs)) continue;
398
395
  const code = fs.readFileSync(scriptAbs, 'utf8'), items = extractResources(pkgDir, code);
399
- for (const item of items)
400
- try { clearItem(path.join(pkgDir, item), refs), cleared++, pkgItems.push(item); } catch { }
396
+ for (const item of items) {
397
+ const cnt = clearItem(path.join(pkgDir, item), refs);
398
+ if (cnt > 0) cleared += cnt, pkgItems.push(`${item}(${cnt})`);
399
+ }
401
400
  }
402
401
  if (process.env.OPTIMIZE_DEBUG && pkgItems.length > 0) console.log(` ✓ ${pkgRel}: ${pkgItems.join(', ')}`);
403
402
  }
@@ -586,11 +585,25 @@ const findPackages = dir => {
586
585
 
587
586
  // ==================== 导出函数 ====================
588
587
  /**
589
- * 标记快照 verified=true(构建全流程成功后由 build.js / test.js 调用)
588
+ * 清除快照中的 verified 标记(优化开始或构建失败时调用)
590
589
  * >查看定义:@see {@link clearVerified}
591
590
  * @param {string} snapshotDir 快照目录
592
591
  * @returns {Promise<void>}
593
592
  */
593
+ const clearVerified = async snapshotDir => {
594
+ const p = path.join(snapshotDir, SNAPSHOT_FILE);
595
+ if (!fs.existsSync(p)) return;
596
+ try {
597
+ const old = JSON.parse(fs.readFileSync(p, 'utf-8'));
598
+ if (old.verified !== false) old.verified = false, fs.writeFileSync(p, JSON.stringify(old, null, 2));
599
+ } catch { }
600
+ };
601
+ /**
602
+ * 标记快照 verified=true(构建全流程成功后由 build.js / test.js 调用)
603
+ * >查看定义:@see {@link markVerified}
604
+ * @param {string} snapshotDir 快照目录
605
+ * @returns {Promise<void>}
606
+ */
594
607
  const markVerified = async snapshotDir => {
595
608
  const p = path.join(snapshotDir, SNAPSHOT_FILE);
596
609
  if (!fs.existsSync(p)) return;
@@ -601,7 +614,7 @@ const markVerified = async snapshotDir => {
601
614
  };
602
615
  /**
603
616
  * 执行 node_modules 六阶段优化
604
- * >查看定义:@see {@link SELF_HASH}
617
+ * >查看定义:@see {@link optimizeNodeModules}
605
618
  * @param {string} tempDir node_modules 所在目录(nodejs-assets/nodejs-project)
606
619
  * @param {object} [config] 优化配置
607
620
  * @param {string} [config.snapshotDir] 快照目录(默认同 tempDir)
@@ -615,29 +628,28 @@ const optimizeNodeModules = async (tempDir, config = {}) => {
615
628
  const targetPlatform = config.targetPlatform || 'android', abiFilters = config.abiFilters || ['arm64-v8a'],
616
629
  runtimeMajor = config.runtimeMajor, excludePkgs = new Set(config.exclude || []),
617
630
  nmDir = path.join(tempDir, NM_DIR), snapshotDir = config.snapshotDir || tempDir,
618
- snapPath = path.join(snapshotDir, SNAPSHOT_FILE),
619
- myPlat = TARGET_PLAT_MAP[targetPlatform] || [],
631
+ snapPath = path.join(snapshotDir, SNAPSHOT_FILE), myPlat = TARGET_PLAT_MAP[targetPlatform] || [],
620
632
  myArch = targetPlatform === 'ios' ? IOS_ARCHES : (abiFilters || []).map(a => ABI_ARCH_MAP[a]).filter(Boolean);
621
- if (!runtimeMajor) throw new Error('optimizeNodeModules 缺少 config.runtimeMajor(应从 libnode/node_version.h 读取)');
622
633
 
634
+ if (!runtimeMajor) throw new Error('optimizeNodeModules 缺少 config.runtimeMajor(应从 libnode/node_version.h 读取)');
623
635
  if (!fs.existsSync(nmDir)) {
624
636
  console.log('[优化] node_modules 不存在,跳过');
625
637
  return { skipped: true };
626
638
  }
639
+
627
640
  await clearVerified(snapshotDir); // 执行优化前清 verified,构建成功后再由 build.js / test.js 写回 true
628
- const t0 = Date.now(),
629
- nativePkgs = findPackages(nmDir).filter(p => fs.existsSync(path.join(p, 'binding.gyp')));
641
+ const t0 = Date.now(), nativePkgs = findPackages(nmDir).filter(p => fs.existsSync(path.join(p, 'binding.gyp')));
630
642
  try {
631
- // 阶段1: 合并
632
- console.log(' [1/6] 合并 JS...');
643
+ // 阶段1: 清空资源(跳过 js,留待合并阶段处理)
644
+ console.log(' [1/6] 清空复制到项目根的资源...');
645
+ const c = await clearResources(nmDir, excludePkgs);
646
+ console.log(` 清空 ${c} 个文件(js 文件保留,留待合并阶段处理)`);
647
+
648
+ // 阶段2: 合并
649
+ console.log(' [2/6] 合并 JS...');
633
650
  const m = await mergeAll(nmDir, excludePkgs, runtimeMajor);
634
651
  console.log(` 合并 ${m.merged} 包,跳过 ${m.skipped},删 ${m.totalDeleted} 个 JS`);
635
652
 
636
- // 阶段2: 清空资源
637
- console.log(' [2/6] 清空复制到项目根的资源...');
638
- const c = await clearResources(nmDir, excludePkgs);
639
- console.log(` 清空 ${c} 项`);
640
-
641
653
  // 阶段3: 清平台二进制
642
654
  console.log(' [3/6] 清理非当前平台二进制...');
643
655
  const p = await prunePlatform(nmDir, myPlat, myArch);
@@ -670,23 +682,9 @@ const optimizeNodeModules = async (tempDir, config = {}) => {
670
682
  throw err;
671
683
  }
672
684
  };
673
- /**
674
- * 清除快照中的 verified 标记(优化开始或构建失败时调用)
675
- * >查看定义:@see {@link markVerified}
676
- * @param {string} snapshotDir 快照目录
677
- * @returns {Promise<void>}
678
- */
679
- const clearVerified = async snapshotDir => {
680
- const p = path.join(snapshotDir, SNAPSHOT_FILE);
681
- if (!fs.existsSync(p)) return;
682
- try {
683
- const old = JSON.parse(fs.readFileSync(p, 'utf-8'));
684
- if (old.verified !== false) old.verified = false, fs.writeFileSync(p, JSON.stringify(old, null, 2));
685
- } catch { }
686
- };
687
685
  /**
688
686
  * 当前优化文件的 SHA256 前 16 位(缓存指纹:优化代码一改,缓存自动失效)
689
- * >查看定义:@see {@link optimizeNodeModules}
687
+ * >查看定义:@see {@link SELF_HASH}
690
688
  */
691
689
  const SELF_HASH = (() => {
692
690
  try {
@@ -695,4 +693,4 @@ const SELF_HASH = (() => {
695
693
  } catch { return 'unknown'; }
696
694
  })();
697
695
 
698
- export { markVerified, optimizeNodeModules, clearVerified, SELF_HASH };
696
+ export { clearVerified, markVerified, optimizeNodeModules, SELF_HASH };
package/src/utils.js CHANGED
@@ -85,7 +85,7 @@ const ensureDir = dir => {
85
85
  */
86
86
  run = (bin, args, opts = {}) => {
87
87
  const { quiet, ...restOpts } = opts;
88
- if (!quiet) {
88
+ if (!quiet || process.env.OPTIMIZE_DEBUG) {
89
89
  const r = spawnCmd(bin, args, { stdio: 'inherit', ...restOpts });
90
90
  if (r.status !== 0) throw new Error(`${bin} ${args.join(' ')} 退出码 ${r.status}`);
91
91
  return r;