@flun/node-mobile-app 2.1.2 → 3.1.0

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.
@@ -307,7 +307,7 @@ Remove-Item -Recurse -Force D:\你的项目\node-mobile-app-build
307
307
  Remove-Item -Recurse -Force D:\你的项目\node_modules\@flun\nodejs-mobile-react-native\android\.cxx
308
308
  Remove-Item -Recurse -Force D:\你的项目\node_modules\@flun\nodejs-mobile-react-native\android\build
309
309
 
310
- # 或删指纹文件(下次跑 = 首次构建)
310
+ # 或删指纹文件(下次跑 = 全新构建)
311
311
  Remove-Item -Force D:\你的项目\node-mobile-app-build\.build-fingerprint.json
312
312
  ```
313
313
 
package/CHANGELOG.md CHANGED
@@ -1,28 +1,45 @@
1
1
  # 变更日志
2
- ## [2.1.2] - 2026-09-29 19:53
3
- ### 更改
4
- - 加固了拷贝文件的一些逻辑;
5
-
6
- ## [2.1.1] - 2026-09-29 13:55
7
- ### 优化
8
- - 优化了 'README.md' 文档;
9
-
10
- ## [2.1.0] - 2026-09-29 10:39
11
- ### 变更
12
-
13
- - **ESLint 插件换代**:`@babel/eslint-parser`、`eslint-plugin-react`、`eslint-plugin-react-native` 三个包的最新版均不支持 ESLint 10(分别止步于 9.0.0 / 9.7 / 9),导致安装 `eslint@10` 时被额外拉入一份 `eslint@9.39.5`(npm 的 peer 兼容回退),并触发 npm 的 deprecated 警告。现替换为:
14
2
 
15
- | 原 | 新 |
16
- | ---------------------------------------------------- | --------------------------------------------------- |
17
- | `@babel/eslint-parser` | `@typescript-eslint/parser`(已支持 ESLint 10) |
18
- | `eslint-plugin-react` + `eslint-plugin-react-native` | `@eslint-react/eslint-plugin`(原生支持 ESLint 10) |
19
-
20
- 生成的 `eslint.config.js` 随之更新。`eslint-plugin-react-hooks` 保留(其 7.1.x 已支持 ESLint 10)。
21
-
22
- > **注意**:lint 规则集与原先不同,升级后对同一份代码可能报出不同的检查结果。这只影响静态检查,不影响构建产物与运行。
3
+ ## [3.1.0] - 2026-10-15 20:15
4
+ ### 修复
5
+ - **优化阶段步骤顺序调整(重要)**:构建时原优化顺序是「先合并 JS、后清空资源」——合并会删除 postinstall 脚本文件,导致清空阶段读不到脚本、无法提取资源目录,报「清空 0 项」、资源目录残留;现改为**先清空资源、后合并**:
6
+ - 清空资源时**跳过 js 文件**,留待合并阶段处理;
7
+ - 日志如实反映:「清空 N 个文件(js 文件保留,留待合并阶段处理)」;
8
+ - **`clearItem` 单文件分支漏判 js(重要)**:目录分支已有「跳过 js」判断,单文件分支遗漏,导致资源目录中的**单个 js 文件**被清空为 0 字节,合并阶段读到空文件、产物损坏;现补充 js 判断,js 一律保留给合并阶段;
9
+ - **合并阶段日志标题**:由「处理 JS 资源(用到则合并,未用到则提前清除)」改为「处理保留的资源 JS(被引用则合并,否则删除)」,与新流程呼应;
10
+ - **Gradle 原生编译报 postinstall 失败(重要)**:Gradle 编译原生模块时执行 `npm rebuild`,会触发各包 postinstall;而合并阶段已删除脚本文件,导致 `Cannot find module ... copy-files.js`、构建失败;现合并阶段同步**删除 `package.json` 的 `scripts` 字段**(运行时无安装期,scripts 无用),rebuild 不再触发;
11
+ - **`--debug` 下 `quiet` 命令仍被静默**:`run` 的 quiet 分支未判断 debug,导致 `--debug` 时 `gradlew` / `xcodebuild` 等命令输出仍被吞;现改为「debug 时忽略 quiet,全量输出」(不影响默认模式下 test 的精简行为);
12
+
13
+ ## [3.0.0] - 2026-10-13 21:19
14
+ ### 新增
15
+ - **node_modules 优化系统(默认开启)**:构建时自动执行六阶段优化,大幅减少打包文件数、加速启动。不改变项目源码,对任意 Node.js 项目通用;
16
+ - **阶段 1 合并 JS**:按包入口 bundle,多个 JS 文件合并为单文件(`bundled/` 目录下的 `.cjs` / `.mjs`);CJS 保持 CJS、ESM 保持 ESM;
17
+ - **阶段 2 清空资源**:扫描各包 `postinstall` 引用的目录/文件,保留目录结构、清空内容;被 import/require 的保留不删;
18
+ - **阶段 3 清平台二进制**:按目标平台 + `abiFilters` 逐文件判定,删除非本机二进制(三层识别:目录名、文件后缀、文件头魔数);
19
+ - **阶段 4 清开发文件**:删除 `.md` / `.map` / `.d.ts` / `.h` / `.c` 等非运行文件;**原生模块包(含 `binding.gyp`)内的编译必需文件保留**,确保原生编译通过;
20
+ - **阶段 5 清中间产物**;**阶段 6 清不参与运行的文件和空目录**;
21
+ - **优化缓存(快照 + 指纹)**:`.deps-snapshot.json` 记录依赖、目标平台、架构、运行时 Node 主版本、优化代码指纹、上次构建是否完成;
22
+ - 构建时自动判断并执行三种动作:**重装依赖并优化** / **仅优化** / **跳过**,日志说明当次动作与原因;
23
+ - 运行时 Node 主版本从 `libnode/include/node/node_version.h` 读取(读不到则报错退出);
24
+ - **模板指纹**:`template/` 目录内容纳入缓存指纹,模板改动自动触发重编;
25
+ - **`--debug` 调试开关**:`npx node-mobile-app build --debug` / `test --debug` 显示优化各阶段明细与各命令完整输出;
26
+ - **优化逃生舱口**(`mobileAppConfig.js`):`optimize: { enabled: false }` 整体关闭优化;`optimize: { exclude: ['包名'] }` 精确排除特定包;
27
+ - **构建进度提示**:默认模式在打包阶段显示「打包进行中...」,避免长时间无输出被误认为卡死;`--debug` 下输出已完整,无需此提示;
23
28
 
24
29
  ### 修复
30
+ - **`bundled` 目录在 APK 中丢失(重要)**:aapt2 默认过滤 `<dir>_*`,原产物目录名以下划线开头,被打包时整体丢弃,应用启动报 `Node assets copy failed`;产物目录改用 `bundled`,绕开该过滤;
31
+ - **scripts 引用保护过宽**:原逻辑保护 `package.json` `scripts` 引用的所有 js(含 test / lint),但移动端无安装期、这些脚本永不执行;移除该保护,测试脚本等被正常清理;
32
+ - **`run` 输出策略**:`run` 默认实时输出(inherit);新增 `quiet` 参数——捕获输出、成功静默、失败才打印,用于 `gradlew` / `xcodebuild` 等命令;
33
+ - **`clean` 清理不完整**:原只清 `node-mobile-app-build` 与插件 `.cxx`,现补上插件 `android/build`,确保清环境彻底;
34
+
35
+ ### 升级注意事项
36
+ - **3.0.0 起 node_modules 优化默认开启**,无需配置即享优化收益;
37
+ - **升级后首次构建**会重装依赖并执行优化,比平时慢;后续构建命中缓存后无额外开销;
38
+ - 若**构建或运行异常**,按以下顺序排查:
39
+ 1. 在 `mobileAppConfig.js` 中设置 `optimize: { enabled: false }` 关闭优化,验证是否为优化引起;
40
+ 2. 若确认是某个包合并后异常,用 `optimize: { exclude: ['包名'] }` 排除优化时异常或不想优化的包;
41
+ 3. 若需彻底重来,删除 `node-mobile-app-build` 目录后重新构建。
25
42
 
26
- - **`DEP0190` 弃用警告**:`src/utils.js` 中的 `run` / `runQuiet` / `runSilent` 原先统一用 `spawnSync(bin, args, { shell: true })`。Node 20+ 对「参数数组 + shell」这种组合会提示参数未转义(`DEP0190`),每次执行 CLI 都会在输出末尾出现该警告。现改为:
27
- - Windows:把命令与参数拼成单个字符串(参数按需加引号)后交给 shell——`.cmd` / `.bat` 仍能执行,且不再触发警告;
28
- - 其它平台:不使用 shell,参数交由 Node 转义。
43
+ ## [2.1.2] - 2026-09-29 19:53
44
+ ### 更改
45
+ - 加固了拷贝文件的一些逻辑;
package/README.md CHANGED
@@ -17,6 +17,7 @@
17
17
  - **增量构建**:文件指纹判断缓存,无变化时 10 秒级重跑
18
18
  - **跨平台**:Android(Windows / macOS / Linux)+ iOS(仅 macOS)
19
19
  - **离线可用**:`build` 打出的 APK / IPA 内置 JS,无需连电脑也能跑
20
+ - **node_modules 优化(默认开启)**:构建时自动执行六阶段优化——先清空 postinstall 资源(保留 js 供合并)、再合并 JS、清平台二进制、清开发文件(原生模块编译必需文件保留)、清中间产物、清冗余与空目录;大幅减少打包文件数与体积、加速启动,无需配置
20
21
 
21
22
  ---
22
23
 
@@ -53,6 +54,7 @@
53
54
  │ ├── fingerprint.js # 缓存指纹(判断是否需要清理构建缓存)
54
55
  │ ├── generate.js # 生成 main.js(桥接)+ mobileApp.runtime.ts(运行时配置)
55
56
  │ ├── install.js # 依赖安装 + 插件 abiFilters patch + WebView SSL patch
57
+ │ ├── optimize-node-modules.js # node_modules 优化(六阶段:清资源 / 合并 JS / 清平台二进制 / 清开发文件 / 清中间产物 / 清冗余)
56
58
  │ ├── patcher.js # 应用信息 / Android / iOS 配置 patch
57
59
  │ ├── platform-ios.js # iOS 构建(pod install / xcodebuild archive / export)
58
60
  │ ├── utils.js # 通用工具(文件操作 / 进程调用 / adb / hermesc 定位)
@@ -166,12 +168,13 @@ npx node-mobile-app build --release
166
168
 
167
169
  ### 常用选项
168
170
 
169
- | 选项 | 用途 |
170
- | --------------- | --------------------------------------- |
171
- | `--ios` | 目标平台为 iOS(默认 Android) |
172
- | `--release` | 打 release 包(仅 `build`) |
173
- | `--device <id>` | 指定设备(`test`) |
174
- | `--arch <abi>` | 指定 ABI,逗号分隔(`build`,仅 Android) |
171
+ | 选项 | 用途 |
172
+ | --------------- | --------------------------------------------- |
173
+ | `--ios` | 目标平台为 iOS(默认 Android) |
174
+ | `--release` | 打 release 包(仅 `build`) |
175
+ | `--device <id>` | 指定设备(`test`) |
176
+ | `--arch <abi>` | 指定 ABI,逗号分隔(`build`,仅 Android) |
177
+ | `--debug` | 显示详细日志(优化各阶段明细、各命令完整输出) |
175
178
 
176
179
  ### 安装 APK 到手机
177
180
 
@@ -336,6 +339,32 @@ signing: {
336
339
 
337
340
  ---
338
341
 
342
+ ### node_modules 优化
343
+
344
+ 构建时自动优化 `node_modules`(默认开启,无需配置),减少打包文件数与体积、加速启动。
345
+
346
+ | 字段 | 类型 | 默认 | 说明 |
347
+ | --------- | ---------- | ------ | ------------------------------------------------------------- |
348
+ | `enabled` | `boolean` | `true` | 是否启用优化 |
349
+ | `exclude` | `string[]` | `[]` | 排除优化时异常或不想优化的包(写 `package.json` 的 `name`) |
350
+
351
+ **六阶段流程**:
352
+
353
+ 1. **清空资源**:扫描各包 `postinstall` 引用的目录/文件,保留目录结构、清空内容;js 文件一律保留,留待合并阶段处理;
354
+ 2. **合并 JS**:按包入口 bundle,多个 JS 文件合并为单文件(`bundled/` 目录);合并后同步删除 `package.json` 的 `scripts` 字段(运行时无安装期,且 Gradle `npm rebuild` 会触发脚本导致失败);
355
+ 3. **清平台二进制**:按目标平台 + `abiFilters` 逐文件判定,删除非本机二进制;
356
+ 4. **清开发文件**:删除 `.md` / `.map` / `.d.ts` / `.h` / `.c` 等非运行文件;**原生模块包(含 `binding.gyp`)内的编译必需文件保留**;
357
+ 5. **清中间产物**;
358
+ 6. **清不参与运行的文件和空目录**。
359
+
360
+ **缓存**:快照记录在 `node-mobile-app-build/.deps-snapshot.json`,构建时自动判断「重装并优化 / 仅优化 / 跳过」;改动模板或依赖会自动触发重编。
361
+
362
+ **异常排查**:
363
+
364
+ - `optimize: { enabled: false }` 整体关闭优化;
365
+ - `optimize: { exclude: ['包名'] }` 排除优化时异常或不想优化的包;
366
+ - 加 `--debug` 查看优化各阶段明细与各命令完整输出。
367
+
339
368
  ## 服务地址说明
340
369
 
341
370
  CLI **自动识别 `server.js` 实际监听的协议和端口**——拦截 `net.Server.prototype.listen`,从 `server.address()` 取真实端口,从 `Server` 类型判断 `http` / `https`;
@@ -503,4 +532,4 @@ RN 0.87 的 API 若变动,`AppDelegate.swift` 里的 fallback 可能不生效—
503
532
 
504
533
  ## 许可
505
534
 
506
- ISC
535
+ ISC
package/bin/cli.js CHANGED
@@ -6,18 +6,18 @@ import { runClean } from '../src/commands/clean.js';
6
6
  const HELP = `
