@flun/node-mobile-app 3.1.5 → 4.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/bin/cli.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { runTest } from '../src/commands/test.js';
3
3
  import { runBuild } from '../src/commands/build.js';
4
4
  import { runClean } from '../src/commands/clean.js';
5
+ import chalk from 'chalk';
5
6
 
6
7
  const HELP = `
7
8
  @flun/node-mobile-app
@@ -41,8 +42,8 @@ const HELP = `
41
42
  else if (cmd === 'clean') await runClean(flags);
42
43
  else { console.log(HELP); process.exit(cmd ? 1 : 0); }
43
44
  } catch (e) {
44
- console.error('\n❌ ' + e.message);
45
- if (process.env.DEBUG) console.error(e.stack);
45
+ console.error(chalk.red('\n❌ ' + e.message));
46
+ if (process.env.DEBUG) console.error(chalk.red(e.stack));
46
47
  process.exit(1);
47
48
  }
48
49
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flun/node-mobile-app",
3
- "version": "3.1.5",
3
+ "version": "4.0.1",
4
4
  "description": "将 Node.js 项目一键打包为 Android / iOS 移动应用,运行时由 @flun/nodejs-mobile-react-native 提供,无需修改你的 Node 代码;",
5
5
  "author": "flun <open@flun.top>",
