@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.
- package/Android /346/236/204/345/273/272/346/214/207/345/215/227.md" +38 -38
- package/CHANGELOG.md +8 -47
- package/bin/cli.js +5 -5
- package/mobileAppConfig.js +18 -18
- package/package.json +1 -1
- package/scripts/copy-files.js +1 -1
- package/src/commands/build.js +1 -1
- package/src/commands/test.js +21 -15
- package/src/config.js +8 -11
- package/src/copy.js +24 -28
- package/src/fingerprint.js +6 -6
- package/src/generate.js +7 -10
- package/src/install.js +13 -13
- package/src/optimize-node-modules.js +10 -8
- package/src/patcher.js +21 -21
- package/src/platform-ios.js +7 -7
- package/src/utils.js +13 -13
- package/template/App.tsx +83 -87
- package/template/index.js +1 -1
- package/template/metro.config.js +1 -3
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +68 -68
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Android 构建指南
|
|
2
2
|
|
|
3
|
-
本文档是 [README.md](./README.md)
|
|
3
|
+
本文档是 [README.md](./README.md) 的**深度附件**;日常使用(`test` / `build`)不需要看——CLI 自动处理;
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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
|
|
54
|
+
<buildDir>/nodejs-assets/BUILD_NATIVE_MODULES.txt # 1=有原生模块,0=无
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
- **检测到原生模块**(如 `bcrypt`、`sqlite3`):写 `1
|
|
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
|
|
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
|
|
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
|
|
84
|
+
从 `@flun/nodejs-mobile-react-native` 1.0.1 起,`libnode.so` **不在 npm 包里**,安装时由 postinstall 脚本从 Gitee / GitHub Release 自动下载:
|
|
85
85
|
|
|
86
|
-
- **下载源**:Gitee
|
|
87
|
-
- **下载内容**:`android-libnode.zip`(约 52 MB
|
|
86
|
+
- **下载源**:Gitee 优先,失败自动回退 GitHub
|
|
87
|
+
- **下载内容**:`android-libnode.zip`(约 52 MB),含 `bin/` + `include/`
|
|
88
88
|
- **覆盖目标**:`node_modules/@flun/nodejs-mobile-react-native/android/libnode/`
|
|
89
|
-
-
|
|
90
|
-
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
161
|
+
**体积对照**(实测,strip 后):
|
|
162
162
|
|
|
163
|
-
| 版本
|
|
164
|
-
|
|
|
165
|
-
| v18
|
|
166
|
-
| v22
|
|
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
|
|
168
|
+
体积差异约 50 MB,来自三部分:Node v22 核心引擎 + full-icu 数据(约 25–30 MB)+ V8 版本升级;**不是编译出错**;
|
|
169
169
|
|
|
170
170
|
---
|
|
171
171
|
|
|
172
172
|
## 三、Express 版本选择(踩坑清单)
|
|
173
173
|
|
|
174
|
-
**选错版本会在 Node
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
256
|
-
- **误区 2**:只改路由语法就想让 Express 5 跑 → 若 `libnode.so` 无 full-icu
|
|
257
|
-
- **误区 3**:用 `overrides` 强制替换 `path-to-regexp` →
|
|
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
|
|
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
|
+
## [4.0.4] - 2026-10-21 10:02
|
|
3
|
+
### 优化了大量文件细节
|
|
3
4
|
|
|
5
|
+
## [4.0.3] - 2026-10-21 08:28
|
|
4
6
|
### 修复
|
|
5
|
-
-
|
|
6
|
-
-
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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();
|
package/mobileAppConfig.js
CHANGED
|
@@ -1,24 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @flun/node-mobile-app 配置文件
|
|
3
|
-
* 放置在用户 Node.js
|
|
4
|
-
*
|
|
3
|
+
* 放置在用户 Node.js 项目根目录,CLI 会自动读取
|
|
4
|
+
* 如不提供此文件,CLI 使用默认配置
|
|
5
5
|
* 所有路径相对于用户项目根目录
|
|
6
6
|
*
|
|
7
7
|
* ===== 常用命令 =====
|
|
8
|
-
* npx node-mobile-app test 编译并安装到设备(需连 USB
|
|
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
|
|
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
|
|
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
|
-
//
|
|
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
|
|
128
|
+
// 不重置的模板文件(路径相对于 node-mobile-app-build/,如 'android/app/src/main/AndroidManifest.xml')
|
|
129
129
|
keepFiles: [],
|
|
130
|
-
// 额外 RN 原生依赖包名数组(用于 autolink
|
|
131
|
-
// 安装命令用 --save-dev
|
|
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
package/scripts/copy-files.js
CHANGED
|
@@ -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}`);
|
package/src/commands/build.js
CHANGED
package/src/commands/test.js
CHANGED
|
@@ -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 {
|
|
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',
|
|
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
|
|
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
|
-
|
|
125
|
-
|
|
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
|
|
134
|
+
if (await metroReady()) { ready = true, console.log(chalk.green(' ✓ Metro 已就绪(后台常驻,支持热更新)')); break; }
|
|
132
135
|
}
|
|
133
|
-
if (!ready) console.log(chalk.yellow(' ⚠️ Metro 30
|
|
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
|
-
|
|
139
|
-
}
|
|
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'], {
|
|
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 不允许
|
|
89
|
-
if (
|
|
90
|
-
|
|
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(
|
|
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 =
|
|
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 };
|