7
7
  @flun/node-mobile-app
8
8
 
9
- 用法:
10
- node-mobile-app test 编译并安装到设备(开发调试)
11
- node-mobile-app test --ios 同上,iOS(仅 macOS)
12
- node-mobile-app build 打 APK
9
+ 指令:
10
+ node-mobile-app test 编译并安装到设备(Android,需连 USB,走 Metro 热更新)
11
+ node-mobile-app test --ios iOS 版(仅 macOS)
12
+ node-mobile-app build 打 debug APK(Android,离线可用)
13
+ node-mobile-app build --release 打 release APK(Android,默认 debug keystore 签名,可安装)
13
14
  node-mobile-app build --ios 打 IPA(仅 macOS)
14
- node-mobile-app clean 清理构建缓存
15
+ node-mobile-app clean 清理构建缓存(不限平台)
15
16
 
16
17
  选项:
17
- --ios 目标平台为 iOS(默认 Android,仅 macOS 可执行)
18
- --release 打 release 包(build)
19
- --device <id> 指定设备(test)
20
- --arch <abi> 指定 ABI,逗号分隔(build,仅 Android)
18
+ --device <id> 指定设备(仅 test)
19
+ --arch <abi> 指定 ABI,逗号分隔(仅 build,Android)
20
+ --debug 显示详细日志(所有指令通用)
21
21
  `,
22
22
 
23
23
  parseFlags = args => {
@@ -34,6 +34,7 @@ const HELP = `
34
34
 
