@flun/node-mobile-app 4.0.5 → 4.0.7
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" +84 -76
- package/CHANGELOG.md +9 -9
- package/LICENSE +9 -4
- package/README.md +206 -113
- package/bin/cli.js +9 -9
- package/cli-assets/server.js +7 -4
- package/mobileAppConfig.js +32 -32
- package/package.json +5 -5
- package/scripts/copy-files.js +1 -1
- package/src/commands/build.js +9 -4
- package/src/commands/clean.js +3 -2
- package/src/commands/test.js +13 -9
- package/src/config.js +3 -3
- package/src/copy.js +41 -36
- package/src/fingerprint.js +4 -3
- package/src/generate.js +5 -3
- package/src/install.js +38 -27
- package/src/optimize-node-modules.js +10 -5
- package/src/patcher.js +37 -32
- package/src/platform-ios.js +6 -6
- package/src/utils.js +26 -22
- package/template/App.tsx +4 -4
- package/template/Gemfile +1 -1
- package/template/android/app/build.gradle +1 -1
- package/template/android/app/proguard-rules.pro +1 -1
- package/template/android/app/src/main/java/com/myapp/MainActivity.kt +1 -1
- package/template/android/app/src/main/java/com/myapp/MainApplication.kt +1 -1
- package/template/android/app/src/main/res/drawable/rn_edit_text_material.xml +6 -3
- package/template/android/app/src/main/res/raw/keep.xml +1 -1
- package/template/android/app/src/main/res/values/strings.xml +1 -1
- package/template/android/app/src/main/res/values/styles.xml +1 -1
- package/template/android/build.gradle +1 -1
- package/template/android/gradle.properties +1 -1
- package/template/android/gradlew +3 -2
- package/template/android/gradlew.bat +3 -2
- package/template/android/settings.gradle +1 -1
- package/template/app.json +1 -1
- package/template/index.js +2 -2
- package/template/ios/.xcode.env +1 -1
- package/template/ios/MyApp/Images.xcassets/AppIcon.appiconset/Contents.json +1 -1
- package/template/ios/MyApp/Images.xcassets/Contents.json +1 -1
- package/template/ios/MyApp/Info.plist +1 -1
- package/template/ios/MyApp/LaunchScreen.storyboard +30 -12
- package/template/ios/MyApp/PrivacyInfo.xcprivacy +1 -1
- package/template/ios/MyApp.xcodeproj/project.pbxproj +34 -15
- package/template/ios/MyApp.xcodeproj/xcshareddata/xcschemes/MyApp.xcscheme +1 -1
- package/template/ios/Podfile +1 -1
- package/template/package.json +1 -1
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +131 -98
package/mobileAppConfig.js
CHANGED
|
@@ -5,20 +5,20 @@
|
|
|
5
5
|
* 所有路径相对于用户项目根目录
|
|
6
6
|
*
|
|
7
7
|
* ===== 常用命令 =====
|
|
8
|
-
* npx node-mobile-app test
|
|
9
|
-
* npx node-mobile-app test --ios iOS
|
|
10
|
-
* npx node-mobile-app build 打 debug APK
|
|
11
|
-
* npx node-mobile-app build --release 打 release APK
|
|
12
|
-
* npx node-mobile-app build --ios 打 IPA
|
|
8
|
+
* npx node-mobile-app test 编译并安装到设备(需连 USB,走 Metro 热更新)
|
|
9
|
+
* npx node-mobile-app test --ios iOS 版(仅 macOS)
|
|
10
|
+
* npx node-mobile-app build 打 debug APK(离线可用)
|
|
11
|
+
* npx node-mobile-app build --release 打 release APK(默认 debug keystore 签名,可安装;上架需配 android.signing)
|
|
12
|
+
* npx node-mobile-app build --ios 打 IPA(仅 macOS)
|
|
13
13
|
* npx node-mobile-app clean 清理构建缓存
|
|
14
14
|
*
|
|
15
|
-
* 任意命令加 --debug
|
|
15
|
+
* 任意命令加 --debug 可显示详细日志(优化各阶段明细、各命令完整输出)
|
|
16
16
|
*
|
|
17
17
|
* ===== 服务地址说明 =====
|
|
18
|
-
* CLI 会自动识别 server.js
|
|
18
|
+
* CLI 会自动识别 server.js 实际监听的协议和端口(拦截 listen 调用),
|
|
19
19
|
* App 使用该地址加载页面,无需在此配置;
|
|
20
20
|
* 若 5 秒内未能识别,App 显示启动失败并可在页面展开详情查看原因;
|
|
21
|
-
*
|
|
21
|
+
* 动态端口(listen(0))会自动获取系统分配的真实端口;
|
|
22
22
|
*/
|
|
23
23
|
export default {
|
|
24
24
|
// ===== 必填 =====
|
|
@@ -39,16 +39,16 @@ 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
|
-
// 'system'
|
|
44
|
+
// 'system' 跟随系统(内部用默认浅/深色)
|
|
45
45
|
// { light: '#fff', dark: '#1a1a1a' } 按系统主题切换
|
|
46
46
|
backgroundColor: '#ffffff',
|
|
47
|
-
// 仅影响 HTTPS 场景下 App
|
|
48
|
-
// - 用 HTTP
|
|
49
|
-
// - 用 HTTPS + 真证书(如 Let's Encrypt)
|
|
50
|
-
// - 用 HTTPS + 自签证书 +
|
|
51
|
-
// - 用 HTTPS + 自签证书 + 未装设备(本地测试最常见)
|
|
47
|
+
// 仅影响 HTTPS 场景下 App 是否加载自签证书的页面:
|
|
48
|
+
// - 用 HTTP:与本开关无关
|
|
49
|
+
// - 用 HTTPS + 真证书(如 Let's Encrypt):与本开关无关
|
|
50
|
+
// - 用 HTTPS + 自签证书 + 已装进设备信任库:与本开关无关
|
|
51
|
+
// - 用 HTTPS + 自签证书 + 未装设备(本地测试最常见):必须设为 true
|
|
52
52
|
// ⚠️ 开启后仅对 127.0.0.1 / localhost 放行不受信任的 HTTPS 证书,其他域名仍严格校验
|
|
53
53
|
// 用于本地自签证书测试,生产环境建议关闭
|
|
54
54
|
allowSelfSignedCert: false,
|
|
@@ -71,24 +71,24 @@ export default {
|
|
|
71
71
|
// 'android.permission.CAMERA',
|
|
72
72
|
// 'android.permission.READ_EXTERNAL_STORAGE',
|
|
73
73
|
],
|
|
74
|
-
//
|
|
74
|
+
// 签名配置(可选)
|
|
75
75
|
//
|
|
76
|
-
// 不配置也能打 release
|
|
77
|
-
//
|
|
76
|
+
// 不配置也能打 release:模板默认用 debug keystore 签名(可安装、可本地测试,不可上架);
|
|
77
|
+
// 需要上架时再配置下面任一种:
|
|
78
78
|
//
|
|
79
|
-
//
|
|
79
|
+
// 方式一:Android SDK 自带 debug keystore(本地测试常用)
|
|
80
80
|
// keystore: %USERPROFILE%\.android\debug.keystore (Windows)
|
|
81
81
|
// ~/.android/debug.keystore (macOS/Linux)
|
|
82
82
|
// storePassword: 'android'
|
|
83
83
|
// keyAlias: 'androiddebugkey'
|
|
84
84
|
// keyPassword: 'android'
|
|
85
85
|
//
|
|
86
|
-
//
|
|
86
|
+
// 取消下方整块注释即视为"启用签名",此时 4 项均为必填(缺项会在构建前报错)
|
|
87
87
|
signing: {
|
|
88
|
-
// keystore: './build/release.keystore', //
|
|
89
|
-
// storePassword: '', //
|
|
90
|
-
// keyAlias: '', //
|
|
91
|
-
// keyPassword: '', //
|
|
88
|
+
// keystore: './build/release.keystore', // 必填:keystore 路径(相对用户项目根)
|
|
89
|
+
// storePassword: '', // 必填:keystore 密码
|
|
90
|
+
// keyAlias: '', // 必填:密钥别名
|
|
91
|
+
// keyPassword: '', // 必填:密钥密码(与 storePassword 相同可填同值)
|
|
92
92
|
},
|
|
93
93
|
},
|
|
94
94
|
|
|
@@ -97,10 +97,10 @@ 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
|
-
//
|
|
100
|
+
// 签名配置(仅 macOS,发布时必填):
|
|
101
101
|
// teamId Apple Developer Team ID
|
|
102
|
-
// provisioningProfile
|
|
103
|
-
// certificate P12
|
|
102
|
+
// provisioningProfile 描述文件路径(写入 exportOptions.plist)
|
|
103
|
+
// certificate P12 证书路径(导入临时 keychain 供 xcodebuild 签名用)
|
|
104
104
|
// certificatePassword P12 密码
|
|
105
105
|
signing: {
|
|
106
106
|
// teamId: '',
|
|
@@ -120,14 +120,14 @@ export default {
|
|
|
120
120
|
// 构建时合并 JS + 清空已复制到项目根的资源 + 清平台二进制 + 清开发文件,加速启动
|
|
121
121
|
// 默认开启;若构建或运行异常,可按下述方式关闭或排除问题包
|
|
122
122
|
optimize: {
|
|
123
|
-
enabled: true, //
|
|
124
|
-
// exclude: ['包名'], //
|
|
123
|
+
enabled: true, // 是否启用(默认 true)
|
|
124
|
+
// exclude: ['包名'], // 排除优化时异常或不想优化的包(默认空)
|
|
125
125
|
},
|
|
126
126
|
|
|
127
127
|
// ===== 扩展点 =====
|
|
128
|
-
//
|
|
128
|
+
// 不重置的模板文件(路径相对于 node-mobile-app-build/,如 'android/app/src/main/AndroidManifest.xml')
|
|
129
129
|
keepFiles: [],
|
|
130
|
-
// 额外 RN
|
|
130
|
+
// 额外 RN 原生依赖包名数组(用于 autolink 识别,含原生模块的包必须在此声明)
|
|
131
131
|
// 安装命令用 --save-dev,这些是开发依赖,不会进移动端 Node 环境
|
|
132
132
|
// 示例: npm install --save-dev react-native-camera
|
|
133
133
|
// extraRnDependencies: ['react-native-camera']
|
|
@@ -163,4 +163,4 @@ export default {
|
|
|
163
163
|
'@parcel/watcher': true,
|
|
164
164
|
'bcrypt': true,
|
|
165
165
|
},
|
|
166
|
-
};
|
|
166
|
+
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flun/node-mobile-app",
|
|
3
|
-
"version": "4.0.
|
|
4
|
-
"description": "将 Node.js 项目一键打包为 Android / iOS 移动应用,运行时由 @flun/nodejs-mobile-react-native
|
|
3
|
+
"version": "4.0.7",
|
|
4
|
+
"description": "将 Node.js 项目一键打包为 Android / iOS 移动应用,运行时由 @flun/nodejs-mobile-react-native 提供;",
|
|
5
5
|
"author": "flun <open@flun.top>",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"node-mobile",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"postinstall": "node scripts/copy-files.js"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@babel/core": "^8.0.
|
|
44
|
-
"@eslint-react/eslint-plugin": "^5.28.
|
|
43
|
+
"@babel/core": "^8.0.9",
|
|
44
|
+
"@eslint-react/eslint-plugin": "^5.28.8",
|
|
45
45
|
"@eslint/js": "^10.0.1",
|
|
46
|
-
"@flun/nodejs-mobile-react-native": "^2.1.
|
|
46
|
+
"@flun/nodejs-mobile-react-native": "^2.1.1",
|
|
47
47
|
"@react-native/babel-preset": "^0.88.0",
|
|
48
48
|
"@react-native/metro-config": "^0.88.0",
|
|
49
49
|
"@react-native-community/cli": "^20.2.0",
|
package/scripts/copy-files.js
CHANGED
|
@@ -15,7 +15,7 @@ const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__fi
|
|
|
15
15
|
const copyFile = () => {
|
|
16
16
|
console.log(`🔍 检查 ${fileName} 文件...`), console.log(`📁 项目根目录:${targetDir}`);
|
|
17
17
|
try {
|
|
18
|
-
// 目标目录不是 Node 项目 || 包自身的 npm install
|
|
18
|
+
// 目标目录不是 Node 项目 || 包自身的 npm install(开发调试)→ 跳过
|
|
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; // 目标文件已存在 → 直接返回
|
package/src/commands/build.js
CHANGED
|
@@ -21,7 +21,9 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
21
21
|
runBuild = async (flags) => {
|
|
22
22
|
const userProjectDir = process.cwd(), release = !!flags.release, isIos = !!flags.ios, total = isIos ? 8 : 7;
|
|
23
23
|
|
|
24
|
-
console.log(chalk.cyan(
|
|
24
|
+
console.log(chalk.cyan(
|
|
25
|
+
`\n=== node-mobile-app build (${isIos ? 'iOS' : 'Android'}, ${release ? 'release' : 'debug'}) ===\n`
|
|
26
|
+
));
|
|
25
27
|
console.log(chalk.gray('用户项目: ' + userProjectDir));
|
|
26
28
|
if (isIos) ensureMac(), checkIosTools();
|
|
27
29
|
console.log(chalk.cyan(`\n[1/${total}] 检查用户配置与入口文件`)), ensureUserConfig(userProjectDir, PKG_ROOT);
|
|
@@ -59,10 +61,12 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
59
61
|
|
|
60
62
|
if (isIos) {
|
|
61
63
|
console.log(chalk.cyan(`\n[7/${total}] pod install`)), podInstall(path.join(buildDir, 'ios'));
|
|
62
|
-
console.log(chalk.cyan(`\n[8/${total}] xcodebuild archive + export`)),
|
|
64
|
+
console.log(chalk.cyan(`\n[8/${total}] xcodebuild archive + export`)),
|
|
65
|
+
xcodebuildArchive(buildDir, config, release, userProjectDir);
|
|
63
66
|
|
|
64
67
|
const ipaPath = xcodebuildExport(buildDir, config, release, outDir, userProjectDir),
|
|
65
|
-
dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.ipa`,
|
|
68
|
+
dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.ipa`,
|
|
69
|
+
dst = path.join(outDir, dstName);
|
|
66
70
|
fs.copyFileSync(ipaPath, dst), console.log(chalk.green('\n✅ 打包完成'));
|
|
67
71
|
|
|
68
72
|
const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
|
|
@@ -100,7 +104,8 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
100
104
|
if (apks.length === 0) console.log(chalk.yellow(' ⚠️ 未找到 APK'));
|
|
101
105
|
|
|
102
106
|
for (const f of apks) {
|
|
103
|
-
const src = path.join(apkDir, f),
|
|
107
|
+
const src = path.join(apkDir, f),
|
|
108
|
+
dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.apk`,
|
|
104
109
|
dst = path.join(outDir, dstName);
|
|
105
110
|
|
|
106
111
|
fs.copyFileSync(src, dst);
|
package/src/commands/clean.js
CHANGED
|
@@ -20,8 +20,9 @@ const runClean = async () => {
|
|
|
20
20
|
|
|
21
21
|
for (const t of targets) {
|
|
22
22
|
if (fs.existsSync(t.path))
|
|
23
|
-
removeDir(t.path),
|
|
24
|
-
|
|
23
|
+
removeDir(t.path),
|
|
24
|
+
console.log(chalk.green('已清理: ' + t.label + ' -> ' + (path.relative(userProjectDir, t.path) || t.path)));
|
|
25
|
+
else console.log(chalk.gray('跳过(不存在): ' + t.label));
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
console.log(chalk.green('\n✅ 清理完成'));
|
package/src/commands/test.js
CHANGED
|
@@ -45,7 +45,8 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
45
45
|
console.log(chalk.cyan(`\n[4/${total}] 复制用户项目`));
|
|
46
46
|
const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
|
|
47
47
|
|
|
48
|
-
console.log(chalk.cyan(`\n[5/${total}] 安装依赖与编译优化`)),
|
|
48
|
+
console.log(chalk.cyan(`\n[5/${total}] 安装依赖与编译优化`)),
|
|
49
|
+
await installUserDeps(targetDir, userProjectDir, config, buildDir, isIos ? 'ios' : 'android');
|
|
49
50
|
patchNodejsMobilePlugin(userProjectDir, config);
|
|
50
51
|
patchWebViewSsl(userProjectDir, config), writeLocalProperties(buildDir, config);
|
|
51
52
|
|
|
@@ -54,7 +55,7 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
54
55
|
|
|
55
56
|
if (isIos) {
|
|
56
57
|
console.log(chalk.cyan(`\n[7/${total}] pod install`)), podInstall(path.join(buildDir, 'ios'));
|
|
57
|
-
console.log(chalk.cyan(`\n[8/${total}] 生成 JS bundle + Hermes
|
|
58
|
+
console.log(chalk.cyan(`\n[8/${total}] 生成 JS bundle + Hermes 字节码(由 Xcode Run Script 自动)`));
|
|
58
59
|
const bundlePath = path.join(buildDir, 'ios', 'main.jsbundle');
|
|
59
60
|
runSilent('npx', [
|
|
60
61
|
'react-native', 'bundle',
|
|
@@ -68,12 +69,12 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
68
69
|
console.log(chalk.cyan(`\n[9/${total}] 编译并安装到模拟器/设备`));
|
|
69
70
|
const args = ['react-native', 'run-ios'];
|
|
70
71
|
if (flags.device) args.push('--device', flags.device);
|
|
71
|
-
// FORCE_BUNDLING=1
|
|
72
|
+
// FORCE_BUNDLING=1:即使 Debug+模拟器也把 main.jsbundle 打进 App
|
|
72
73
|
const env = { ...process.env, FORCE_BUNDLING: '1', NODE_MOBILE_MODE: 'test' };
|
|
73
74
|
run('npx', args, { cwd: buildDir, env, inherit: true });
|
|
74
75
|
await markVerified(buildDir);
|
|
75
76
|
} else {
|
|
76
|
-
console.log(chalk.cyan(`\n[7/${total}] 生成 JS bundle
|
|
77
|
+
console.log(chalk.cyan(`\n[7/${total}] 生成 JS bundle(离线可用)`));
|
|
77
78
|
const assetsDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'assets'),
|
|
78
79
|
resDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'res');
|
|
79
80
|
ensureDir(assetsDir);
|
|
@@ -89,7 +90,7 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
89
90
|
], { cwd: buildDir });
|
|
90
91
|
console.log(chalk.green(' ✓ bundle 已生成'));
|
|
91
92
|
|
|
92
|
-
console.log(chalk.cyan(`\n[8/${total}] 编译 Hermes
|
|
93
|
+
console.log(chalk.cyan(`\n[8/${total}] 编译 Hermes 字节码(加速离线启动)`));
|
|
93
94
|
const hermesc = findHermesc(userProjectDir);
|
|
94
95
|
if (hermesc) {
|
|
95
96
|
const hbcPath = bundlePath + '.hbc';
|
|
@@ -97,7 +98,7 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
97
98
|
fs.copyFileSync(hbcPath, bundlePath), fs.unlinkSync(hbcPath);
|
|
98
99
|
console.log(chalk.green(' ✓ 已替换为 Hermes 字节码'));
|
|
99
100
|
}
|
|
100
|
-
else console.warn(chalk.yellow(' ⚠️ 未找到 hermesc
|
|
101
|
+
else console.warn(chalk.yellow(' ⚠️ 未找到 hermesc,跳过字节码编译(离线启动会较慢)'));
|
|
101
102
|
|
|
102
103
|
const mergedAsset = path.join(buildDir, 'android', 'app', 'build', 'intermediates', 'assets', 'debug',
|
|
103
104
|
'mergeDebugAssets', 'index.android.bundle');
|
|
@@ -131,9 +132,11 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
131
132
|
let ready = false;
|
|
132
133
|
for (let i = 0; i < 60; i++) {
|
|
133
134
|
await new Promise(r => setTimeout(r, 500));
|
|
134
|
-
if (await metroReady()) {
|
|
135
|
+
if (await metroReady()) {
|
|
136
|
+
ready = true, console.log(chalk.green(' ✓ Metro 已就绪(后台常驻,支持热更新)')); break;
|
|
137
|
+
}
|
|
135
138
|
}
|
|
136
|
-
if (!ready) console.log(chalk.yellow(' ⚠️ Metro 30 秒内未就绪,详见 .metro.log
|
|
139
|
+
if (!ready) console.log(chalk.yellow(' ⚠️ Metro 30 秒内未就绪,详见 .metro.log;app 将回落到本地 bundle'));
|
|
137
140
|
}
|
|
138
141
|
console.log(chalk.cyan(`\n[10/${total}] 编译 Debug 包`));
|
|
139
142
|
const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
|
|
@@ -157,7 +160,8 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
157
160
|
const devPrefix = flags.device ? ['-s', flags.device] : [];
|
|
158
161
|
run(adb, [...devPrefix, 'install', '-r', apkPath], { quiet: true });
|
|
159
162
|
console.log(chalk.green(' ✓ 已安装到设备'));
|
|
160
|
-
run(adb, [...devPrefix, 'shell', 'monkey', '-p', config.appId, '-c', 'android.intent.category.LAUNCHER', '1'],
|
|
163
|
+
run(adb, [...devPrefix, 'shell', 'monkey', '-p', config.appId, '-c', 'android.intent.category.LAUNCHER', '1'],
|
|
164
|
+
{ quiet: true });
|
|
161
165
|
writeInstallState(buildDir, { appId: config.appId, variant: 'debug', installedAt: new Date().toISOString() });
|
|
162
166
|
await markVerified(buildDir);
|
|
163
167
|
}
|
package/src/config.js
CHANGED
|
@@ -84,10 +84,10 @@ 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
|
-
//
|
|
88
|
-
// 具体平台是否合法交给 Gradle / Xcode
|
|
87
|
+
// 极简防呆:至少 2 段、每段非空、只含 [a-zA-Z0-9_-],不允许首尾点或连续点
|
|
88
|
+
// 具体平台是否合法交给 Gradle / Xcode 报错(Android 不允许 -,iOS 允许)
|
|
89
89
|
if (typeof config.appId !== 'string' || !/^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(config.appId))
|
|
90
|
-
errors.push('appId
|
|
90
|
+
errors.push('appId 格式不合法(需反向域名格式,如 com.example.app): ' + config.appId);
|
|
91
91
|
if (!Number.isInteger(config.versionCode) || config.versionCode < 1) errors.push('versionCode 必须是正整数');
|
|
92
92
|
if (!config.android.abiFilters?.length) errors.push('android.abiFilters 不能为空');
|
|
93
93
|
if (errors.length) {
|
package/src/copy.js
CHANGED
|
@@ -14,7 +14,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
14
14
|
'Pods/', 'local.properties', '*.bak', 'gradle.properties.bak',
|
|
15
15
|
],
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* 内部:递归覆盖式复制模板,跳过 keepSet 命中的文件
|
|
18
18
|
*/
|
|
19
19
|
copyDirOverwrite = (src, dst, exclude, buildDir, keepSet) => {
|
|
20
20
|
ensureDir(dst);
|
|
@@ -30,7 +30,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
30
30
|
}
|
|
31
31
|
},
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* 内部:增量复制用户项目到 nodejs-project
|
|
34
34
|
* 保留 node_modules 等由其他步骤生成的产物
|
|
35
35
|
*/
|
|
36
36
|
copyDirIncremental = (src, dst, exclude) => {
|
|
@@ -53,15 +53,15 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
53
53
|
}
|
|
54
54
|
},
|
|
55
55
|
/**
|
|
56
|
-
* 获取 build
|
|
56
|
+
* 获取 build 目录路径:<userProject>/node-mobile-app-build
|
|
57
57
|
*/
|
|
58
58
|
getBuildDir = userProjectDir => path.join(userProjectDir, BUILD_DIR_NAME),
|
|
59
59
|
|
|
60
60
|
/**
|
|
61
61
|
* 把 buildDir/package.json 的依赖版本同步为主包 dependencies 的版本
|
|
62
|
-
* - 依赖名单来自 buildDir/package.json
|
|
63
|
-
* - 版本号来自 pkgRoot/package.json 的 dependencies
|
|
64
|
-
* -
|
|
62
|
+
* - 依赖名单来自 buildDir/package.json(模板声明需要哪些依赖)
|
|
63
|
+
* - 版本号来自 pkgRoot/package.json 的 dependencies(单一数据源)
|
|
64
|
+
* - 若主包缺少某个依赖,抛错中断(避免模板占位值被真正使用)
|
|
65
65
|
*/
|
|
66
66
|
syncTemplateDependencies = (pkgRoot, buildDir) => {
|
|
67
67
|
const buildPkgPath = path.join(buildDir, 'package.json');
|
|
@@ -84,25 +84,26 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
84
84
|
|
|
85
85
|
if (needWrite) {
|
|
86
86
|
writeIfChanged(buildPkgPath, JSON.stringify(buildPkg, null, 2) + '\n');
|
|
87
|
-
if (depsUpdated > 0)
|
|
87
|
+
if (depsUpdated > 0)
|
|
88
|
+
console.log(chalk.green(' ✓ 已同步 ' + depsUpdated + ' 个依赖版本到 ' + BUILD_DIR_NAME + '/package.json'));
|
|
88
89
|
}
|
|
89
90
|
|
|
90
91
|
if (missing.length > 0) {
|
|
91
92
|
throw new Error(
|
|
92
|
-
'template/package.json 声明的以下依赖在主包 dependencies
|
|
93
|
+
'template/package.json 声明的以下依赖在主包 dependencies 中不存在:\n' +
|
|
93
94
|
' ' + missing.join(', ') + '\n' +
|
|
94
|
-
' 请在 @flun/node-mobile-app 的 package.json
|
|
95
|
-
' 但必须存在,以便 syncTemplateDependencies
|
|
95
|
+
' 请在 @flun/node-mobile-app 的 package.json 中补上对应声明(版本号随意,\n' +
|
|
96
|
+
' 但必须存在,以便 syncTemplateDependencies 覆盖模板里的占位值);'
|
|
96
97
|
);
|
|
97
98
|
}
|
|
98
99
|
},
|
|
99
100
|
/**
|
|
100
|
-
* 把主包 node_modules 里被 npm
|
|
101
|
-
* 让 RN CLI / Metro
|
|
102
|
-
* -
|
|
103
|
-
* -
|
|
104
|
-
* -
|
|
105
|
-
* -
|
|
101
|
+
* 把主包 node_modules 里被 npm 嵌套(未提升到用户根)的依赖,按需链接到用户根 node_modules 下,
|
|
102
|
+
* 让 RN CLI / Metro 从用户根也能解析到它们;
|
|
103
|
+
* - 判据:从用户根视角解析不到,才建链接;能解析则跳过;
|
|
104
|
+
* - 范围:遍历主包 node_modules 顶层全部条目(含 @scope/ 下每个子项),不列举任何包名;
|
|
105
|
+
* - 落点:只在用户根 node_modules 缺失条目时建 symlink,不覆盖已有内容、不碰 package.json / package-lock.json;
|
|
106
|
+
* - 建链接失败:中断报错,不静默;
|
|
106
107
|
*/
|
|
107
108
|
linkNestedDeps = (pkgRoot, buildDir) => {
|
|
108
109
|
const pkgNm = path.join(pkgRoot, 'node_modules');
|
|
@@ -133,8 +134,8 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
133
134
|
fs.symlinkSync(realSrc, dst, 'junction'), linked++;
|
|
134
135
|
} catch (err) {
|
|
135
136
|
throw new Error(
|
|
136
|
-
'
|
|
137
|
-
'
|
|
137
|
+
'建立兜底链接失败:' + dst + ' -> ' + realSrc + '\n' +
|
|
138
|
+
' 原因:' + err.message + '\n' +
|
|
138
139
|
' 请检查 node_modules 是否被其它程序占用后重试'
|
|
139
140
|
);
|
|
140
141
|
}
|
|
@@ -143,8 +144,8 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
143
144
|
},
|
|
144
145
|
/**
|
|
145
146
|
* 把 CLI 包里的 template/ 逐文件覆盖到 build 目录
|
|
146
|
-
* - 只覆盖模板里存在的文件,不删除 buildDir
|
|
147
|
-
* -
|
|
147
|
+
* - 只覆盖模板里存在的文件,不删除 buildDir 里已有的其他内容(保留 Gradle 增量缓存)
|
|
148
|
+
* - 模板文件被改写时自动同步;新增文件自动添加
|
|
148
149
|
* - 若模板里删除了文件,旧副本残留由 CLI 版本指纹触发全量重建处理
|
|
149
150
|
*/
|
|
150
151
|
copyTemplate = (pkgRoot, buildDir, config) => {
|
|
@@ -163,7 +164,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
163
164
|
},
|
|
164
165
|
/**
|
|
165
166
|
* 复制用户 Node 项目到 <buildDir>/nodejs-assets/nodejs-project
|
|
166
|
-
* 复制后重写 package.json
|
|
167
|
+
* 复制后重写 package.json:只保留 dependencies,剔除 devDependencies;用 config 的 allowScripts
|
|
167
168
|
*/
|
|
168
169
|
copyUserProject = (userProjectDir, buildDir, config) => {
|
|
169
170
|
const targetDir = path.join(buildDir, 'nodejs-assets', 'nodejs-project');
|
|
@@ -174,14 +175,15 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
174
175
|
'mobileAppConfig.js', 'mobileAppConfig.mjs', 'mobileAppConfig.cjs'
|
|
175
176
|
];
|
|
176
177
|
|
|
177
|
-
//
|
|
178
|
+
// 增量复制:内容变化的文件才覆盖,保留 node_modules 和其他未变文件
|
|
178
179
|
copyDirIncremental(userProjectDir, targetDir, exclude);
|
|
179
|
-
//
|
|
180
|
+
// 黑名单式过滤:只删 devDependencies 和 allowScripts,其余字段原样保留
|
|
180
181
|
const userPkgPath = path.join(userProjectDir, 'package.json');
|
|
181
182
|
if (!fs.existsSync(userPkgPath)) throw new Error('用户项目缺少 package.json');
|
|
182
183
|
const pkg = readJson(userPkgPath), out = { ...pkg };
|
|
183
184
|
delete out.devDependencies;
|
|
184
|
-
out.allowScripts
|
|
185
|
+
out.allowScripts
|
|
186
|
+
(config.allowScripts && Object.keys(config.allowScripts).length) ? config.allowScripts : { node: true };
|
|
185
187
|
writeIfChanged(path.join(targetDir, 'package.json'), JSON.stringify(out, null, 2) + '\n');
|
|
186
188
|
|
|
187
189
|
console.log(chalk.green(' ✓ 已同步用户项目到 nodejs-assets/nodejs-project'));
|
|
@@ -191,11 +193,12 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
191
193
|
return { pkg: out, moduleType: out.type === 'module' ? 'module' : 'commonjs', targetDir };
|
|
192
194
|
},
|
|
193
195
|
/**
|
|
194
|
-
* 生成 <buildDir>/android/local.properties
|
|
196
|
+
* 生成 <buildDir>/android/local.properties(Gradle 需要 sdk.dir)
|
|
195
197
|
*/
|
|
196
198
|
writeLocalProperties = (buildDir, config) => {
|
|
197
199
|
const sdkPath = resolveSdkPath(config);
|
|
198
|
-
if (!sdkPath)
|
|
200
|
+
if (!sdkPath)
|
|
201
|
+
return console.warn(chalk.yellow(' ⚠️ 未找到 Android SDK,请在 mobileAppConfig.js 里配置 android.sdkPath'));
|
|
199
202
|
|
|
200
203
|
const localProps = path.join(buildDir, 'android', 'local.properties'),
|
|
201
204
|
escaped = sdkPath.replace(/\\/g, '\\\\').replace(/:/g, '\\:');
|
|
@@ -211,17 +214,17 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
211
214
|
if (fs.existsSync(entryPath)) return { created: false, path: entryPath };
|
|
212
215
|
|
|
213
216
|
const fallback = path.join(pkgRoot, 'cli-assets', 'server.js');
|
|
214
|
-
if (!fs.existsSync(fallback)) throw new Error('入口文件不存在: ' + config.serverPath + '
|
|
217
|
+
if (!fs.existsSync(fallback)) throw new Error('入口文件不存在: ' + config.serverPath + '(CLI 内置模板也缺失)');
|
|
215
218
|
|
|
216
219
|
ensureDir(path.dirname(entryPath)), fs.copyFileSync(fallback, entryPath);
|
|
217
220
|
console.log(chalk.green(' ✓ 生成默认入口文件: ' + config.serverPath));
|
|
218
|
-
console.log(chalk.gray('
|
|
221
|
+
console.log(chalk.gray(' (因为用户项目里没有这个文件,从 CLI 内置模板复制)'));
|
|
219
222
|
|
|
220
223
|
return { created: true, path: entryPath };
|
|
221
224
|
},
|
|
222
225
|
/**
|
|
223
226
|
* 确保用户项目根存在 mobileAppConfig.js
|
|
224
|
-
* 不存在时从 CLI
|
|
227
|
+
* 不存在时从 CLI 包根复制一份(带注释的模板)
|
|
225
228
|
*/
|
|
226
229
|
ensureUserConfig = (userProjectDir, pkgRoot) => {
|
|
227
230
|
const configPath = path.join(userProjectDir, 'mobileAppConfig.js');
|
|
@@ -232,16 +235,16 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
232
235
|
|
|
233
236
|
fs.copyFileSync(fallback, configPath);
|
|
234
237
|
console.log(chalk.green(' ✓ 生成默认配置文件: mobileAppConfig.js'));
|
|
235
|
-
console.log(chalk.gray('
|
|
238
|
+
console.log(chalk.gray(' (因为用户项目里没有这个文件,从 CLI 内置模板复制)'));
|
|
236
239
|
|
|
237
240
|
return { created: true, path: configPath };
|
|
238
241
|
},
|
|
239
242
|
/**
|
|
240
|
-
* 按 config.android.abiFilters 修改 buildDir 里的 app/build.gradle
|
|
243
|
+
* 按 config.android.abiFilters 修改 buildDir 里的 app/build.gradle:
|
|
241
244
|
* - defaultConfig.ndk.abiFilters
|
|
242
245
|
* - gradle.properties 的 reactNativeArchitectures
|
|
243
246
|
* 插件 build.gradle 读的是 project(":app").android.defaultConfig.ndk.abiFilters,
|
|
244
|
-
* 所以必须改这里,改插件自身的 abiFilters
|
|
247
|
+
* 所以必须改这里,改插件自身的 abiFilters 无效;
|
|
245
248
|
*/
|
|
246
249
|
patchAndroidAbi = (buildDir, config) => {
|
|
247
250
|
const file = path.join(buildDir, 'android', 'app', 'build.gradle');
|
|
@@ -253,10 +256,10 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
253
256
|
|
|
254
257
|
// defaultConfig 里的 ndk { abiFilters ... }
|
|
255
258
|
c = c.replace(/(defaultConfig\s*\{[\s\S]*?ndk\s*\{\s*abiFilters\s+)("[^"]*"(?:\s*,\s*"[^"]*")*)/, `$1${abiList}`);
|
|
256
|
-
//
|
|
259
|
+
// 兜底:没有 ndk {} 块时,插到 defaultConfig 里
|
|
257
260
|
if (!/ndk\s*\{\s*abiFilters/.test(c))
|
|
258
261
|
c = c.replace(/(defaultConfig\s*\{)/, `$1\n ndk {\n abiFilters ${abiList}\n }`);
|
|
259
|
-
// 不 patch splits.abi.include
|
|
262
|
+
// 不 patch splits.abi.include:AGP 禁止它和 ndk.abiFilters 同时存在
|
|
260
263
|
if (c !== before) writeIfChanged(file, c), console.log(chalk.green(` ✓ 已设置 app ndk.abiFilters = [${abiList}]`));
|
|
261
264
|
|
|
262
265
|
// 同步 gradle.properties 的 reactNativeArchitectures
|
|
@@ -265,7 +268,8 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
265
268
|
let p = fs.readFileSync(propsFile, 'utf-8');
|
|
266
269
|
const abiPlain = (config.android.abiFilters || []).join(','), before2 = p;
|
|
267
270
|
p = p.replace(/^reactNativeArchitectures=.*/gm, `reactNativeArchitectures=${abiPlain}`);
|
|
268
|
-
if (p !== before2) writeIfChanged(propsFile, p),
|
|
271
|
+
if (p !== before2) writeIfChanged(propsFile, p),
|
|
272
|
+
console.log(chalk.green(` ✓ 已设置 reactNativeArchitectures = ${abiPlain}`));
|
|
269
273
|
}
|
|
270
274
|
},
|
|
271
275
|
/**
|
|
@@ -278,7 +282,8 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
278
282
|
if (extras.length === 0) return;
|
|
279
283
|
|
|
280
284
|
const buildPkgPath = path.join(buildDir, 'package.json');
|
|
281
|
-
if (!fs.existsSync(buildPkgPath))
|
|
285
|
+
if (!fs.existsSync(buildPkgPath))
|
|
286
|
+
return console.warn(chalk.yellow(' ⚠️ 未找到 buildDir/package.json,跳过 extraRnDependencies'));
|
|
282
287
|
|
|
283
288
|
const buildPkg = readJson(buildPkgPath), missing = [];
|
|
284
289
|
if (!buildPkg.dependencies) buildPkg.dependencies = {};
|
package/src/fingerprint.js
CHANGED
|
@@ -63,7 +63,8 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
63
63
|
|
|
64
64
|
let pluginVersion = null, cliVersion = null;
|
|
65
65
|
const pluginPkg = path.join(pluginDir, 'package.json'), cliPkg = path.join(pkgRoot, 'package.json');
|
|
66
|
-
if (fs.existsSync(pluginPkg))
|
|
66
|
+
if (fs.existsSync(pluginPkg))
|
|
67
|
+
try { pluginVersion = JSON.parse(fs.readFileSync(pluginPkg, 'utf-8')).version; } catch { };
|
|
67
68
|
if (fs.existsSync(cliPkg)) try { cliVersion = JSON.parse(fs.readFileSync(cliPkg, 'utf-8')).version; } catch { };
|
|
68
69
|
|
|
69
70
|
const depsHash = hashUserDeps(userProjectDir), a = config.android || {},
|
|
@@ -108,7 +109,7 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
108
109
|
if (templateChanged) reasons.push('模板文件');
|
|
109
110
|
if (depsChanged) reasons.push('用户依赖');
|
|
110
111
|
if (appInfoChanged) reasons.push('应用信息');
|
|
111
|
-
console.log(chalk.yellow(' ⚠️
|
|
112
|
+
console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 Gradle 缓存、CMake 缓存与依赖缓存'));
|
|
112
113
|
clearGradleCaches(buildDir, userProjectDir);
|
|
113
114
|
removeDir(path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android', '.cxx'));
|
|
114
115
|
clearDepsCaches(buildDir);
|
|
@@ -117,7 +118,7 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
117
118
|
const reasons = [], targets = [];
|
|
118
119
|
if (depsChanged) reasons.push('用户依赖'), targets.push('依赖缓存', 'Gradle 缓存');
|
|
119
120
|
if (appInfoChanged) reasons.push('应用信息'), targets.push('Gradle 缓存');
|
|
120
|
-
console.log(chalk.yellow(' ⚠️
|
|
121
|
+
console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 ' + [...new Set(targets)].join('、')));
|
|
121
122
|
clearGradleCaches(buildDir, userProjectDir);
|
|
122
123
|
if (depsChanged) clearDepsCaches(buildDir);
|
|
123
124
|
}
|
package/src/generate.js
CHANGED
|
@@ -4,9 +4,9 @@ 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
|
|
11
11
|
* - 'Node failed: <原因>' 加载异常或 5 秒内未捕获 listen
|
|
12
12
|
*/
|
|
@@ -74,7 +74,9 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
74
74
|
}
|
|
75
75
|
})();
|
|
76
76
|
|
|
77
|
-
${config.advanced.keepAlive === false
|
|
77
|
+
${config.advanced.keepAlive === false
|
|
78
|
+
? '// keepAlive: false - 不发送心跳'
|
|
79
|
+
: "setInterval(() => rn_bridge.channel.send('heartbeat'), " + hb + ");"}
|
|
78
80
|
|
|
79
81
|
rn_bridge.channel.on('message', msg => rn_bridge.channel.send('Echo: ' + msg));
|
|
80
82
|
})();`,
|