@flun/node-mobile-app 4.1.2 → 4.2.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.
@@ -129,7 +129,7 @@ NODE_MOBILE_PREBUILT_ANDROID_SKIP=1 # 只跳过 Android
129
129
  **预编译产物仓库**:
130
130
 
131
131
  - GitHub:https://github.com/OpenFlun/nodejs-mobile
132
- - Gitee(国内):https://gitee.com/OpenFlun/nodejs-mobile
132
+ - Gitee(中国):https://gitee.com/OpenFlun/nodejs-mobile
133
133
 
134
134
  **架构支持(按版本)**:
135
135
 
package/CHANGELOG.md CHANGED
@@ -1,32 +1,25 @@
1
1
  # 变更日志
2
2
 
3
+ ## [4.2.0] - 2026-11-09 15:33
4
+ ### 新增
5
+ - 新增 `android.gradleDistributionUrl` 配置项,可指定 Gradle 发行版的完整下载地址;未配置时按腾讯、阿里、
6
+ 官方顺序探测,三者均不可达则回落官方并警告;已配置时直接使用,失败不回退,并提示可改回 null 或换可达地址;
7
+ - 新增 `android.mavenRepositories` 配置项,可自定义 Maven 仓库数组,同时替换 `settings.gradle` 与根
8
+ `build.gradle` 的仓库列表;未配置时保持模板默认(阿里云 public/google/gradle-plugin + 官方 google/mavenCentral);
9
+ ### 优化
10
+ - 导出函数统一改为独立 `const` 声明并按字母序集中导出,未导出内容仍用逗号链;导出项补全标准 JSDoc;
11
+ - 各文件 import 统一为第三方包在前、相对路径在后、各自字母序,花括号内名字按大小写不敏感字母序;
12
+ - 删除无调用方的 `readInstallState`,移除 `ensureInstallable` 未使用的 `buildDir` 参数;
13
+ - 模板 `App.tsx`、`.prettierrc.js`、`babel.config.js`、`eslint.config.js`、`metro.config.js` 改为
14
+ `const` 声明加末尾导出;
15
+
3
16
  ## [4.1.2] - 2026-11-07 22:27
4
17
  ### 修复
5
18
  - 修复 Android 构建长时间卡在 Gradle 下载的问题;模板 `gradle-wrapper.properties` 的 `distributionUrl`
6
- 由 `services.gradle.org` 改为腾讯云镜像,国内直连官方源常超时且不报错;
19
+ 由 `services.gradle.org` 改为腾讯云镜像,中国直连官方源常超时且不报错;
7
20
  - 修复 `--debug` 下 Gradle 进度条被拆成大量点与数字的问题;调试模式编译改为 `stdio: 'inherit'`,
8
21
  让进度条直连终端刷新,不再经管道逐块拆分;
9
22
 
10
23
  ## [4.1.1] - 2026-11-06 11:11
11
24
  ### 优化
12
- - 细节优化;
13
-
14
- ## [4.1.0] - 2026-11-06 10:10
15
- ### 新增
16
- - 新增 `example` 子命令,重新生成 `mobileAppConfig.js` 到项目根目录;此前该文件只由 `postinstall` 自动复制,
17
- 误删后无法手动恢复;文件已存在时跳过,不覆盖用户的修改;
18
-
19
- ### 优化
20
- - 命令行帮助改用分组结构,按「命令」「选项」分节,子说明以 `- ` 前缀对齐,便于区分每个命令的注释边界;
21
- - 帮助与运行输出统一使用 chalk 按语义着色:`用法:` 用 `blueBright`,分组名用 `cyan`,
22
- 命令与选项名用 `cyanBright`,说明用 `gray`,成功用 `green` 与 `greenBright`,错误用 `red`;
23
- - `scripts/copy-files.js` 的输出改用同套着色,移除 emoji 前缀;
24
- - 未知命令分支由逗号链跨行改为分号结束的多行块,符合行宽与语句分隔规则;
25
-
26
- ### 修复
27
- - 指南文件名由 `Android 构建指南.md` 改为 `Android构建指南.md`,去除空格;
28
- README 中的链接同步去除 `%20` 编码,此前指向不存在的路径;
29
-
30
- ### 文档
31
- - README 命令列表与 `mobileAppConfig.js` 的「常用命令」注释补 `example`;
32
- - README 包目录结构按 `files` 字段顺序重排;
25
+ - 细节优化;
package/README.md CHANGED
@@ -14,6 +14,8 @@
14
14
  - **自动识别服务地址**:拦截 `server.js` 里的 `listen()` 调用,动态获取真实协议和端口