35
35
  main = async () => {
36
36
  const [cmd, ...rest] = process.argv.slice(2), flags = parseFlags(rest);
37
+ if (flags.debug) process.env.OPTIMIZE_DEBUG = '1';
37
38
  try {
38
39
  if (cmd === 'test') await runTest(flags);
39
40
  else if (cmd === 'build') await runBuild(flags);
@@ -46,4 +47,4 @@ const HELP = `
46
47
  }
47
48
  };
48
49
 
49
- main();
50
+ main();
@@ -12,6 +12,8 @@
12
12
  * npx node-mobile-app build --ios 打 IPA(仅 macOS)
13
13
  * npx node-mobile-app clean 清理构建缓存
14
14
  *
15
+ * 任意命令加 --debug 可显示详细日志(优化各阶段明细、各命令完整输出)
16
+ *
15
17
  * ===== 服务地址说明 =====
16
18
  * CLI 会自动识别 server.js 实际监听的协议和端口(拦截 listen 调用),
17
19
  * App 使用该地址加载页面,无需在此配置。
@@ -114,6 +116,14 @@ export default {
114
116
  heartbeatInterval: 5000, // 心跳间隔(毫秒)
115
117
  },
116
118
 
119
+ // ===== node_modules 优化 =====
120
+ // 构建时合并 JS + 清空已复制到项目根的资源 + 清平台二进制 + 清开发文件,加速启动
121
+ // 默认开启。若构建或运行异常,可按下述方式关闭或排除问题包
122
+ optimize: {
123
+ enabled: true, // 是否启用(默认 true)
124
+ // exclude: ['包名'], // 排除优化时异常或不想优化的包(默认空)
125
+ },
126
+
117
127
  // ===== 扩展点 =====
118
128
  // 不重置的模板文件(路径相对于 node-mobile-app-build/,如 'android/app/src/main/AndroidManifest.xml')
119
129
  keepFiles: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flun/node-mobile-app",
3
- "version": "2.1.2",
3
+ "version": "3.1.0",
4
4
  "description": "将 Node.js 项目一键打包为 Android / iOS 移动应用,运行时由 @flun/nodejs-mobile-react-native 提供,无需修改你的 Node 代码;",
5
5
  "author": "flun <open@flun.top>",
6
6
  "keywords": [
@@ -41,32 +41,34 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@babel/core": "^8.0.6",
44
+ "@eslint-react/eslint-plugin": "^5.21.3",
45
+ "@eslint/js": "^10.0.1",
46
+ "@flun/nodejs-mobile-react-native": "^2.0.8",
47
+ "@react-native/babel-preset": "^0.87.1",
44
48
  "@react-native-community/cli": "^20.2.0",
45
49
  "@react-native-community/cli-platform-android": "^20.2.0",
46
50
  "@react-native-community/cli-platform-ios": "^20.2.0",
47
- "@react-native/babel-preset": "^0.87.1",
48
51
  "@react-native/metro-config": "^0.87.1",
49
- "jimp": "^1.6.1",
50
- "react": "^19.3.0",
51
- "react-native": "^0.87.1",
52
- "react-native-webview": "^14.0.1",
53
52
  "@types/react": "^19.3.0",
54
- "prettier": "^3.9.8",
55
- "eslint": "^10.11.0",
56
- "@eslint/js": "^10.0.1",
57
53
  "@typescript-eslint/eslint-plugin": "^8.71.0",
58
54
  "@typescript-eslint/parser": "^8.71.0",
55
+ "esbuild": "^0.28.2",
56
+ "eslint": "^10.11.0",
59
57
  "eslint-config-prettier": "^10.1.8",
60
58
  "eslint-plugin-eslint-comments": "^3.2.0",
61
59
  "eslint-plugin-react-hooks": "^7.1.1",
62
- "@eslint-react/eslint-plugin": "^5.21.3",
63
- "@flun/nodejs-mobile-react-native": "^2.0.7"
60
+ "jimp": "^1.6.1",
61
+ "prettier": "^3.9.8",
62
+ "react": "^19.3.0",
63
+ "react-native": "^0.87.1",
64
+ "react-native-webview": "^14.0.1"
64
65
  },
65
66
  "allowScripts": {
66
67
  "node": true,
67
68
  "@flun/nodejs-mobile-react-native": true,
68
- "unrs-resolver": true,
69
- "@parcel/watcher": true
69
+ "@parcel/watcher": true,
70
+ "esbuild": true,
71
+ "unrs-resolver": true
70
72
  },
71
73
  "homepage": "https://github.com/OpenFlun/node-mobile-app#readme",
72
74
  "repository": {
@@ -7,6 +7,7 @@ import {
7
7
  patchExtraRnDependencies
8
8
  } from '../copy.js';
9
9
  import { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl } from '../install.js';
10
+ import { markVerified } from '../optimize-node-modules.js';
10
11
  import { generateMainJs, generateRuntime } from '../generate.js';
11
12
  import { checkAndResetCache } from '../fingerprint.js';
12
13
  import {
@@ -17,18 +18,18 @@ import { run, ensureDir, sanitizeFileName } from '../utils.js';
17
18
 
18
19
  const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'),
19
20
  runBuild = async (flags) => {
20
- const userProjectDir = process.cwd(), release = !!flags.release, isIos = !!flags.ios;
21
+ const userProjectDir = process.cwd(), release = !!flags.release, isIos = !!flags.ios, total = isIos ? 8 : 7;
22
+
21
23
  console.log(`\n=== node-mobile-app build (${isIos ? 'iOS' : 'Android'}, ${release ? 'release' : 'debug'}) ===\n`);
22
24
  console.log('用户项目:', userProjectDir);
23
-
24
25
  if (isIos) ensureMac(), checkIosTools();
25
- console.log('\n[0/7] 检查用户配置与入口文件'), ensureUserConfig(userProjectDir, PKG_ROOT);
26
+ console.log(`\n[1/${total}] 检查用户配置与入口文件`), ensureUserConfig(userProjectDir, PKG_ROOT);
26
27
 
27
28
  const config = await loadConfig(userProjectDir);
28
29
  ensureUserEntry(userProjectDir, config, PKG_ROOT), validateConfig(config, userProjectDir);
29
30
 
30
31
  const buildDir = getBuildDir(userProjectDir);
31
- console.log('\n[1/7] 重置 RN 工程模板');
32
+ console.log(`\n[2/${total}] 重置 RN 工程模板`);
32
33
  copyTemplate(PKG_ROOT, buildDir, config);
33
34
  patchAndroidAbi(buildDir, config);
34
35
  patchExtraRnDependencies(buildDir, userProjectDir, config);
@@ -39,15 +40,16 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
39
40
  patchIosConfig(buildDir, userProjectDir, config);
40
41
  await patchIosIcon(buildDir, userProjectDir, config);
41
42
 
42
- console.log('\n[2/7] 检查缓存指纹'), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
43
- console.log('\n[3/7] 复制用户项目');
43
+ console.log(`\n[3/${total}] 检查缓存指纹`), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
44
+ console.log(`\n[4/${total}] 复制用户项目`);
44
45
  const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
45
- console.log('\n[4/7] 安装依赖'), installUserDeps(targetDir, userProjectDir, config);
46
+ console.log(`\n[5/${total}] 安装依赖`);
47
+ await installUserDeps(targetDir, userProjectDir, config, buildDir, isIos ? 'ios' : 'android');
46
48
  patchNodejsMobilePlugin(userProjectDir, config);
47
49
  patchWebViewSsl(userProjectDir, config);
48
50
  writeLocalProperties(buildDir, config);
49
51
 
50
- console.log('\n[5/7] 生成桥接文件');
52
+ console.log(`\n[6/${total}] 生成桥接文件`);
51
53
  generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
52
54
 
53
55
  const appName = resolveAppName(userProjectDir, config), safeName = sanitizeFileName(appName),
@@ -55,8 +57,8 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
55
57
  ensureDir(outDir);
56
58
 
57
59
  if (isIos) {
58
- console.log('\n[6/7] pod install'), podInstall(path.join(buildDir, 'ios'));
59
- console.log('\n[7/7] xcodebuild archive + export'), xcodebuildArchive(buildDir, config, release, userProjectDir);
60
+ console.log(`\n[7/${total}] pod install`), podInstall(path.join(buildDir, 'ios'));
61
+ console.log(`\n[8/${total}] xcodebuild archive + export`), xcodebuildArchive(buildDir, config, release, userProjectDir);
60
62
 
61
63
  const ipaPath = xcodebuildExport(buildDir, config, release, outDir, userProjectDir),
62
64
  dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.ipa`, dst = path.join(outDir, dstName);
@@ -64,10 +66,12 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
64
66
 
65
67
  const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
66
68
  console.log(` ${dst} (${mb} MB)`);
69
+ await markVerified(buildDir);
67
70
  return;
68
71
  }
69
72
 
70
- console.log('\n[6/7] 打包 APK');
73
+ console.log(`\n[7/${total}] 打包 APK`);
74
+ if (!process.env.OPTIMIZE_DEBUG) console.log(' 打包进行中...');
71
75
  const assetsDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'assets'),
