@flun/node-mobile-app 1.0.2 → 2.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.
@@ -63,7 +63,6 @@ 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',
@@ -71,18 +70,18 @@ export default {
71
70
  // 'android.permission.READ_EXTERNAL_STORAGE',
72
71
  ],
73
72
  // 签名配置(可选)
74
- //
75
- // 不配置也能打 release:模板默认用 debug keystore 签名(可安装、可本地测试,不可上架)。
76
- // 需要上架时再配置下面任一种:
77
- //
78
- // 方式一:Android SDK 自带 debug keystore(本地测试常用)
79
- // keystore: %USERPROFILE%\.android\debug.keystore (Windows)
80
- // ~/.android/debug.keystore (macOS/Linux)
81
- // storePassword: 'android'
82
- // keyAlias: 'androiddebugkey'
83
- // keyPassword: 'android'
84
- //
85
- // 自定义签名:取消下面注释,填入你的 keystore 信息
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 信息
86
85
  signing: {
87
86
  // keystore: './build/release.keystore',
88
87
  // storePassword: '',
@@ -144,8 +143,7 @@ export default {
144
143
  'out/',
145
144
  'build/',
146
145
  'tests/',
147
- './mobileAppConfig.js', // 配置文件本身不打包
148
- 'nodejs-assets/', // 插件 postinstall 生成的示例骨架
146
+ './mobileAppConfig.js' // 配置文件本身不打包
149
147
  ],
150
148
 
151
149
  // ===== 允许执行安装脚本的包 =====
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@flun/node-mobile-app",
3
- "version": "1.0.2",
4
- "description": "将 Node.js 项目一键打包为 Android / iOS 移动应用(基于 nodejs-mobile-react-native)",
3
+ "version": "2.0.1",
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",
@@ -47,7 +47,6 @@
47
47
  "@react-native/babel-preset": "^0.87.1",
48
48
  "@react-native/metro-config": "^0.87.1",
49
49
  "jimp": "^1.6.1",
50
- "nodejs-mobile-react-native": "^18.20.4",
51
50
  "react": "^19.3.0",
52
51
  "react-native": "^0.87.1",
53
52
  "react-native-webview": "^14.0.1",
@@ -56,17 +55,18 @@
56
55
  "eslint": "^10.11.0",
57
56
  "@eslint/js": "^10.0.1",
58
57
  "@babel/eslint-parser": "^7.29.9",
59
- "@typescript-eslint/eslint-plugin": "^8.70.0",
60
- "@typescript-eslint/parser": "^8.70.0",
58
+ "@typescript-eslint/eslint-plugin": "^8.70.1",
59
+ "@typescript-eslint/parser": "^8.70.1",
61
60
  "eslint-config-prettier": "^10.1.8",
62
61
  "eslint-plugin-eslint-comments": "^3.2.0",
63
62
  "eslint-plugin-react": "^7.37.5",
64
63
  "eslint-plugin-react-hooks": "^7.1.1",
65
- "eslint-plugin-react-native": "^5.0.0"
64
+ "eslint-plugin-react-native": "^5.0.0",
65
+ "@flun/nodejs-mobile-react-native": "^1.0.3"
66
66
  },
67
67
  "allowScripts": {
68
68
  "node": true,
69
- "nodejs-mobile-react-native": true,
69
+ "@flun/nodejs-mobile-react-native": true,
70
70
  "unrs-resolver": true,
71
71
  "@parcel/watcher": true
72
72
  },
@@ -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);
@@ -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
  ];
@@ -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
@@ -54,6 +54,55 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
54
54
  * 获取 build 目录路径:<userProject>/node-mobile-app-build
55
55
  */
56
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
+ },
57
106
  /**
58
107
  * 把 CLI 包里的 template/ 逐文件覆盖到 build 目录
59
108
  * - 只覆盖模板里存在的文件,不删除 buildDir 里已有的其他内容(保留 Gradle 增量缓存)
@@ -69,6 +118,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
69
118
  const keepSet = new Set((config && config.keepFiles) || []);
70
119
  copyDirOverwrite(templateDir, buildDir, TEMPLATE_EXCLUDE, buildDir, keepSet);
71
120
  fs.writeFileSync(path.join(buildDir, 'eslint.config.js'), buildEslintConfigSource(), 'utf-8');
121
+ syncTemplateDependencies(pkgRoot, buildDir);
72
122
  console.log(' ✓ 模板已更新到', BUILD_DIR_NAME + '/');
73
123
  if (keepSet.size > 0) console.log(' 保留文件:', Array.from(keepSet).join(', '));
74
124
  },
@@ -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 = `const rn_bridge = require('rn-bridge');
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('Uncaught Exception:', err);
38
+ console.error('未捕获的异常:', err);
38
39
  emitFailure('未捕获异常', err && err.message);
39
40
  });
40
41
  process.on('unhandledRejection', (reason) => {
41
- console.error('Unhandled Rejection:', reason);
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
- * 内部:定位 nodejs-mobile-react-native 插件目录
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'), buildNative = !!config.android.buildNativeModules;
69
- fs.writeFileSync(bnmFile, buildNative ? '1' : '0');
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'), currentHash = hashDeps(pkgPath),
73
- nodeModulesDir = path.join(targetDir, 'node_modules'), hasModules = fs.existsSync(nodeModulesDir);
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
- if (hasModules && oldHash === currentHash) return console.log(' ✓ 依赖无变化,跳过安装');
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 (buildNative) env.NODEJS_MOBILE_BUILD_NATIVE_MODULES = '1';
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/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
  // 背景色解析:支持以下三种配置
@@ -1,18 +1,18 @@
1
- {
2
- "name": "node-mobile-app-build",
3
- "version": "0.0.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "android": "react-native run-android",
8
- "ios": "react-native run-ios",
9
- "start": "react-native start"
10
- },
11
- "dependencies": {
12
- "react": "^19.2.8",
13
- "react-native": "^0.87.1",
14
- "nodejs-mobile-react-native": "^18.20.4",
15
- "react-native-safe-area-context": "^5.9.1",
16
- "react-native-webview": "^14.0.1"
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
+ }