@flun/node-mobile-app 1.0.1
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" +315 -0
- package/LICENSE +29 -0
- package/README.md +448 -0
- package/bin/cli.js +49 -0
- package/cli-assets/server.js +37 -0
- package/mobileAppConfig.js +146 -0
- package/package.json +88 -0
- package/scripts/copy-files.js +37 -0
- package/src/commands/build.js +109 -0
- package/src/commands/clean.js +25 -0
- package/src/commands/test.js +108 -0
- package/src/config.js +112 -0
- package/src/copy.js +235 -0
- package/src/fingerprint.js +102 -0
- package/src/generate.js +102 -0
- package/src/install.js +144 -0
- package/src/patcher.js +374 -0
- package/src/platform-ios.js +142 -0
- package/src/utils.js +131 -0
- package/template/.prettierrc.js +5 -0
- package/template/.watchmanconfig +22 -0
- package/template/@types/nodejs-mobile-react-native/index.d.ts +9 -0
- package/template/App.tsx +108 -0
- package/template/Gemfile +17 -0
- package/template/android/app/build.gradle +127 -0
- package/template/android/app/debug.keystore +0 -0
- package/template/android/app/proguard-rules.pro +10 -0
- package/template/android/app/src/main/AndroidManifest.xml +27 -0
- package/template/android/app/src/main/java/com/myapp/MainActivity.kt +22 -0
- package/template/android/app/src/main/java/com/myapp/MainApplication.kt +27 -0
- package/template/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/template/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/template/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/template/android/app/src/main/res/raw/keep.xml +1 -0
- package/template/android/app/src/main/res/values/strings.xml +3 -0
- package/template/android/app/src/main/res/values/styles.xml +9 -0
- package/template/android/build.gradle +21 -0
- package/template/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/template/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/template/android/gradle.properties +49 -0
- package/template/android/gradlew +248 -0
- package/template/android/gradlew.bat +98 -0
- package/template/android/settings.gradle +6 -0
- package/template/app.json +4 -0
- package/template/babel.config.js +3 -0
- package/template/eslint.config.js +6 -0
- package/template/index.js +12 -0
- package/template/ios/.xcode.env +11 -0
- package/template/ios/MyApp/AppDelegate.swift +55 -0
- package/template/ios/MyApp/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
- package/template/ios/MyApp/Images.xcassets/Contents.json +6 -0
- package/template/ios/MyApp/Info.plist +74 -0
- package/template/ios/MyApp/LaunchScreen.storyboard +47 -0
- package/template/ios/MyApp/PrivacyInfo.xcprivacy +37 -0
- package/template/ios/MyApp.xcodeproj/project.pbxproj +475 -0
- package/template/ios/MyApp.xcodeproj/xcshareddata/xcschemes/MyApp.xcscheme +88 -0
- package/template/ios/Podfile +34 -0
- package/template/metro.config.js +18 -0
- package/template/mobileApp.runtime.d.ts +16 -0
- package/template/package.json +18 -0
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +447 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @flun/node-mobile-app 配置文件
|
|
3
|
+
* 放置在用户 Node.js 项目根目录,CLI 会自动读取
|
|
4
|
+
* 如不提供此文件,CLI 使用默认配置
|
|
5
|
+
* 所有路径相对于用户项目根目录
|
|
6
|
+
*
|
|
7
|
+
* ===== 常用命令 =====
|
|
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(发布用,需先配置 android.signing)
|
|
12
|
+
* npx node-mobile-app build --ios 打 IPA(仅 macOS)
|
|
13
|
+
* npx node-mobile-app clean 清理构建缓存
|
|
14
|
+
*
|
|
15
|
+
* ===== 服务地址说明 =====
|
|
16
|
+
* CLI 会自动识别 server.js 实际监听的协议和端口(拦截 listen 调用),
|
|
17
|
+
* App 使用该地址加载页面,无需在此配置。
|
|
18
|
+
* 若 5 秒内未能识别,App 显示启动失败并可在页面展开详情查看原因。
|
|
19
|
+
* 动态端口(listen(0))会自动获取系统分配的真实端口。
|
|
20
|
+
*/
|
|
21
|
+
export default {
|
|
22
|
+
// ===== 必填 =====
|
|
23
|
+
serverPath: './server.js', // Node.js 启动脚本路径
|
|
24
|
+
|
|
25
|
+
// ===== 应用信息 =====
|
|
26
|
+
appName: null, // 应用显示名称(默认从 package.json 的 name 读取)
|
|
27
|
+
outputDir: null, // APK 输出目录(默认 ./out,相对于用户项目根)
|
|
28
|
+
appId: 'com.example.app', // 应用唯一标识(反向域名格式,Android/iOS 通用)
|
|
29
|
+
versionCode: 1, // 内部版本号(整数,Android versionCode / iOS CFBundleVersion,每次发版递增)
|
|
30
|
+
versionName: '1.0.0', // 用户可见版本名(Android versionName / iOS CFBundleShortVersionString)
|
|
31
|
+
|
|
32
|
+
// ===== WebView 配置 =====
|
|
33
|
+
webview: {
|
|
34
|
+
showLoading: true, // 是否显示加载指示器
|
|
35
|
+
loadingText: '正在启动服务...', // 加载提示文字
|
|
36
|
+
enableJavaScript: true, // 是否启用 JavaScript
|
|
37
|
+
enableDomStorage: true, // 是否启用 DOM Storage(localStorage 等)
|
|
38
|
+
allowFileAccess: true, // 是否允许访问本地文件
|
|
39
|
+
mixedContentMode: 'always', // 混合内容模式:never / always / compatibility
|
|
40
|
+
// WebView 背景色,支持三种写法:
|
|
41
|
+
// '#ffffff' / 'white' 固定色
|
|
42
|
+
// 'system' 跟随系统(内部用默认浅/深色)
|
|
43
|
+
// { light: '#fff', dark: '#1a1a1a' } 按系统主题切换
|
|
44
|
+
backgroundColor: '#ffffff',
|
|
45
|
+
// 仅影响 HTTPS 场景下 App 是否加载自签证书的页面:
|
|
46
|
+
// - 用 HTTP:与本开关无关
|
|
47
|
+
// - 用 HTTPS + 真证书(如 Let's Encrypt):与本开关无关
|
|
48
|
+
// - 用 HTTPS + 自签证书 + 已装进设备信任库:与本开关无关
|
|
49
|
+
// - 用 HTTPS + 自签证书 + 未装设备(本地测试最常见):必须设为 true
|
|
50
|
+
// ⚠️ 开启后仅对 127.0.0.1 / localhost 放行不受信任的 HTTPS 证书,其他域名仍严格校验
|
|
51
|
+
// 用于本地自签证书测试,生产环境建议关闭
|
|
52
|
+
allowSelfSignedCert: false,
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
// ===== Android 平台配置 =====
|
|
56
|
+
android: {
|
|
57
|
+
sdkPath: null, // Android SDK 路径(默认自动查找)
|
|
58
|
+
adbPath: null, // adb.exe 路径(默认自动查找)
|
|
59
|
+
minSdkVersion: 29, // 最低 SDK(29=Android 10.0)
|
|
60
|
+
targetSdkVersion: 36, // 目标 SDK
|
|
61
|
+
compileSdkVersion: 37, // 编译 SDK
|
|
62
|
+
buildToolsVersion: '37.0.0', // 构建工具版本
|
|
63
|
+
abiFilters: ['arm64-v8a'], // 打包架构(真机用 arm64-v8a, 模拟器加 x86_64)
|
|
64
|
+
icon: './build/icon.png', // 应用图标(建议 512x512 PNG)
|
|
65
|
+
usesCleartextTraffic: true, // 是否允许明文流量(http)
|
|
66
|
+
buildNativeModules: false, // 是否在 nodejs-project 里编译原生模块(bcrypt 等)
|
|
67
|
+
permissions: [ // 额外权限(按需添加)
|
|
68
|
+
// 'android.permission.INTERNET', // INTERNET 始终包含,不需配置
|
|
69
|
+
// 'android.permission.ACCESS_NETWORK_STATE',
|
|
70
|
+
// 'android.permission.CAMERA',
|
|
71
|
+
// 'android.permission.READ_EXTERNAL_STORAGE',
|
|
72
|
+
],
|
|
73
|
+
// 签名配置(发布 Release 版本时必填)
|
|
74
|
+
signing: {
|
|
75
|
+
// keystore: './build/release.keystore',
|
|
76
|
+
// storePassword: '',
|
|
77
|
+
// keyAlias: '',
|
|
78
|
+
// keyPassword: '',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
// ===== iOS 平台配置 =====
|
|
83
|
+
// 说明: 当前仅支持 Node 18;Node 18+ 需自行适配 libnode.so
|
|
84
|
+
ios: {
|
|
85
|
+
deploymentTarget: '15.1', // 最低 iOS 版本(RN 0.87 要求 15.1+)
|
|
86
|
+
icon: './build/icon.png', // 应用图标(建议 1024x1024 PNG)
|
|
87
|
+
// 签名配置(仅 macOS,发布时必填):
|
|
88
|
+
// teamId Apple Developer Team ID
|
|
89
|
+
// provisioningProfile 描述文件路径(写入 exportOptions.plist)
|
|
90
|
+
// certificate P12 证书路径(导入临时 keychain 供 xcodebuild 签名用)
|
|
91
|
+
// certificatePassword P12 密码
|
|
92
|
+
signing: {
|
|
93
|
+
// teamId: '',
|
|
94
|
+
// provisioningProfile: './build/profile.mobileprovision',
|
|
95
|
+
// certificate: './build/cert.p12',
|
|
96
|
+
// certificatePassword: '',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// ===== 高级选项 =====
|
|
101
|
+
advanced: {
|
|
102
|
+
keepAlive: true, // 是否发送心跳保持 Node.js 进程活跃
|
|
103
|
+
heartbeatInterval: 5000, // 心跳间隔(毫秒)
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
// ===== 扩展点 =====
|
|
107
|
+
// 不重置的模板文件(路径相对于 node-mobile-app-build/,如 'android/app/src/main/AndroidManifest.xml')
|
|
108
|
+
keepFiles: [],
|
|
109
|
+
// 额外 RN 原生依赖包名数组(用于 autolink 识别,含原生模块的包必须在此声明)
|
|
110
|
+
// 安装命令用 --save-dev,这些是开发依赖,不会进移动端 Node 环境
|
|
111
|
+
// 示例: npm install --save-dev react-native-camera
|
|
112
|
+
// extraRnDependencies: ['react-native-camera']
|
|
113
|
+
// 未安装的包会在 test/build 时中断并提示安装命令
|
|
114
|
+
extraRnDependencies: [],
|
|
115
|
+
|
|
116
|
+
// ===== 排除文件/目录(复制到壳工程时排除) =====
|
|
117
|
+
excludeFiles: [
|
|
118
|
+
'.git/',
|
|
119
|
+
'.vscode/',
|
|
120
|
+
'.idea/',
|
|
121
|
+
'.vs/',
|
|
122
|
+
'node_modules/', // 如需保留请删除此行
|
|
123
|
+
'sessions/',
|
|
124
|
+
'users.json',
|
|
125
|
+
'.env',
|
|
126
|
+
'*.log',
|
|
127
|
+
'*.md',
|
|
128
|
+
'*.tgz',
|
|
129
|
+
'*.bak',
|
|
130
|
+
'.backup*',
|
|
131
|
+
'dist/',
|
|
132
|
+
'out/',
|
|
133
|
+
'build/',
|
|
134
|
+
'tests/',
|
|
135
|
+
'./mobileAppConfig.js', // 配置文件本身不打包
|
|
136
|
+
'nodejs-assets/', // 插件 postinstall 生成的示例骨架
|
|
137
|
+
],
|
|
138
|
+
|
|
139
|
+
// ===== 允许执行安装脚本的包 =====
|
|
140
|
+
allowScripts: {
|
|
141
|
+
'node': true,
|
|
142
|
+
'unrs-resolver': true,
|
|
143
|
+
'@parcel/watcher': true,
|
|
144
|
+
'bcrypt': true,
|
|
145
|
+
},
|
|
146
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@flun/node-mobile-app",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "将 Node.js 项目一键打包为 Android / iOS 移动应用(基于 nodejs-mobile-react-native)",
|
|
5
|
+
"author": "flun <open@flun.top>",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"node-mobile",
|
|
8
|
+
"nodejs",
|
|
9
|
+
"nodejs-mobile",
|
|
10
|
+
"react-native",
|
|
11
|
+
"android",
|
|
12
|
+
"ios",
|
|
13
|
+
"mobile",
|
|
14
|
+
"mobile-app",
|
|
15
|
+
"apk",
|
|
16
|
+
"ipa",
|
|
17
|
+
"express",
|
|
18
|
+
"webview",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"type": "module",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"bin/",
|
|
27
|
+
"cli-assets/",
|
|
28
|
+
"scripts/",
|
|
29
|
+
"src/",
|
|
30
|
+
"template/",
|
|
31
|
+
"LICENSE",
|
|
32
|
+
"mobileAppConfig.js",
|
|
33
|
+
"README.md",
|
|
34
|
+
"自定义libnode指南.md",
|
|
35
|
+
"Android 构建指南.md",
|
|
36
|
+
"package.json"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"postinstall": "node scripts/copy-files.js"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@babel/core": "^8.0.6",
|
|
43
|
+
"@babel/preset-env": "^8.0.6",
|
|
44
|
+
"@babel/runtime": "^8.0.5",
|
|
45
|
+
"@react-native-community/cli": "^20.2.0",
|
|
46
|
+
"@react-native-community/cli-platform-android": "^20.2.0",
|
|
47
|
+
"@react-native-community/cli-platform-ios": "^20.2.0",
|
|
48
|
+
"@react-native/babel-preset": "^0.87.1",
|
|
49
|
+
"@react-native/eslint-config": "^0.87.1",
|
|
50
|
+
"@react-native/jest-preset": "^0.87.1",
|
|
51
|
+
"@react-native/metro-config": "^0.87.1",
|
|
52
|
+
"@react-native/new-app-screen": "^0.87.1",
|
|
53
|
+
"@react-native/typescript-config": "^0.87.1",
|
|
54
|
+
"jimp": "^1.6.0",
|
|
55
|
+
"nodejs-mobile-react-native": "^18.20.4",
|
|
56
|
+
"react": "^19.3.0",
|
|
57
|
+
"react-native": "^0.87.1",
|
|
58
|
+
"react-native-safe-area-context": "^5.10.0",
|
|
59
|
+
"react-native-webview": "^14.0.1"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/react": "^19.3.0",
|
|
63
|
+
"prettier": "^3.9.8",
|
|
64
|
+
"typescript": "^7.0.2"
|
|
65
|
+
},
|
|
66
|
+
"allowScripts": {
|
|
67
|
+
"node": true,
|
|
68
|
+
"nodejs-mobile-react-native": true,
|
|
69
|
+
"unrs-resolver": true,
|
|
70
|
+
"@parcel/watcher": true
|
|
71
|
+
},
|
|
72
|
+
"homepage": "https://github.com/OpenFlun/node-mobile-app#readme",
|
|
73
|
+
"repository": {
|
|
74
|
+
"type": "git",
|
|
75
|
+
"url": "git+https://github.com/OpenFlun/node-mobile-app.git"
|
|
76
|
+
},
|
|
77
|
+
"gitee": "https://gitee.com/OpenFlun/node-mobile-app.git",
|
|
78
|
+
"bugs": {
|
|
79
|
+
"url": "https://github.com/OpenFlun/node-mobile-app/issues"
|
|
80
|
+
},
|
|
81
|
+
"license": "ISC",
|
|
82
|
+
"engines": {
|
|
83
|
+
"node": ">= 18.20.4"
|
|
84
|
+
},
|
|
85
|
+
"bin": {
|
|
86
|
+
"node-mobile-app": "./bin/cli.js"
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
// 要复制的文件位置和文件目标位置
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url), __dirname = path.dirname(__filename),
|
|
7
|
+
initCwd = process.env.INIT_CWD, fileName = 'mobileAppConfig.js', targetDir = initCwd ? path.resolve(initCwd) : '',
|
|
8
|
+
// 要拷贝的文件和目标文件路径
|
|
9
|
+
sourceFile = path.join(__dirname, '..', fileName), targetFile = path.join(targetDir, fileName);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 复制文件到项目根目录
|
|
13
|
+
* >查看定义:@see {@link copyFile}
|
|
14
|
+
* @returns {boolean} - 复制是否成功
|
|
15
|
+
*/
|
|
16
|
+
const copyFile = () => {
|
|
17
|
+
console.log(`🔍 检查 ${fileName} 文件...`), console.log(`📁 项目根目录:${targetDir}`);
|
|
18
|
+
try {
|
|
19
|
+
// 无 INIT_CWD(非用户项目安装场景)|| 目标目录不是 Node 项目 || 包自身的 npm install(开发调试)→ 跳过
|
|
20
|
+
if (!targetDir) return true;
|
|
21
|
+
if (!fs.existsSync(path.join(targetDir, 'package.json'))) return true;
|
|
22
|
+
if (targetDir === path.resolve(__dirname, '..')) return true;
|
|
23
|
+
if (fs.existsSync(targetFile)) return true; // 目标文件已存在 → 直接返回
|
|
24
|
+
console.log(`⚠️ 在项目根目录未找到 ${fileName} 文件,正在创建...`);
|
|
25
|
+
|
|
26
|
+
fs.copyFileSync(sourceFile, targetFile); // 复制源文件到项目根目录
|
|
27
|
+
console.log(`✓ 已创建 ${fileName} 示例文件:${targetFile}`);
|
|
28
|
+
return true;
|
|
29
|
+
} catch (error) {
|
|
30
|
+
console.error(`✗ 创建 ${fileName} 文件失败:`, error.message);
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 执行脚本并导出函数
|
|
36
|
+
if (process.argv[1] === __filename) copyFile();
|
|
37
|
+
export { copyFile };
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { loadConfig, validateConfig } from '../config.js';
|
|
5
|
+
import {
|
|
6
|
+
getBuildDir, copyTemplate, copyUserProject, writeLocalProperties, ensureUserEntry, ensureUserConfig, patchAndroidAbi,
|
|
7
|
+
patchExtraRnDependencies
|
|
8
|
+
} from '../copy.js';
|
|
9
|
+
import { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl } from '../install.js';
|
|
10
|
+
import { generateMainJs, generateRuntime } from '../generate.js';
|
|
11
|
+
import { checkAndResetCache } from '../fingerprint.js';
|
|
12
|
+
import {
|
|
13
|
+
patchAppConfig, patchAndroidConfig, patchSigning, patchIcon, patchIosConfig, patchIosIcon, resolveAppName
|
|
14
|
+
} from '../patcher.js';
|
|
15
|
+
import { ensureMac, checkIosTools, podInstall, xcodebuildArchive, xcodebuildExport } from '../platform-ios.js';
|
|
16
|
+
import { run, ensureDir, sanitizeFileName } from '../utils.js';
|
|
17
|
+
|
|
18
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'),
|
|
19
|
+
runBuild = async (flags) => {
|
|
20
|
+
const userProjectDir = process.cwd(), release = !!flags.release, isIos = !!flags.ios;
|
|
21
|
+
console.log(`\n=== node-mobile-app build (${isIos ? 'iOS' : 'Android'}, ${release ? 'release' : 'debug'}) ===\n`);
|
|
22
|
+
console.log('用户项目:', userProjectDir);
|
|
23
|
+
|
|
24
|
+
if (isIos) ensureMac(), checkIosTools();
|
|
25
|
+
console.log('\n[0/7] 检查用户配置与入口文件'), ensureUserConfig(userProjectDir, PKG_ROOT);
|
|
26
|
+
|
|
27
|
+
const config = await loadConfig(userProjectDir);
|
|
28
|
+
ensureUserEntry(userProjectDir, config, PKG_ROOT), validateConfig(config, userProjectDir);
|
|
29
|
+
|
|
30
|
+
const buildDir = getBuildDir(userProjectDir);
|
|
31
|
+
console.log('\n[1/7] 重置 RN 工程模板');
|
|
32
|
+
copyTemplate(PKG_ROOT, buildDir, config);
|
|
33
|
+
patchAndroidAbi(buildDir, config);
|
|
34
|
+
patchExtraRnDependencies(buildDir, userProjectDir, config);
|
|
35
|
+
patchAppConfig(buildDir, userProjectDir, config);
|
|
36
|
+
patchAndroidConfig(buildDir, config);
|
|
37
|
+
patchSigning(buildDir, userProjectDir, config);
|
|
38
|
+
await patchIcon(buildDir, userProjectDir, config);
|
|
39
|
+
patchIosConfig(buildDir, userProjectDir, config);
|
|
40
|
+
await patchIosIcon(buildDir, userProjectDir, config);
|
|
41
|
+
|
|
42
|
+
console.log('\n[2/7] 检查缓存指纹'), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
|
|
43
|
+
console.log('\n[3/7] 复制用户项目');
|
|
44
|
+
const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
|
|
45
|
+
console.log('\n[4/7] 安装依赖'), installUserDeps(targetDir, config);
|
|
46
|
+
patchNodejsMobilePlugin(userProjectDir, config);
|
|
47
|
+
patchWebViewSsl(userProjectDir, config);
|
|
48
|
+
writeLocalProperties(buildDir, config);
|
|
49
|
+
|
|
50
|
+
console.log('\n[5/7] 生成桥接文件');
|
|
51
|
+
generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
|
|
52
|
+
|
|
53
|
+
const appName = resolveAppName(userProjectDir, config), safeName = sanitizeFileName(appName),
|
|
54
|
+
outDir = path.resolve(userProjectDir, config.outputDir || './out');
|
|
55
|
+
ensureDir(outDir);
|
|
56
|
+
|
|
57
|
+
if (isIos) {
|
|
58
|
+
console.log('\n[6/7] pod install'), podInstall(path.join(buildDir, 'ios'));
|
|
59
|
+
console.log('\n[7/7] xcodebuild archive + export'), xcodebuildArchive(buildDir, config, release, userProjectDir);
|
|
60
|
+
|
|
61
|
+
const ipaPath = xcodebuildExport(buildDir, config, release, outDir, userProjectDir),
|
|
62
|
+
dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.ipa`, dst = path.join(outDir, dstName);
|
|
63
|
+
fs.copyFileSync(ipaPath, dst), console.log('\n✅ 打包完成');
|
|
64
|
+
|
|
65
|
+
const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
|
|
66
|
+
console.log(` ${dst} (${mb} MB)`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log('\n[6/7] 打包 APK');
|
|
71
|
+
const assetsDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'assets'),
|
|
72
|
+
resDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'res');
|
|
73
|
+
|
|
74
|
+
ensureDir(assetsDir);
|
|
75
|
+
run('npx', [
|
|
76
|
+
'react-native', 'bundle',
|
|
77
|
+
'--platform', 'android',
|
|
78
|
+
'--dev', release ? 'false' : 'true',
|
|
79
|
+
'--entry-file', 'index.js',
|
|
80
|
+
'--bundle-output', path.join(assetsDir, 'index.android.bundle'),
|
|
81
|
+
'--assets-dest', resDir,
|
|
82
|
+
], { cwd: buildDir });
|
|
83
|
+
|
|
84
|
+
const task = release ? 'assembleRelease' : 'assembleDebug', gradleArgs = [task];
|
|
85
|
+
if (flags.arch) gradleArgs.push(`-PreactNativeArchitectures=${flags.arch}`);
|
|
86
|
+
|
|
87
|
+
const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
|
|
88
|
+
run(gradlew, gradleArgs, { cwd: path.join(buildDir, 'android') });
|
|
89
|
+
|
|
90
|
+
const apkDir = path.join(buildDir, 'android', 'app', 'build', 'outputs', 'apk', release ? 'release' : 'debug');
|
|
91
|
+
console.log('\n✅ 打包完成');
|
|
92
|
+
|
|
93
|
+
if (fs.existsSync(apkDir)) {
|
|
94
|
+
const apks = fs.readdirSync(apkDir).filter((f) => f.endsWith('.apk'));
|
|
95
|
+
if (apks.length === 0) console.log(' ⚠️ 未找到 APK');
|
|
96
|
+
|
|
97
|
+
for (const f of apks) {
|
|
98
|
+
const src = path.join(apkDir, f), dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.apk`,
|
|
99
|
+
dst = path.join(outDir, dstName);
|
|
100
|
+
|
|
101
|
+
fs.copyFileSync(src, dst);
|
|
102
|
+
const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
|
|
103
|
+
console.log(` ${dst} (${mb} MB)`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
else console.log(' ⚠️ 未找到 APK 输出目录:', apkDir);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export { runBuild };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { removeDir } from '../utils.js';
|
|
4
|
+
import { getBuildDir } from '../copy.js';
|
|
5
|
+
|
|
6
|
+
const runClean = async () => {
|
|
7
|
+
const userProjectDir = process.cwd(), buildDir = getBuildDir(userProjectDir),
|
|
8
|
+
targets = [
|
|
9
|
+
{ label: '构建目录', path: buildDir },
|
|
10
|
+
{
|
|
11
|
+
label: '插件 CMake 缓存', path: path.join(userProjectDir, 'node_modules', 'nodejs-mobile-react-native', 'android',
|
|
12
|
+
'.cxx')
|
|
13
|
+
},
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
for (const t of targets) {
|
|
17
|
+
if (fs.existsSync(t.path))
|
|
18
|
+
removeDir(t.path), console.log('已清理:', t.label, '->', path.relative(userProjectDir, t.path) || t.path);
|
|
19
|
+
else console.log('跳过(不存在):', t.label);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log('\n✅ 清理完成');
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export { runClean };
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { loadConfig, validateConfig } from '../config.js';
|
|
5
|
+
import {
|
|
6
|
+
getBuildDir, copyTemplate, copyUserProject, writeLocalProperties, ensureUserEntry, ensureUserConfig, patchAndroidAbi,
|
|
7
|
+
patchExtraRnDependencies
|
|
8
|
+
} from '../copy.js';
|
|
9
|
+
import { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl } from '../install.js';
|
|
10
|
+
import { generateMainJs, generateRuntime } from '../generate.js';
|
|
11
|
+
import { checkAndResetCache } from '../fingerprint.js';
|
|
12
|
+
import { patchAppConfig, patchAndroidConfig, patchSigning, patchIcon, patchIosConfig, patchIosIcon } from '../patcher.js';
|
|
13
|
+
import { ensureMac, checkIosTools, podInstall } from '../platform-ios.js';
|
|
14
|
+
import { run, runSilent, findAdb, ensureDir, findHermesc } from '../utils.js';
|
|
15
|
+
|
|
16
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'),
|
|
17
|
+
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);
|
|
20
|
+
|
|
21
|
+
if (isIos) ensureMac(), checkIosTools();
|
|
22
|
+
console.log('\n[0/9] 检查用户配置与入口文件'), ensureUserConfig(userProjectDir, PKG_ROOT);
|
|
23
|
+
|
|
24
|
+
const config = await loadConfig(userProjectDir);
|
|
25
|
+
ensureUserEntry(userProjectDir, config, PKG_ROOT), validateConfig(config, userProjectDir);
|
|
26
|
+
|
|
27
|
+
const buildDir = getBuildDir(userProjectDir);
|
|
28
|
+
console.log('\n[1/9] 重置 RN 工程模板');
|
|
29
|
+
copyTemplate(PKG_ROOT, buildDir, config);
|
|
30
|
+
patchAndroidAbi(buildDir, config);
|
|
31
|
+
patchExtraRnDependencies(buildDir, userProjectDir, config);
|
|
32
|
+
patchAppConfig(buildDir, userProjectDir, config);
|
|
33
|
+
patchAndroidConfig(buildDir, config);
|
|
34
|
+
patchSigning(buildDir, userProjectDir, config);
|
|
35
|
+
await patchIcon(buildDir, userProjectDir, config);
|
|
36
|
+
patchIosConfig(buildDir, userProjectDir, config);
|
|
37
|
+
await patchIosIcon(buildDir, userProjectDir, config);
|
|
38
|
+
|
|
39
|
+
console.log('\n[2/9] 检查缓存指纹'), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
|
|
40
|
+
console.log('\n[3/9] 复制用户项目');
|
|
41
|
+
const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
|
|
42
|
+
|
|
43
|
+
console.log('\n[4/9] 安装依赖'), installUserDeps(targetDir, config);
|
|
44
|
+
patchNodejsMobilePlugin(userProjectDir, config);
|
|
45
|
+
patchWebViewSsl(userProjectDir, config);
|
|
46
|
+
writeLocalProperties(buildDir, config);
|
|
47
|
+
|
|
48
|
+
console.log('\n[5/9] 生成桥接文件');
|
|
49
|
+
generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
|
|
50
|
+
|
|
51
|
+
if (isIos) {
|
|
52
|
+
console.log('\n[6/9] pod install'), podInstall(path.join(buildDir, 'ios'));
|
|
53
|
+
console.log('\n[7/9] 生成 JS bundle + Hermes 字节码(由 Xcode Run Script 自动)');
|
|
54
|
+
const bundlePath = path.join(buildDir, 'ios', 'main.jsbundle');
|
|
55
|
+
runSilent('npx', [
|
|
56
|
+
'react-native', 'bundle',
|
|
57
|
+
'--platform', 'ios',
|
|
58
|
+
'--dev', 'false',
|
|
59
|
+
'--entry-file', 'index.js',
|
|
60
|
+
'--bundle-output', bundlePath,
|
|
61
|
+
], { cwd: buildDir });
|
|
62
|
+
console.log(' ✓ bundle 已生成');
|
|
63
|
+
|
|
64
|
+
console.log('\n[8/9] 编译并安装到模拟器/设备');
|
|
65
|
+
const args = ['react-native', 'run-ios'];
|
|
66
|
+
if (flags.device) args.push('--device', flags.device);
|
|
67
|
+
// FORCE_BUNDLING=1:即使 Debug+模拟器也把 main.jsbundle 打进 App
|
|
68
|
+
const env = { ...process.env, FORCE_BUNDLING: '1' };
|
|
69
|
+
run('npx', args, { cwd: buildDir, env });
|
|
70
|
+
} else {
|
|
71
|
+
console.log('\n[6/9] 生成 JS bundle(离线可用)');
|
|
72
|
+
const assetsDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'assets'),
|
|
73
|
+
resDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'res');
|
|
74
|
+
ensureDir(assetsDir);
|
|
75
|
+
|
|
76
|
+
const bundlePath = path.join(assetsDir, 'index.android.bundle');
|
|
77
|
+
runSilent('npx', [
|
|
78
|
+
'react-native', 'bundle',
|
|
79
|
+
'--platform', 'android',
|
|
80
|
+
'--dev', 'false',
|
|
81
|
+
'--entry-file', 'index.js',
|
|
82
|
+
'--bundle-output', bundlePath,
|
|
83
|
+
'--assets-dest', resDir,
|
|
84
|
+
], { cwd: buildDir });
|
|
85
|
+
console.log(' ✓ bundle 已生成');
|
|
86
|
+
|
|
87
|
+
console.log('\n[7/9] 编译 Hermes 字节码(加速离线启动)');
|
|
88
|
+
const hermesc = findHermesc(userProjectDir);
|
|
89
|
+
if (hermesc) {
|
|
90
|
+
const hbcPath = bundlePath + '.hbc';
|
|
91
|
+
runSilent(hermesc, ['-emit-binary', '-out', hbcPath, bundlePath]);
|
|
92
|
+
fs.copyFileSync(hbcPath, bundlePath), fs.unlinkSync(hbcPath);
|
|
93
|
+
console.log(' ✓ 已替换为 Hermes 字节码');
|
|
94
|
+
}
|
|
95
|
+
else console.warn(' ⚠️ 未找到 hermesc,跳过字节码编译(离线启动会较慢)');
|
|
96
|
+
|
|
97
|
+
console.log('\n[8/9] 配置 adb 端口转发');
|
|
98
|
+
const adb = findAdb(config);
|
|
99
|
+
console.log(' adb:', adb), run(adb, ['reverse', 'tcp:8081', 'tcp:8081']);
|
|
100
|
+
|
|
101
|
+
console.log('\n[9/9] 编译并安装到设备');
|
|
102
|
+
const args = ['react-native', 'run-android'];
|
|
103
|
+
if (flags.device) args.push('--deviceId', flags.device);
|
|
104
|
+
run('npx', args, { cwd: buildDir });
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export { runTest };
|
package/src/config.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { pathToFileURL } from 'url';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_CONFIG = {
|
|
6
|
+
serverPath: './server.js',
|
|
7
|
+
appName: null,
|
|
8
|
+
outputDir: null,
|
|
9
|
+
appId: 'com.example.app',
|
|
10
|
+
versionCode: 1,
|
|
11
|
+
versionName: '1.0.0',
|
|
12
|
+
webview: {
|
|
13
|
+
showLoading: true,
|
|
14
|
+
loadingText: '正在启动服务...',
|
|
15
|
+
enableJavaScript: true,
|
|
16
|
+
enableDomStorage: true,
|
|
17
|
+
allowFileAccess: true,
|
|
18
|
+
mixedContentMode: 'always',
|
|
19
|
+
backgroundColor: '#ffffff',
|
|
20
|
+
allowSelfSignedCert: false,
|
|
21
|
+
},
|
|
22
|
+
android: {
|
|
23
|
+
sdkPath: null,
|
|
24
|
+
adbPath: null,
|
|
25
|
+
minSdkVersion: 29,
|
|
26
|
+
targetSdkVersion: 36,
|
|
27
|
+
compileSdkVersion: 37,
|
|
28
|
+
buildToolsVersion: '37.0.0',
|
|
29
|
+
abiFilters: ['arm64-v8a'],
|
|
30
|
+
icon: './build/icon.png',
|
|
31
|
+
usesCleartextTraffic: true,
|
|
32
|
+
permissions: [],
|
|
33
|
+
buildNativeModules: false,
|
|
34
|
+
signing: {},
|
|
35
|
+
},
|
|
36
|
+
ios: {
|
|
37
|
+
deploymentTarget: '15.1',
|
|
38
|
+
icon: './build/icon.png',
|
|
39
|
+
signing: {},
|
|
40
|
+
},
|
|
41
|
+
advanced: {
|
|
42
|
+
keepAlive: true,
|
|
43
|
+
heartbeatInterval: 5000,
|
|
44
|
+
},
|
|
45
|
+
keepFiles: [],
|
|
46
|
+
extraRnDependencies: [],
|
|
47
|
+
excludeFiles: [
|
|
48
|
+
'.git/', '.vscode/', '.idea/', '.vs/',
|
|
49
|
+
'node_modules/', 'sessions/', 'users.json', '.env',
|
|
50
|
+
'*.log', '*.md', '*.tgz', '*.bak', '.backup*', 'dist/', 'out/', 'build/', 'tests/',
|
|
51
|
+
'node-mobile-app-build/', 'nodejs-assets/',
|
|
52
|
+
],
|
|
53
|
+
allowScripts: {
|
|
54
|
+
'node': true,
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
merge = (def, user) => {
|
|
58
|
+
const out = { ...def };
|
|
59
|
+
for (const k of Object.keys(user)) {
|
|
60
|
+
if (user[k] && typeof user[k] === 'object' && !Array.isArray(user[k])) out[k] = merge(def[k] || {}, user[k]);
|
|
61
|
+
else if (user[k] !== undefined && user[k] !== null) out[k] = user[k];
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
},
|
|
65
|
+
loadConfig = async userProjectDir => {
|
|
66
|
+
for (const name of ['mobileAppConfig.js', 'mobileAppConfig.mjs', 'mobileAppConfig.cjs']) {
|
|
67
|
+
const p = path.join(userProjectDir, name);
|
|
68
|
+
if (fs.existsSync(p)) {
|
|
69
|
+
console.log('读取配置:', name);
|
|
70
|
+
const mod = await import(pathToFileURL(p).href);
|
|
71
|
+
return merge(DEFAULT_CONFIG, mod.default || mod);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
console.log('未找到 mobileAppConfig.js,使用默认配置');
|
|
75
|
+
return DEFAULT_CONFIG;
|
|
76
|
+
},
|
|
77
|
+
validateConfig = (config, userProjectDir) => {
|
|
78
|
+
const errors = [], entry = path.join(userProjectDir, config.serverPath);
|
|
79
|
+
if (!fs.existsSync(entry)) errors.push('serverPath 不存在: ' + config.serverPath);
|
|
80
|
+
|
|
81
|
+
const pkg = path.join(userProjectDir, 'package.json');
|
|
82
|
+
if (!fs.existsSync(pkg)) errors.push('用户项目缺少 package.json');
|
|
83
|
+
// 极简防呆:至少 2 段、每段非空、只含 [a-zA-Z0-9_-],不允许首尾点或连续点
|
|
84
|
+
// 具体平台是否合法交给 Gradle / Xcode 报错(Android 不允许 -,iOS 允许)
|
|
85
|
+
if (
|
|
86
|
+
typeof config.appId !== 'string' ||
|
|
87
|
+
!/^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(config.appId)
|
|
88
|
+
)
|
|
89
|
+
errors.push('appId 格式不合法(需反向域名格式,如 com.example.app): ' + config.appId);
|
|
90
|
+
if (!Number.isInteger(config.versionCode) || config.versionCode < 1) errors.push('versionCode 必须是正整数');
|
|
91
|
+
if (!config.android.abiFilters?.length) errors.push('android.abiFilters 不能为空');
|
|
92
|
+
if (errors.length) {
|
|
93
|
+
errors.forEach((e) => console.error(' - ' + e));
|
|
94
|
+
throw new Error('配置校验失败');
|
|
95
|
+
}
|
|
96
|
+
console.log('配置校验通过');
|
|
97
|
+
},
|
|
98
|
+
resolveSdkPath = (config) => {
|
|
99
|
+
if (config.android && config.android.sdkPath) return config.android.sdkPath;
|
|
100
|
+
for (const env of ['ANDROID_HOME', 'ANDROID_SDK_ROOT']) {
|
|
101
|
+
const base = process.env[env];
|
|
102
|
+
if (base && fs.existsSync(base)) return base;
|
|
103
|
+
}
|
|
104
|
+
const localAppData = process.env.LOCALAPPDATA || '', home = process.env.USERPROFILE || process.env.HOME || '',
|
|
105
|
+
candidates = [path.join(localAppData, 'Android', 'Sdk'), path.join(home, 'Library', 'Android', 'sdk'),
|
|
106
|
+
path.join(home, 'Android', 'Sdk')];
|
|
107
|
+
|
|
108
|
+
for (const c of candidates) if (c && fs.existsSync(path.join(c, 'platform-tools'))) return c;
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export { DEFAULT_CONFIG, loadConfig, validateConfig, resolveSdkPath };
|