72
76
  resDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'res');
73
77
 
@@ -79,13 +83,13 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
79
83
  '--entry-file', 'index.js',
80
84
  '--bundle-output', path.join(assetsDir, 'index.android.bundle'),
81
85
  '--assets-dest', resDir,
82
- ], { cwd: buildDir });
86
+ ], { cwd: buildDir, quiet: true });
83
87
 
84
88
  const task = release ? 'assembleRelease' : 'assembleDebug', gradleArgs = [task];
85
89
  if (flags.arch) gradleArgs.push(`-PreactNativeArchitectures=${flags.arch}`);
86
90
 
87
91
  const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
88
- run(gradlew, gradleArgs, { cwd: path.join(buildDir, 'android') });
92
+ run(gradlew, gradleArgs, { cwd: path.join(buildDir, 'android'), quiet: true });
89
93
 
90
94
  const apkDir = path.join(buildDir, 'android', 'app', 'build', 'outputs', 'apk', release ? 'release' : 'debug');
91
95
  console.log('\n✅ 打包完成');
@@ -104,6 +108,7 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
104
108
  }
105
109
  }
106
110
  else console.log(' ⚠️ 未找到 APK 输出目录:', apkDir);
111
+ await markVerified(buildDir);
107
112
  }
108
113
 
109
- export { runBuild };
114
+ export { runBuild };
@@ -8,8 +8,12 @@ const runClean = async () => {
8
8
  targets = [
9
9
  { label: '构建目录', path: buildDir },
10
10
  {
11
- label: '插件 CMake 缓存', path: path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android',
12
- '.cxx')
11
+ label: '插件构建缓存', path: path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native',
12
+ 'android', 'build')
13
+ },
14
+ {
15
+ label: '插件 CMake 缓存', path: path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native',
16
+ 'android', '.cxx')
13
17
  },