6
6
  "keywords": [
@@ -41,19 +41,20 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@babel/core": "^8.0.7",
44
- "@eslint-react/eslint-plugin": "^5.27.2",
44
+ "@eslint-react/eslint-plugin": "^5.28.2",
45
45
  "@eslint/js": "^10.0.1",
46
- "@flun/nodejs-mobile-react-native": "^2.0.8",
46
+ "@flun/nodejs-mobile-react-native": "^2.1.0",
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",
50
50
  "@react-native-community/cli-platform-android": "^20.2.0",
51
51
  "@react-native-community/cli-platform-ios": "^20.2.0",
52
52
  "@types/react": "^19.3.0",
53
- "@typescript-eslint/eslint-plugin": "^8.71.2",
54
- "@typescript-eslint/parser": "^8.71.2",
53
+ "@typescript-eslint/eslint-plugin": "^8.72.0",
54
+ "@typescript-eslint/parser": "^8.72.0",
55
+ "chalk": "^6.0.1",
55
56
  "esbuild": "^0.28.2",
56
- "eslint": "^10.12.0",
57
+ "eslint": "^10.13.0",
57
58
  "eslint-config-prettier": "^10.1.8",
58
59
  "eslint-plugin-eslint-comments": "^3.2.0",
59
60
  "eslint-plugin-react-hooks": "^7.1.1",
@@ -1,114 +1,115 @@
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 { markVerified } from '../optimize-node-modules.js';
11
- import { generateMainJs, generateRuntime } from '../generate.js';
12
- import { checkAndResetCache } from '../fingerprint.js';
13
- import {
14
- patchAppConfig, patchAndroidConfig, patchSigning, patchIcon, patchIosConfig, patchIosIcon, resolveAppName
15
- } from '../patcher.js';
16
- import { ensureMac, checkIosTools, podInstall, xcodebuildArchive, xcodebuildExport } from '../platform-ios.js';
17
- import { run, ensureDir, sanitizeFileName } from '../utils.js';
18
-
19
- const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'),
20
- runBuild = async (flags) => {
21
- const userProjectDir = process.cwd(), release = !!flags.release, isIos = !!flags.ios, total = isIos ? 8 : 7;
22
-
23
- console.log(`\n=== node-mobile-app build (${isIos ? 'iOS' : 'Android'}, ${release ? 'release' : 'debug'}) ===\n`);
24
- console.log('用户项目:', userProjectDir);
25
- if (isIos) ensureMac(), checkIosTools();
26
- console.log(`\n[1/${total}] 检查用户配置与入口文件`), ensureUserConfig(userProjectDir, PKG_ROOT);
27
-
28
- const config = await loadConfig(userProjectDir);
29
- ensureUserEntry(userProjectDir, config, PKG_ROOT), validateConfig(config, userProjectDir);
30
-
31
- const buildDir = getBuildDir(userProjectDir);
32
- console.log(`\n[2/${total}] 重置 RN 工程模板`);
33
- copyTemplate(PKG_ROOT, buildDir, config);
34
- patchAndroidAbi(buildDir, config);
35
- patchExtraRnDependencies(buildDir, userProjectDir, config);
36
- patchAppConfig(buildDir, userProjectDir, config);
37
- patchAndroidConfig(buildDir, config);
38
- patchSigning(buildDir, userProjectDir, config);
39
- await patchIcon(buildDir, userProjectDir, config);
40
- patchIosConfig(buildDir, userProjectDir, config);
41
- await patchIosIcon(buildDir, userProjectDir, config);
42
-
43
- console.log(`\n[3/${total}] 检查缓存指纹`), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
44
- console.log(`\n[4/${total}] 复制用户项目`);
45
- const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
46
- console.log(`\n[5/${total}] 安装依赖`);
47
- await installUserDeps(targetDir, userProjectDir, config, buildDir, isIos ? 'ios' : 'android');
48
- patchNodejsMobilePlugin(userProjectDir, config);
49
- patchWebViewSsl(userProjectDir, config);
50
- writeLocalProperties(buildDir, config);
51
-
52
- console.log(`\n[6/${total}] 生成桥接文件`);
53
- generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
54
-
55
- const appName = resolveAppName(userProjectDir, config), safeName = sanitizeFileName(appName),
56
- outDir = path.resolve(userProjectDir, config.outputDir || './out');
57
- ensureDir(outDir);
58
-
59
- if (isIos) {
60
- console.log(`\n[7/${total}] pod install`), podInstall(path.join(buildDir, 'ios'));
61
- console.log(`\n[8/${total}] xcodebuild archive + export`), xcodebuildArchive(buildDir, config, release, userProjectDir);
62
-
63
- const ipaPath = xcodebuildExport(buildDir, config, release, outDir, userProjectDir),
64
- dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.ipa`, dst = path.join(outDir, dstName);
65
- fs.copyFileSync(ipaPath, dst), console.log('\n✅ 打包完成');
66
-
67
- const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
68
- console.log(` ${dst} (${mb} MB)`);
69
- await markVerified(buildDir);
70
- return;
71
- }
72
-
73
- console.log(`\n[7/${total}] 打包 APK`);
74
- if (!process.env.OPTIMIZE_DEBUG) console.log(' 打包进行中...');
75
- const assetsDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'assets'),
76
- resDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'res');
77
-
78
- ensureDir(assetsDir);
79
- run('npx', [
80
- 'react-native', 'bundle',
81
- '--platform', 'android',
82
- '--dev', release ? 'false' : 'true',
83
- '--entry-file', 'index.js',
84
- '--bundle-output', path.join(assetsDir, 'index.android.bundle'),
85
- '--assets-dest', resDir,
86
- ], { cwd: buildDir, quiet: true });
87
-
88
- const task = release ? 'assembleRelease' : 'assembleDebug', gradleArgs = [task];
89
- if (flags.arch) gradleArgs.push(`-PreactNativeArchitectures=${flags.arch}`);
90
-
91
- const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
92
- run(gradlew, gradleArgs, { cwd: path.join(buildDir, 'android'), quiet: true });
93
-
94
- const apkDir = path.join(buildDir, 'android', 'app', 'build', 'outputs', 'apk', release ? 'release' : 'debug');
95
- console.log('\n✅ 打包完成');
96
-
97
- if (fs.existsSync(apkDir)) {
98
- const apks = fs.readdirSync(apkDir).filter((f) => f.endsWith('.apk'));
99
- if (apks.length === 0) console.log(' ⚠️ 未找到 APK');
100
-
101
- for (const f of apks) {
102
- const src = path.join(apkDir, f), dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.apk`,
103
- dst = path.join(outDir, dstName);
104
-
105
- fs.copyFileSync(src, dst);
106
- const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
107
- console.log(` ${dst} (${mb} MB)`);
108
- }
109
- }
110
- else console.log(' ⚠️ 未找到 APK 输出目录:', apkDir);
111
- await markVerified(buildDir);
112
- }
113
-
114
- export { runBuild };
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 { markVerified } from '../optimize-node-modules.js';
11
+ import { generateMainJs, generateRuntime } from '../generate.js';
12
+ import { checkAndResetCache } from '../fingerprint.js';
13
+ import {
14
+ patchAppConfig, patchAndroidConfig, patchSigning, patchIcon, patchIosConfig, patchIosIcon, resolveAppName
15
+ } from '../patcher.js';
16
+ import { ensureMac, checkIosTools, podInstall, xcodebuildArchive, xcodebuildExport } from '../platform-ios.js';
17
+ import { run, ensureDir, sanitizeFileName } from '../utils.js';
18
+ import chalk from 'chalk';
19
+
20
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'),
21
+ runBuild = async (flags) => {
22
+ const userProjectDir = process.cwd(), release = !!flags.release, isIos = !!flags.ios, total = isIos ? 8 : 7;
23
+
24
+ console.log(chalk.cyan(`\n=== node-mobile-app build (${isIos ? 'iOS' : 'Android'}, ${release ? 'release' : 'debug'}) ===\n`));
25
+ console.log(chalk.gray('用户项目: ' + userProjectDir));
26
+ if (isIos) ensureMac(), checkIosTools();
27
+ console.log(chalk.cyan(`\n[1/${total}] 检查用户配置与入口文件`)), ensureUserConfig(userProjectDir, PKG_ROOT);
28
+
29
+ const config = await loadConfig(userProjectDir);
30
+ ensureUserEntry(userProjectDir, config, PKG_ROOT), validateConfig(config, userProjectDir);
31
+
32
+ const buildDir = getBuildDir(userProjectDir);
33
+ console.log(chalk.cyan(`\n[2/${total}] 重置 RN 工程模板`));
34
+ copyTemplate(PKG_ROOT, buildDir, config);
35
+ patchAndroidAbi(buildDir, config);
36
+ patchExtraRnDependencies(buildDir, userProjectDir, config);
37
+ patchAppConfig(buildDir, userProjectDir, config);
38
+ patchAndroidConfig(buildDir, config);
39
+ patchSigning(buildDir, userProjectDir, config);
40
+ await patchIcon(buildDir, userProjectDir, config);
41
+ patchIosConfig(buildDir, userProjectDir, config);
42
+ await patchIosIcon(buildDir, userProjectDir, config);
43
+
44
+ console.log(chalk.cyan(`\n[3/${total}] 检查缓存指纹`)), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
45
+ console.log(chalk.cyan(`\n[4/${total}] 复制用户项目`));
46
+ const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
47
+ console.log(chalk.cyan(`\n[5/${total}] 安装依赖与编译优化`));
48
+ await installUserDeps(targetDir, userProjectDir, config, buildDir, isIos ? 'ios' : 'android');
49
+ patchNodejsMobilePlugin(userProjectDir, config);
50
+ patchWebViewSsl(userProjectDir, config);
51
+ writeLocalProperties(buildDir, config);
52
+
53
+ console.log(chalk.cyan(`\n[6/${total}] 生成桥接文件`));
54
+ generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
55
+
56
+ const appName = resolveAppName(userProjectDir, config), safeName = sanitizeFileName(appName),
57
+ outDir = path.resolve(userProjectDir, config.outputDir || './out');
58
+ ensureDir(outDir);
59
+
60
+ if (isIos) {
61
+ 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`)), xcodebuildArchive(buildDir, config, release, userProjectDir);
63
+
64
+ const ipaPath = xcodebuildExport(buildDir, config, release, outDir, userProjectDir),
65
+ dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.ipa`, dst = path.join(outDir, dstName);
66
+ fs.copyFileSync(ipaPath, dst), console.log(chalk.green('\n✅ 打包完成'));
67
+
68
+ const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
69
+ console.log(chalk.gray(` ${dst} (${mb} MB)`));
70
+ await markVerified(buildDir);
71
+ return;
72
+ }
73
+
74
+ console.log(chalk.cyan(`\n[7/${total}] 打包 APK`));
75
+ if (!process.env.OPTIMIZE_DEBUG) console.log(chalk.gray(' 打包进行中...'));
76
+ const assetsDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'assets'),
77
+ resDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'res');
78
+
79
+ ensureDir(assetsDir);
80
+ run('npx', [
81
+ 'react-native', 'bundle',
82
+ '--platform', 'android',
83
+ '--dev', release ? 'false' : 'true',
84
+ '--entry-file', 'index.js',
85
+ '--bundle-output', path.join(assetsDir, 'index.android.bundle'),
86
+ '--assets-dest', resDir,
87
+ ], { cwd: buildDir, quiet: true });
88
+
89
+ const task = release ? 'assembleRelease' : 'assembleDebug', gradleArgs = [task];
90
+ if (flags.arch) gradleArgs.push(`-PreactNativeArchitectures=${flags.arch}`);
91
+
92
+ const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
93
+ run(gradlew, gradleArgs, { cwd: path.join(buildDir, 'android'), quiet: true });
94
+
95
+ const apkDir = path.join(buildDir, 'android', 'app', 'build', 'outputs', 'apk', release ? 'release' : 'debug');
96
+ console.log(chalk.green('\n✅ 打包完成'));
97
+
98
+ if (fs.existsSync(apkDir)) {
99
+ const apks = fs.readdirSync(apkDir).filter((f) => f.endsWith('.apk'));
100
+ if (apks.length === 0) console.log(chalk.yellow(' ⚠️ 未找到 APK'));
101
+
102
+ for (const f of apks) {
103
+ const src = path.join(apkDir, f), dstName = `${safeName}-${config.versionName}-${release ? 'release' : 'debug'}.apk`,
104
+ dst = path.join(outDir, dstName);
105
+
106
+ fs.copyFileSync(src, dst);
107
+ const mb = (fs.statSync(dst).size / 1024 / 1024).toFixed(1);
108
+ console.log(chalk.gray(` ${dst} (${mb} MB)`));
109
+ }
110
+ }
111
+ else console.log(chalk.yellow(' ⚠️ 未找到 APK 输出目录: ' + apkDir));
112
+ await markVerified(buildDir);
113
+ }
114
+
115
+ export { runBuild };
@@ -2,6 +2,7 @@ import path from 'path';
2
2
  import fs from 'fs';
