@flun/node-mobile-app 4.0.3 → 4.0.5
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" +347 -348
- package/CHANGELOG.md +9 -19
- 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 +5 -5
- 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 +444 -444
package/CHANGELOG.md
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
# 变更日志
|
|
2
|
-
## [4.0.
|
|
2
|
+
## [4.0.4] - 2026-10-21 12:10
|
|
3
|
+
### 优化
|
|
4
|
+
- 进一步优化 .md 文件;
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
## [4.0.2] - 2026-10-20 15:56
|
|
6
|
+
## [4.0.4] - 2026-10-21 10:02
|
|
7
|
+
### 优化
|
|
8
|
+
- 优化了大量文件细节
|
|
9
9
|
|
|
10
|
+
## [4.0.3] - 2026-10-21 08:28
|
|
10
11
|
### 修复
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
|
|
14
|
-
## [4.0.1] - 2026-10-20 14:48
|
|
15
|
-
|
|
16
|
-
### 紧急修复
|
|
17
|
-
- **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 替换、跳过重复编译;
|
|
18
|
-
|
|
19
|
-
### 升级注意事项
|
|
20
|
-
- **已装 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;
|
|
21
|
-
|
|
22
|
-
### 更新
|
|
23
|
-
- 同步更新了一些依赖包;
|
|
12
|
+
- **`[3/11]` 提示中清理目标重复**:`depsChanged` 与 `appInfoChanged` 同时为真时,`targets` 数组会推入两次 `Gradle 缓存`,提示显示为"清理 依赖缓存、Gradle 缓存、Gradle 缓存";现用 `[...new Set(targets)]` 去重,提示与清理目标一致;
|
|
13
|
+
- **删除依赖快照误用 `removeDir`**:`.deps-snapshot.json` 是文件,原代码调用 `removeDir` 删除;该函数名与实现均针对目录,当前靠 `fs.rmSync` 的兼容性正常工作,但语义错位,一旦 `removeDir` 实现收紧(如增加 `isDirectory` 判断)会静默失效,导致快照残留、`[5/11]` 误判"依赖与环境未变化";现改用 `fs.rmSync(path, { force: true })` 直接删除文件;
|
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 };
|
package/src/copy.js
CHANGED
|
@@ -61,7 +61,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
61
61
|
* 把 buildDir/package.json 的依赖版本同步为主包 dependencies 的版本
|
|
62
62
|
* - 依赖名单来自 buildDir/package.json(模板声明需要哪些依赖)
|
|
63
63
|
* - 版本号来自 pkgRoot/package.json 的 dependencies(单一数据源)
|
|
64
|
-
* -
|
|
64
|
+
* - 若主包缺少某个依赖,抛错中断(避免模板占位值被真正使用)
|
|
65
65
|
*/
|
|
66
66
|
syncTemplateDependencies = (pkgRoot, buildDir) => {
|
|
67
67
|
const buildPkgPath = path.join(buildDir, 'package.json');
|
|
@@ -74,7 +74,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
74
74
|
const mainDeps = mainPkg.dependencies || {}, missing = [];
|
|
75
75
|
let depsUpdated = 0, needWrite = false;
|
|
76
76
|
|
|
77
|
-
// 移除仅用于模板占位说明的 _comment
|
|
77
|
+
// 移除仅用于模板占位说明的 _comment 字段,避免带进最终 build 工程
|
|
78
78
|
if ('_comment' in buildPkg) delete buildPkg._comment, needWrite = true;
|
|
79
79
|
for (const name of Object.keys(buildPkg.dependencies)) {
|
|
80
80
|
const v = mainDeps[name];
|
|
@@ -91,27 +91,25 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
91
91
|
throw new Error(
|
|
92
92
|
'template/package.json 声明的以下依赖在主包 dependencies 中不存在:\n' +
|
|
93
93
|
' ' + missing.join(', ') + '\n' +
|
|
94
|
-
' 请在 @flun/node-mobile-app 的 package.json
|
|
95
|
-
'
|
|
94
|
+
' 请在 @flun/node-mobile-app 的 package.json 中补上对应声明(版本号随意,\n' +
|
|
95
|
+
' 但必须存在,以便 syncTemplateDependencies 覆盖模板里的占位值)。'
|
|
96
96
|
);
|
|
97
97
|
}
|
|
98
98
|
},
|
|
99
99
|
/**
|
|
100
|
-
* 把主包 node_modules 里被 npm
|
|
100
|
+
* 把主包 node_modules 里被 npm 嵌套(未提升到用户根)的依赖,按需链接到用户根 node_modules 下,
|
|
101
101
|
* 让 RN CLI / Metro 从用户根也能解析到它们。
|
|
102
|
-
* -
|
|
103
|
-
* - 范围:遍历主包 node_modules 顶层全部条目(含 @scope/
|
|
104
|
-
* - 落点:只在用户根 node_modules 缺失条目时建 symlink
|
|
105
|
-
* -
|
|
102
|
+
* - 判据:从用户根视角解析不到,才建链接;能解析则跳过;
|
|
103
|
+
* - 范围:遍历主包 node_modules 顶层全部条目(含 @scope/ 下每个子项),不列举任何包名;
|
|
104
|
+
* - 落点:只在用户根 node_modules 缺失条目时建 symlink,不覆盖已有内容、不碰 package.json / package-lock.json;
|
|
105
|
+
* - 建链接失败:中断报错,不静默。
|
|
106
106
|
*/
|
|
107
107
|
linkNestedDeps = (pkgRoot, buildDir) => {
|
|
108
108
|
const pkgNm = path.join(pkgRoot, 'node_modules');
|
|
109
109
|
if (!fs.existsSync(pkgNm)) return;
|
|
110
110
|
|
|
111
|
-
const userProjectDir = path.dirname(buildDir),
|
|
112
|
-
|
|
113
|
-
req = createRequire(path.join(userProjectDir, 'index.js')),
|
|
114
|
-
candidates = [];
|
|
111
|
+
const userProjectDir = path.dirname(buildDir), userNm = path.join(userProjectDir, 'node_modules'),
|
|
112
|
+
req = createRequire(path.join(userProjectDir, 'index.js')), candidates = [];
|
|
115
113
|
for (const entry of fs.readdirSync(pkgNm, { withFileTypes: true })) {
|
|
116
114
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
|
|
117
115
|
if (entry.name.startsWith('@')) {
|
|
@@ -123,7 +121,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
123
121
|
|
|
124
122
|
let linked = 0;
|
|
125
123
|
for (const name of candidates) {
|
|
126
|
-
try { req.resolve(name); continue; } catch { /*
|
|
124
|
+
try { req.resolve(name); continue; } catch { /* 解析不到,进入兜底 */ }
|
|
127
125
|
const src = path.join(pkgNm, name);
|
|
128
126
|
if (!fs.existsSync(src)) continue;
|
|
129
127
|
const realSrc = fs.realpathSync(src), dst = path.join(userNm, name);
|
|
@@ -132,8 +130,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
132
130
|
if (occupied) continue;
|
|
133
131
|
ensureDir(path.dirname(dst));
|
|
134
132
|
try {
|
|
135
|
-
fs.symlinkSync(realSrc, dst, 'junction')
|
|
136
|
-
linked++;
|
|
133
|
+
fs.symlinkSync(realSrc, dst, 'junction'), linked++;
|
|
137
134
|
} catch (err) {
|
|
138
135
|
throw new Error(
|
|
139
136
|
'建立兜底链接失败:' + dst + ' -> ' + realSrc + '\n' +
|
|
@@ -146,9 +143,9 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
146
143
|
},
|
|
147
144
|
/**
|
|
148
145
|
* 把 CLI 包里的 template/ 逐文件覆盖到 build 目录
|
|
149
|
-
* -
|
|
146
|
+
* - 只覆盖模板里存在的文件,不删除 buildDir 里已有的其他内容(保留 Gradle 增量缓存)
|
|
150
147
|
* - 模板文件被改写时自动同步;新增文件自动添加
|
|
151
|
-
* -
|
|
148
|
+
* - 若模板里删除了文件,旧副本残留由 CLI 版本指纹触发全量重建处理
|
|
152
149
|
*/
|
|
153
150
|
copyTemplate = (pkgRoot, buildDir, config) => {
|
|
154
151
|
ensureDir(buildDir);
|
|
@@ -166,7 +163,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
166
163
|
},
|
|
167
164
|
/**
|
|
168
165
|
* 复制用户 Node 项目到 <buildDir>/nodejs-assets/nodejs-project
|
|
169
|
-
* 复制后重写 package.json:只保留 dependencies
|
|
166
|
+
* 复制后重写 package.json:只保留 dependencies,剔除 devDependencies;用 config 的 allowScripts
|
|
170
167
|
*/
|
|
171
168
|
copyUserProject = (userProjectDir, buildDir, config) => {
|
|
172
169
|
const targetDir = path.join(buildDir, 'nodejs-assets', 'nodejs-project');
|
|
@@ -177,7 +174,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
177
174
|
'mobileAppConfig.js', 'mobileAppConfig.mjs', 'mobileAppConfig.cjs'
|
|
178
175
|
];
|
|
179
176
|
|
|
180
|
-
//
|
|
177
|
+
// 增量复制:内容变化的文件才覆盖,保留 node_modules 和其他未变文件
|
|
181
178
|
copyDirIncremental(userProjectDir, targetDir, exclude);
|
|
182
179
|
// 黑名单式过滤:只删 devDependencies 和 allowScripts,其余字段原样保留
|
|
183
180
|
const userPkgPath = path.join(userProjectDir, 'package.json');
|
|
@@ -198,7 +195,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
198
195
|
*/
|
|
199
196
|
writeLocalProperties = (buildDir, config) => {
|
|
200
197
|
const sdkPath = resolveSdkPath(config);
|
|
201
|
-
if (!sdkPath) return console.warn(chalk.yellow(' ⚠️ 未找到 Android SDK
|
|
198
|
+
if (!sdkPath) return console.warn(chalk.yellow(' ⚠️ 未找到 Android SDK,请在 mobileAppConfig.js 里配置 android.sdkPath'));
|
|
202
199
|
|
|
203
200
|
const localProps = path.join(buildDir, 'android', 'local.properties'),
|
|
204
201
|
escaped = sdkPath.replace(/\\/g, '\\\\').replace(/:/g, '\\:');
|
|
@@ -243,12 +240,12 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
243
240
|
* 按 config.android.abiFilters 修改 buildDir 里的 app/build.gradle:
|
|
244
241
|
* - defaultConfig.ndk.abiFilters
|
|
245
242
|
* - gradle.properties 的 reactNativeArchitectures
|
|
246
|
-
* 插件 build.gradle 读的是 project(":app").android.defaultConfig.ndk.abiFilters
|
|
247
|
-
*
|
|
243
|
+
* 插件 build.gradle 读的是 project(":app").android.defaultConfig.ndk.abiFilters,
|
|
244
|
+
* 所以必须改这里,改插件自身的 abiFilters 无效。
|
|
248
245
|
*/
|
|
249
246
|
patchAndroidAbi = (buildDir, config) => {
|
|
250
247
|
const file = path.join(buildDir, 'android', 'app', 'build.gradle');
|
|
251
|
-
if (!fs.existsSync(file)) return console.warn(chalk.yellow(' ⚠️ 未找到 app/build.gradle
|
|
248
|
+
if (!fs.existsSync(file)) return console.warn(chalk.yellow(' ⚠️ 未找到 app/build.gradle,跳过 ABI patch'));
|
|
252
249
|
|
|
253
250
|
const abiList = (config.android.abiFilters || []).map((x) => `"${x}"`).join(', ');
|
|
254
251
|
let c = fs.readFileSync(file, 'utf-8');
|
|
@@ -256,7 +253,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
256
253
|
|
|
257
254
|
// defaultConfig 里的 ndk { abiFilters ... }
|
|
258
255
|
c = c.replace(/(defaultConfig\s*\{[\s\S]*?ndk\s*\{\s*abiFilters\s+)("[^"]*"(?:\s*,\s*"[^"]*")*)/, `$1${abiList}`);
|
|
259
|
-
// 兜底:没有 ndk {}
|
|
256
|
+
// 兜底:没有 ndk {} 块时,插到 defaultConfig 里
|
|
260
257
|
if (!/ndk\s*\{\s*abiFilters/.test(c))
|
|
261
258
|
c = c.replace(/(defaultConfig\s*\{)/, `$1\n ndk {\n abiFilters ${abiList}\n }`);
|
|
262
259
|
// 不 patch splits.abi.include:AGP 禁止它和 ndk.abiFilters 同时存在
|
|
@@ -289,8 +286,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
289
286
|
for (const name of extras) {
|
|
290
287
|
const pkgJson = path.join(userProjectDir, 'node_modules', name, 'package.json');
|
|
291
288
|
if (!fs.existsSync(pkgJson)) {
|
|
292
|
-
missing.push(name);
|
|
293
|
-
continue;
|
|
289
|
+
missing.push(name); continue;
|
|
294
290
|
}
|
|
295
291
|
try {
|
|
296
292
|
const v = JSON.parse(fs.readFileSync(pkgJson, 'utf-8')).version;
|
|
@@ -317,4 +313,4 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
317
313
|
export {
|
|
318
314
|
getBuildDir, copyTemplate, copyUserProject, writeLocalProperties, ensureUserEntry, ensureUserConfig, patchAndroidAbi,
|
|
319
315
|
patchExtraRnDependencies
|
|
320
|
-
};
|
|
316
|
+
};
|
package/src/fingerprint.js
CHANGED
|
@@ -35,7 +35,7 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
35
35
|
for (const e of entries) {
|
|
36
36
|
if (e.name === 'node_modules' || e.name === '.git') continue;
|
|
37
37
|
const full = path.join(d, e.name), rel = path.relative(templateDir, full).replace(/\\/g, '/');
|
|
38
|
-
if (e.isDirectory())
|
|
38
|
+
if (e.isDirectory()) h.update('D:' + rel + '|'), walk(full);
|
|
39
39
|
else if (e.isFile()) {
|
|
40
40
|
h.update('F:' + rel + '|');
|
|
41
41
|
try { h.update(fs.readFileSync(full)); } catch { h.update('err'); }
|
|
@@ -108,7 +108,7 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
108
108
|
if (templateChanged) reasons.push('模板文件');
|
|
109
109
|
if (depsChanged) reasons.push('用户依赖');
|
|
110
110
|
if (appInfoChanged) reasons.push('应用信息');
|
|
111
|
-
console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '
|
|
111
|
+
console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 Gradle 缓存、CMake 缓存与依赖缓存'));
|
|
112
112
|
clearGradleCaches(buildDir, userProjectDir);
|
|
113
113
|
removeDir(path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android', '.cxx'));
|
|
114
114
|
clearDepsCaches(buildDir);
|
|
@@ -117,13 +117,13 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
117
117
|
const reasons = [], targets = [];
|
|
118
118
|
if (depsChanged) reasons.push('用户依赖'), targets.push('依赖缓存', 'Gradle 缓存');
|
|
119
119
|
if (appInfoChanged) reasons.push('应用信息'), targets.push('Gradle 缓存');
|
|
120
|
-
console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '
|
|
120
|
+
console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 ' + [...new Set(targets)].join('、')));
|
|
121
121
|
clearGradleCaches(buildDir, userProjectDir);
|
|
122
122
|
if (depsChanged) clearDepsCaches(buildDir);
|
|
123
123
|
}
|
|
124
|
-
else console.log(chalk.gray(' ✓
|
|
124
|
+
else console.log(chalk.gray(' ✓ 缓存有效,跳过清理'));
|
|
125
125
|
|
|
126
126
|
writeFp(fpFile, newFp);
|
|
127
127
|
};
|
|
128
128
|
|
|
129
|
-
export { computeFingerprint, checkAndResetCache };
|
|
129
|
+
export { computeFingerprint, checkAndResetCache };
|
package/src/generate.js
CHANGED
|
@@ -4,7 +4,7 @@ import { writeIfChanged } from './utils.js';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* 生成 main.js
|
|
7
|
+
* 生成 main.js(桥接代码,用户不需要手写)
|
|
8
8
|
*
|
|
9
9
|
* 上报机制(通过 rn-bridge channel 发消息给 App.tsx):
|
|
10
10
|
* - 'Node started on <URL>' server.js 成功调用 listen
|
|
@@ -14,9 +14,8 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
14
14
|
const entry = config.serverPath.startsWith('./') ? config.serverPath : './' + config.serverPath,
|
|
15
15
|
entryLabel = config.serverPath, hb = config.advanced.heartbeatInterval,
|
|
16
16
|
body = `(async () => {
|
|
17
|
-
const rn_bridge = require('rn-bridge')
|
|
18
|
-
|
|
19
|
-
ENTRY_LABEL = ${JSON.stringify(entryLabel)}, START_TIMEOUT_MS = 5000;
|
|
17
|
+
const rn_bridge = require('rn-bridge'), net = require('net'), https = require('https'),
|
|
18
|
+
USER_ENTRY = ${JSON.stringify(entry)}, ENTRY_LABEL = ${JSON.stringify(entryLabel)}, START_TIMEOUT_MS = 5000;
|
|
20
19
|
|
|
21
20
|
let resolved = false;
|
|
22
21
|
function emitUrl(url) {
|
|
@@ -44,7 +43,7 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
44
43
|
emitFailure('未处理的 Promise rejection', String(reason));
|
|
45
44
|
});
|
|
46
45
|
|
|
47
|
-
// 拦截 net.Server.prototype.listen
|
|
46
|
+
// 拦截 net.Server.prototype.listen,捕获协议和真实端口
|
|
48
47
|
const origListen = net.Server.prototype.listen;
|
|
49
48
|
net.Server.prototype.listen = function (...args) {
|
|
50
49
|
const server = this;
|
|
@@ -77,9 +76,7 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
77
76
|
|
|
78
77
|
${config.advanced.keepAlive === false ? '// keepAlive: false — 不发送心跳' : "setInterval(() => rn_bridge.channel.send('heartbeat'), " + hb + ");"}
|
|
79
78
|
|
|
80
|
-
rn_bridge.channel.on('message',
|
|
81
|
-
rn_bridge.channel.send('Echo: ' + msg);
|
|
82
|
-
});
|
|
79
|
+
rn_bridge.channel.on('message', msg => rn_bridge.channel.send('Echo: ' + msg));
|
|
83
80
|
})();`,
|
|
84
81
|
header = moduleType === 'module'
|
|
85
82
|
? `import { createRequire } from 'module';\nconst require = createRequire(import.meta.url);\n\n` : '',
|
|
@@ -97,9 +94,9 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
97
94
|
mixedContentMode: config.webview.mixedContentMode,
|
|
98
95
|
backgroundColor: config.webview.backgroundColor
|
|
99
96
|
},
|
|
100
|
-
}, content = `//
|
|
97
|
+
}, content = `// 自动生成,请勿手改\nexport default ${JSON.stringify(data, null, 2)} as const;\n`,
|
|
101
98
|
changed = writeIfChanged(path.join(buildDir, 'mobileApp.runtime.ts'), content);
|
|
102
99
|
console.log(chalk.green(changed ? ' ✓ 已生成 mobileApp.runtime.ts' : ' ✓ mobileApp.runtime.ts 无变化'));
|
|
103
100
|
};
|
|
104
101
|
|
|
105
|
-
export { generateMainJs, generateRuntime }
|
|
102
|
+
export { generateMainJs, generateRuntime }
|