14
18
  ];
15
19
 
@@ -7,6 +7,7 @@ import {
7
7
  patchExtraRnDependencies
8
8
  } from '../copy.js';
9
9
  import { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl } from '../install.js';
10
+ import { markVerified } from '../optimize-node-modules.js';
10
11
  import { generateMainJs, generateRuntime } from '../generate.js';
11
12
  import { checkAndResetCache } from '../fingerprint.js';
12
13
  import { patchAppConfig, patchAndroidConfig, patchSigning, patchIcon, patchIosConfig, patchIosIcon } from '../patcher.js';
@@ -15,42 +16,40 @@ import { run, runSilent, findAdb, ensureDir, findHermesc } from '../utils.js';
15
16
 
16
17
  const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'),
17
18
  runTest = async flags => {
18
- const userProjectDir = process.cwd(), isIos = !!flags.ios;
19
- console.log(`\n=== node-mobile-app test (${isIos ? 'iOS' : 'Android'}) ===\n`), console.log('用户项目:', userProjectDir);
19
+ const userProjectDir = process.cwd(), isIos = !!flags.ios, total = isIos ? 9 : 10;
20
20
 
21
+ console.log(`\n=== node-mobile-app test (${isIos ? 'iOS' : 'Android'}) ===\n`);
22
+ console.log('用户项目:', userProjectDir);
21
23
  if (isIos) ensureMac(), checkIosTools();
22
- console.log('\n[0/9] 检查用户配置与入口文件'), ensureUserConfig(userProjectDir, PKG_ROOT);
24
+ console.log(`\n[1/${total}] 检查用户配置与入口文件`), ensureUserConfig(userProjectDir, PKG_ROOT);
23
25
 
24
26
  const config = await loadConfig(userProjectDir);
25
27
  ensureUserEntry(userProjectDir, config, PKG_ROOT), validateConfig(config, userProjectDir);
26
28
 
27
29
  const buildDir = getBuildDir(userProjectDir);
28
- console.log('\n[1/9] 重置 RN 工程模板');
29
- copyTemplate(PKG_ROOT, buildDir, config);
30
- patchAndroidAbi(buildDir, config);
30
+ console.log(`\n[2/${total}] 重置 RN 工程模板`);
31
+ copyTemplate(PKG_ROOT, buildDir, config), patchAndroidAbi(buildDir, config);
31
32
  patchExtraRnDependencies(buildDir, userProjectDir, config);
32
33
  patchAppConfig(buildDir, userProjectDir, config);
33
- patchAndroidConfig(buildDir, config);
34
- patchSigning(buildDir, userProjectDir, config);
34
+ patchAndroidConfig(buildDir, config), patchSigning(buildDir, userProjectDir, config);
35
35
  await patchIcon(buildDir, userProjectDir, config);
36
36
  patchIosConfig(buildDir, userProjectDir, config);
37
37
  await patchIosIcon(buildDir, userProjectDir, config);
38
38
 
39
- console.log('\n[2/9] 检查缓存指纹'), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
40
- console.log('\n[3/9] 复制用户项目');
39
+ console.log(`\n[3/${total}] 检查缓存指纹`), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
40
+ console.log(`\n[4/${total}] 复制用户项目`);
41
41
  const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
42
42
 
43
- console.log('\n[4/9] 安装依赖'), installUserDeps(targetDir, userProjectDir, config);
43
+ console.log(`\n[5/${total}] 安装依赖`), await installUserDeps(targetDir, userProjectDir, config, buildDir, isIos ? 'ios' : 'android');
44
44
  patchNodejsMobilePlugin(userProjectDir, config);
45
- patchWebViewSsl(userProjectDir, config);
46
- writeLocalProperties(buildDir, config);
45
+ patchWebViewSsl(userProjectDir, config), writeLocalProperties(buildDir, config);
47
46
 
48
- console.log('\n[5/9] 生成桥接文件');
47
+ console.log(`\n[6/${total}] 生成桥接文件`);
49
48
  generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
50
49
 
51
50
  if (isIos) {
52
- console.log('\n[6/9] pod install'), podInstall(path.join(buildDir, 'ios'));
53
- console.log('\n[7/9] 生成 JS bundle + Hermes 字节码(由 Xcode Run Script 自动)');
51
+ console.log(`\n[7/${total}] pod install`), podInstall(path.join(buildDir, 'ios'));
52
+ console.log(`\n[8/${total}] 生成 JS bundle + Hermes 字节码(由 Xcode Run Script 自动)`);
54
53
  const bundlePath = path.join(buildDir, 'ios', 'main.jsbundle');
55
54
  runSilent('npx', [
56
55
  'react-native', 'bundle',
@@ -61,14 +60,15 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
61
60
  ], { cwd: buildDir });
62
61
  console.log(' ✓ bundle 已生成');
63
62
 
64
- console.log('\n[8/9] 编译并安装到模拟器/设备');
63
+ console.log(`\n[9/${total}] 编译并安装到模拟器/设备`);
65
64
  const args = ['react-native', 'run-ios'];
66
65
  if (flags.device) args.push('--device', flags.device);
67
66
  // FORCE_BUNDLING=1:即使 Debug+模拟器也把 main.jsbundle 打进 App
68
- const env = { ...process.env, FORCE_BUNDLING: '1' };
69
- run('npx', args, { cwd: buildDir, env });
67
+ const env = { ...process.env, FORCE_BUNDLING: '1', NODE_MOBILE_MODE: 'test' };
68
+ run('npx', args, { cwd: buildDir, env, inherit: true });
69
+ await markVerified(buildDir);
70
70
  } else {
71
- console.log('\n[6/9] 生成 JS bundle(离线可用)');
71
+ console.log(`\n[7/${total}] 生成 JS bundle(离线可用)`);
72
72
  const assetsDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'assets'),
73
73
  resDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'res');
