@flun/node-mobile-app 4.0.7 → 4.0.9

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