15
15
  - **失败可见**:5 秒内未启动,加载异常,App 显示错误页并支持展开详情(开发者可查)
16
16
  - **增量构建**:文件指纹判断缓存,无变化时 10 秒级重跑
17
+ - **内置镜像探测 + 可自定义**:Gradle 发行版默认走腾讯云,未配置时按腾讯、阿里、官方顺序探测;Maven 仓库默认
18
+ 阿里云 public/google/gradle-plugin 加官方源,两者均可在 `mobileAppConfig.js` 里自定义;
17
19
  - **跨平台**:Android(Windows / macOS / Linux)+ iOS(仅 macOS)
18
20
  - **离线可用**:`build` 打出的 APK / IPA 内置 JS,无需连电脑也能跑
19
21
  - **node_modules 优化(默认开启)**:构建时先交叉编译原生模块,再自动执行六阶段优化--先清空 postinstall 资源(保留 js 供合并),
@@ -154,7 +156,7 @@ npm i -D @flun/node-mobile-app
154
156
  ### 快速开始
155
157
 
156
158
  ```bash
157
- # 1. 编辑配置:打开 mobileAppConfig.js,改 appId / appName / serverPath 等
159
+ # 1. 编辑配置:打开 mobileAppConfig.js,改必填项 serverPath 或完整自定义;
158
160
 
159
161
  # 2. 开发调试(需 adb 可连的设备,走 Metro 热更新)
160
162
  npx node-mobile-app test
@@ -231,9 +233,28 @@ adb install -r "./out/myapp-1.0.0-release.apk"
231
233
 
232
234
  ## 参考配置
233
235
 
234
- 所有定制集中在这一个文件;**不配置也能跑**(用默认值);
236
+ 所有定制集中在这一个文件;`serverPath` 字段值是必填项;
235
237
 
236
- ### 必填
238
+ ### 入口与标识
239
+
240
+ - `serverPath`
241
+ - 类型:`string`
242
+ - 默认:`'./server.js'`
243
+ - 说明:Node.js 启动脚本路径,指向的文件必须存在
244
+ - `appId`
245
+ - 类型:`string`
246
+ - 默认:`'com.example.app'`
247
+ - 说明:应用唯一标识(反向域名格式,Android/iOS 通用)
248
+ - `versionCode`
249
+ - 类型:`int`
250
+ - 默认:`1`
251
+ - 说明:内部版本号,必须为正整数
252
+
253
+ 构建前会校验以下项,不满足即报错:用户项目根必须有 `package.json`;`appId` 需为反向域名格式;
254
+ `versionCode` 需为正整数;`android.abiFilters` 不能为空;`android.gradleDistributionUrl` 与
255
+ `android.mavenRepositories` 若配置,格式不合法也会报错(见 Android 章节).
256
+
257
+ ### 应用信息
237
258
 
238
259
  - `appName`
239
260
  - 类型:`string \| null`
@@ -243,35 +264,44 @@ adb install -r "./out/myapp-1.0.0-release.apk"
243
264
  - 类型:`string \| null`
244
265
  - 默认:`null`
245
266
  - 说明:APK / IPA 输出目录(默认 `./out`)
246
- - `showLoading`
267
+ - `versionName`
268
+ - 类型:`string`
269
+ - 默认:`'1.0.0'`
270
+ - 说明:用户可见版本名(Android versionName / iOS CFBundleShortVersionString)
271
+
272
+ ### WebView
273
+
274
+ 以下字段均位于 `webview` 对象下:
275
+
276
+ - `webview.showLoading`
247
277
  - 类型:`boolean`
248
278
  - 默认:`true`
249
279
  - 说明:是否显示加载指示器
250
- - `loadingText`
280
+ - `webview.loadingText`
251
281
  - 类型:`string`
252
282
  - 默认:`'正在启动服务...'`
253
283
  - 说明:加载提示文字
254
- - `enableJavaScript`
284
+ - `webview.enableJavaScript`
255
285
  - 类型:`boolean`
256
286
  - 默认:`true`
257
287
  - 说明:是否启用 JavaScript
258
- - `enableDomStorage`
288
+ - `webview.enableDomStorage`
259
289
  - 类型:`boolean`
260
290
  - 默认:`true`
261
291
  - 说明:是否启用 DOM Storage
262
- - `allowFileAccess`
292
+ - `webview.allowFileAccess`
263
293
  - 类型:`boolean`
264
294
  - 默认:`true`
265
295
  - 说明:是否允许访问本地文件
266
- - `mixedContentMode`
296
+ - `webview.mixedContentMode`
267
297
  - 类型:`string`
268
298
  - 默认:`'always'`
269
299
  - 说明:混合内容模式:`never` / `always` / `compatibility`
270
- - `backgroundColor`
300
+ - `webview.backgroundColor`
271
301
  - 类型:见下
272
302
  - 默认:`'#ffffff'`
273
303
  - 说明:WebView 背景色;支持:固定色 `'#ffffff'` / 跟随系统 `'system'` / 深浅两值 `{ light, dark }`
274
- - `allowSelfSignedCert`
304
+ - `webview.allowSelfSignedCert`
275
305
  - 类型:`boolean`
276
306
  - 默认:`false`
277
307
  - 说明:见下文"HTTPS 自签证书"
@@ -302,6 +332,16 @@ adb install -r "./out/myapp-1.0.0-release.apk"
302
332
  - 类型:`string`
303
333
  - 默认:`'37.0.0'`
304
334
  - 说明:构建工具版本
335
+ - `gradleDistributionUrl`
336
+ - 类型:`string \| null`
337
+ - 默认:`null`
338
+ - 说明:Gradle 发行版的完整下载地址.未配置时按腾讯、阿里、官方顺序探测,三者均不可达则回落官方并警告;
339
+ 已配置时直接使用,失败不回退,并提示可改回 `null` 或换可达地址
340
+ - `mavenRepositories`
341
+ - 类型:`string[] \| null`
342
+ - 默认:`null`
343
+ - 说明:自定义 Maven 仓库数组,同时替换 `settings.gradle` 与根 `build.gradle` 的仓库列表;未配置时保持模板默认
344
+ (阿里云 `public`/`google`/`gradle-plugin` + 官方 `google`/`mavenCentral`)
305
345
  - `abiFilters`
306
346
  - 类型:`string[]`
307
347
  - 默认:`['arm64-v8a']`
@@ -591,14 +631,28 @@ adb logcat | grep -E "NODEJS-MOBILE|ReactNativeJS"
591
631
  ### Q:Android 构建卡在拉取依赖(`react-android` / `hermes-android` 等)?
592
632
 
593
633
  Android 模板的 `settings.gradle` 与 `build.gradle` 已在 `google()` / `mavenCentral()` 之前插入阿里云镜像,
594
- 国内网络优先走镜像;海外网络镜像不命中会自动回退官方源,不影响功能.
634
+ 中国网络优先走镜像;海外网络镜像不命中会自动回退官方源,不影响功能.
595
635
 
596
636
  若仍卡住:
597
637
 
598
638
  - 检查镜像可达:浏览器打开 `https://maven.aliyun.com/repository/public` 是否正常;