74
74
  ensureDir(assetsDir);
@@ -84,7 +84,7 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
84
84
  ], { cwd: buildDir });
85
85
  console.log(' ✓ bundle 已生成');
86
86
 
87
- console.log('\n[7/9] 编译 Hermes 字节码(加速离线启动)');
87
+ console.log(`\n[8/${total}] 编译 Hermes 字节码(加速离线启动)`);
88
88
  const hermesc = findHermesc(userProjectDir);
89
89
  if (hermesc) {
90
90
  const hbcPath = bundlePath + '.hbc';
@@ -94,14 +94,15 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
94
94
  }
95
95
  else console.warn(' ⚠️ 未找到 hermesc,跳过字节码编译(离线启动会较慢)');
96
96
 
97
- console.log('\n[8/9] 配置 adb 端口转发');
97
+ console.log(`\n[9/${total}] 配置 adb 端口转发`);
98
98
  const adb = findAdb(config);
99
- console.log(' adb:', adb), run(adb, ['reverse', 'tcp:8081', 'tcp:8081']);
99
+ console.log(' adb:', adb), run(adb, ['reverse', 'tcp:8081', 'tcp:8081'], { quiet: true });
100
100
 
101
- console.log('\n[9/9] 编译并安装到设备');
101
+ console.log(`\n[10/${total}] 编译并安装到设备`);
102
102
  const args = ['react-native', 'run-android'];