3
3
  import { removeDir } from '../utils.js';
4
4
  import { getBuildDir } from '../copy.js';
5
+ import chalk from 'chalk';
5
6
 
6
7
  const runClean = async () => {
7
8
  const userProjectDir = process.cwd(), buildDir = getBuildDir(userProjectDir),
@@ -19,11 +20,11 @@ const runClean = async () => {
19
20
 
20
21
  for (const t of targets) {
21
22
  if (fs.existsSync(t.path))
22
- removeDir(t.path), console.log('已清理:', t.label, '->', path.relative(userProjectDir, t.path) || t.path);
23
- else console.log('跳过(不存在):', t.label);
23
+ removeDir(t.path), console.log(chalk.green('已清理: ' + t.label + ' -> ' + (path.relative(userProjectDir, t.path) || t.path)));
24
+ else console.log(chalk.gray('跳过(不存在): ' + t.label));
24
25
  }
25
26
 
26
- console.log('\n✅ 清理完成');
27
+ console.log(chalk.green('\n✅ 清理完成'));
27
28
  };
28
29
 
29
30
  export { runClean };
@@ -1,109 +1,160 @@
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 { markVerified } from '../optimize-node-modules.js';
11
- import { generateMainJs, generateRuntime } from '../generate.js';
12
- import { checkAndResetCache } from '../fingerprint.js';
13
- import { patchAppConfig, patchAndroidConfig, patchSigning, patchIcon, patchIosConfig, patchIosIcon } from '../patcher.js';
14
- import { ensureMac, checkIosTools, podInstall } from '../platform-ios.js';
15
- import { run, runSilent, findAdb, ensureDir, findHermesc } from '../utils.js';
16
-
17
- const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'),
18
- runTest = async flags => {
19
- const userProjectDir = process.cwd(), isIos = !!flags.ios, total = isIos ? 9 : 10;
20
-
21
- console.log(`\n=== node-mobile-app test (${isIos ? 'iOS' : 'Android'}) ===\n`);
22
- console.log('用户项目:', userProjectDir);
23
- if (isIos) ensureMac(), checkIosTools();
24
- console.log(`\n[1/${total}] 检查用户配置与入口文件`), ensureUserConfig(userProjectDir, PKG_ROOT);
25
-
26
- const config = await loadConfig(userProjectDir);
27
- ensureUserEntry(userProjectDir, config, PKG_ROOT), validateConfig(config, userProjectDir);
28
-
29
- const buildDir = getBuildDir(userProjectDir);
30
- console.log(`\n[2/${total}] 重置 RN 工程模板`);
31
- copyTemplate(PKG_ROOT, buildDir, config), patchAndroidAbi(buildDir, config);
32
- patchExtraRnDependencies(buildDir, userProjectDir, config);
33
- patchAppConfig(buildDir, userProjectDir, config);
34
- patchAndroidConfig(buildDir, config), 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[3/${total}] 检查缓存指纹`), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
40
- console.log(`\n[4/${total}] 复制用户项目`);
41
- const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
42
-
43
- console.log(`\n[5/${total}] 安装依赖`), await installUserDeps(targetDir, userProjectDir, config, buildDir, isIos ? 'ios' : 'android');
44
- patchNodejsMobilePlugin(userProjectDir, config);
45
- patchWebViewSsl(userProjectDir, config), writeLocalProperties(buildDir, config);
46
-
47
- console.log(`\n[6/${total}] 生成桥接文件`);
48
- generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
49
-
50
- if (isIos) {
51
- console.log(`\n[7/${total}] pod install`), podInstall(path.join(buildDir, 'ios'));
52
- console.log(`\n[8/${total}] 生成 JS bundle + Hermes 字节码(由 Xcode Run Script 自动)`);
53
- const bundlePath = path.join(buildDir, 'ios', 'main.jsbundle');
54
- runSilent('npx', [
55
- 'react-native', 'bundle',
56
- '--platform', 'ios',
57
- '--dev', 'false',
58
- '--entry-file', 'index.js',
59
- '--bundle-output', bundlePath,
60
- ], { cwd: buildDir });
61
- console.log(' ✓ bundle 已生成');
62
-
63
- console.log(`\n[9/${total}] 编译并安装到模拟器/设备`);
64
- const args = ['react-native', 'run-ios'];
65
- if (flags.device) args.push('--device', flags.device);
66
- // FORCE_BUNDLING=1:即使 Debug+模拟器也把 main.jsbundle 打进 App
67
- const env = { ...process.env, FORCE_BUNDLING: '1', NODE_MOBILE_MODE: 'test' };
68
- run('npx', args, { cwd: buildDir, env, inherit: true });
69
- await markVerified(buildDir);
70
- } else {
71
- console.log(`\n[7/${total}] 生成 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[8/${total}] 编译 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[9/${total}] 配置 adb 端口转发`);
98
- const adb = findAdb(config);
99
- console.log(' adb:', adb), run(adb, ['reverse', 'tcp:8081', 'tcp:8081'], { quiet: true });
100
-
101
- console.log(`\n[10/${total}] 编译并安装到设备`);
102
- const args = ['react-native', 'run-android'];
103
- if (flags.device) args.push('--deviceId', flags.device);
104
- run('npx', args, { cwd: buildDir, inherit: true, env: { ...process.env, NODE_MOBILE_MODE: 'test' } });
105
- await markVerified(buildDir);
106
- }
107
- };
108
-
109
- export { runTest };
1
+ import path from 'path';
2
+ import fs from 'fs';
3
+ import { fileURLToPath } from 'url';
4
+ import { loadConfig, validateConfig } from '../config.js';
5
+ import { createHash } from 'crypto';
6
+ import http from 'http';
7
+ import {
8
+ getBuildDir, copyTemplate, copyUserProject, writeLocalProperties, ensureUserEntry, ensureUserConfig, patchAndroidAbi,
9
+ patchExtraRnDependencies
10
+ } from '../copy.js';
11
+ import { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl } from '../install.js';
12
+ import { markVerified } from '../optimize-node-modules.js';
13
+ import { generateMainJs, generateRuntime } from '../generate.js';
14
+ import { checkAndResetCache } from '../fingerprint.js';
15
+ import { patchAppConfig, patchAndroidConfig, patchSigning, patchIcon, patchIosConfig, patchIosIcon } from '../patcher.js';
16
+ import { ensureMac, checkIosTools, podInstall } from '../platform-ios.js';
17
+ import { run, runSilent, runStream, findAdb, ensureDir, findHermesc, ensureInstallable, writeInstallState, spawnCmdAsync } from '../utils.js';
18
+ import chalk from 'chalk';
19
+
20
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..'),
21
+ runTest = async flags => {
22
+ const userProjectDir = process.cwd(), isIos = !!flags.ios, total = isIos ? 9 : 11;
23
+
24
+ console.log(chalk.cyan(`\n=== node-mobile-app test (${isIos ? 'iOS' : 'Android'}) ===\n`));
25
+ console.log(chalk.gray('用户项目: ' + userProjectDir));
26
+ if (isIos) ensureMac(), checkIosTools();
27
+ console.log(chalk.cyan(`\n[1/${total}] 检查用户配置与入口文件`)), ensureUserConfig(userProjectDir, PKG_ROOT);
28
+
29
+ const config = await loadConfig(userProjectDir);
30
+ ensureUserEntry(userProjectDir, config, PKG_ROOT), validateConfig(config, userProjectDir);
31
+
32
+ const buildDir = getBuildDir(userProjectDir);
33
+ console.log(chalk.cyan(`\n[2/${total}] 重置 RN 工程模板`));
34
+ copyTemplate(PKG_ROOT, buildDir, config), patchAndroidAbi(buildDir, config);
35
+ patchExtraRnDependencies(buildDir, userProjectDir, config);
36
+ patchAppConfig(buildDir, userProjectDir, config);
37
+ patchAndroidConfig(buildDir, config), 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(chalk.cyan(`\n[3/${total}] 检查缓存指纹`)), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
43
+ console.log(chalk.cyan(`\n[4/${total}] 复制用户项目`));
44
+ const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
45
+
46
+ console.log(chalk.cyan(`\n[5/${total}] 安装依赖与编译优化`)), await installUserDeps(targetDir, userProjectDir, config, buildDir, isIos ? 'ios' : 'android');
47
+ patchNodejsMobilePlugin(userProjectDir, config);
48
+ patchWebViewSsl(userProjectDir, config), writeLocalProperties(buildDir, config);
49
+
50
+ console.log(chalk.cyan(`\n[6/${total}] 生成桥接文件`));
51
+ generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
52
+
53
+ if (isIos) {
54
+ console.log(chalk.cyan(`\n[7/${total}] pod install`)), podInstall(path.join(buildDir, 'ios'));
55
+ console.log(chalk.cyan(`\n[8/${total}] 生成 JS bundle + Hermes 字节码(由 Xcode Run Script 自动)`));
56
+ const bundlePath = path.join(buildDir, 'ios', 'main.jsbundle');
57
+ runSilent('npx', [
58
+ 'react-native', 'bundle',
59
+ '--platform', 'ios',
60
+ '--dev', 'false',
61
+ '--entry-file', 'index.js',
62
+ '--bundle-output', bundlePath,
63
+ ], { cwd: buildDir });
64
+ console.log(chalk.green(' ✓ bundle 已生成'));
65
+
66
+ console.log(chalk.cyan(`\n[9/${total}] 编译并安装到模拟器/设备`));
67
+ const args = ['react-native', 'run-ios'];
68
+ if (flags.device) args.push('--device', flags.device);
69
+ // FORCE_BUNDLING=1:即使 Debug+模拟器也把 main.jsbundle 打进 App
70
+ const env = { ...process.env, FORCE_BUNDLING: '1', NODE_MOBILE_MODE: 'test' };
71
+ run('npx', args, { cwd: buildDir, env, inherit: true });
72
+ await markVerified(buildDir);
73
+ } else {
74
+ console.log(chalk.cyan(`\n[7/${total}] 生成 JS bundle(离线可用)`));
75
+ const assetsDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'assets'),
76
+ resDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'res');
77
+ ensureDir(assetsDir);
78
+
79
+ const bundlePath = path.join(assetsDir, 'index.android.bundle');
80
+ runSilent('npx', [
81
+ 'react-native', 'bundle',
82
+ '--platform', 'android',
83
+ '--dev', 'false',
84
+ '--entry-file', 'index.js',
85
+ '--bundle-output', bundlePath,
86
+ '--assets-dest', resDir,
87
+ ], { cwd: buildDir });
88
+ console.log(chalk.green(' ✓ bundle 已生成'));
89
+
90
+ console.log(chalk.cyan(`\n[8/${total}] 编译 Hermes 字节码(加速离线启动)`));
91
+ const hermesc = findHermesc(userProjectDir);
92
+ if (hermesc) {
93
+ const hbcPath = bundlePath + '.hbc';
94
+ runSilent(hermesc, ['-emit-binary', '-out', hbcPath, bundlePath]);
95
+ fs.copyFileSync(hbcPath, bundlePath), fs.unlinkSync(hbcPath);
96
+ console.log(chalk.green(' ✓ 已替换为 Hermes 字节码'));
97
+ }
98
+ else console.warn(chalk.yellow(' ⚠️ 未找到 hermesc,跳过字节码编译(离线启动会较慢)'));
99
+
100
+ const mergedAsset = path.join(buildDir, 'android', 'app', 'build', 'intermediates', 'assets', 'debug', 'mergeDebugAssets', 'index.android.bundle');
101
+ if (fs.existsSync(mergedAsset)) {
102
+ const h = f => createHash('sha256').update(fs.readFileSync(f)).digest('hex');
103
+ if (h(bundlePath) !== h(mergedAsset)) {
104
+ fs.rmSync(path.dirname(mergedAsset), { recursive: true, force: true });
105
+ console.log(chalk.gray(' bundle 已变化,清理 mergeDebugAssets 缓存'));
106
+ }
107
+ }
108
+ console.log(chalk.cyan(`\n[9/${total}] 配置 adb 端口转发`));
109
+ const adb = findAdb(config);
110
+ console.log(chalk.gray(' adb: ' + adb)), run(adb, ['reverse', 'tcp:8081', 'tcp:8081'], { quiet: true });
111
+
112
+ const metroReady = () => new Promise(resolve => {
113
+ const req = http.get('http://localhost:8081/status', r => {
114
+ let d = '';
115
+ r.on('data', c => d += c);
116
+ r.on('end', () => resolve(d.includes('packager-status:running')));
117
+ });
118
+ req.on('error', () => resolve(false));
119
+ req.setTimeout(1000, () => { req.destroy(); resolve(false); });
120
+ });
121
+ if (!await metroReady()) {
122
+ console.log(chalk.gray(' 启动 Metro...'));
123
+ const metroLog = fs.openSync(path.join(buildDir, '.metro.log'), 'a');
124
+ const metro = spawnCmdAsync('npx', ['react-native', 'start'], {
125
+ cwd: buildDir, detached: true, stdio: ['ignore', metroLog, metroLog]
126
+ });
127
+ metro.unref();
128
+ let ready = false;
129
+ for (let i = 0; i < 60; i++) {
130
+ await new Promise(r => setTimeout(r, 500));
131
+ if (await metroReady()) { ready = true; console.log(chalk.green(' ✓ Metro 已就绪(后台常驻,支持热更新)')); break; }
132
+ }
133
+ if (!ready) console.log(chalk.yellow(' ⚠️ Metro 30 秒内未就绪,详见 .metro.log;app 将回落到本地 bundle'));
134
+ }
135
+ console.log(chalk.cyan(`\n[10/${total}] 编译 Debug 包`));
136
+ const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
137
+ if (process.env.OPTIMIZE_DEBUG) {
138
+ await runStream(gradlew, ['assembleDebug'], { cwd: path.join(buildDir, 'android'), env: { ...process.env, NODE_MOBILE_MODE: 'test' } });
139
+ } else {
140
+ console.log(chalk.gray(' 编译中...'));
141
+ run(gradlew, ['assembleDebug'], { cwd: path.join(buildDir, 'android'), env: { ...process.env, NODE_MOBILE_MODE: 'test' }, quiet: true });
142
+ console.log(chalk.green(' ✓ 编译完成'));
143
+ }
144
+
145
+ console.log(chalk.cyan(`\n[11/${total}] 安装到设备`));
146
+ ensureInstallable(adb, config.appId, 'debug', buildDir, flags.device);
147
+ const apkDir = path.join(buildDir, 'android', 'app', 'build', 'outputs', 'apk', 'debug');
148
+ if (!fs.existsSync(apkDir)) console.error(chalk.red(' [错误] 未找到 APK 输出目录: ' + apkDir)), process.exit(1);
149
+ const apkPath = path.join(apkDir, 'app-debug.apk');
150
+ if (!fs.existsSync(apkPath)) console.error(chalk.red(' [错误] 未找到 app-debug.apk: ' + apkPath)), process.exit(1);
151
+ const devPrefix = flags.device ? ['-s', flags.device] : [];
152
+ run(adb, [...devPrefix, 'install', '-r', apkPath], { quiet: true });
153
+ console.log(chalk.green(' ✓ 已安装到设备'));
154
+ run(adb, [...devPrefix, 'shell', 'monkey', '-p', config.appId, '-c', 'android.intent.category.LAUNCHER', '1'], { quiet: true });
155
+ writeInstallState(buildDir, { appId: config.appId, variant: 'debug', installedAt: new Date().toISOString() });
156
+ await markVerified(buildDir);
157
+ }
158
+ };
159
+
160
+ export { runTest };