@flun/node-mobile-app 1.0.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Android /346/236/204/345/273/272/346/214/207/345/215/227.md" +348 -315
- package/CHANGELOG.md +103 -0
- package/README.md +450 -448
- package/mobileAppConfig.js +15 -5
- package/package.json +20 -19
- package/src/commands/build.js +1 -1
- package/src/commands/clean.js +1 -1
- package/src/commands/test.js +1 -1
- package/src/config.js +1 -3
- package/src/copy.js +58 -13
- package/src/fingerprint.js +3 -3
- package/src/generate.js +6 -4
- package/src/install.js +51 -14
- package/src/utils.js +34 -3
- package/template/App.tsx +3 -2
- package/template/package.json +18 -18
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +444 -446
- package/template/@types/nodejs-mobile-react-native/index.d.ts +0 -9
package/mobileAppConfig.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
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
|
|
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
|
*
|
|
@@ -63,14 +63,25 @@ export default {
|
|
|
63
63
|
abiFilters: ['arm64-v8a'], // 打包架构(真机用 arm64-v8a, 模拟器加 x86_64)
|
|
64
64
|
icon: './build/icon.png', // 应用图标(建议 512x512 PNG)
|
|
65
65
|
usesCleartextTraffic: true, // 是否允许明文流量(http)
|
|
66
|
-
buildNativeModules: false, // 是否在 nodejs-project 里编译原生模块(bcrypt 等)
|
|
67
66
|
permissions: [ // 额外权限(按需添加)
|
|
68
67
|
// 'android.permission.INTERNET', // INTERNET 始终包含,不需配置
|
|
69
68
|
// 'android.permission.ACCESS_NETWORK_STATE',
|
|
70
69
|
// 'android.permission.CAMERA',
|
|
71
70
|
// 'android.permission.READ_EXTERNAL_STORAGE',
|
|
72
71
|
],
|
|
73
|
-
//
|
|
72
|
+
// 签名配置(可选)
|
|
73
|
+
//
|
|
74
|
+
// 不配置也能打 release:模板默认用 debug keystore 签名(可安装、可本地测试,不可上架)。
|
|
75
|
+
// 需要上架时再配置下面任一种:
|
|
76
|
+
//
|
|
77
|
+
// 方式一:Android SDK 自带 debug keystore(本地测试常用)
|
|
78
|
+
// keystore: %USERPROFILE%\.android\debug.keystore (Windows)
|
|
79
|
+
// ~/.android/debug.keystore (macOS/Linux)
|
|
80
|
+
// storePassword: 'android'
|
|
81
|
+
// keyAlias: 'androiddebugkey'
|
|
82
|
+
// keyPassword: 'android'
|
|
83
|
+
//
|
|
84
|
+
// 自定义签名:取消下面注释,填入你的 keystore 信息
|
|
74
85
|
signing: {
|
|
75
86
|
// keystore: './build/release.keystore',
|
|
76
87
|
// storePassword: '',
|
|
@@ -132,8 +143,7 @@ export default {
|
|
|
132
143
|
'out/',
|
|
133
144
|
'build/',
|
|
134
145
|
'tests/',
|
|
135
|
-
'./mobileAppConfig.js'
|
|
136
|
-
'nodejs-assets/', // 插件 postinstall 生成的示例骨架
|
|
146
|
+
'./mobileAppConfig.js' // 配置文件本身不打包
|
|
137
147
|
],
|
|
138
148
|
|
|
139
149
|
// ===== 允许执行安装脚本的包 =====
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flun/node-mobile-app",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "将 Node.js 项目一键打包为 Android / iOS
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "将 Node.js 项目一键打包为 Android / iOS 移动应用,运行时由 @flun/nodejs-mobile-react-native 提供,无需修改你的 Node 代码;",
|
|
5
5
|
"author": "flun <open@flun.top>",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"node-mobile",
|
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
"scripts/",
|
|
29
29
|
"src/",
|
|
30
30
|
"template/",
|
|
31
|
-
"LICENSE",
|
|
32
|
-
"mobileAppConfig.js",
|
|
33
|
-
"README.md",
|
|
34
31
|
"自定义libnode指南.md",
|
|
35
32
|
"Android 构建指南.md",
|
|
33
|
+
"CHANGELOG.md",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"mobileAppConfig.js",
|
|
36
37
|
"package.json"
|
|
37
38
|
],
|
|
38
39
|
"scripts": {
|
|
@@ -40,32 +41,32 @@
|
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@babel/core": "^8.0.6",
|
|
43
|
-
"@babel/preset-env": "^8.0.6",
|
|
44
|
-
"@babel/runtime": "^8.0.5",
|
|
45
44
|
"@react-native-community/cli": "^20.2.0",
|
|
46
45
|
"@react-native-community/cli-platform-android": "^20.2.0",
|
|
47
46
|
"@react-native-community/cli-platform-ios": "^20.2.0",
|
|
48
47
|
"@react-native/babel-preset": "^0.87.1",
|
|
49
|
-
"@react-native/eslint-config": "^0.87.1",
|
|
50
|
-
"@react-native/jest-preset": "^0.87.1",
|
|
51
48
|
"@react-native/metro-config": "^0.87.1",
|
|
52
|
-
"
|
|
53
|
-
"@react-native/typescript-config": "^0.87.1",
|
|
54
|
-
"jimp": "^1.6.0",
|
|
55
|
-
"nodejs-mobile-react-native": "^18.20.4",
|
|
49
|
+
"jimp": "^1.6.1",
|
|
56
50
|
"react": "^19.3.0",
|
|
57
51
|
"react-native": "^0.87.1",
|
|
58
|
-
"react-native-
|
|
59
|
-
"react-native-webview": "^14.0.1"
|
|
60
|
-
},
|
|
61
|
-
"devDependencies": {
|
|
52
|
+
"react-native-webview": "^14.0.1",
|
|
62
53
|
"@types/react": "^19.3.0",
|
|
63
54
|
"prettier": "^3.9.8",
|
|
64
|
-
"
|
|
55
|
+
"eslint": "^10.11.0",
|
|
56
|
+
"@eslint/js": "^10.0.1",
|
|
57
|
+
"@babel/eslint-parser": "^7.29.9",
|
|
58
|
+
"@typescript-eslint/eslint-plugin": "^8.70.1",
|
|
59
|
+
"@typescript-eslint/parser": "^8.70.1",
|
|
60
|
+
"eslint-config-prettier": "^10.1.8",
|
|
61
|
+
"eslint-plugin-eslint-comments": "^3.2.0",
|
|
62
|
+
"eslint-plugin-react": "^7.37.5",
|
|
63
|
+
"eslint-plugin-react-hooks": "^7.1.1",
|
|
64
|
+
"eslint-plugin-react-native": "^5.0.0",
|
|
65
|
+
"@flun/nodejs-mobile-react-native": "^1.0.1"
|
|
65
66
|
},
|
|
66
67
|
"allowScripts": {
|
|
67
68
|
"node": true,
|
|
68
|
-
"nodejs-mobile-react-native": true,
|
|
69
|
+
"@flun/nodejs-mobile-react-native": true,
|
|
69
70
|
"unrs-resolver": true,
|
|
70
71
|
"@parcel/watcher": true
|
|
71
72
|
},
|
package/src/commands/build.js
CHANGED
|
@@ -42,7 +42,7 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
42
42
|
console.log('\n[2/7] 检查缓存指纹'), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
|
|
43
43
|
console.log('\n[3/7] 复制用户项目');
|
|
44
44
|
const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
|
|
45
|
-
console.log('\n[4/7] 安装依赖'), installUserDeps(targetDir, config);
|
|
45
|
+
console.log('\n[4/7] 安装依赖'), installUserDeps(targetDir, userProjectDir, config);
|
|
46
46
|
patchNodejsMobilePlugin(userProjectDir, config);
|
|
47
47
|
patchWebViewSsl(userProjectDir, config);
|
|
48
48
|
writeLocalProperties(buildDir, config);
|
package/src/commands/clean.js
CHANGED
|
@@ -8,7 +8,7 @@ const runClean = async () => {
|
|
|
8
8
|
targets = [
|
|
9
9
|
{ label: '构建目录', path: buildDir },
|
|
10
10
|
{
|
|
11
|
-
label: '插件 CMake 缓存', path: path.join(userProjectDir, 'node_modules', 'nodejs-mobile-react-native', 'android',
|
|
11
|
+
label: '插件 CMake 缓存', path: path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android',
|
|
12
12
|
'.cxx')
|
|
13
13
|
},
|
|
14
14
|
];
|
package/src/commands/test.js
CHANGED
|
@@ -40,7 +40,7 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
40
40
|
console.log('\n[3/9] 复制用户项目');
|
|
41
41
|
const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
|
|
42
42
|
|
|
43
|
-
console.log('\n[4/9] 安装依赖'), installUserDeps(targetDir, config);
|
|
43
|
+
console.log('\n[4/9] 安装依赖'), installUserDeps(targetDir, userProjectDir, config);
|
|
44
44
|
patchNodejsMobilePlugin(userProjectDir, config);
|
|
45
45
|
patchWebViewSsl(userProjectDir, config);
|
|
46
46
|
writeLocalProperties(buildDir, config);
|
package/src/config.js
CHANGED
|
@@ -29,9 +29,7 @@ const DEFAULT_CONFIG = {
|
|
|
29
29
|
abiFilters: ['arm64-v8a'],
|
|
30
30
|
icon: './build/icon.png',
|
|
31
31
|
usesCleartextTraffic: true,
|
|
32
|
-
permissions: [],
|
|
33
|
-
buildNativeModules: false,
|
|
34
|
-
signing: {},
|
|
32
|
+
permissions: [],
|
|
35
33
|
signing: {},
|
|
36
34
|
},
|
|
37
35
|
ios: {
|
|
38
36
|
deploymentTarget: '15.1',
|
package/src/copy.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
ensureDir, removeDir, readJson, isExcluded, copyFileIfChanged, writeIfChanged, buildEslintConfigSource
|
|
5
|
+
} from './utils.js';
|
|
4
6
|
import { resolveSdkPath } from './config.js';
|
|
5
7
|
|
|
6
8
|
const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
@@ -52,6 +54,55 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
52
54
|
* 获取 build 目录路径:<userProject>/node-mobile-app-build
|
|
53
55
|
*/
|
|
54
56
|
getBuildDir = userProjectDir => path.join(userProjectDir, BUILD_DIR_NAME),
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 把 buildDir/package.json 的依赖版本同步为主包 dependencies 的版本
|
|
60
|
+
* - 依赖名单来自 buildDir/package.json(模板声明需要哪些依赖)
|
|
61
|
+
* - 版本号来自 pkgRoot/package.json 的 dependencies(单一数据源)
|
|
62
|
+
* - 若主包缺少某个依赖,抛错中断(避免模板占位值被真正使用)
|
|
63
|
+
*/
|
|
64
|
+
syncTemplateDependencies = (pkgRoot, buildDir) => {
|
|
65
|
+
const buildPkgPath = path.join(buildDir, 'package.json');
|
|
66
|
+
if (!fs.existsSync(buildPkgPath)) return;
|
|
67
|
+
const mainPkgPath = path.join(pkgRoot, 'package.json');
|
|
68
|
+
if (!fs.existsSync(mainPkgPath)) return;
|
|
69
|
+
|
|
70
|
+
const buildPkg = readJson(buildPkgPath);
|
|
71
|
+
const mainPkg = readJson(mainPkgPath);
|
|
72
|
+
if (!buildPkg.dependencies) return;
|
|
73
|
+
|
|
74
|
+
const mainDeps = mainPkg.dependencies || {};
|
|
75
|
+
const missing = [];
|
|
76
|
+
let updated = 0;
|
|
77
|
+
|
|
78
|
+
// 移除仅用于模板占位说明的 _comment 字段,避免带进最终 build 工程
|
|
79
|
+
if ('_comment' in buildPkg) {
|
|
80
|
+
delete buildPkg._comment;
|
|
81
|
+
updated++;
|
|
82
|
+
}
|
|
83
|
+
for (const name of Object.keys(buildPkg.dependencies)) {
|
|
84
|
+
const v = mainDeps[name];
|
|
85
|
+
if (!v) { missing.push(name); continue; }
|
|
86
|
+
if (buildPkg.dependencies[name] !== v) {
|
|
87
|
+
buildPkg.dependencies[name] = v;
|
|
88
|
+
updated++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (updated > 0) {
|
|
93
|
+
writeIfChanged(buildPkgPath, JSON.stringify(buildPkg, null, 2) + '\n');
|
|
94
|
+
console.log(' ✓ 已同步 ' + updated + ' 个依赖版本到 ' + BUILD_DIR_NAME + '/package.json');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (missing.length > 0) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
'template/package.json 声明的以下依赖在主包 dependencies 中不存在:\n' +
|
|
100
|
+
' ' + missing.join(', ') + '\n' +
|
|
101
|
+
' 请在 @flun/node-mobile-app 的 package.json 中补上对应声明(版本号随意,\n' +
|
|
102
|
+
' 但必须存在,以便 syncTemplateDependencies 覆盖模板里的占位值)。'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
},
|
|
55
106
|
/**
|
|
56
107
|
* 把 CLI 包里的 template/ 逐文件覆盖到 build 目录
|
|
57
108
|
* - 只覆盖模板里存在的文件,不删除 buildDir 里已有的其他内容(保留 Gradle 增量缓存)
|
|
@@ -66,6 +117,8 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
66
117
|
|
|
67
118
|
const keepSet = new Set((config && config.keepFiles) || []);
|
|
68
119
|
copyDirOverwrite(templateDir, buildDir, TEMPLATE_EXCLUDE, buildDir, keepSet);
|
|
120
|
+
fs.writeFileSync(path.join(buildDir, 'eslint.config.js'), buildEslintConfigSource(), 'utf-8');
|
|
121
|
+
syncTemplateDependencies(pkgRoot, buildDir);
|
|
69
122
|
console.log(' ✓ 模板已更新到', BUILD_DIR_NAME + '/');
|
|
70
123
|
if (keepSet.size > 0) console.log(' 保留文件:', Array.from(keepSet).join(', '));
|
|
71
124
|
},
|
|
@@ -121,8 +174,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
121
174
|
const fallback = path.join(pkgRoot, 'cli-assets', 'server.js');
|
|
122
175
|
if (!fs.existsSync(fallback)) throw new Error('入口文件不存在: ' + config.serverPath + '(CLI 内置模板也缺失)');
|
|
123
176
|
|
|
124
|
-
ensureDir(path.dirname(entryPath));
|
|
125
|
-
fs.copyFileSync(fallback, entryPath);
|
|
177
|
+
ensureDir(path.dirname(entryPath)), fs.copyFileSync(fallback, entryPath);
|
|
126
178
|
console.log(' ✓ 生成默认入口文件:', config.serverPath);
|
|
127
179
|
console.log(' (因为用户项目里没有这个文件,从 CLI 内置模板复制)');
|
|
128
180
|
|
|
@@ -161,17 +213,10 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
|
|
|
161
213
|
const before = c;
|
|
162
214
|
|
|
163
215
|
// defaultConfig 里的 ndk { abiFilters ... }
|
|
164
|
-
c = c.replace(
|
|
165
|
-
/(defaultConfig\s*\{[\s\S]*?ndk\s*\{\s*abiFilters\s+)("[^"]*"(?:\s*,\s*"[^"]*")*)/,
|
|
166
|
-
`$1${abiList}`
|
|
167
|
-
);
|
|
216
|
+
c = c.replace(/(defaultConfig\s*\{[\s\S]*?ndk\s*\{\s*abiFilters\s+)("[^"]*"(?:\s*,\s*"[^"]*")*)/, `$1${abiList}`);
|
|
168
217
|
// 兜底:没有 ndk {} 块时,插到 defaultConfig 里
|
|
169
|
-
if (!/ndk\s*\{\s*abiFilters/.test(c))
|
|
170
|
-
c = c.replace(
|
|
171
|
-
/(defaultConfig\s*\{)/,
|
|
172
|
-
`$1\n ndk {\n abiFilters ${abiList}\n }`
|
|
173
|
-
);
|
|
174
|
-
}
|
|
218
|
+
if (!/ndk\s*\{\s*abiFilters/.test(c))
|
|
219
|
+
c = c.replace(/(defaultConfig\s*\{)/, `$1\n ndk {\n abiFilters ${abiList}\n }`);
|
|
175
220
|
// 不 patch splits.abi.include:AGP 禁止它和 ndk.abiFilters 同时存在
|
|
176
221
|
if (c !== before) writeIfChanged(file, c), console.log(` ✓ 已设置 app ndk.abiFilters = [${abiList}]`);
|
|
177
222
|
|
package/src/fingerprint.js
CHANGED
|
@@ -19,11 +19,11 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
19
19
|
clearCommonCaches = (buildDir, userProjectDir) => {
|
|
20
20
|
removeDir(path.join(buildDir, 'android', 'build'));
|
|
21
21
|
removeDir(path.join(buildDir, 'android', 'app', 'build'));
|
|
22
|
-
removeDir(path.join(userProjectDir, 'node_modules', 'nodejs-mobile-react-native', 'android', 'build'));
|
|
22
|
+
removeDir(path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android', 'build'));
|
|
23
23
|
},
|
|
24
24
|
computeFingerprint = (userProjectDir, config, pkgRoot) => {
|
|
25
25
|
const abiFilters = [...(config.android.abiFilters || [])].sort(), libnode = {},
|
|
26
|
-
pluginDir = path.join(userProjectDir, 'node_modules', 'nodejs-mobile-react-native'),
|
|
26
|
+
pluginDir = path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native'),
|
|
27
27
|
libnodeBin = path.join(pluginDir, 'android', 'libnode', 'bin');
|
|
28
28
|
|
|
29
29
|
if (fs.existsSync(libnodeBin)) {
|
|
@@ -85,7 +85,7 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
|
|
|
85
85
|
if (appInfoChanged) reasons.push('应用信息');
|
|
86
86
|
console.log(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理全部缓存');
|
|
87
87
|
clearCommonCaches(buildDir, userProjectDir);
|
|
88
|
-
removeDir(path.join(userProjectDir, 'node_modules', 'nodejs-mobile-react-native', 'android', '.cxx'));
|
|
88
|
+
removeDir(path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android', '.cxx'));
|
|
89
89
|
}
|
|
90
90
|
else if (depsChanged || appInfoChanged) {
|
|
91
91
|
const reasons = [];
|
package/src/generate.js
CHANGED
|
@@ -12,7 +12,8 @@ import { writeIfChanged } from './utils.js';
|
|
|
12
12
|
const generateMainJs = (targetDir, config, moduleType) => {
|
|
13
13
|
const entry = config.serverPath.startsWith('./') ? config.serverPath : './' + config.serverPath,
|
|
14
14
|
entryLabel = config.serverPath, hb = config.advanced.heartbeatInterval,
|
|
15
|
-
body = `
|
|
15
|
+
body = `(async () => {
|
|
16
|
+
const rn_bridge = require('rn-bridge');
|
|
16
17
|
const net = require('net'), https = require('https'), USER_ENTRY = ${JSON.stringify(entry)},
|
|
17
18
|
ENTRY_LABEL = ${JSON.stringify(entryLabel)}, START_TIMEOUT_MS = 5000;
|
|
18
19
|
|
|
@@ -34,11 +35,11 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
34
35
|
|
|
35
36
|
rn_bridge.channel.send('Node was initialized.');
|
|
36
37
|
process.on('uncaughtException', (err) => {
|
|
37
|
-
console.error('
|
|
38
|
+
console.error('未捕获的异常:', err);
|
|
38
39
|
emitFailure('未捕获异常', err && err.message);
|
|
39
40
|
});
|
|
40
41
|
process.on('unhandledRejection', (reason) => {
|
|
41
|
-
console.error('
|
|
42
|
+
console.error('未处理的 Promise rejection:', reason);
|
|
42
43
|
emitFailure('未处理的 Promise rejection', String(reason));
|
|
43
44
|
});
|
|
44
45
|
|
|
@@ -77,7 +78,8 @@ const generateMainJs = (targetDir, config, moduleType) => {
|
|
|
77
78
|
|
|
78
79
|
rn_bridge.channel.on('message', (msg) => {
|
|
79
80
|
rn_bridge.channel.send('Echo: ' + msg);
|
|
80
|
-
})
|
|
81
|
+
});
|
|
82
|
+
})();`,
|
|
81
83
|
header = moduleType === 'module'
|
|
82
84
|
? `import { createRequire } from 'module';\nconst require = createRequire(import.meta.url);\n\n` : '',
|
|
83
85
|
file = path.join(targetDir, 'main.js'), changed = writeIfChanged(file, header + body);
|
package/src/install.js
CHANGED
|
@@ -4,9 +4,11 @@ import crypto from 'crypto';
|
|
|
4
4
|
import { run } from './utils.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* 内部:依赖 hash(dependencies + type
|
|
7
|
+
* 内部:依赖 hash(dependencies + type + 是否含原生模块 + 是否强制重编)
|
|
8
|
+
* hasNative 纳入哈希:装/卸原生模块时会触发重装
|
|
9
|
+
* forceRebuild 纳入哈希:用户显式要求重编时会强制重装
|
|
8
10
|
*/
|
|
9
|
-
const hashDeps = pkgPath => {
|
|
11
|
+
const hashDeps = (pkgPath, hasNative, forceRebuild) => {
|
|
10
12
|
const h = crypto.createHash('sha256');
|
|
11
13
|
if (fs.existsSync(pkgPath)) {
|
|
12
14
|
try {
|
|
@@ -15,14 +17,36 @@ const hashDeps = pkgPath => {
|
|
|
15
17
|
} catch { h.update('read-error') }
|
|
16
18
|
}
|
|
17
19
|
else h.update('no-package');
|
|
20
|
+
h.update('|native=' + (hasNative ? '1' : '0'));
|
|
21
|
+
h.update('|force=' + (forceRebuild ? '1' : '0'));
|
|
18
22
|
return h.digest('hex');
|
|
19
23
|
},
|
|
20
24
|
/**
|
|
21
|
-
*
|
|
25
|
+
* 内部:判断用户项目的生产依赖里是否含原生模块(binding.gyp)
|
|
26
|
+
* 按用户根 package.json 的 dependencies 键名,去 userProjectDir/node_modules 下逐个查
|
|
27
|
+
* 不用扫 nodejs-project/node_modules:首次构建时它还不存在
|
|
28
|
+
*/
|
|
29
|
+
hasNativeModules = (userProjectDir) => {
|
|
30
|
+
const pkgPath = path.join(userProjectDir, 'package.json');
|
|
31
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
32
|
+
let deps = {};
|
|
33
|
+
try {
|
|
34
|
+
deps = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).dependencies || {};
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
const nodeModulesDir = path.join(userProjectDir, 'node_modules');
|
|
39
|
+
for (const name of Object.keys(deps)) {
|
|
40
|
+
if (fs.existsSync(path.join(nodeModulesDir, name, 'binding.gyp'))) return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* 内部:定位 @flun/nodejs-mobile-react-native 插件目录
|
|
22
46
|
*/
|
|
23
47
|
findPluginDir = userProjectDir => {
|
|
24
48
|
const candidates = [
|
|
25
|
-
path.join(userProjectDir, 'node_modules', 'nodejs-mobile-react-native'),
|
|
49
|
+
path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native'),
|
|
26
50
|
];
|
|
27
51
|
for (const c of candidates) if (fs.existsSync(path.join(c, 'android', 'build.gradle'))) return c;
|
|
28
52
|
return null;
|
|
@@ -63,20 +87,33 @@ const hashDeps = pkgPath => {
|
|
|
63
87
|
* 只装 dependencies,不装 devDependencies
|
|
64
88
|
* 依赖未变化且 node_modules 存在时跳过(用 .deps-hash 记录)
|
|
65
89
|
*/
|
|
66
|
-
installUserDeps = (targetDir, config) => {
|
|
90
|
+
installUserDeps = (targetDir, userProjectDir, config) => {
|
|
67
91
|
const assetsDir = path.dirname(targetDir), bnmFile = path.join(assetsDir, 'BUILD_NATIVE_MODULES.txt'),
|
|
68
|
-
hashFile = path.join(targetDir, '.deps-hash'),
|
|
69
|
-
|
|
92
|
+
hashFile = path.join(targetDir, '.deps-hash'),
|
|
93
|
+
nodeModulesDir = path.join(targetDir, 'node_modules'),
|
|
94
|
+
forceRebuild = process.env.NODE_MOBILE_FORCE_REBUILD === '1';
|
|
95
|
+
|
|
96
|
+
// 自动检测是否有原生模块(binding.gyp)
|
|
97
|
+
const hasNative = hasNativeModules(userProjectDir);
|
|
98
|
+
fs.writeFileSync(bnmFile, hasNative ? '1' : '0');
|
|
70
99
|
|
|
71
|
-
// 计算当前依赖 hash
|
|
72
|
-
const pkgPath = path.join(targetDir, 'package.json'),
|
|
73
|
-
|
|
100
|
+
// 计算当前依赖 hash(含 native + force 状态)
|
|
101
|
+
const pkgPath = path.join(targetDir, 'package.json'),
|
|
102
|
+
currentHash = hashDeps(pkgPath, hasNative, forceRebuild),
|
|
103
|
+
hasModules = fs.existsSync(nodeModulesDir);
|
|
74
104
|
let oldHash = null;
|
|
75
105
|
if (fs.existsSync(hashFile)) try { oldHash = fs.readFileSync(hashFile, 'utf-8').trim(); } catch { };
|
|
76
|
-
|
|
106
|
+
|
|
107
|
+
if (hasModules && oldHash === currentHash && !forceRebuild) {
|
|
108
|
+
return console.log(' ✓ 依赖无变化,跳过安装');
|
|
109
|
+
}
|
|
110
|
+
if (forceRebuild) console.log(' ℹ️ NODE_MOBILE_FORCE_REBUILD=1,强制重装依赖');
|
|
77
111
|
|
|
78
112
|
const env = { ...process.env };
|
|
79
|
-
if (
|
|
113
|
+
if (hasNative) {
|
|
114
|
+
env.NODEJS_MOBILE_BUILD_NATIVE_MODULES = '1';
|
|
115
|
+
console.log(' ℹ️ 检测到原生模块(binding.gyp),将编译原生代码');
|
|
116
|
+
}
|
|
80
117
|
|
|
81
118
|
console.log(' 安装用户生产依赖(nodejs-assets/nodejs-project)...');
|
|
82
119
|
run('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], { cwd: targetDir, env });
|
|
@@ -84,11 +121,11 @@ const hashDeps = pkgPath => {
|
|
|
84
121
|
console.log(' ✓ 用户依赖安装完成');
|
|
85
122
|
},
|
|
86
123
|
/**
|
|
87
|
-
* 修改 nodejs-mobile-react-native 插件 build.gradle 的 abiFilters
|
|
124
|
+
* 修改 @flun/nodejs-mobile-react-native 插件 build.gradle 的 abiFilters
|
|
88
125
|
*/
|
|
89
126
|
patchNodejsMobilePlugin = (userProjectDir, config) => {
|
|
90
127
|
const pluginDir = findPluginDir(userProjectDir);
|
|
91
|
-
if (!pluginDir) return console.warn(' ⚠️ 未找到 nodejs-mobile-react-native 插件,跳过 patch');
|
|
128
|
+
if (!pluginDir) return console.warn(' ⚠️ 未找到 @flun/nodejs-mobile-react-native 插件,跳过 patch');
|
|
92
129
|
|
|
93
130
|
const file = path.join(pluginDir, 'android', 'build.gradle'),
|
|
94
131
|
abiList = config.android.abiFilters.map((x) => `"${x}"`).join(', ');
|
package/src/utils.js
CHANGED
|
@@ -123,9 +123,40 @@ const ensureDir = dir => {
|
|
|
123
123
|
sanitizeFileName = s => {
|
|
124
124
|
const cleaned = String(s).replace(/[\\/:*?"<>|]/g, '_').trim();
|
|
125
125
|
return cleaned || 'app';
|
|
126
|
-
}
|
|
126
|
+
},
|
|
127
|
+
/**
|
|
128
|
+
* 生成 ESLint flat config 源码(替代 @react-native/eslint-config/flat)
|
|
129
|
+
*/
|
|
130
|
+
buildEslintConfigSource = () => `import js from '@eslint/js';
|
|
131
|
+
import babelParser from '@babel/eslint-parser';
|
|
132
|
+
import tsParser from '@typescript-eslint/parser';
|
|
133
|
+
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
|
134
|
+
import reactPlugin from 'eslint-plugin-react';
|
|
135
|
+
import reactHooks from 'eslint-plugin-react-hooks';
|
|
136
|
+
import reactNative from 'eslint-plugin-react-native';
|
|
137
|
+
import prettier from 'eslint-config-prettier';
|
|
138
|
+
|
|
139
|
+
export default [
|
|
140
|
+
js.configs.recommended,
|
|
141
|
+
{
|
|
142
|
+
files: ['**/*.{js,jsx,ts,tsx}'],
|
|
143
|
+
languageOptions: {
|
|
144
|
+
parser: babelParser,
|
|
145
|
+
parserOptions: { requireConfigFile: false, babelOptions: { presets: ['module:@react-native/babel-preset'] } },
|
|
146
|
+
},
|
|
147
|
+
plugins: { react: reactPlugin, 'react-hooks': reactHooks, 'react-native': reactNative },
|
|
148
|
+
settings: { react: { version: 'detect' } },
|
|
149
|
+
rules: {
|
|
150
|
+
...reactPlugin.configs.recommended.rules,
|
|
151
|
+
...reactHooks.configs.recommended.rules,
|
|
152
|
+
'react/react-in-jsx-scope': 'off',
|
|
153
|
+
'react/prop-types': 'off',
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
{ files: ['**/*.{ts,tsx}'], languageOptions: { parser: tsParser }, plugins: { '@typescript-eslint': tsPlugin },
|
|
157
|
+
rules: tsPlugin.configs.recommended.rules }, prettier];`;
|
|
127
158
|
|
|
128
159
|
export {
|
|
129
|
-
ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged,
|
|
130
|
-
|
|
160
|
+
ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged, run, runQuiet,
|
|
161
|
+
runSilent, findAdb, findHermesc, sanitizeFileName, buildEslintConfigSource
|
|
131
162
|
};
|
package/template/App.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
2
|
import { View, Text, ActivityIndicator, StyleSheet, useColorScheme } from 'react-native';
|
|
3
3
|
import { WebView } from 'react-native-webview';
|
|
4
|
-
import * as NodeMobile from 'nodejs-mobile-react-native';
|
|
4
|
+
import * as NodeMobile from '@flun/nodejs-mobile-react-native';
|
|
5
5
|
import runtime from './mobileApp.runtime';
|
|
6
6
|
|
|
7
7
|
// 背景色解析:支持以下三种配置
|
|
@@ -50,7 +50,8 @@ const App = () => {
|
|
|
50
50
|
if (typeof msg !== 'string') return;
|
|
51
51
|
if (msg.startsWith('Node started on ')) {
|
|
52
52
|
const target = msg.slice('Node started on '.length).trim();
|
|
53
|
-
setFailedReason(null)
|
|
53
|
+
setFailedReason(null);
|
|
54
|
+
setUrl(target);
|
|
54
55
|
} else if (msg.startsWith('Node failed: ')) {
|
|
55
56
|
const reason = msg.slice('Node failed: '.length).trim();
|
|
56
57
|
setFailedReason(reason);
|
package/template/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "node-mobile-app-build",
|
|
3
|
-
"version": "
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"react-native-
|
|
16
|
-
"react-native
|
|
17
|
-
}
|
|
18
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "node-mobile-app-build",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"_comment": "dependencies 的版本号由 @flun/node-mobile-app 在 copyTemplate 阶段从主包 package.json 同步;此处 \"*\" 仅为占位,请勿依赖此文件里的值。",
|
|
7
|
+
"dependencies": {
|
|
8
|
+
"react": "*",
|
|
9
|
+
"react-native": "*",
|
|
10
|
+
"@flun/nodejs-mobile-react-native": "*",
|
|
11
|
+
"react-native-webview": "*"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"android": "react-native run-android",
|
|
15
|
+
"ios": "react-native run-ios",
|
|
16
|
+
"start": "react-native start"
|
|
17
|
+
}
|
|
18
|
+
}
|