@flun/node-mobile-app 4.0.2 → 4.0.4

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.
@@ -1,8 +1,8 @@
1
1
  # Android 构建指南
2
2
 
3
- 本文档是 [README.md](./README.md) 的**深度附件**。日常使用(`test` / `build`)不需要看——CLI 自动处理。
3
+ 本文档是 [README.md](./README.md) 的**深度附件**;日常使用(`test` / `build`)不需要看——CLI 自动处理;
4
4
 
5
- 本文档只讲四件事,都是 README 没细说的:
5
+ 本文档只讲四件事,都是 README 没细说的:
6
6
  1. **环境准备**(JDK / SDK / NDK / adb / 原生模块编译)
7
7
  2. **libnode.so**(v18 vs v22、预编译仓库、替换、full-icu 验证)
8
8
  3. **Express 版本选择**(踩坑清单)
@@ -14,15 +14,15 @@
14
14
 
15
15
  ### 1.1 JDK 17(必须)
16
16
 
17
- Android Gradle Plugin 不支持 JDK 18+。
17
+ Android Gradle Plugin 不支持 JDK 18+;
18
18
 
19
19
  下载:[Adoptium JDK 17](https://adoptium.net/temurin/releases/?version=17)
20
20
 
21
- **CLI 不读 `JAVA_HOME`**,直接用系统 PATH 里的 `java`。确保 `java -version` 输出是 17。
21
+ **CLI 不读 `JAVA_HOME`**,直接用系统 PATH 里的 `java`;确保 `java -version` 输出是 17;
22
22
 
23
23
  ### 1.2 Android SDK
24
24
 
25
- CLI 自动查找,顺序:
25
+ CLI 自动查找,顺序:
26
26
 
27
27
  1. `mobileAppConfig.js` 的 `android.sdkPath`
28
28
  2. 环境变量 `ANDROID_HOME` / `ANDROID_SDK_ROOT`
@@ -39,7 +39,7 @@ CLI 自动查找,顺序:
39
39
 
40
40
  ### 1.3 adb
41
41
 
42
- 查找顺序同 SDK。常用命令:
42
+ 查找顺序同 SDK;常用命令:
43
43
 
44
44
  ```powershell
45
45
  $adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
@@ -48,16 +48,16 @@ $adb = "$env:LOCALAPPDATA\Android\Sdk\platform-tools\adb.exe"
48
48
 
49
49
  ### 1.4 原生模块编译(自动检测)
50
50
 
51
- CLI 会扫描 `<buildDir>/nodejs-assets/nodejs-project/node_modules/` 下是否有 `binding.gyp` 文件(原生模块的标志),自动写:
51
+ CLI 会扫描 `<buildDir>/nodejs-assets/nodejs-project/node_modules/` 下是否有 `binding.gyp` 文件(原生模块的标志),自动写:
52
52
 
53
53
  ```
54
- <buildDir>/nodejs-assets/BUILD_NATIVE_MODULES.txt # 1=有原生模块,0=无
54
+ <buildDir>/nodejs-assets/BUILD_NATIVE_MODULES.txt # 1=有原生模块,0=无
55
55
  ```
56
56
 
57
- - **检测到原生模块**(如 `bcrypt`、`sqlite3`):写 `1`,并设环境变量 `NODEJS_MOBILE_BUILD_NATIVE_MODULES=1`,`npm install` 时自动重编
58
- - **未检测到**:写 `0`,跳过编译
57
+ - **检测到原生模块**(如 `bcrypt`、`sqlite3`):写 `1`,并设环境变量 `NODEJS_MOBILE_BUILD_NATIVE_MODULES=1`,`npm install` 时自动重编
58
+ - **未检测到**:写 `0`,跳过编译
59
59
 
60
- **用户无需手动配置**。检测结果会纳入依赖哈希,装/卸原生模块时会自动触发重装重编。
60
+ **用户无需手动配置**;检测结果会纳入依赖哈希,装/卸原生模块时会自动触发重装重编;
61
61
 
62
62
  **手动强制重编**:换过 `libnode.so` 但依赖没变、需要重新编译原生模块时:
63
63
 
@@ -71,23 +71,23 @@ NODE_MOBILE_FORCE_REBUILD=1 npx node-mobile-app test
71
71
  $env:NODE_MOBILE_FORCE_REBUILD=1; npx node-mobile-app test
72
72
  ```
73
73
 
74
- **平台限制**:Windows 上 nodejs-mobile 无法编译原生模块(上游限制)。此功能仅 Linux / macOS 可用。
74
+ **平台限制**:Windows 上 nodejs-mobile 无法编译原生模块(上游限制);此功能仅 Linux / macOS 可用;
75
75
 
76
76
  ---
77
77
 
78
78
  ## 二、libnode.so
79
79
 
80
- `nodejs-mobile-react-native` 用它提供 Node.js 运行时。**是否带 full-icu** 决定能跑哪个 Express 版本。
80
+ `nodejs-mobile-react-native` 用它提供 Node.js 运行时;**是否带 full-icu** 决定能跑哪个 Express 版本;
81
81
 
82
82
  ### 2.0 预编译二进制怎么来的
83
83
 
84
- 从 `@flun/nodejs-mobile-react-native` 1.0.1 起,`libnode.so` **不在 npm 包里**,安装时由 postinstall 脚本从 Gitee / GitHub Release 自动下载:
84
+ 从 `@flun/nodejs-mobile-react-native` 1.0.1 起,`libnode.so` **不在 npm 包里**,安装时由 postinstall 脚本从 Gitee / GitHub Release 自动下载:
85
85
 
86
- - **下载源**:Gitee 优先,失败自动回退 GitHub
87
- - **下载内容**:`android-libnode.zip`(约 52 MB),含 `bin/` + `include/`
86
+ - **下载源**:Gitee 优先,失败自动回退 GitHub
87
+ - **下载内容**:`android-libnode.zip`(约 52 MB),含 `bin/` + `include/`
88
88
  - **覆盖目标**:`node_modules/@flun/nodejs-mobile-react-native/android/libnode/`
89
- - **幂等**:已装版本匹配则跳过,不重复下载
90
- - **失败处理**:全部源失败时打印下载链接,不中断 npm 安装
89
+ - **幂等**:已装版本匹配则跳过,不重复下载
90
+ - **失败处理**:全部源失败时打印下载链接,不中断 npm 安装
91
91
 
92
92
  环境变量(按需):
93
93
 
@@ -97,7 +97,7 @@ NODE_MOBILE_PREBUILT_SKIP=1 # 跳过全部下载
97
97
  NODE_MOBILE_PREBUILT_ANDROID_SKIP=1 # 只跳过 Android
98
98
  ```
99
99
 
100
- **用户不需要手动下载和替换**——除非要换成自编译的版本,见 2.2。
100
+ **用户不需要手动下载和替换**——除非要换成自编译的版本,见 2.2;
101
101
 
102
102
  ### 2.1 官方 v18(默认自动下载)
103
103
 
@@ -113,14 +113,14 @@ NODE_MOBILE_PREBUILT_ANDROID_SKIP=1 # 只跳过 Android
113
113
  | armeabi-v7a | 56 MB |
114
114
  | x86_64 | 62.3 MB |
115
115
 
116
- **特点**:`--with-intl=none`,**不含 ICU**。
116
+ **特点**:`--with-intl=none`,**不含 ICU**;
117
117
 
118
118
  - 不能用 `\p{...}` / `\P{...}` Unicode 正则
119
119
  - **必须 Express 4**
120
120
 
121
121
  ### 2.2 自编译 v22+(推荐)
122
122
 
123
- **官方 v18 已停更**。要跑 Express 5 或新语法,需替换。
123
+ **官方 v18 已停更**;要跑 Express 5 或新语法,需替换;
124
124
 
125
125
  **预编译产物仓库**:
126
126
 
@@ -135,7 +135,7 @@ NODE_MOBILE_PREBUILT_ANDROID_SKIP=1 # 只跳过 Android
135
135
  | x86_64 | ✓ | ✓ |
136
136
  | armeabi-v7a | ✓ | ✗ |
137
137
 
138
- **未来计划**:v24、v26 逐步适配。
138
+ **未来计划**:v24、v26 逐步适配;
139
139
 
140
140
  **替换步骤**:
141
141
 
@@ -144,9 +144,9 @@ NODE_MOBILE_PREBUILT_ANDROID_SKIP=1 # 只跳过 Android
144
144
  3. `mobileAppConfig.js` 的 `android.abiFilters` 只写实际存在的架构(v22 是 `['arm64-v8a', 'x86_64']`)
145
145
  4. 跑 `npx node-mobile-app test` 或 `build`
146
146
 
147
- CLI 指纹机制会自动清 `.cxx` / `android/build` / `app/build` 并全量重编——**无需手动清缓存**。
147
+ CLI 指纹机制会自动清 `.cxx` / `android/build` / `app/build` 并全量重编——**无需手动清缓存**;
148
148
 
149
- **完整自编译流程**见 `自定义libnode指南.md`。
149
+ **完整自编译流程**见 `自定义libnode指南.md`;
150
150
 
151
151
  ### 2.3 验证是否带 full-icu
152
152
 
@@ -158,20 +158,20 @@ strings libnode.so | grep -c icudt78l
158
158
  - 输出几千(约 4300)→ 带 full-icu
159
159
  - 输出 0 → 不带
160
160
 
161
- **体积对照**(实测,strip 后):
161
+ **体积对照**(实测,strip 后):
162
162
 
163
- | 版本 | arm64-v8a | x86_64 |
164
- | ----------------------- | --------- | -------- |
165
- | v18(官方,无 ICU) | 59.6 MB | 62.3 MB |
166
- | v22(自编译,full-icu) | 112.4 MB | 117.2 MB |
163
+ | 版本 | arm64-v8a | x86_64 |
164
+ | ---------------------- | --------- | -------- |
165
+ | v18(官方,无 ICU) | 59.6 MB | 62.3 MB |
166
+ | v22(自编译,full-icu) | 112.4 MB | 117.2 MB |
167
167
 
168
- 体积差异约 50 MB,来自三部分:Node v22 核心引擎 + full-icu 数据(约 25–30 MB)+ V8 版本升级。**不是编译出错**。
168
+ 体积差异约 50 MB,来自三部分:Node v22 核心引擎 + full-icu 数据(约 25–30 MB)+ V8 版本升级;**不是编译出错**;
169
169
 
170
170
  ---
171
171
 
172
172
  ## 三、Express 版本选择(踩坑清单)
173
173
 
174
- **选错版本会在 Node 进程启动时崩溃**,且错误信息极具误导性(如 `number 116 is not a function`)。
174
+ **选错版本会在 Node 进程启动时崩溃**,且错误信息极具误导性(如 `number 116 is not a function`);
175
175
 
176
176
  ### 3.1 版本对照
177
177
 
@@ -192,7 +192,7 @@ strings libnode.so | grep -c icudt78l
192
192
  }
193
193
  ```
194
194
 
195
- 依赖里如有 `\P{ASCII}` 等 Unicode 正则,改写成 ASCII 等价:
195
+ 依赖里如有 `\P{ASCII}` 等 Unicode 正则,改写成 ASCII 等价:
196
196
 
197
197
  | 原写法 | ASCII 写法 |
198
198
  | ----------- | -------------- |
@@ -211,7 +211,7 @@ strings libnode.so | grep -c icudt78l
211
211
  **三个前提**:
212
212
 
213
213
  1. `libnode.so` 确实带 full-icu(用 `strings` 验证过)
214
- 2. **不要**用 `@alloc/path-to-regexp` 替换原版——它的 `exports` 只有 `import` 条件,CJS 下 `require()` 报 `No "exports" main defined`
214
+ 2. **不要**用 `@alloc/path-to-regexp` 替换原版——它的 `exports` 只有 `import` 条件,CJS 下 `require()` 报 `No "exports" main defined`
215
215
  3. **不要**加任何 `overrides` 强制替换 `path-to-regexp`
216
216
 
217
217
  ### 3.4 Express 5 路由语法迁移
@@ -243,7 +243,7 @@ express@5.2.1
243
243
  └── path-to-regexp@8.4.2
244
244
  ```
245
245
 
246
- 如出现 `@alloc/path-to-regexp`,说明 `package-lock.json` 里有残留 `overrides`。**彻底清理**:
246
+ 如出现 `@alloc/path-to-regexp`,说明 `package-lock.json` 里有残留 `overrides`;**彻底清理**:
247
247
 
248
248
  ```powershell
249
249
  Remove-Item -Recurse -Force node_modules, package-lock.json
@@ -252,9 +252,9 @@ npm install
252
252
 
253
253
  ### 3.6 三个常见误区
254
254
 
255
- - **误区 1**:以为 `@alloc/path-to-regexp` 能解决 ESM 问题 → 它的 `exports` 也是纯 `import`,`require()` 依然失败
256
- - **误区 2**:只改路由语法就想让 Express 5 跑 → 若 `libnode.so` 无 full-icu,加载 `path-to-regexp@8` 时仍崩
257
- - **误区 3**:用 `overrides` 强制替换 `path-to-regexp` → 破坏依赖链,且 `package-lock.json` 会残留解析结果,删掉 `overrides` 也不生效
255
+ - **误区 1**:以为 `@alloc/path-to-regexp` 能解决 ESM 问题 → 它的 `exports` 也是纯 `import`,`require()` 依然失败
256
+ - **误区 2**:只改路由语法就想让 Express 5 跑 → 若 `libnode.so` 无 full-icu,加载 `path-to-regexp@8` 时仍崩
257
+ - **误区 3**:用 `overrides` 强制替换 `path-to-regexp` → 破坏依赖链,且 `package-lock.json` 会残留解析结果,删掉 `overrides` 也不生效
258
258
 
259
259
  ---
260
260
 
@@ -297,7 +297,7 @@ NODEJS-MOBILE: [node-mobile-app] failed: Node 服务 5 秒内未启动 -- 请检
297
297
 
298
298
  ### 4.2 手动清缓存(兜底)
299
299
 
300
- **正常情况不需要**——CLI 指纹机制自动清。以下仅当指纹判断失效(如手动改过 `node-mobile-app-build/` 内文件)时用:
300
+ **正常情况不需要**——CLI 指纹机制自动清;以下仅当指纹判断失效(如手动改过 `node-mobile-app-build/` 内文件)时用:
301
301
 
302
302
  ```powershell
303
303
  # 最彻底:删整个 RN 工程
package/CHANGELOG.md CHANGED
@@ -1,52 +1,13 @@
1
1
  # 变更日志
2
- ## [4.0.2] - 2026-10-20 15:56
2
+ ## [4.0.4] - 2026-10-21 10:02
3
+ ### 优化了大量文件细节
3
4
 
5
+ ## [4.0.3] - 2026-10-21 08:28
4
6
  ### 修复
5
- - **CLI 或插件版本变化时跳过依赖重装,产物沿用旧优化规则**:`[3/11]` 检测到 CLI 版本、插件版本、ABI、libnode、模板文件变化时,原逻辑只清 Gradle 构建目录与插件 CMake 缓存,不清 `nodejs-assets` 与 `.deps-snapshot.json`;`[5/11]` 读到的仍是旧快照,判定"依赖与环境未变化",跳过安装与优化;结果是新版本代码配旧规则产物,原生模块的编译源文件、中间产物、异平台 prebuilds 可能残留;现拆分为 `clearGradleCaches` 与 `clearDepsCaches`,前者清 Gradle 与插件构建目录,后者清 `nodejs-assets` 与依赖快照;版本类变化同时清两者,触发重装、重编、重优化;
6
- - **`[3/11]` 提示与实际清理范围不符**:原提示"清理全部缓存"实际只清 Gradle 构建目录;`depsChanged` / `appInfoChanged` 分支提示"清理 nodejs-assets 缓存 + app/build",但 `nodejs-assets` 从未被清理;现提示按实际清理目标拼接--版本类变化为"清理 Gradle 缓存、CMake 缓存与依赖缓存",依赖变化为"清理依赖缓存、Gradle 缓存",仅应用信息变化为"清理 Gradle 缓存";
7
-
8
- ## [4.0.1] - 2026-10-20 14:48
9
-
10
- ### 紧急修复
11
- - **4.0.0 在全新安装时原生模块产物错误,应用无法正常加载**:4.0.0 发布时依赖 `@flun/nodejs-mobile-react-native` 的版本被锁定为 2.0.8,而 2.0.8 缺少配合编译前移的 Gradle 改动;结果 `DeleteIncorrectPrebuilds` 删除前移阶段已编译的 `.node`、`DetectCorrectPrebuilds` 用 prebuild 覆盖已编译结果、`BuildNpmModules` 重复编译,APK 内原生模块缺失或不匹配,应用启动后加载失败;现依赖 `@flun/nodejs-mobile-react-native@^2.1.0`,该版本在标记文件存在时保留 `build/Release/*.node`、跳过 prebuild 替换、跳过重复编译;
12
-
13
- ### 升级注意事项
14
- - **已装 4.0.0 必须升级**:4.0.0 的锁文件已固定 `nodejs-mobile-react-native@2.0.8`,直接使用会构建失败或运行异常;请安装 4.0.1,或手动执行 `npm update @flun/nodejs-mobile-react-native` 升至 2.1.0;
15
-
16
- ### 更新
17
- - 同步更新了一些依赖包
18
-
19
- ## [4.0.0] - 2026-10-20 12:09
20
-
21
- ### 升级注意事项
22
- - **`test` 步骤由 10 步变为 11 步**:编译与安装拆分为 `[10/11] 编译 Debug 包`(`gradlew assembleDebug`)与 `[11/11] 安装到设备`(`adb install` + 启动);脚本或文档引用原 `[10/10]` 时需同步更新;
23
- - **签名冲突时自动卸载设备应用**:`test` 安装前检测设备已装包的 `DEBUGGABLE` 标志,与待装变体不一致时先 `adb uninstall` 再安装;卸载会清除应用数据(登录态、缓存),切换 debug/release 前请自行备份;
24
- - **原生模块编译前移至优化之前**:`npm install` 后立即交叉编译 Android 原生模块,再由优化阶段清理编译源文件与中间产物;NDK 版本与编译 SDK 从 `android/build.gradle` 的实际值读取,用户自定义 NDK 版本同样生效;
7
+ - **`[3/11]` 提示中清理目标重复**:`depsChanged` 与 `appInfoChanged` 同时为真时,`targets` 数组会推入两次 `Gradle 缓存`,提示显示为"清理 依赖缓存、Gradle 缓存、Gradle 缓存";现用 `[...new Set(targets)]` 去重,提示与清理目标一致;
8
+ - **删除依赖快照误用 `removeDir`**:`.deps-snapshot.json` 是文件,原代码调用 `removeDir` 删除;该函数名与实现均针对目录,当前靠 `fs.rmSync` 的兼容性正常工作,但语义错位,一旦 `removeDir` 实现收紧(如增加 `isDirectory` 判断)会静默失效,导致快照残留、`[5/11]` 误判"依赖与环境未变化";现改用 `fs.rmSync(path, { force: true })` 直接删除文件;
25
9
 
26
- ### 新增
27
- - **`installerHash` 纳入快照**:记录 `install.js` 内容哈希,该文件变化时触发重装、重编、重优化;避免流程控制代码更新后仍复用旧缓存;
28
- - **Metro 后台常驻**:`test` 执行前检测 `localhost:8081/status`,未运行则 detached 启动 `react-native start`(日志写入 `.metro.log`),就绪后继续;`run-android` 加 `--no-packager`,命令结束后热更新仍可用;30 秒未就绪打印警告并回落到 APK 内置 bundle;
29
- - **bundle 变化自动失效 Gradle 缓存**:比对 `src/main/assets/index.android.bundle` 与 `mergeDebugAssets` 合并结果的 SHA256,不一致时删除合并目录强制重新合并;避免 Gradle UP-TO-DATE 判定导致装入旧 bundle;
30
- - **编译前执行 `patch-package.js`**:把 `node-gyp-build` 替换为 `node-gyp-build-mobile`,使原生模块走 `@flun/nodejs-mobile-gyp` 而非 npm 自带 `node-gyp`;
31
- - **提示配色与缩进分级**:引入 `chalk`,按「步骤标题 0 空格 / 动作与状态 2 空格 / 结果与完成 4 空格 / 子项详情 8 空格」分级;优化阶段作为步骤子项,标题 4 空格、结果 6 空格;
32
- - **精简与详细双模式**:`run`/`runStream` 支持 `quiet`,默认只输出关键行与结果;`OPTIMIZE_DEBUG=1` 或 `--debug` 时全量输出;
33
-
34
- ### 修复
35
- - **优化阶段不再依赖包名白名单**:`pruneDevFiles` 移除 `COMPILE_EXTS`、`inNativePkg`、`isSkippedPkg` 保护,编译已在优化前完成,编译源文件无条件清理;解决无 `binding.gyp` 的头文件包(`node-addon-api`、`nan` 等)需靠白名单枚举才不被误删的问题;
36
- - **`prebuilds/<platform>-*` 按目标平台清理**:Android 只保留 `android` 平台目录,`linux`/`darwin`/`win32` 整目录删除;iOS 只保留 `darwin`;`delFile` 支持目录递归删除,此前 `unlinkSync` 对目录静默失败;
37
- - **`PATTERNS` 覆盖完整编译源文件**:扩展名由 `.gyp|gypi`、`.h|c` 扩展为 `.gyp|gypi|h|hh|hpp|c|cc|cpp|cxx|m|mm`(带 `/i`);`.cc` 等此前残留的源文件现可清理;
38
- - **阶段 5 覆盖 node-gyp 中间产物**:增加 `obj.target` 路径判定与 `.o`/`.d`/`.mk`/`.stamp`/`Makefile` 扩展名;源码编译的中间文件不再进入 APK;
39
- - **多设备环境下 `test` 无法安装,报 `INSTALL_FAILED_UPDATE_INCOMPATIBLE`**:连接多台 Android 设备、并用 `--device <serial>` 指定目标时,安装前的签名检测未把 `-s <serial>` 传给 adb;adb 无法确定操作哪台设备,返回 `more than one device/emulator`;检测逻辑把非零状态当作"包未安装",跳过卸载;随后 `adb install` 因签名冲突失败;现 `getInstalledVariant`、`ensureInstallable` 增加 `serial` 参数,`test` 传入 `flags.device`,检测与安装操作同一台设备;
40
- - **APK 目录缺失时抛原始 `ENOENT`**:`[11/11]` 先检查目录与 `app-debug.apk` 是否存在,缺失时输出明确错误并退出;
41
- - **Metro 启动触发 `DEP0190` 弃用警告**:改用 `spawnCmdAsync`(Windows 拼单字符串、其它平台 `shell: false`);
42
-
43
- ### 性能
44
- - **优化后 `node_modules` 体积与文件数下降**:编译前移使优化阶段可无条件清理编译源文件与中间产物,`bcrypt` 编译后仅保留 `build/Release/bcrypt_lib.node`;`prebuilds` 按目标平台裁剪,APK 不再包含异平台二进制;
45
- - **Gradle 增量缓存保持有效**:bundle 哈希一致时不删 `mergeDebugAssets`,连续 `test` 走 UP-TO-DATE;
46
-
47
- ## [3.1.5] - 2026-10-18 10:04
10
+ ## [4.0.2] - 2026-10-20 15:56
48
11
  ### 修复
49
- - **修复原生模块编译时 `node-addon-api` 的 gyp / 头文件被误删**:`pruneDevFiles` 原保护条件为 `COMPILE_EXTS && inNativePkg`,而 `node-addon-api` 自身没有 `binding.gyp`,不在原生模块包列表内,其 `node_addon_api.gyp` 与头文件被当作开发文件删除,导致依赖它的原生模块(如 `bcrypt`)在 Gradle `npm rebuild` 时报 `../node-addon-api/node_addon_api.gyp not found`,构建中断;现新增 `isSkippedPkg`,保护条件改为 `COMPILE_EXTS && (inNativePkg || isSkippedPkg)`,仅保留 `SKIP_PKGS` 包内的编译文件(`.gyp`/`.gypi`/`.h`/`.c` 等),`.md`/`.map`/`.ts` 等仍照常清理,不扩大保留范围;
50
-
51
- ### 变更
52
- - **阶段 4 不再清理 `.d.ts`**:`.d.ts` 已由阶段 6 的 `TS_RE`(`/\.(ts|cts|mts)$/`)覆盖,阶段 4 重复清理属冗余;移除后 `.d.ts` 统一由阶段 6 处理,清理职责不重叠;
12
+ - **CLI 或插件版本变化时跳过依赖重装,产物沿用旧优化规则**:`[3/11]` 检测到 CLI 版本、插件版本、ABI、libnode、模板文件变化时,原逻辑只清 Gradle 构建目录与插件 CMake 缓存,不清 `nodejs-assets` 与 `.deps-snapshot.json`;`[5/11]` 读到的仍是旧快照,判定"依赖与环境未变化",跳过安装与优化;结果是新版本代码配旧规则产物,原生模块的编译源文件、中间产物、异平台 prebuilds 可能残留;现拆分为 `clearGradleCaches` 与 `clearDepsCaches`,前者清 Gradle 与插件构建目录,后者清 `nodejs-assets` 与依赖快照;版本类变化同时清两者,触发重装、重编、重优化;
13
+ - **`[3/11]` 提示与实际清理范围不符**:原提示"清理全部缓存"实际只清 Gradle 构建目录;`depsChanged` / `appInfoChanged` 分支提示"清理 nodejs-assets 缓存 + app/build",但 `nodejs-assets` 从未被清理;现提示按实际清理目标拼接--版本类变化为"清理 Gradle 缓存、CMake 缓存与依赖缓存",依赖变化为"清理依赖缓存、Gradle 缓存",仅应用信息变化为"清理 Gradle 缓存";
package/bin/cli.js CHANGED
@@ -8,16 +8,16 @@ const HELP = `
8
8
  @flun/node-mobile-app
9
9
 
10
10
  指令:
11
- node-mobile-app test 编译并安装到设备(Android,需连 USB,走 Metro 热更新)
11
+ node-mobile-app test 编译并安装到设备(Android,需连 USB,走 Metro 热更新)
12
12
  node-mobile-app test --ios iOS 版(仅 macOS)
13
- node-mobile-app build 打 debug APK(Android,离线可用)
14
- node-mobile-app build --release 打 release APK(Android,默认 debug keystore 签名,可安装)
13
+ node-mobile-app build 打 debug APK(Android,离线可用)
14
+ node-mobile-app build --release 打 release APK(Android,默认 debug keystore 签名,可安装)
15
15
  node-mobile-app build --ios 打 IPA(仅 macOS)
16
16
  node-mobile-app clean 清理构建缓存(不限平台)
17
17
 
18
18
  选项:
19
19
  --device <id> 指定设备(仅 test)
20
- --arch <abi> 指定 ABI,逗号分隔(仅 build,Android)
20
+ --arch <abi> 指定 ABI,逗号分隔(仅 build,Android)
21
21
  --debug 显示详细日志(所有指令通用)
22
22
  `,
23
23
 
@@ -48,4 +48,4 @@ const HELP = `
48
48
  }
49
49
  };
50
50
 
51
- main();
51
+ main();
@@ -1,24 +1,24 @@
1
1
  /**
2
2
  * @flun/node-mobile-app 配置文件
3
- * 放置在用户 Node.js 项目根目录,CLI 会自动读取
4
- * 如不提供此文件,CLI 使用默认配置
3
+ * 放置在用户 Node.js 项目根目录,CLI 会自动读取
4
+ * 如不提供此文件,CLI 使用默认配置
5
5
  * 所有路径相对于用户项目根目录
6
6
  *
7
7
  * ===== 常用命令 =====
8
- * npx node-mobile-app test 编译并安装到设备(需连 USB,走 Metro 热更新)
8
+ * npx node-mobile-app test 编译并安装到设备(需连 USB,走 Metro 热更新)
9
9
  * npx node-mobile-app test --ios iOS 版(仅 macOS)
10
10
  * npx node-mobile-app build 打 debug APK(离线可用)
11
- * npx node-mobile-app build --release 打 release APK(默认 debug keystore 签名,可安装;上架需配 android.signing)
11
+ * npx node-mobile-app build --release 打 release APK(默认 debug keystore 签名,可安装;上架需配 android.signing)
12
12
  * npx node-mobile-app build --ios 打 IPA(仅 macOS)
13
13
  * npx node-mobile-app clean 清理构建缓存
14
14
  *
15
15
  * 任意命令加 --debug 可显示详细日志(优化各阶段明细、各命令完整输出)
16
16
  *
17
17
  * ===== 服务地址说明 =====
18
- * CLI 会自动识别 server.js 实际监听的协议和端口(拦截 listen 调用),
19
- * App 使用该地址加载页面,无需在此配置。
20
- * 若 5 秒内未能识别,App 显示启动失败并可在页面展开详情查看原因。
21
- * 动态端口(listen(0))会自动获取系统分配的真实端口。
18
+ * CLI 会自动识别 server.js 实际监听的协议和端口(拦截 listen 调用),
19
+ * App 使用该地址加载页面,无需在此配置;
20
+ * 若 5 秒内未能识别,App 显示启动失败并可在页面展开详情查看原因;
21
+ * 动态端口(listen(0))会自动获取系统分配的真实端口;
22
22
  */
23
23
  export default {
24
24
  // ===== 必填 =====
@@ -39,7 +39,7 @@ export default {
39
39
  enableDomStorage: true, // 是否启用 DOM Storage(localStorage 等)
40
40
  allowFileAccess: true, // 是否允许访问本地文件
41
41
  mixedContentMode: 'always', // 混合内容模式:never / always / compatibility
42
- // WebView 背景色,支持三种写法:
42
+ // WebView 背景色,支持三种写法:
43
43
  // '#ffffff' / 'white' 固定色
44
44
  // 'system' 跟随系统(内部用默认浅/深色)
45
45
  // { light: '#fff', dark: '#1a1a1a' } 按系统主题切换
@@ -49,8 +49,8 @@ export default {
49
49
  // - 用 HTTPS + 真证书(如 Let's Encrypt):与本开关无关
50
50
  // - 用 HTTPS + 自签证书 + 已装进设备信任库:与本开关无关
51
51
  // - 用 HTTPS + 自签证书 + 未装设备(本地测试最常见):必须设为 true
52
- // ⚠️ 开启后仅对 127.0.0.1 / localhost 放行不受信任的 HTTPS 证书,其他域名仍严格校验
53
- // 用于本地自签证书测试,生产环境建议关闭
52
+ // ⚠️ 开启后仅对 127.0.0.1 / localhost 放行不受信任的 HTTPS 证书,其他域名仍严格校验
53
+ // 用于本地自签证书测试,生产环境建议关闭
54
54
  allowSelfSignedCert: false,
55
55
  },
56
56
 
@@ -73,7 +73,7 @@ export default {
73
73
  ],
74
74
  // 签名配置(可选)
75
75
  //
76
- // 不配置也能打 release:模板默认用 debug keystore 签名(可安装、可本地测试,不可上架)。
76
+ // 不配置也能打 release:模板默认用 debug keystore 签名(可安装、可本地测试,不可上架);
77
77
  // 需要上架时再配置下面任一种:
78
78
  //
79
79
  // 方式一:Android SDK 自带 debug keystore(本地测试常用)
@@ -83,7 +83,7 @@ export default {
83
83
  // keyAlias: 'androiddebugkey'
84
84
  // keyPassword: 'android'
85
85
  //
86
- // 取消下方整块注释即视为「启用签名」,此时 4 项均为必填(缺项会在构建前报错)
86
+ // 取消下方整块注释即视为「启用签名」,此时 4 项均为必填(缺项会在构建前报错)
87
87
  signing: {
88
88
  // keystore: './build/release.keystore', // 必填:keystore 路径(相对用户项目根)
89
89
  // storePassword: '', // 必填:keystore 密码
@@ -97,7 +97,7 @@ export default {
97
97
  ios: {
98
98
  deploymentTarget: '15.1', // 最低 iOS 版本(RN 0.87 要求 15.1+)
99
99
  icon: './build/icon.png', // 应用图标(建议 1024x1024 PNG)
100
- // 签名配置(仅 macOS,发布时必填):
100
+ // 签名配置(仅 macOS,发布时必填):
101
101
  // teamId Apple Developer Team ID
102
102
  // provisioningProfile 描述文件路径(写入 exportOptions.plist)
103
103
  // certificate P12 证书路径(导入临时 keychain 供 xcodebuild 签名用)
@@ -118,17 +118,17 @@ export default {
118
118
 
119
119
  // ===== node_modules 优化 =====
120
120
  // 构建时合并 JS + 清空已复制到项目根的资源 + 清平台二进制 + 清开发文件,加速启动
121
- // 默认开启。若构建或运行异常,可按下述方式关闭或排除问题包
121
+ // 默认开启;若构建或运行异常,可按下述方式关闭或排除问题包
122
122
  optimize: {
123
123
  enabled: true, // 是否启用(默认 true)
124
124
  // exclude: ['包名'], // 排除优化时异常或不想优化的包(默认空)
125
125
  },
126
126
 
127
127
  // ===== 扩展点 =====
128
- // 不重置的模板文件(路径相对于 node-mobile-app-build/,如 'android/app/src/main/AndroidManifest.xml')
128
+ // 不重置的模板文件(路径相对于 node-mobile-app-build/,如 'android/app/src/main/AndroidManifest.xml')
129
129
  keepFiles: [],
130
- // 额外 RN 原生依赖包名数组(用于 autolink 识别,含原生模块的包必须在此声明)
131
- // 安装命令用 --save-dev,这些是开发依赖,不会进移动端 Node 环境
130
+ // 额外 RN 原生依赖包名数组(用于 autolink 识别,含原生模块的包必须在此声明)
131
+ // 安装命令用 --save-dev,这些是开发依赖,不会进移动端 Node 环境
132
132
  // 示例: npm install --save-dev react-native-camera
133
133
  // extraRnDependencies: ['react-native-camera']
134
134
  // 未安装的包会在 test/build 时中断并提示安装命令
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flun/node-mobile-app",
3
- "version": "4.0.2",
3
+ "version": "4.0.4",
4
4
  "description": "将 Node.js 项目一键打包为 Android / iOS 移动应用,运行时由 @flun/nodejs-mobile-react-native 提供,无需修改你的 Node 代码;",
5
5
  "author": "flun <open@flun.top>",
6
6
  "keywords": [
@@ -19,7 +19,7 @@ const copyFile = () => {
19
19
  if (!fs.existsSync(path.join(targetDir, 'package.json'))) return true;
20
20
  if (targetDir === path.resolve(__dirname, '..')) return true;
21
21
  if (fs.existsSync(targetFile)) return true; // 目标文件已存在 → 直接返回
22
- console.log(`⚠️ 在项目根目录未找到 ${fileName} 文件,正在创建...`);
22
+ console.log(`⚠️ 在项目根目录未找到 ${fileName} 文件,正在创建...`);
23
23
 
24
24
  fs.copyFileSync(sourceFile, targetFile); // 复制源文件到项目根目录
25
25
  console.log(`✓ 已创建 ${fileName} 示例文件:${targetFile}`);
@@ -112,4 +112,4 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
112
112
  await markVerified(buildDir);
113
113
  }
114
114
 
115
- export { runBuild };
115
+ export { runBuild };
@@ -14,7 +14,9 @@ import { generateMainJs, generateRuntime } from '../generate.js';
14
14
  import { checkAndResetCache } from '../fingerprint.js';
15
15
  import { patchAppConfig, patchAndroidConfig, patchSigning, patchIcon, patchIosConfig, patchIosIcon } from '../patcher.js';
16
16
  import { ensureMac, checkIosTools, podInstall } from '../platform-ios.js';
17
- import { run, runSilent, runStream, findAdb, ensureDir, findHermesc, ensureInstallable, writeInstallState, spawnCmdAsync } from '../utils.js';
17
+ import {
18
+ run, runSilent, runStream, findAdb, ensureDir, findHermesc, ensureInstallable, writeInstallState, spawnCmdAsync
19
+ } from '../utils.js';
18
20
  import chalk from 'chalk';
19
21
 
20
22
  const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'),
@@ -95,14 +97,15 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
95
97
  fs.copyFileSync(hbcPath, bundlePath), fs.unlinkSync(hbcPath);
96
98
  console.log(chalk.green(' ✓ 已替换为 Hermes 字节码'));
97
99
  }
98
- else console.warn(chalk.yellow(' ⚠️ 未找到 hermesc,跳过字节码编译(离线启动会较慢)'));
100
+ else console.warn(chalk.yellow(' ⚠️ 未找到 hermesc,跳过字节码编译(离线启动会较慢)'));
99
101
 
100
- const mergedAsset = path.join(buildDir, 'android', 'app', 'build', 'intermediates', 'assets', 'debug', 'mergeDebugAssets', 'index.android.bundle');
102
+ const mergedAsset = path.join(buildDir, 'android', 'app', 'build', 'intermediates', 'assets', 'debug',
103
+ 'mergeDebugAssets', 'index.android.bundle');
101
104
  if (fs.existsSync(mergedAsset)) {
102
105
  const h = f => createHash('sha256').update(fs.readFileSync(f)).digest('hex');
103
106
  if (h(bundlePath) !== h(mergedAsset)) {
104
107
  fs.rmSync(path.dirname(mergedAsset), { recursive: true, force: true });
105
- console.log(chalk.gray(' bundle 已变化,清理 mergeDebugAssets 缓存'));
108
+ console.log(chalk.gray(' bundle 已变化,清理 mergeDebugAssets 缓存'));
106
109
  }
107
110
  }
108
111
  console.log(chalk.cyan(`\n[9/${total}] 配置 adb 端口转发`));
@@ -120,25 +123,28 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
120
123
  });
121
124
  if (!await metroReady()) {
122
125
  console.log(chalk.gray(' 启动 Metro...'));
123
- const metroLog = fs.openSync(path.join(buildDir, '.metro.log'), 'a');
124
- const metro = spawnCmdAsync('npx', ['react-native', 'start'], {
125
- cwd: buildDir, detached: true, stdio: ['ignore', metroLog, metroLog]
126
- });
126
+ const metroLog = fs.openSync(path.join(buildDir, '.metro.log'), 'a'),
127
+ metro = spawnCmdAsync('npx', ['react-native', 'start'], {
128
+ cwd: buildDir, detached: true, stdio: ['ignore', metroLog, metroLog]
129
+ });
127
130
  metro.unref();
128
131
  let ready = false;
129
132
  for (let i = 0; i < 60; i++) {
130
133
  await new Promise(r => setTimeout(r, 500));
131
- if (await metroReady()) { ready = true; console.log(chalk.green(' ✓ Metro 已就绪(后台常驻,支持热更新)')); break; }
134
+ if (await metroReady()) { ready = true, console.log(chalk.green(' ✓ Metro 已就绪(后台常驻,支持热更新)')); break; }
132
135
  }
133
- if (!ready) console.log(chalk.yellow(' ⚠️ Metro 30 秒内未就绪,详见 .metro.log;app 将回落到本地 bundle'));
136
+ if (!ready) console.log(chalk.yellow(' ⚠️ Metro 30 秒内未就绪,详见 .metro.log;app 将回落到本地 bundle'));
134
137
  }
135
138
  console.log(chalk.cyan(`\n[10/${total}] 编译 Debug 包`));
136
139
  const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
137
- if (process.env.OPTIMIZE_DEBUG) {
138
- await runStream(gradlew, ['assembleDebug'], { cwd: path.join(buildDir, 'android'), env: { ...process.env, NODE_MOBILE_MODE: 'test' } });
139
- } else {
140
+ if (process.env.OPTIMIZE_DEBUG) await runStream(gradlew, ['assembleDebug'], {
141
+ cwd: path.join(buildDir, 'android'), env: { ...process.env, NODE_MOBILE_MODE: 'test' }
142
+ });
143
+ else {
140
144
  console.log(chalk.gray(' 编译中...'));
141
- run(gradlew, ['assembleDebug'], { cwd: path.join(buildDir, 'android'), env: { ...process.env, NODE_MOBILE_MODE: 'test' }, quiet: true });
145
+ run(gradlew, ['assembleDebug'], {
146
+ cwd: path.join(buildDir, 'android'), env: { ...process.env, NODE_MOBILE_MODE: 'test' }, quiet: true
147
+ });
142
148
  console.log(chalk.green(' ✓ 编译完成'));
143
149
  }
144
150
 
@@ -157,4 +163,4 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
157
163
  }
158
164
  };
159
165
 
160
- export { runTest };
166
+ export { runTest };
package/src/config.js CHANGED
@@ -75,7 +75,7 @@ const DEFAULT_CONFIG = {
75
75
  return merge(DEFAULT_CONFIG, mod.default || mod);
76
76
  }
77
77
  }
78
- console.log(chalk.yellow('未找到 mobileAppConfig.js,使用默认配置'));
78
+ console.log(chalk.yellow('未找到 mobileAppConfig.js,使用默认配置'));
79
79
  return DEFAULT_CONFIG;
80
80
  },
81
81
  validateConfig = (config, userProjectDir) => {
@@ -84,22 +84,19 @@ const DEFAULT_CONFIG = {
84
84
 
85
85
  const pkg = path.join(userProjectDir, 'package.json');
86
86
  if (!fs.existsSync(pkg)) errors.push('用户项目缺少 package.json');
87
- // 极简防呆:至少 2 段、每段非空、只含 [a-zA-Z0-9_-],不允许首尾点或连续点
88
- // 具体平台是否合法交给 Gradle / Xcode 报错(Android 不允许 -,iOS 允许)
89
- if (
90
- typeof config.appId !== 'string' ||
91
- !/^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(config.appId)
92
- )
93
- errors.push('appId 格式不合法(需反向域名格式,如 com.example.app): ' + config.appId);
87
+ // 极简防呆:至少 2 段、每段非空、只含 [a-zA-Z0-9_-],不允许首尾点或连续点
88
+ // 具体平台是否合法交给 Gradle / Xcode 报错(Android 不允许 -,iOS 允许)
89
+ if (typeof config.appId !== 'string' || !/^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(config.appId))
90
+ errors.push('appId 格式不合法(需反向域名格式,如 com.example.app): ' + config.appId);
94
91
  if (!Number.isInteger(config.versionCode) || config.versionCode < 1) errors.push('versionCode 必须是正整数');
95
92
  if (!config.android.abiFilters?.length) errors.push('android.abiFilters 不能为空');
96
93
  if (errors.length) {
97
- errors.forEach((e) => console.error(chalk.red(' - ' + e)));
94
+ errors.forEach(e => console.error(chalk.red(' - ' + e)));
98
95
  throw new Error('配置校验失败');
99
96
  }
100
97
  console.log(chalk.green(' ✓ 配置校验通过'));
101
98
  },
102
- resolveSdkPath = (config) => {
99
+ resolveSdkPath = config => {
103
100
  if (config.android && config.android.sdkPath) return config.android.sdkPath;
104
101
  for (const env of ['ANDROID_HOME', 'ANDROID_SDK_ROOT']) {
105
102
  const base = process.env[env];
@@ -113,4 +110,4 @@ const DEFAULT_CONFIG = {
113
110
  return null;
114
111
  };
115
112
 
116
- export { DEFAULT_CONFIG, loadConfig, validateConfig, resolveSdkPath };
113
+ export { DEFAULT_CONFIG, loadConfig, validateConfig, resolveSdkPath };