103
103
  if (flags.device) args.push('--deviceId', flags.device);
104
- run('npx', args, { cwd: buildDir });
104
+ run('npx', args, { cwd: buildDir, inherit: true, env: { ...process.env, NODE_MOBILE_MODE: 'test' } });
105
+ await markVerified(buildDir);
105
106
  }
106
107
  };
107
108
 
package/src/config.js CHANGED
@@ -29,7 +29,8 @@ const DEFAULT_CONFIG = {
29
29
  abiFilters: ['arm64-v8a'],
30
30
  icon: './build/icon.png',
31
31
  usesCleartextTraffic: true,
32
- permissions: [],
33
32
  signing: {},
33
+ permissions: [],
34
+ signing: {},
34
35
  },
35
36
  ios: {
36
37
  deploymentTarget: '15.1',
@@ -48,6 +49,10 @@ const DEFAULT_CONFIG = {
48
49
  '*.log', '*.md', '*.tgz', '*.bak', '.backup*', 'dist/', 'out/', 'build/', 'tests/',
49
50
  'node-mobile-app-build/', 'nodejs-assets/',
50
51
  ],
52
+ optimize: {
53
+ enabled: true,
54
+ exclude: [],
55
+ },
51
56
  allowScripts: {
52
57
  'node': true,
53
58
  }
package/src/copy.js CHANGED
@@ -34,7 +34,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
34
34
  copyDirIncremental = (src, dst, exclude) => {
35
35
  ensureDir(dst);
36
36
 
37
- const dstKeep = new Set(['node_modules', '.deps-hash', 'main.js', 'package.json', 'package-lock.json']),
37
+ const dstKeep = new Set(['node_modules', 'main.js', 'package.json', 'package-lock.json']),
38
38
  srcNames = new Set(fs.readdirSync(src));
39
39
  // 1. 删除 dst 里不该存在的条目
40
40
  for (const name of fs.readdirSync(dst)) {
@@ -66,36 +66,23 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
66
66
  if (!fs.existsSync(buildPkgPath)) return;
67
67
  const mainPkgPath = path.join(pkgRoot, 'package.json');
68
68
  if (!fs.existsSync(mainPkgPath)) return;
69
-
70
- const buildPkg = readJson(buildPkgPath);
71
- const mainPkg = readJson(mainPkgPath);
69
+ const buildPkg = readJson(buildPkgPath), mainPkg = readJson(mainPkgPath);
72
70
  if (!buildPkg.dependencies) return;
73
71
 
74
- const mainDeps = mainPkg.dependencies || {};
75
- const missing = [];
76
- let depsUpdated = 0;
77
- let needWrite = false;
72
+ const mainDeps = mainPkg.dependencies || {}, missing = [];
73
+ let depsUpdated = 0, needWrite = false;
78
74
 
79
75
  // 移除仅用于模板占位说明的 _comment 字段,避免带进最终 build 工程
80
- if ('_comment' in buildPkg) {
81
- delete buildPkg._comment;
82
- needWrite = true;
83
- }
76
+ if ('_comment' in buildPkg) delete buildPkg._comment, needWrite = true;
84
77
  for (const name of Object.keys(buildPkg.dependencies)) {
85
78
  const v = mainDeps[name];
86
79
  if (!v) { missing.push(name); continue; }
87
- if (buildPkg.dependencies[name] !== v) {
88
- buildPkg.dependencies[name] = v;
89
- depsUpdated++;
90
- needWrite = true;
91
- }
80
+ if (buildPkg.dependencies[name] !== v) buildPkg.dependencies[name] = v, depsUpdated++, needWrite = true;
92
81
  }
93
82
 
94
83
  if (needWrite) {
95
84
  writeIfChanged(buildPkgPath, JSON.stringify(buildPkg, null, 2) + '\n');
96
- if (depsUpdated > 0) {
97
- console.log(' ✓ 已同步 ' + depsUpdated + ' 个依赖版本到 ' + BUILD_DIR_NAME + '/package.json');
98
- }
85
+ if (depsUpdated > 0) console.log(' ✓ 已同步 ' + depsUpdated + ' 个依赖版本到 ' + BUILD_DIR_NAME + '/package.json');
99
86
  }
100
87
 
101
88
  if (missing.length > 0) {
@@ -245,10 +232,8 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
245
232
  const buildPkgPath = path.join(buildDir, 'package.json');
246
233
  if (!fs.existsSync(buildPkgPath)) return console.warn(' ⚠️ 未找到 buildDir/package.json,跳过 extraRnDependencies');
247
234
 
248
- const buildPkg = readJson(buildPkgPath);
235
+ const buildPkg = readJson(buildPkgPath), missing = [];
249
236
  if (!buildPkg.dependencies) buildPkg.dependencies = {};
250
-
251
- const missing = [];
252
237
  let added = 0;
253
238
  for (const name of extras) {
254
239
  const pkgJson = path.join(userProjectDir, 'node_modules', name, 'package.json');