@flun/node-mobile-app 4.1.2 → 4.2.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 +1 -1
- package/CHANGELOG.md +15 -22
- package/README.md +67 -13
- package/bin/cli.js +3 -3
- package/mobileAppConfig.js +17 -4
- package/package.json +1 -1
- package/scripts/copy-files.js +3 -2
- package/src/commands/build.js +105 -99
- package/src/commands/clean.js +12 -6
- package/src/commands/test.js +141 -133
- package/src/config.js +88 -52
- package/src/copy.js +302 -215
- package/src/fingerprint.js +96 -77
- package/src/generate.js +33 -21
- package/src/install.js +176 -162
- package/src/optimize-node-modules.js +5 -4
- package/src/patcher.js +438 -358
- package/src/platform-ios.js +122 -106
- package/src/utils.js +351 -252
- package/template/.prettierrc.js +8 -2
- package/template/App.tsx +98 -87
- package/template/babel.config.js +8 -2
- package/template/eslint.config.js +8 -2
- package/template/metro.config.js +9 -3
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +2 -2
package/src/commands/test.js
CHANGED
|
@@ -1,148 +1,153 @@
|
|
|
1
|
-
import
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { createHash } from 'crypto';
|
|
2
3
|
import fs from 'fs';
|
|
4
|
+
import http from 'http';
|
|
5
|
+
import path from 'path';
|
|
3
6
|
import { fileURLToPath } from 'url';
|
|
4
7
|
import { loadConfig, validateConfig } from '../config.js';
|
|
5
|
-
import { createHash } from 'crypto';
|
|
6
|
-
import http from 'http';
|
|
7
8
|
import {
|
|
8
|
-
|
|
9
|
-
patchExtraRnDependencies
|
|
9
|
+
copyTemplate, copyUserProject, ensureUserConfig, ensureUserEntry, getBuildDir, patchAndroidAbi,
|
|
10
|
+
patchExtraRnDependencies, writeLocalProperties
|
|
10
11
|
} from '../copy.js';
|
|
12
|
+
import { checkAndResetCache } from '../fingerprint.js';
|
|
13
|
+
import { generateMainJs, generateRuntime } from '../generate.js';
|
|
11
14
|
import { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl } from '../install.js';
|
|
12
15
|
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
16
|
import {
|
|
18
|
-
|
|
17
|
+
patchAndroidConfig, patchAppConfig, patchIcon, patchIosConfig, patchIosIcon, patchMavenRepositories,
|
|
18
|
+
patchSigning
|
|
19
|
+
} from '../patcher.js';
|
|
20
|
+
import { checkIosTools, ensureMac, podInstall } from '../platform-ios.js';
|
|
21
|
+
import {
|
|
22
|
+
ensureDir, ensureInstallable, findAdb, findHermesc, run, runSilent, runStream, spawnCmdAsync,
|
|
23
|
+
warnGradleDownloadHint, writeInstallState
|
|
19
24
|
} from '../utils.js';
|
|
20
|
-
import chalk from 'chalk';
|
|
21
25
|
|
|
22
|
-
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..')
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
27
|
+
/**
|
|
28
|
+
* 编译并安装到设备;走 Metro 热更新,支持 --ios 与 --device
|
|
29
|
+
* >查看定义:@see {@link runTest}
|
|
30
|
+
* @param {object} flags 命令行参数,含 ios / device
|
|
31
|
+
* @returns {Promise<void>}
|
|
32
|
+
*/
|
|
33
|
+
const runTest = async flags => {
|
|
34
|
+
const userProjectDir = process.cwd(), isIos = !!flags.ios, total = isIos ? 9 : 11;
|
|
25
35
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
36
|
+
console.log(chalk.cyan(`\n=== node-mobile-app test (${isIos ? 'iOS' : 'Android'}) ===\n`));
|
|
37
|
+
console.log(chalk.gray('用户项目: ' + userProjectDir));
|
|
38
|
+
if (isIos) ensureMac(), checkIosTools();
|
|
39
|
+
console.log(chalk.cyan(`\n[1/${total}] 检查用户配置与入口文件`)), ensureUserConfig(userProjectDir, PKG_ROOT);
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
const config = await loadConfig(userProjectDir);
|
|
42
|
+
ensureUserEntry(userProjectDir, config, PKG_ROOT), validateConfig(config, userProjectDir);
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
const buildDir = getBuildDir(userProjectDir);
|
|
45
|
+
console.log(chalk.cyan(`\n[2/${total}] 重置 RN 工程模板`));
|
|
46
|
+
await copyTemplate(PKG_ROOT, buildDir, config);
|
|
47
|
+
patchAndroidAbi(buildDir, config);
|
|
48
|
+
patchExtraRnDependencies(buildDir, userProjectDir, config);
|
|
49
|
+
patchAppConfig(buildDir, userProjectDir, config);
|
|
50
|
+
patchAndroidConfig(buildDir, config), patchMavenRepositories(buildDir, config);
|
|
51
|
+
patchSigning(buildDir, userProjectDir, config);
|
|
52
|
+
await patchIcon(buildDir, userProjectDir, config);
|
|
53
|
+
patchIosConfig(buildDir, userProjectDir, config);
|
|
54
|
+
await patchIosIcon(buildDir, userProjectDir, config);
|
|
43
55
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
56
|
+
console.log(chalk.cyan(`\n[3/${total}] 检查缓存指纹`)), checkAndResetCache(buildDir, userProjectDir, config, PKG_ROOT);
|
|
57
|
+
console.log(chalk.cyan(`\n[4/${total}] 复制用户项目`));
|
|
58
|
+
const { moduleType, targetDir } = copyUserProject(userProjectDir, buildDir, config);
|
|
47
59
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
60
|
+
console.log(chalk.cyan(`\n[5/${total}] 安装依赖与编译优化`));
|
|
61
|
+
await installUserDeps(targetDir, userProjectDir, config, buildDir, isIos ? 'ios' : 'android');
|
|
62
|
+
patchNodejsMobilePlugin(userProjectDir, config);
|
|
63
|
+
patchWebViewSsl(userProjectDir, config), writeLocalProperties(buildDir, config);
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
65
|
+
console.log(chalk.cyan(`\n[6/${total}] 生成桥接文件`));
|
|
66
|
+
generateMainJs(targetDir, config, moduleType), generateRuntime(buildDir, config);
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
'--bundle-output', bundlePath,
|
|
66
|
-
], { cwd: buildDir });
|
|
67
|
-
console.log(chalk.green(' ✓ bundle 已生成'));
|
|
68
|
+
if (isIos) {
|
|
69
|
+
console.log(chalk.cyan(`\n[7/${total}] pod install`)), podInstall(path.join(buildDir, 'ios'));
|
|
70
|
+
console.log(chalk.cyan(`\n[8/${total}] 生成 JS bundle + Hermes 字节码(由 Xcode Run Script 自动)`));
|
|
71
|
+
const bundlePath = path.join(buildDir, 'ios', 'main.jsbundle');
|
|
72
|
+
runSilent('npx', [
|
|
73
|
+
'react-native', 'bundle', '--platform', 'ios', '--dev', 'false',
|
|
74
|
+
'--entry-file', 'index.js', '--bundle-output', bundlePath,
|
|
75
|
+
], { cwd: buildDir });
|
|
76
|
+
console.log(chalk.green(' ✓ bundle 已生成'));
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
console.log(chalk.cyan(`\n[9/${total}] 编译并安装到模拟器/设备`));
|
|
79
|
+
const args = ['react-native', 'run-ios'];
|
|
80
|
+
if (flags.device) args.push('--device', flags.device);
|
|
81
|
+
// FORCE_BUNDLING=1:即使 Debug+模拟器也把 main.jsbundle 打进 App
|
|
82
|
+
const env = { ...process.env, FORCE_BUNDLING: '1', NODE_MOBILE_MODE: 'test' };
|
|
83
|
+
run('npx', args, { cwd: buildDir, env, inherit: true });
|
|
84
|
+
await markVerified(buildDir);
|
|
85
|
+
} else {
|
|
86
|
+
console.log(chalk.cyan(`\n[7/${total}] 生成 JS bundle(离线可用)`));
|
|
87
|
+
const assetsDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'assets'),
|
|
88
|
+
resDir = path.join(buildDir, 'android', 'app', 'src', 'main', 'res');
|
|
89
|
+
ensureDir(assetsDir);
|
|
81
90
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
'--bundle-output', bundlePath,
|
|
89
|
-
'--assets-dest', resDir,
|
|
90
|
-
], { cwd: buildDir });
|
|
91
|
-
console.log(chalk.green(' ✓ bundle 已生成'));
|
|
91
|
+
const bundlePath = path.join(assetsDir, 'index.android.bundle');
|
|
92
|
+
runSilent('npx', [
|
|
93
|
+
'react-native', 'bundle', '--platform', 'android', '--dev', 'false', '--entry-file', 'index.js',
|
|
94
|
+
'--bundle-output', bundlePath, '--assets-dest', resDir,
|
|
95
|
+
], { cwd: buildDir });
|
|
96
|
+
console.log(chalk.green(' ✓ bundle 已生成'));
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
console.log(chalk.cyan(`\n[8/${total}] 编译 Hermes 字节码(加速离线启动)`));
|
|
99
|
+
const hermesc = findHermesc(userProjectDir);
|
|
100
|
+
if (hermesc) {
|
|
101
|
+
const hbcPath = bundlePath + '.hbc';
|
|
102
|
+
runSilent(hermesc, ['-emit-binary', '-out', hbcPath, bundlePath]);
|
|
103
|
+
fs.copyFileSync(hbcPath, bundlePath), fs.unlinkSync(hbcPath);
|
|
104
|
+
console.log(chalk.green(' ✓ 已替换为 Hermes 字节码'));
|
|
105
|
+
}
|
|
106
|
+
else console.warn(chalk.yellow(' ⚠️ 未找到 hermesc,跳过字节码编译(离线启动会较慢)'));
|
|
102
107
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
108
|
+
const mergedAsset = path.join(buildDir, 'android', 'app', 'build', 'intermediates', 'assets', 'debug',
|
|
109
|
+
'mergeDebugAssets', 'index.android.bundle');
|
|
110
|
+
if (fs.existsSync(mergedAsset)) {
|
|
111
|
+
const h = f => createHash('sha256').update(fs.readFileSync(f)).digest('hex');
|
|
112
|
+
if (h(bundlePath) !== h(mergedAsset)) {
|
|
113
|
+
fs.rmSync(path.dirname(mergedAsset), { recursive: true, force: true });
|
|
114
|
+
console.log(chalk.gray(' bundle 已变化,清理 mergeDebugAssets 缓存'));
|
|
111
115
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
116
|
+
}
|
|
117
|
+
console.log(chalk.cyan(`\n[9/${total}] 配置 adb 端口转发`));
|
|
118
|
+
const adb = findAdb(config);
|
|
119
|
+
console.log(chalk.gray(' adb: ' + adb)), run(adb, ['reverse', 'tcp:8081', 'tcp:8081'], { quiet: true });
|
|
115
120
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
});
|
|
122
|
-
req.on('error', () => resolve(false));
|
|
123
|
-
req.setTimeout(1000, () => { req.destroy(); resolve(false); });
|
|
121
|
+
const metroReady = () => new Promise(resolve => {
|
|
122
|
+
const req = http.get('http://localhost:8081/status', r => {
|
|
123
|
+
let d = '';
|
|
124
|
+
r.on('data', c => d += c);
|
|
125
|
+
r.on('end', () => resolve(d.includes('packager-status:running')));
|
|
124
126
|
});
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
metro
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
127
|
+
req.on('error', () => resolve(false));
|
|
128
|
+
req.setTimeout(1000, () => { req.destroy(); resolve(false); });
|
|
129
|
+
});
|
|
130
|
+
if (!await metroReady()) {
|
|
131
|
+
console.log(chalk.gray(' 启动 Metro...'));
|
|
132
|
+
const metroLog = fs.openSync(path.join(buildDir, '.metro.log'), 'a'),
|
|
133
|
+
metro = spawnCmdAsync('npx', ['react-native', 'start'], {
|
|
134
|
+
cwd: buildDir, detached: true, stdio: ['ignore', metroLog, metroLog]
|
|
135
|
+
});
|
|
136
|
+
metro.unref();
|
|
137
|
+
let ready = false;
|
|
138
|
+
for (let i = 0; i < 60; i++) {
|
|
139
|
+
await new Promise(r => setTimeout(r, 500));
|
|
140
|
+
if (await metroReady()) {
|
|
141
|
+
ready = true, console.log(chalk.green(' ✓ Metro 已就绪(后台常驻,支持热更新)')); break;
|
|
138
142
|
}
|
|
139
|
-
if (!ready) console.log(chalk.yellow(' ⚠️ Metro 30 秒内未就绪,详见 .metro.log;app 将回落到本地 bundle'));
|
|
140
143
|
}
|
|
141
|
-
console.log(chalk.
|
|
142
|
-
|
|
144
|
+
if (!ready) console.log(chalk.yellow(' ⚠️ Metro 30 秒内未就绪,详见 .metro.log;app 将回落到本地 bundle'));
|
|
145
|
+
}
|
|
146
|
+
console.log(chalk.cyan(`\n[10/${total}] 编译 Debug 包`));
|
|
147
|
+
const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew';
|
|
148
|
+
try {
|
|
143
149
|
if (process.env.OPTIMIZE_DEBUG) await runStream(gradlew, ['assembleDebug'], {
|
|
144
|
-
cwd: path.join(buildDir, 'android'), env: { ...process.env, NODE_MOBILE_MODE: 'test' },
|
|
145
|
-
stdio: 'inherit'
|
|
150
|
+
cwd: path.join(buildDir, 'android'), env: { ...process.env, NODE_MOBILE_MODE: 'test' }, stdio: 'inherit'
|
|
146
151
|
});
|
|
147
152
|
else {
|
|
148
153
|
console.log(chalk.gray(' 编译中...'));
|
|
@@ -151,21 +156,24 @@ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'
|
|
|
151
156
|
});
|
|
152
157
|
console.log(chalk.green(' ✓ 编译完成'));
|
|
153
158
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
ensureInstallable(adb, config.appId, 'debug', buildDir, flags.device);
|
|
157
|
-
const apkDir = path.join(buildDir, 'android', 'app', 'build', 'outputs', 'apk', 'debug');
|
|
158
|
-
if (!fs.existsSync(apkDir)) console.error(chalk.red(' [错误] 未找到 APK 输出目录: ' + apkDir)), process.exit(1);
|
|
159
|
-
const apkPath = path.join(apkDir, 'app-debug.apk');
|
|
160
|
-
if (!fs.existsSync(apkPath)) console.error(chalk.red(' [错误] 未找到 app-debug.apk: ' + apkPath)), process.exit(1);
|
|
161
|
-
const devPrefix = flags.device ? ['-s', flags.device] : [];
|
|
162
|
-
run(adb, [...devPrefix, 'install', '-r', apkPath], { quiet: true });
|
|
163
|
-
console.log(chalk.green(' ✓ 已安装到设备'));
|
|
164
|
-
run(adb, [...devPrefix, 'shell', 'monkey', '-p', config.appId, '-c', 'android.intent.category.LAUNCHER', '1'],
|
|
165
|
-
{ quiet: true });
|
|
166
|
-
writeInstallState(buildDir, { appId: config.appId, variant: 'debug', installedAt: new Date().toISOString() });
|
|
167
|
-
await markVerified(buildDir);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
warnGradleDownloadHint(config); throw err;
|
|
168
161
|
}
|
|
169
|
-
|
|
162
|
+
|
|
163
|
+
console.log(chalk.cyan(`\n[11/${total}] 安装到设备`));
|
|
164
|
+
ensureInstallable(adb, config.appId, 'debug', flags.device);
|
|
165
|
+
const apkDir = path.join(buildDir, 'android', 'app', 'build', 'outputs', 'apk', 'debug');
|
|
166
|
+
if (!fs.existsSync(apkDir)) console.error(chalk.red(' [错误] 未找到 APK 输出目录: ' + apkDir)), process.exit(1);
|
|
167
|
+
const apkPath = path.join(apkDir, 'app-debug.apk');
|
|
168
|
+
if (!fs.existsSync(apkPath)) console.error(chalk.red(' [错误] 未找到 app-debug.apk: ' + apkPath)), process.exit(1);
|
|
169
|
+
const devPrefix = flags.device ? ['-s', flags.device] : [];
|
|
170
|
+
run(adb, [...devPrefix, 'install', '-r', apkPath], { quiet: true });
|
|
171
|
+
console.log(chalk.green(' ✓ 已安装到设备'));
|
|
172
|
+
run(adb, [...devPrefix, 'shell', 'monkey', '-p', config.appId, '-c', 'android.intent.category.LAUNCHER', '1'],
|
|
173
|
+
{ quiet: true });
|
|
174
|
+
writeInstallState(buildDir, { appId: config.appId, variant: 'debug', installedAt: new Date().toISOString() });
|
|
175
|
+
await markVerified(buildDir);
|
|
176
|
+
}
|
|
177
|
+
};
|
|
170
178
|
|
|
171
179
|
export { runTest };
|
package/src/config.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import chalk from 'chalk';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
3
4
|
import { pathToFileURL } from 'url';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
5
|
|
|
6
|
+
const merge = (def, user) => {
|
|
7
|
+
const out = { ...def };
|
|
8
|
+
for (const k of Object.keys(user)) {
|
|
9
|
+
if (user[k] && typeof user[k] === 'object' && !Array.isArray(user[k])) out[k] = merge(def[k] || {}, user[k]);
|
|
10
|
+
else if (user[k] !== undefined && user[k] !== null) out[k] = user[k];
|
|
11
|
+
}
|
|
12
|
+
return out;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* 默认配置对象,用户未提供的字段使用此处默认值
|
|
16
|
+
* >查看定义:@see {@link DEFAULT_CONFIG}
|
|
17
|
+
*/
|
|
6
18
|
const DEFAULT_CONFIG = {
|
|
7
19
|
serverPath: './server.js',
|
|
8
20
|
appName: null,
|
|
@@ -27,6 +39,8 @@ const DEFAULT_CONFIG = {
|
|
|
27
39
|
targetSdkVersion: 36,
|
|
28
40
|
compileSdkVersion: 37,
|
|
29
41
|
buildToolsVersion: '37.0.0',
|
|
42
|
+
gradleDistributionUrl: null,
|
|
43
|
+
mavenRepositories: null,
|
|
30
44
|
abiFilters: ['arm64-v8a'],
|
|
31
45
|
icon: './build/icon.png',
|
|
32
46
|
usesCleartextTraffic: true,
|
|
@@ -57,57 +71,79 @@ const DEFAULT_CONFIG = {
|
|
|
57
71
|
allowScripts: {
|
|
58
72
|
'node': true,
|
|
59
73
|
}
|
|
60
|
-
}
|
|
61
|
-
merge = (def, user) => {
|
|
62
|
-
const out = { ...def };
|
|
63
|
-
for (const k of Object.keys(user)) {
|
|
64
|
-
if (user[k] && typeof user[k] === 'object' && !Array.isArray(user[k])) out[k] = merge(def[k] || {}, user[k]);
|
|
65
|
-
else if (user[k] !== undefined && user[k] !== null) out[k] = user[k];
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
},
|
|
69
|
-
loadConfig = async userProjectDir => {
|
|
70
|
-
for (const name of ['mobileAppConfig.js', 'mobileAppConfig.mjs', 'mobileAppConfig.cjs']) {
|
|
71
|
-
const p = path.join(userProjectDir, name);
|
|
72
|
-
if (fs.existsSync(p)) {
|
|
73
|
-
console.log(chalk.gray(' 读取配置: ' + name));
|
|
74
|
-
const mod = await import(pathToFileURL(p).href);
|
|
75
|
-
return merge(DEFAULT_CONFIG, mod.default || mod);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
console.log(chalk.yellow('未找到 mobileAppConfig.js,使用默认配置'));
|
|
79
|
-
return DEFAULT_CONFIG;
|
|
80
|
-
},
|
|
81
|
-
validateConfig = (config, userProjectDir) => {
|
|
82
|
-
const errors = [], entry = path.join(userProjectDir, config.serverPath);
|
|
83
|
-
if (!fs.existsSync(entry)) errors.push('serverPath 不存在: ' + config.serverPath);
|
|
74
|
+
};
|
|
84
75
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
76
|
+
/**
|
|
77
|
+
* 读取用户项目根的 mobileAppConfig.js,与默认配置深度合并
|
|
78
|
+
* >查看定义:@see {@link loadConfig}
|
|
79
|
+
* @param {string} userProjectDir 用户项目目录
|
|
80
|
+
* @returns {Promise<object>} 合并后的配置对象
|
|
81
|
+
*/
|
|
82
|
+
const loadConfig = async userProjectDir => {
|
|
83
|
+
for (const name of ['mobileAppConfig.js', 'mobileAppConfig.mjs', 'mobileAppConfig.cjs']) {
|
|
84
|
+
const p = path.join(userProjectDir, name);
|
|
85
|
+
if (fs.existsSync(p)) {
|
|
86
|
+
console.log(chalk.gray(' 读取配置: ' + name));
|
|
87
|
+
const mod = await import(pathToFileURL(p).href);
|
|
88
|
+
return merge(DEFAULT_CONFIG, mod.default || mod);
|
|
96
89
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
90
|
+
}
|
|
91
|
+
console.log(chalk.yellow('未找到 mobileAppConfig.js,使用默认配置'));
|
|
92
|
+
return DEFAULT_CONFIG;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 定位 Android SDK 路径:配置 > 环境变量 > 默认路径
|
|
97
|
+
* >查看定义:@see {@link resolveSdkPath}
|
|
98
|
+
* @param {object} config 用户配置
|
|
99
|
+
* @returns {string|null} SDK 绝对路径,未找到时返回 null
|
|
100
|
+
*/
|
|
101
|
+
const resolveSdkPath = config => {
|
|
102
|
+
if (config.android && config.android.sdkPath) return config.android.sdkPath;
|
|
103
|
+
for (const env of ['ANDROID_HOME', 'ANDROID_SDK_ROOT']) {
|
|
104
|
+
const base = process.env[env];
|
|
105
|
+
if (base && fs.existsSync(base)) return base;
|
|
106
|
+
}
|
|
107
|
+
const localAppData = process.env.LOCALAPPDATA || '', home = process.env.USERPROFILE || process.env.HOME || '',
|
|
108
|
+
candidates = [path.join(localAppData, 'Android', 'Sdk'), path.join(home, 'Library', 'Android', 'sdk'),
|
|
109
|
+
path.join(home, 'Android', 'Sdk')];
|
|
108
110
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
for (const c of candidates) if (c && fs.existsSync(path.join(c, 'platform-tools'))) return c;
|
|
112
|
+
return null;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 校验配置必填项与格式,失败时打印错误并抛出异常
|
|
117
|
+
* >查看定义:@see {@link validateConfig}
|
|
118
|
+
* @param {object} config 用户配置
|
|
119
|
+
* @param {string} userProjectDir 用户项目目录
|
|
120
|
+
* @returns {void}
|
|
121
|
+
*/
|
|
122
|
+
const validateConfig = (config, userProjectDir) => {
|
|
123
|
+
const errors = [], entry = path.join(userProjectDir, config.serverPath);
|
|
124
|
+
if (!fs.existsSync(entry)) errors.push('serverPath 不存在: ' + config.serverPath);
|
|
125
|
+
|
|
126
|
+
const pkg = path.join(userProjectDir, 'package.json');
|
|
127
|
+
if (!fs.existsSync(pkg)) errors.push('用户项目缺少 package.json');
|
|
128
|
+
// 极简防呆:至少 2 段、每段非空、只含 [a-zA-Z0-9_-],不允许首尾点或连续点
|
|
129
|
+
// 具体平台是否合法交给 Gradle / Xcode 报错(Android 不允许 -,iOS 允许)
|
|
130
|
+
if (typeof config.appId !== 'string' || !/^[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(config.appId))
|
|
131
|
+
errors.push('appId 格式不合法(需反向域名格式,如 com.example.app): ' + config.appId);
|
|
132
|
+
if (!Number.isInteger(config.versionCode) || config.versionCode < 1) errors.push('versionCode 必须是正整数');
|
|
133
|
+
if (!config.android.abiFilters?.length) errors.push('android.abiFilters 不能为空');
|
|
134
|
+
if (config.android.gradleDistributionUrl && !/^https?:\/\/.+\.zip$/.test(config.android.gradleDistributionUrl))
|
|
135
|
+
errors.push('android.gradleDistributionUrl 必须是 http(s) 且以 .zip 结尾: ' + config.android.gradleDistributionUrl);
|
|
136
|
+
if (config.android.mavenRepositories != null && !Array.isArray(config.android.mavenRepositories))
|
|
137
|
+
errors.push('android.mavenRepositories 必须是数组或 null');
|
|
138
|
+
else if (Array.isArray(config.android.mavenRepositories)) {
|
|
139
|
+
const bad = config.android.mavenRepositories.filter(u => typeof u !== 'string' || !/^https?:\/\//.test(u));
|
|
140
|
+
if (bad.length) errors.push('android.mavenRepositories 存在非 http(s) 地址: ' + bad.join(', '));
|
|
141
|
+
}
|
|
142
|
+
if (errors.length) {
|
|
143
|
+
errors.forEach(e => console.error(chalk.red(' - ' + e)));
|
|
144
|
+
throw new Error('配置校验失败');
|
|
145
|
+
}
|
|
146
|
+
console.log(chalk.green(' ✓ 配置校验通过'));
|
|
147
|
+
};
|
|
112
148
|
|
|
113
|
-
export { DEFAULT_CONFIG, loadConfig,
|
|
149
|
+
export { DEFAULT_CONFIG, loadConfig, resolveSdkPath, validateConfig };
|