599
639
  - 检查本机 `~/.gradle/init.gradle` 是否有与仓库顺序冲突的配置(如强制替换仓库),有则临时移除以排除干扰;
640
+ - 若所在网络对默认镜像均不友好,可在 `mobileAppConfig.js` 里配置 `android.mavenRepositories` 指定自己的仓库数组,
641
+ 配置后会完全替换模板默认仓库,未配置则保持模板默认;
600
642
  - 停掉残留 Daemon 后重试:在 `node-mobile-app-build/android/` 下执行 `gradlew --stop`,再重跑 `test` / `build`.
601
643
 
644
+ ### Q:Android 构建卡在下载 Gradle 发行版(`gradle-*-bin.zip`)?
645
+
646
+ Gradle Wrapper 下载发行版走单个 `distributionUrl`,没有多 URL 自动回退机制.默认模板指向腾讯云镜像,
647
+ 中国网络可直接使用.若卡住:
648
+
649
+ - 直接配置完整地址:`android.gradleDistributionUrl` 填一个可达的 `gradle-*-bin.zip` 地址;
650
+ 配置后不再探测,失败不回退,会提示改回 `null` 或换可达地址;
651
+ - 交回自动探测:把 `android.gradleDistributionUrl` 设为 `null`,工具会按腾讯、阿里、官方顺序探测,
652
+ 三者均不可达时回落官方并警告;
653
+ - Wrapper 只对单个 URL 做重试,不会在多个地址间切换;若地址本身不可达,换地址或改回 `null` 走探测才是有效手段;
654
+ - 已下载完成的部分会留在 `~/.gradle/wrapper/dists/` 下,下次构建复用,不会重复下载.
655
+
602
656
  ### Q:Express 版本怎么选?
