@flun/node-mobile-app 2.1.1 → 3.0.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.
- package/Android /346/236/204/345/273/272/346/214/207/345/215/227.md" +1 -1
- package/CHANGELOG.md +34 -32
- package/README.md +36 -7
- package/bin/cli.js +11 -10
- package/mobileAppConfig.js +10 -0
- package/package.json +15 -13
- package/scripts/copy-files.js +24 -25
- package/src/commands/build.js +19 -14
- package/src/commands/clean.js +6 -2
- package/src/commands/test.js +26 -25
- package/src/config.js +6 -1
- package/src/copy.js +8 -23
- package/src/fingerprint.js +28 -8
- package/src/install.js +100 -55
- package/src/optimize-node-modules.js +698 -0
- package/src/patcher.js +21 -24
- package/src/platform-ios.js +139 -141
- package/src/utils.js +189 -176
|
@@ -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,37 +1,39 @@
|
|
|
1
1
|
# 变更日志
|
|
2
2
|
|
|
3
|
-
## [
|
|
4
|
-
###
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
> **注意**:lint 规则集与原先不同,升级后对同一份代码可能报出不同的检查结果。这只影响静态检查,不影响构建产物与运行。
|
|
3
|
+
## [3.0.0] - 2026-10-13 21:19
|
|
4
|
+
### 新增
|
|
5
|
+
- **node_modules 优化系统(默认开启)**:构建时自动执行六阶段优化,大幅减少打包文件数、加速启动。不改变项目源码,对任意 Node.js 项目通用;
|
|
6
|
+
- **阶段 1 合并 JS**:按包入口 bundle,多个 JS 文件合并为单文件(`bundled/` 目录下的 `.cjs` / `.mjs`);CJS 保持 CJS、ESM 保持 ESM;
|
|
7
|
+
- **阶段 2 清空资源**:扫描各包 `postinstall` 引用的目录/文件,保留目录结构、清空内容;被 import/require 的保留不删;
|
|
8
|
+
- **阶段 3 清平台二进制**:按目标平台 + `abiFilters` 逐文件判定,删除非本机二进制(三层识别:目录名、文件后缀、文件头魔数);
|
|
9
|
+
- **阶段 4 清开发文件**:删除 `.md` / `.map` / `.d.ts` / `.h` / `.c` 等非运行文件;**原生模块包(含 `binding.gyp`)内的编译必需文件保留**,确保原生编译通过;
|
|
10
|
+
- **阶段 5 清中间产物**;**阶段 6 清不参与运行的文件和空目录**;
|
|
11
|
+
- **优化缓存(快照 + 指纹)**:`.deps-snapshot.json` 记录依赖、目标平台、架构、运行时 Node 主版本、优化代码指纹、上次构建是否完成;
|
|
12
|
+
- 构建时自动判断并执行三种动作:**重装依赖并优化** / **仅优化** / **跳过**,日志说明当次动作与原因;
|
|
13
|
+
- 运行时 Node 主版本从 `libnode/include/node/node_version.h` 读取(读不到则报错退出);
|
|
14
|
+
- **模板指纹**:`template/` 目录内容纳入缓存指纹,模板改动自动触发重编;
|
|
15
|
+
- **`--debug` 调试开关**:`npx node-mobile-app build --debug` / `test --debug` 显示优化各阶段明细与各命令完整输出;
|
|
16
|
+
- **优化逃生舱口**(`mobileAppConfig.js`):`optimize: { enabled: false }` 整体关闭优化;`optimize: { exclude: ['包名'] }` 精确排除特定包;
|
|
17
|
+
- **构建进度提示**:默认模式在打包阶段显示「打包进行中...」,避免长时间无输出被误认为卡死;`--debug` 下输出已完整,无需此提示;
|
|
20
18
|
|
|
21
19
|
### 修复
|
|
20
|
+
- **`bundled` 目录在 APK 中丢失(重要)**:aapt2 默认过滤 `<dir>_*`,原产物目录名以下划线开头,被打包时整体丢弃,应用启动报 `Node assets copy failed`;产物目录改用 `bundled`,绕开该过滤;
|
|
21
|
+
- **scripts 引用保护过宽**:原逻辑保护 `package.json` `scripts` 引用的所有 js(含 test / lint),但移动端无安装期、这些脚本永不执行;移除该保护,测试脚本等被正常清理;
|
|
22
|
+
- **`run` 输出策略**:`run` 默认实时输出(inherit);新增 `quiet` 参数——捕获输出、成功静默、失败才打印,用于 `gradlew` / `xcodebuild` 等命令;
|
|
23
|
+
- **`clean` 清理不完整**:原只清 `node-mobile-app-build` 与插件 `.cxx`,现补上插件 `android/build`,确保清环境彻底;
|
|
24
|
+
|
|
25
|
+
### 升级注意事项
|
|
26
|
+
- **3.0.0 起 node_modules 优化默认开启**,无需配置即享优化收益;
|
|
27
|
+
- **升级后首次构建**会重装依赖并执行优化,比平时慢;后续构建命中缓存后无额外开销;
|
|
28
|
+
- 若**构建或运行异常**,按以下顺序排查:
|
|
29
|
+
1. 在 `mobileAppConfig.js` 中设置 `optimize: { enabled: false }` 关闭优化,验证是否为优化引起;
|
|
30
|
+
2. 若确认是某个包合并后异常,用 `optimize: { exclude: ['包名'] }` 排除优化时异常或不想优化的包;
|
|
31
|
+
3. 若需彻底重来,删除 `node-mobile-app-build` 目录后重新构建。
|
|
32
|
+
|
|
33
|
+
## [2.1.2] - 2026-09-29 19:53
|
|
34
|
+
### 更改
|
|
35
|
+
- 加固了拷贝文件的一些逻辑;
|
|
22
36
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
## [2.0.8] - 2026-09-28 22:17
|
|
27
|
-
### 修复
|
|
28
|
-
|
|
29
|
-
- **中文路径构建失败**:三处联动修复。
|
|
30
|
-
- **AGP 拒绝非 ASCII 路径**:`template/android/gradle.properties` 增加 `android.overridePathCheck=true`,允许中文路径构建。
|
|
31
|
-
- **`gradle.properties` 中文乱码**:该文件按 ISO-8859-1 读取,非 ASCII 字符(如路径中的「桌面」)会被读成乱码,导致 keystore 找不到。`patcher.js` 现在把写入的非 ASCII 字符转成 `\uXXXX` 转义(Java Properties 规范)。
|
|
32
|
-
- 影响范围:项目路径含中文(或其它非 ASCII 字符)时,release 构建曾失败于 `Your project path contains non-ASCII characters` 与 `Keystore file ... not found`。
|
|
33
|
-
- **签名配置缺项时报错含糊**:`android.signing.keystore` 已配置、但密码 / 别名 / 别名密码为空时,原先会一路跑到 Gradle 才报 `keystore password was incorrect`,容易误导排查方向。现在 `patcher.js` 在构建前即校验四项,缺项时抛出明确错误,并提示「仅本地测试可注释掉整个 signing 块」。
|
|
34
|
-
|
|
35
|
-
### 变更
|
|
36
|
-
|
|
37
|
-
- **`mobileAppConfig.js` 模板**:`signing` 块内每项标注「必填」,并说明「取消整块注释即视为启用签名」。
|
|
37
|
+
## [2.1.1] - 2026-09-29 13:55
|
|
38
|
+
### 优化
|
|
39
|
+
- 优化了 'README.md' 文档;
|
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 优化(默认开启)**:构建时自动合并 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. **合并 JS**:按包入口 bundle,多个 JS 文件合并为单文件(`bundled/` 目录);
|
|
354
|
+
2. **清空资源**:扫描各包 `postinstall` 引用的目录/文件,保留目录结构、清空内容;被 import/require 的保留;
|
|
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
|
|
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
|
-
--
|
|
18
|
-
--
|
|
19
|
-
--
|
|
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();
|
package/mobileAppConfig.js
CHANGED
|
@@ -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": "
|
|
3
|
+
"version": "3.0.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
|
-
"
|
|
63
|
-
"
|
|
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
|
-
"
|
|
69
|
-
"
|
|
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": {
|
package/scripts/copy-files.js
CHANGED
|
@@ -2,34 +2,33 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// 要复制的文件位置、目标目录和目标文件路径
|
|
6
6
|
const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__filename),
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
sourceFile = path.join(__dirname, '..', fileName), targetFile = path.join(targetDir, fileName),
|
|
10
|
-
/**
|
|
11
|
-
* 复制文件到项目根目录
|
|
12
|
-
* >查看定义:@see {@link copyFile}
|
|
13
|
-
* @returns {boolean} - 复制是否成功
|
|
14
|
-
*/
|
|
15
|
-
copyFile = () => {
|
|
16
|
-
console.log(`🔍 检查 ${fileName} 文件...`), console.log(`📁 项目根目录:${targetDir}`);
|
|
17
|
-
try {
|
|
18
|
-
// 无 INIT_CWD(非用户项目安装场景)|| 目标目录不是 Node 项目 || 包自身的 npm install(开发调试)→ 跳过
|
|
19
|
-
if (!targetDir) return true;
|
|
20
|
-
if (!fs.existsSync(path.join(targetDir, 'package.json'))) return true;
|
|
21
|
-
if (targetDir === path.resolve(__dirname, '..')) return true;
|
|
22
|
-
if (fs.existsSync(targetFile)) return true; // 目标文件已存在 → 直接返回
|
|
23
|
-
console.log(`⚠️ 在项目根目录未找到 ${fileName} 文件,正在创建...`);
|
|
7
|
+
fileName = 'mobileAppConfig.js', sourceFile = path.join(__dirname, '..', fileName),
|
|
8
|
+
targetDir = path.resolve(process.env.INIT_CWD || process.cwd()), targetFile = path.join(targetDir, fileName);
|
|
24
9
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
10
|
+
/**
|
|
11
|
+
* 复制文件到项目根目录
|
|
12
|
+
* >查看定义:@see {@link copyFile}
|
|
13
|
+
* @returns {boolean} - 复制是否成功
|
|
14
|
+
*/
|
|
15
|
+
const copyFile = () => {
|
|
16
|
+
console.log(`🔍 检查 ${fileName} 文件...`), console.log(`📁 项目根目录:${targetDir}`);
|
|
17
|
+
try {
|
|
18
|
+
// 目标目录不是 Node 项目 || 包自身的 npm install(开发调试)→ 跳过
|
|
19
|
+
if (!fs.existsSync(path.join(targetDir, 'package.json'))) return true;
|
|
20
|
+
if (targetDir === path.resolve(__dirname, '..')) return true;
|
|
21
|
+
if (fs.existsSync(targetFile)) return true; // 目标文件已存在 → 直接返回
|
|
22
|
+
console.log(`⚠️ 在项目根目录未找到 ${fileName} 文件,正在创建...`);
|
|
23
|
+
|
|
24
|
+
fs.copyFileSync(sourceFile, targetFile); // 复制源文件到项目根目录
|
|
25
|
+
console.log(`✓ 已创建 ${fileName} 示例文件:${targetFile}`);
|
|
26
|
+
return true;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
console.error(`✗ 创建 ${fileName} 文件失败:`, error.message);
|
|
29
|
+
return false;
|
|
32
30
|
}
|
|
31
|
+
};
|
|
33
32
|
|
|
34
33
|
// 执行脚本并导出函数
|
|
35
34
|
if (process.argv[1] === __filename) copyFile();
|
package/src/commands/build.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
43
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
59
|
-
console.log(
|
|
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(
|
|
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 };
|
package/src/commands/clean.js
CHANGED
|
@@ -8,8 +8,12 @@ const runClean = async () => {
|
|
|
8
8
|
targets = [
|
|
9
9
|
{ label: '构建目录', path: buildDir },
|
|
10
10
|
{
|
|
11
|
-
label: '
|
|
12
|
-
'
|
|
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
|
|
package/src/commands/test.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
40
|
-
console.log(
|
|
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(
|
|
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(
|
|
47
|
+
console.log(`\n[6/${total}] 生成桥接文件`);
|
|
49
48
|
generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
|
|
50
49
|
|
|
51
50
|
if (isIos) {
|
|
52
|
-
console.log(
|
|
53
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
}
|