603
657
 
604
658
  - **Android**:内置完整 ICU(full-icu)→ 可以用 Express 5
@@ -658,6 +712,6 @@ ISC
658
712
  ## 贡献
659
713
 
660
714
  - 源码:[GitHub](https://github.com/OpenFlun/node-mobile-app)
661
- - 中国:[gitee](https://gitee.com/OpenFlun/node-mobile-app)
715
+ - 中国镜像:[gitee](https://gitee.com/OpenFlun/node-mobile-app)
662
716
  - 问题反馈:[Issues](https://github.com/OpenFlun/node-mobile-app/issues)
663
717
  - 主页:[npm 包页面](https://www.npmjs.com/package/@flun/node-mobile-app#readme)
package/bin/cli.js CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import chalk from 'chalk';
3
- import { runTest } from '../src/commands/test.js';
3
+ import { copyFile } from '../scripts/copy-files.js';
4
4
  import { runBuild } from '../src/commands/build.js';
5
5
  import { runClean } from '../src/commands/clean.js';
6
- import { copyFile } from '../scripts/copy-files.js';
6
+ import { runTest } from '../src/commands/test.js';
7
7
 
8
8
  // 输出用法;
9
9
  const printHelp = () => {
@@ -34,7 +34,7 @@ const printHelp = () => {
34
34
  /**
35
35
  * 解析 --key value 参数,重复键覆盖为后者;
36
36
  * @param {string[]} args 子命令后的参数
37
- * @returns {Object} 解析结果
37
+ * @returns {object} 解析结果
38
38
  */
39
39
  parseFlags = args => {
40
40
  const f = {};
@@ -20,8 +20,10 @@
20
20
  * App 使用该地址加载页面,无需在此配置;
21
21
  * 若 5 秒内未能识别,App 显示启动失败并可在页面展开详情查看原因;
22
22
  * 动态端口(listen(0))会自动获取系统分配的真实端口;
23
+ *
24
+ * >查看定义:@see {@link mobileAppConfig}
23
25
  */
24
- export default {
26
+ const mobileAppConfig = {
25
27
  // ===== 必填 =====
26
28
  serverPath: './server.js', // Node.js 启动脚本路径
27
29
 
@@ -63,6 +65,15 @@ export default {
63
65
  targetSdkVersion: 36, // 目标 SDK
64
66
  compileSdkVersion: 37, // 编译 SDK
65
67
  buildToolsVersion: '37.0.0', // 构建工具版本
68
+ gradleDistributionUrl: null, // 下载地址,示例:'https://mirrors.cloud.tencent.com/gradle/gradle-9.5.1-bin.zip'
69
+ // Maven 仓库,用于 buildscript 插件与 allprojects 依赖;按顺序尝试,失败回退下一个
70
+ // 未配置时使用内置默认:阿里云 public/google/gradle-plugin + 官方 google/mavenCentral
71
+ mavenRepositories: null, // 示例见下,按需取消注释
72
+ // mavenRepositories: [
73
+ // 'https://maven.aliyun.com/repository/public', // Maven Central:通用开源库
74
+ // 'https://maven.aliyun.com/repository/google', // Google Maven:AGP、AndroidX
75
+ // 'https://maven.aliyun.com/repository/gradle-plugin', // Gradle 插件门户:插件本体
76
+ // ],
66
77
  abiFilters: ['arm64-v8a'], // 打包架构(真机用 arm64-v8a, 模拟器加 x86_64)
67
78
  icon: './build/icon.png', // 应用图标(建议 512x512 PNG)
68
79
  usesCleartextTraffic: true, // 是否允许明文流量(http)
@@ -141,7 +152,7 @@ export default {
141
152
  '.vscode/',
142
153
  '.idea/',
143
154
  '.vs/',
144
- 'node_modules/', // 如需保留请删除此行
155
+ 'node_modules/',
145
156
  'sessions/',
146
157
  'users.json',
147
158
  '.env',
@@ -163,5 +174,7 @@ export default {
163
174
  'unrs-resolver': true,
164
175
  '@parcel/watcher': true,
165
176
  'bcrypt': true,
166
- },
167
- };
177
+ }
178
+ };
179
+
180
+ export default mobileAppConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flun/node-mobile-app",
3
- "version": "4.1.2",
3
+ "version": "4.2.0",
4
4
  "description": "将 Node.js 项目一键打包为 Android / iOS 移动应用,运行时由 @flun/nodejs-mobile-react-native 提供;",
5
5
  "author": "flun <open@flun.top>",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
+ import chalk from 'chalk';
1
2
  import fs from 'fs';
2
3
  import path from 'path';
3
- import chalk from 'chalk';
4
4
  import { fileURLToPath } from 'url';
5
5
 
6
6
  // 要复制的文件位置、目标目录和目标文件路径
@@ -10,7 +10,8 @@ const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__fi
10
10
 
11
11
  /**
12
12
  * 复制配置文件到项目根目录
13
- * @returns {boolean} - 复制是否成功
13
+ * >查看定义:@see {@link copyFile}
14
+ * @returns {boolean} 复制是否成功
14
15
  */
15
16
  const copyFile = () => {
16
17
  console.log(chalk.gray('项目根目录: ') + targetDir);
@@ -1,120 +1,126 @@
1
- import path from 'path';
1
+ import chalk from 'chalk';
2
2
  import fs from 'fs';
3
+ import path from 'path';
3
4
  import { fileURLToPath } from 'url';
4
5
  import { loadConfig, validateConfig } from '../config.js';
5
6
  import {
6
- getBuildDir, copyTemplate, copyUserProject, writeLocalProperties, ensureUserEntry, ensureUserConfig, patchAndroidAbi,
7
- patchExtraRnDependencies
7
+ copyTemplate, copyUserProject, ensureUserConfig, ensureUserEntry, getBuildDir, patchAndroidAbi,
8
+ patchExtraRnDependencies, writeLocalProperties
8
9
  } from '../copy.js';
10
+ import { checkAndResetCache } from '../fingerprint.js';
11
+ import { generateMainJs, generateRuntime } from '../generate.js';
9
12
  import { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl } from '../install.js';
10
13
  import { markVerified } from '../optimize-node-modules.js';
11
- import { generateMainJs, generateRuntime } from '../generate.js';
12
- import { checkAndResetCache } from '../fingerprint.js';
13
14
  import {
14
- patchAppConfig, patchAndroidConfig, patchSigning, patchIcon, patchIosConfig, patchIosIcon, resolveAppName
15
+ patchAndroidConfig, patchAppConfig, patchIcon, patchIosConfig, patchIosIcon, patchMavenRepositories,
16
+ patchSigning, resolveAppName
15
17
  } from '../patcher.js';
16
- import { ensureMac, checkIosTools, podInstall, xcodebuildArchive, xcodebuildExport } from '../platform-ios.js';
17
- import { run, ensureDir, sanitizeFileName } from '../utils.js';
18
- import chalk from 'chalk';
18
+ import { checkIosTools, ensureMac, podInstall, xcodebuildArchive, xcodebuildExport } from '../platform-ios.js';
19
+ import { ensureDir, run, sanitizeFileName, warnGradleDownloadHint } from '../utils.js';
20
+
21
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
22
+ /**
23
+ * 构建桌面应用:读取配置、校验、生成工程、安装依赖、编译、打包
24
+ * >查看定义:@see {@link runBuild}
25
+ * @param {object} flags 命令行参数,含 ios / release
26
+ * @returns {Promise<void>}
27
+ */
28
+ const runBuild = async (flags) => {
29
+ const userProjectDir = process.cwd(), release = !!flags.release, isIos = !!flags.ios, total = isIos ? 8 : 7;
30
+
31
+ console.log(chalk.cyan(
32
+ `\n=== node-mobile-app build (${isIos ? 'iOS' : 'Android'}, ${release ? 'release' : 'debug'}) ===\n`
33
+ ));
34
+ console.log(chalk.gray('用户项目: ' + userProjectDir));
35
+ if (isIos) ensureMac(), checkIosTools();
36
+ console.log(chalk.cyan(`\n[1/${total}] 检查用户配置与入口文件`)), ensureUserConfig(userProjectDir, PKG_ROOT);
37
+
38
+ const config = await loadConfig(userProjectDir);
39
+ ensureUserEntry(userProjectDir, config, PKG_ROOT), validateConfig(config, userProjectDir);
40
+
41
+ const buildDir = getBuildDir(userProjectDir);
42
+ console.log(chalk.cyan(`\n[2/${total}] 重置 RN 工程模板`));
43
+ await copyTemplate(PKG_ROOT, buildDir, config);
44
+ patchAndroidAbi(buildDir, config);
45
+ patchExtraRnDependencies(buildDir, userProjectDir, config);
46
+ patchAppConfig(buildDir, userProjectDir, config);
47
+ patchAndroidConfig(buildDir, config), patchMavenRepositories(buildDir, config);
48
+ patchSigning(buildDir, userProjectDir, config);
49
+ await patchIcon(buildDir, userProjectDir, config);
50
+ patchIosConfig(buildDir, userProjectDir, config);
51
+ await patchIosIcon(buildDir, userProjectDir, config);
52
+
53
+ console.log(chalk.cyan(`\n[3/${total}] 检查缓存指纹`)), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
54
+ console.log(chalk.cyan(`\n[4/${total}] 复制用户项目`));
55
+ const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
56
+ console.log(chalk.cyan(`\n[5/${total}] 安装依赖与编译优化`));
57
+ await installUserDeps(targetDir, userProjectDir, config, buildDir, isIos ? 'ios' : 'android');
58
+ patchNodejsMobilePlugin(userProjectDir, config);
59
+ patchWebViewSsl(userProjectDir, config);
60
+ writeLocalProperties(buildDir, config);
61
+
62
+ console.log(chalk.cyan(`\n[6/${total}] 生成桥接文件`));
63
+ generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
64
+
65
+ const appName = resolveAppName(userProjectDir, config), safeName = sanitizeFileName(appName),
66
+ outDir = path.resolve(userProjectDir, config.outputDir || './out');
67
+ ensureDir(outDir);
68
+
69
+ if (isIos) {
70
+ console.log(chalk.cyan(`\n[7/${total}] pod install`)), podInstall(path.join(buildDir, 'ios'));
71
+ console.log(chalk.cyan(`\n[8/${total}] xcodebuild archive + export`));
72
+ xcodebuildArchive(buildDir, config, release, userProjectDir);
73
+
74
+ const ipaPath = xcodebuildExport(buildDir, config, release, outDir, userProjectDir),
75
+ dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.ipa`,
76
+ dst = path.join(outDir, dstName);
77
+ fs.copyFileSync(ipaPath, dst), console.log(chalk.green('\n✅ 打包完成'));
78
+
79
+ const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
80
+ console.log(chalk.gray(` ${dst} (${mb} MB)`)), await markVerified(buildDir);
81
+ return;
82
+ }
19
83
 
20
- const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'),
21
- runBuild = async (flags) => {
22
- const userProjectDir = process.cwd(), release = !!flags.release, isIos = !!flags.ios, total = isIos ? 8 : 7;
23
-
24
- console.log(chalk.cyan(
25
- `\n=== node-mobile-app build (${isIos ? 'iOS' : 'Android'}, ${release ? 'release' : 'debug'}) ===\n`
26
- ));
27
- console.log(chalk.gray('用户项目: ' + userProjectDir));
28
- if (isIos) ensureMac(), checkIosTools();
29
- console.log(chalk.cyan(`\n[1/${total}] 检查用户配置与入口文件`)), ensureUserConfig(userProjectDir, PKG_ROOT);
30
-
31
- const config = await loadConfig(userProjectDir);
32
- ensureUserEntry(userProjectDir, config, PKG_ROOT), validateConfig(config, userProjectDir);
33
-
34
- const buildDir = getBuildDir(userProjectDir);
35
- console.log(chalk.cyan(`\n[2/${total}] 重置 RN 工程模板`));
36
- copyTemplate(PKG_ROOT, buildDir, config);
37
- patchAndroidAbi(buildDir, config);
38
- patchExtraRnDependencies(buildDir, userProjectDir, config);
39
- patchAppConfig(buildDir, userProjectDir, config);
40
- patchAndroidConfig(buildDir, config);
41
- patchSigning(buildDir, userProjectDir, config);
42
- await patchIcon(buildDir, userProjectDir, config);
43
- patchIosConfig(buildDir, userProjectDir, config);
44
- await patchIosIcon(buildDir, userProjectDir, config);
45
-
46
- console.log(chalk.cyan(`\n[3/${total}] 检查缓存指纹`)), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
47
- console.log(chalk.cyan(`\n[4/${total}] 复制用户项目`));
48
- const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
49
- console.log(chalk.cyan(`\n[5/${total}] 安装依赖与编译优化`));
50
- await installUserDeps(targetDir, userProjectDir, config, buildDir, isIos ? 'ios' : 'android');
51
- patchNodejsMobilePlugin(userProjectDir, config);
52
- patchWebViewSsl(userProjectDir, config);
53
- writeLocalProperties(buildDir, config);
54
-
55
- console.log(chalk.cyan(`\n[6/${total}] 生成桥接文件`));
56
- generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
57
-
58
- const appName = resolveAppName(userProjectDir, config), safeName = sanitizeFileName(appName),
59
- outDir = path.resolve(userProjectDir, config.outputDir || './out');
60
- ensureDir(outDir);
61
-
62
- if (isIos) {
63
- console.log(chalk.cyan(`\n[7/${total}] pod install`)), podInstall(path.join(buildDir, 'ios'));
64
- console.log(chalk.cyan(`\n[8/${total}] xcodebuild archive + export`)),
65
- xcodebuildArchive(buildDir, config, release, userProjectDir);
66
-
67
- const ipaPath = xcodebuildExport(buildDir, config, release, outDir, userProjectDir),
68
- dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.ipa`,
69
- dst = path.join(outDir, dstName);
70
- fs.copyFileSync(ipaPath, dst), console.log(chalk.green('\n✅ 打包完成'));
84
+ console.log(chalk.cyan(`\n[7/${total}] 打包 APK`));
85
+ if (!process.env.OPTIMIZE_DEBUG) console.log(chalk.gray(' 打包进行中...'));
86
+ const assetsDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'assets'),
87
+ resDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'res');
71
88
 
72
- const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
73
- console.log(chalk.gray(` ${dst} (${mb} MB)`));
74
- await markVerified(buildDir);
75
- return;
76
- }
89
+ ensureDir(assetsDir);
90
+ run('npx', [
91
+ 'react-native', 'bundle', '--platform', 'android', '--dev', release ? 'false' : 'true',
92
+ '--entry-file', 'index.js', '--bundle-output',
93
+ path.join(assetsDir, 'index.android.bundle'), '--assets-dest', resDir,
94
+ ], { cwd: buildDir, quiet: true });
77
95
 
78
- console.log(chalk.cyan(`\n[7/${total}] 打包 APK`));
79
- if (!process.env.OPTIMIZE_DEBUG) console.log(chalk.gray(' 打包进行中...'));
80
- const assetsDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'assets'),
81
- resDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'res');
82
-
83
- ensureDir(assetsDir);
84
- run('npx', [
85
- 'react-native', 'bundle',
86
- '--platform', 'android',
87
- '--dev', release ? 'false' : 'true',
88
- '--entry-file', 'index.js',
89
- '--bundle-output', path.join(assetsDir, 'index.android.bundle'),
90
- '--assets-dest', resDir,
91
- ], { cwd: buildDir, quiet: true });
92
-
93
- const task = release ? 'assembleRelease' : 'assembleDebug', gradleArgs = [task];
94
- if (flags.arch) gradleArgs.push(`-PreactNativeArchitectures=${flags.arch}`);
95
-
96
- const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
96
+ const task = release ? 'assembleRelease' : 'assembleDebug', gradleArgs = [task];
97
+ if (flags.arch) gradleArgs.push(`-PreactNativeArchitectures=${flags.arch}`);
98
+
99
+ const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
100
+ try {
97
101
  run(gradlew, gradleArgs, { cwd: path.join(buildDir, 'android'), quiet: true });
102
+ } catch (err) {
103
+ warnGradleDownloadHint(config); throw err;
104
+ }
98
105
 
99
- const apkDir = path.join(buildDir, 'android', 'app', 'build', 'outputs', 'apk', release ? 'release' : 'debug');
100
- console.log(chalk.green('\n✅ 打包完成'));
106
+ const apkDir = path.join(buildDir, 'android', 'app', 'build', 'outputs', 'apk', release ? 'release' : 'debug');
107
+ console.log(chalk.green('\n✅ 打包完成'));
101
108
 
102
- if (fs.existsSync(apkDir)) {
103
- const apks = fs.readdirSync(apkDir).filter((f) => f.endsWith('.apk'));
104
- if (apks.length === 0) console.log(chalk.yellow(' ⚠️ 未找到 APK'));
109
+ if (fs.existsSync(apkDir)) {
110
+ const apks = fs.readdirSync(apkDir).filter((f) => f.endsWith('.apk'));
111
+ if (apks.length === 0) console.log(chalk.yellow(' ⚠️ 未找到 APK'));
105
112
 
106
- for (const f of apks) {
107
- const src = path.join(apkDir, f),
108
- dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.apk`,
109
- dst = path.join(outDir, dstName);
113
+ for (const f of apks) {
114
+ const dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.apk`,
115
+ dst = path.join(outDir, dstName), src = path.join(apkDir, f);
110
116
 
111
- fs.copyFileSync(src, dst);
112
- const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
113
- console.log(chalk.gray(` ${dst} (${mb} MB)`));
114
- }
117
+ fs.copyFileSync(src, dst);
118
+ const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
119
+ console.log(chalk.gray(` ${dst} (${mb} MB)`));
115
120
  }
116
- else console.log(chalk.yellow(' ⚠️ 未找到 APK 输出目录: ' + apkDir));
117
- await markVerified(buildDir);
118
121
  }
122
+ else console.log(chalk.yellow(' ⚠️ 未找到 APK 输出目录: ' + apkDir));
123
+ await markVerified(buildDir);
124
+ };
119
125
 
120
126
  export { runBuild };
@@ -1,9 +1,14 @@
1
- import path from 'path';
1
+ import chalk from 'chalk';
2
2
  import fs from 'fs';
3
- import { removeDir } from '../utils.js';
3
+ import path from 'path';
4
4
  import { getBuildDir } from '../copy.js';
5
- import chalk from 'chalk';
5
+ import { removeDir } from '../utils.js';
6
6
 
7
+ /**
8
+ * 清理构建缓存:构建目录、插件构建缓存、插件 CMake 缓存
9
+ * >查看定义:@see {@link runClean}
10
+ * @returns {Promise<void>}
11
+ */
7
12
  const runClean = async () => {
8
13
  const userProjectDir = process.cwd(), buildDir = getBuildDir(userProjectDir),
9
14
  targets = [
@@ -19,9 +24,10 @@ const runClean = async () => {
19
24
  ];
20
25
 
21
26
  for (const t of targets) {
22
- if (fs.existsSync(t.path))
23
- removeDir(t.path),
24
- console.log(chalk.green('已清理: ' + t.label + ' -> ' + (path.relative(userProjectDir, t.path) || t.path)));
27
+ if (fs.existsSync(t.path)) {
28
+ removeDir(t.path);
29
+ console.log(chalk.green('已清理: ' + t.label + ' -> ' + (path.relative(userProjectDir, t.path) || t.path)));
30
+ }
25
31
  else console.log(chalk.gray('跳过(不存在): ' + t.label));
26
32
  }
27
33