@flun/node-mobile-app 4.0.8 → 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.
- package/Android /346/236/204/345/273/272/346/214/207/345/215/227.md" +354 -354
- package/CHANGELOG.md +20 -14
- package/LICENSE +33 -33
- package/README.md +661 -652
- package/bin/cli.js +20 -15
- package/mobileAppConfig.js +165 -165
- package/package.json +8 -8
- package/src/commands/build.js +119 -119
- package/src/commands/test.js +169 -169
- package/src/config.js +112 -112
- package/src/copy.js +320 -320
- package/src/fingerprint.js +129 -129
- package/src/generate.js +103 -103
- package/src/install.js +327 -327
- package/src/optimize-node-modules.js +711 -711
- package/src/patcher.js +393 -393
- package/src/platform-ios.js +140 -140
- package/src/utils.js +280 -280
- package/template/android/gradle.properties +48 -48
- package/template/eslint.config.js +5 -5
- package/template/ios/MyApp/Info.plist +73 -73
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +477 -477
package/src/install.js
CHANGED
|
@@ -1,328 +1,328 @@
|
|
|
1
|
-
import path from 'path';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import { run } from './utils.js';
|
|
4
|
-
import { clearVerified, optimizeNodeModules, SELF_HASH } from './optimize-node-modules.js';
|
|
5
|
-
import { createRequire } from 'module';
|
|
6
|
-
import chalk from 'chalk';
|
|
7
|
-
import { resolveSdkPath } from './config.js';
|
|
8
|
-
import { createHash } from 'crypto';
|
|
9
|
-
const
|
|
10
|
-
/**
|
|
11
|
-
* 内部:从 libnode 的 node_version.h 读取运行时 Node 主版本号
|
|
12
|
-
* 读不到直接报错退出(说明 libnode 目录不完整,继续执行必失败)
|
|
13
|
-
*/
|
|
14
|
-
resolveRuntimeMajor = (userProjectDir, targetPlatform) => {
|
|
15
|
-
const sub = targetPlatform === 'ios' ? 'ios' : 'android',
|
|
16
|
-
header = path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native',
|
|
17
|
-
sub, 'libnode', 'include', 'node', 'node_version.h');
|
|
18
|
-
if (!fs.existsSync(header)) {
|
|
19
|
-
console.error(chalk.red(' [错误] 未找到 node_version.h: ' + header));
|
|
20
|
-
console.error(chalk.red(' 说明 libnode 目录不完整,请重新安装依赖;')), process.exit(1);
|
|
21
|
-
}
|
|
22
|
-
const m = fs.readFileSync(header, 'utf-8').match(/#define\s+NODE_MAJOR_VERSION\s+(\d+)/);
|
|
23
|
-
if (!m) console.error(chalk.red(' [错误] 无法解析 node_version.h 中的 NODE_MAJOR_VERSION')), process.exit(1);
|
|
24
|
-
return m[1];
|
|
25
|
-
},
|
|
26
|
-
/**
|
|
27
|
-
* 内部:判断用户项目的生产依赖里是否含原生模块(binding.gyp)
|
|
28
|
-
* 按用户根 package.json 的 dependencies 键名,去 userProjectDir/node_modules 下逐个查
|
|
29
|
-
* 不用扫 nodejs-project/node_modules:全新构建时它还不存在
|
|
30
|
-
*/
|
|
31
|
-
hasNativeModules = userProjectDir => {
|
|
32
|
-
const pkgPath = path.join(userProjectDir, 'package.json');
|
|
33
|
-
if (!fs.existsSync(pkgPath)) return false;
|
|
34
|
-
let deps = {};
|
|
35
|
-
try {
|
|
36
|
-
deps = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).dependencies || {};
|
|
37
|
-
} catch { return false; }
|
|
38
|
-
|
|
39
|
-
const nodeModulesDir = path.join(userProjectDir, 'node_modules');
|
|
40
|
-
for (const name of Object.keys(deps))
|
|
41
|
-
if (fs.existsSync(path.join(nodeModulesDir, name, 'binding.gyp'))) return true;
|
|
42
|
-
return false;
|
|
43
|
-
},
|
|
44
|
-
/**
|
|
45
|
-
* 内部:定位 @flun/nodejs-mobile-react-native 插件目录
|
|
46
|
-
*/
|
|
47
|
-
findPluginDir = userProjectDir => {
|
|
48
|
-
const candidates = [path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native')];
|
|
49
|
-
for (const c of candidates) if (fs.existsSync(path.join(c, 'android', 'build.gradle'))) return c;
|
|
50
|
-
return null;
|
|
51
|
-
},
|
|
52
|
-
ABI_MAP = {
|
|
53
|
-
'armeabi-v7a': { arch: 'arm', prefix: 'armv7a-linux-androideabi' },
|
|
54
|
-
'arm64-v8a': { arch: 'arm64', prefix: 'aarch64-linux-android' },
|
|
55
|
-
'x86_64': { arch: 'x64', prefix: 'x86_64-linux-android' }
|
|
56
|
-
},
|
|
57
|
-
resolveNdkVersion = buildDir => {
|
|
58
|
-
const gradle = path.join(buildDir, 'android', 'build.gradle');
|
|
59
|
-
if (!fs.existsSync(gradle))
|
|
60
|
-
console.error(chalk.red(' [错误] 未找到 android/build.gradle: ' + gradle)), process.exit(1);
|
|
61
|
-
const m = fs.readFileSync(gradle, 'utf-8').match(/ndkVersion\s*=\s*"([^"]+)"/);
|
|
62
|
-
if (!m) console.error(chalk.red(' [错误] 无法从 android/build.gradle 解析 ndkVersion')), process.exit(1);
|
|
63
|
-
return m[1];
|
|
64
|
-
},
|
|
65
|
-
resolveCompileSdk = buildDir => {
|
|
66
|
-
const gradle = path.join(buildDir, 'android', 'build.gradle'),
|
|
67
|
-
m = fs.readFileSync(gradle, 'utf-8').match(/minSdkVersion\s*=\s*(\d+)/);
|
|
68
|
-
return Math.max(m ? +m[1] : 24, 24);
|
|
69
|
-
},
|
|
70
|
-
findNativePackages = targetDir => {
|
|
71
|
-
const nm = path.join(targetDir, 'node_modules'), result = [];
|
|
72
|
-
if (!fs.existsSync(nm)) return result;
|
|
73
|
-
for (const entry of fs.readdirSync(nm)) {
|
|
74
|
-
if (entry.startsWith('.')) continue;
|
|
75
|
-
const full = path.join(nm, entry);
|
|
76
|
-
if (!fs.statSync(full).isDirectory()) continue;
|
|
77
|
-
if (entry.startsWith('@')) {
|
|
78
|
-
for (const sub of fs.readdirSync(full)) {
|
|
79
|
-
const pkgDir = path.join(full, sub);
|
|
80
|
-
if (fs.statSync(pkgDir).isDirectory()
|
|
81
|
-
&& fs.existsSync(path.join(pkgDir, 'binding.gyp'))) result.push(entry + '/' + sub);
|
|
82
|
-
}
|
|
83
|
-
} else if (fs.existsSync(path.join(full, 'binding.gyp'))) result.push(entry);
|
|
84
|
-
}
|
|
85
|
-
return result;
|
|
86
|
-
},
|
|
87
|
-
compileNativeModules = (targetDir, userProjectDir, config, buildDir, abiFilters) => {
|
|
88
|
-
const sdkPath = resolveSdkPath(config);
|
|
89
|
-
if (!sdkPath) console.error(chalk.red(' [错误] 未找到 Android SDK,无法编译原生模块')), process.exit(1);
|
|
90
|
-
const ndkVersion = resolveNdkVersion(buildDir), compileSdk = resolveCompileSdk(buildDir),
|
|
91
|
-
ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
|
92
|
-
if (!fs.existsSync(ndkPath))
|
|
93
|
-
console.error(chalk.red(' [错误] 未找到 NDK ' + ndkVersion + ': ' + ndkPath)), process.exit(1);
|
|
94
|
-
const pluginDir = findPluginDir(userProjectDir);
|
|
95
|
-
if (!pluginDir) console.error(chalk.red(' [错误] 未找到 @flun/nodejs-mobile-react-native 插件')), process.exit(1);
|
|
96
|
-
const libnodeDir = path.join(pluginDir, 'android', 'libnode'),
|
|
97
|
-
gypPath = createRequire(path.join(pluginDir, 'package.json')).resolve('@flun/nodejs-mobile-gyp/bin/node-gyp.js'),
|
|
98
|
-
hostTag = { win32: 'windows-x86_64', darwin: 'darwin-x86_64', linux: 'linux-x86_64' }[process.platform],
|
|
99
|
-
hostOs = process.platform === 'darwin' ? 'mac' : 'linux',
|
|
100
|
-
toolSuffix = process.platform === 'win32' ? '.cmd' : '',
|
|
101
|
-
binSuffix = process.platform === 'win32' ? '.exe' : '';
|
|
102
|
-
if (!hostTag) console.error(chalk.red(' [错误] 不支持的操作系统: ' + process.platform)), process.exit(1);
|
|
103
|
-
const toolchain = path.join(ndkPath, 'toolchains', 'llvm', 'prebuilt', hostTag),
|
|
104
|
-
assetsDir = path.dirname(targetDir), gradleProjectDir = path.join(buildDir, 'android');
|
|
105
|
-
const patchScript = path.join(pluginDir, 'scripts', 'patch-package.js');
|
|
106
|
-
if (fs.existsSync(patchScript)) run(process.execPath, [patchScript, path.join(targetDir, 'node_modules')],
|
|
107
|
-
{ cwd: targetDir, inherit: true });
|
|
108
|
-
for (const abi of abiFilters) {
|
|
109
|
-
const map = ABI_MAP[abi];
|
|
110
|
-
if (!map) console.error(chalk.red(' [错误] 不支持的 ABI: ' + abi)), process.exit(1);
|
|
111
|
-
const compilerPrefix = map.prefix + compileSdk,
|
|
112
|
-
env = {
|
|
113
|
-
...process.env,
|
|
114
|
-
npm_config_node_engine: 'v8', npm_config_nodedir: libnodeDir + path.sep,
|
|
115
|
-
npm_config_node_gyp: gypPath, npm_config_arch: map.arch,
|
|
116
|
-
npm_config_platform: 'android', npm_config_format: 'make-android',
|
|
117
|
-
PROJECT_DIR: gradleProjectDir, NODEJS_MOBILE_GYP: gypPath,
|
|
118
|
-
TOOLCHAIN: toolchain,
|
|
119
|
-
AR: path.join(toolchain, 'bin', 'llvm-ar' + binSuffix),
|
|
120
|
-
CC: path.join(toolchain, 'bin', compilerPrefix + '-clang' + toolSuffix),
|
|
121
|
-
CXX: path.join(toolchain, 'bin', compilerPrefix + '-clang++' + toolSuffix),
|
|
122
|
-
LINK: path.join(toolchain, 'bin', compilerPrefix + '-clang++' + toolSuffix),
|
|
123
|
-
RANLIB: path.join(toolchain, 'bin', 'llvm-ranlib' + binSuffix),
|
|
124
|
-
GYP_DEFINES: 'target_arch=' + map.arch + ' v8_target_arch=' + map.arch + ' android_target_arch=' + map.arch +
|
|
125
|
-
' host_os=' + hostOs + ' OS=android android_ndk_path=' + ndkPath.replace(/\\/g, '/'),
|
|
126
|
-
PATH: [path.join(ndkPath, 'prebuilt', hostTag, 'bin'), path.join(toolchain, 'bin'),
|
|
127
|
-
path.join(targetDir, 'node_modules', '.bin'), process.env.PATH].filter(Boolean).join(path.delimiter)
|
|
128
|
-
};
|
|
129
|
-
console.log(chalk.cyan(' ℹ️ 编译原生模块 (' + abi + ')...'));
|
|
130
|
-
try {
|
|
131
|
-
run('npm', ['rebuild', '--build-from-source', '--no-audit', '--no-fund'], { cwd: targetDir, env, quiet: true });
|
|
132
|
-
}
|
|
133
|
-
catch (e) { console.error(chalk.red(' [错误] 原生模块编译失败 (' + abi + '): ' + e.message)), process.exit(1); }
|
|
134
|
-
const missing = findNativePackages(targetDir).filter(name => {
|
|
135
|
-
const pkgDir = path.join(targetDir, 'node_modules', name),
|
|
136
|
-
prebuildDir = path.join(pkgDir, 'prebuilds', 'android-' + map.arch),
|
|
137
|
-
hasPrebuild = fs.existsSync(prebuildDir) && fs.readdirSync(prebuildDir).some(f => f.endsWith('.node')),
|
|
138
|
-
releaseDir = path.join(pkgDir, 'build', 'Release'),
|
|
139
|
-
hasBuild = fs.existsSync(releaseDir) && fs.readdirSync(releaseDir).some(f => f.endsWith('.node'));
|
|
140
|
-
return !hasPrebuild && !hasBuild;
|
|
141
|
-
});
|
|
142
|
-
if (missing.length > 0) {
|
|
143
|
-
console.error(chalk.red(' [错误] 以下原生模块编译后缺少 .node (' + abi + '):'));
|
|
144
|
-
for (const n of missing) console.error(chalk.red(' - ' + n));
|
|
145
|
-
process.exit(1);
|
|
146
|
-
}
|
|
147
|
-
fs.writeFileSync(path.join(assetsDir, '.native-built-' + abi), new Date().toISOString());
|
|
148
|
-
console.log(chalk.green(' ✓ 原生模块编译完成 (' + abi + ')'));
|
|
149
|
-
}
|
|
150
|
-
},
|
|
151
|
-
/**
|
|
152
|
-
* 内部:iOS 的 WebView SSL 补丁
|
|
153
|
-
*/
|
|
154
|
-
patchWebViewSslIos = userProjectDir => {
|
|
155
|
-
const file = path.join(userProjectDir, 'node_modules', 'react-native-webview', 'apple', 'RNCWebViewImpl.m');
|
|
156
|
-
if (!fs.existsSync(file)) return;
|
|
157
|
-
|
|
158
|
-
let c = fs.readFileSync(file, 'utf-8');
|
|
159
|
-
if (c.includes('LOCALHOST_SSL_BYPASS_IOS')) return console.log(chalk.green(' ✓ iOS WebView SSL 补丁已应用'));
|
|
160
|
-
|
|
161
|
-
// 在 "NSString* host = nil;" 那一段之后插入 localhost 放行
|
|
162
|
-
const anchor =
|
|
163
|
-
' if ([[challenge protectionSpace] authenticationMethod] == NSURLAuthenticationMethodClientCertificate) {',
|
|
164
|
-
idx = c.indexOf(anchor);
|
|
165
|
-
if (idx === -1) return console.warn(chalk.yellow(' ⚠️ RNCWebViewImpl.m 里未找到锚点,跳过 iOS SSL 补丁'));
|
|
166
|
-
|
|
167
|
-
const injected = [
|
|
168
|
-
' // LOCALHOST_SSL_BYPASS_IOS: 对 localhost/127.0.0.1 放行自签名证书(仅本地测试用)',
|
|
169
|
-
' if (host != nil && ([host isEqualToString:@"127.0.0.1"] || [host isEqualToString:@"localhost"])) {',
|
|
170
|
-
' NSURLCredential *cred = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];',
|
|
171
|
-
' completionHandler(NSURLSessionAuthChallengeUseCredential, cred);',
|
|
172
|
-
' return;',
|
|
173
|
-
' }',
|
|
174
|
-
'',
|
|
175
|
-
].join('\n');
|
|
176
|
-
|
|
177
|
-
c = c.substring(0, idx) + injected + c.substring(idx);
|
|
178
|
-
fs.writeFileSync(file, c, 'utf-8');
|
|
179
|
-
console.log(chalk.green(' ✓ 已应用 iOS WebView SSL 补丁(允许 localhost 自签证书)'));
|
|
180
|
-
},
|
|
181
|
-
/**
|
|
182
|
-
* 装用户项目的生产依赖到 <buildDir>/nodejs-assets/nodejs-project/
|
|
183
|
-
* 只装 dependencies,不装 devDependencies
|
|
184
|
-
* 依赖 + 环境未变化且上次构建完成时跳过,否则按需重装 / 仅优化
|
|
185
|
-
* 快照记录在 buildDir/.deps-snapshot.json(与桌面版一致)
|
|
186
|
-
*/
|
|
187
|
-
installUserDeps = async (targetDir, userProjectDir, config, buildDir, targetPlatform) => {
|
|
188
|
-
const assetsDir = path.dirname(targetDir), bnmFile = path.join(assetsDir, 'BUILD_NATIVE_MODULES.txt'),
|
|
189
|
-
nodeModulesDir = path.join(targetDir, 'node_modules'), snapshotPath = path.join(buildDir, '.deps-snapshot.json'),
|
|
190
|
-
forceRebuild = process.env.NODE_MOBILE_FORCE_REBUILD === '1', hasNative = hasNativeModules(userProjectDir),
|
|
191
|
-
optimizeCfg = config.optimize || {}, thisOptimize = optimizeCfg.enabled !== false,
|
|
192
|
-
abiFilters = [...(config.android.abiFilters || [])].sort(),
|
|
193
|
-
runtimeMajor = resolveRuntimeMajor(userProjectDir, targetPlatform);
|
|
194
|
-
|
|
195
|
-
fs.writeFileSync(bnmFile, hasNative ? '1' : '0');
|
|
196
|
-
if (hasNative) console.log(chalk.gray(' ℹ️ 检测到原生模块(binding.gyp)'));
|
|
197
|
-
|
|
198
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(targetDir, 'package.json'), 'utf-8')), deps = pkg.dependencies || {},
|
|
199
|
-
sortedDeps = Object.keys(deps).sort().reduce((acc, k) => { acc[k] = deps[k]; return acc; }, {}),
|
|
200
|
-
installerHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url), 'utf-8'))
|
|
201
|
-
.digest('hex').slice(0, 16),
|
|
202
|
-
snapshotData = { deps: sortedDeps, type: pkg.type || '', hasNative, forceRebuild };
|
|
203
|
-
|
|
204
|
-
let shouldInstall = true, shouldOptimize = thisOptimize, reasons = [];
|
|
205
|
-
if (fs.existsSync(snapshotPath)) {
|
|
206
|
-
const old = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')), verifiedOk = old.verified === true,
|
|
207
|
-
depsSame = JSON.stringify({
|
|
208
|
-
deps: old.deps, type: old.type, hasNative: old.hasNative, forceRebuild: old.forceRebuild
|
|
209
|
-
}) === JSON.stringify(snapshotData),
|
|
210
|
-
envSame = old.platform === targetPlatform && JSON.stringify(old.abiFilters) === JSON.stringify(abiFilters) &&
|
|
211
|
-
old.runtimeMajor === runtimeMajor,
|
|
212
|
-
lastOptimized = old.optimized !== false, selfHashSame = old.optimizeHash === SELF_HASH,
|
|
213
|
-
installerSame = old.installerHash === installerHash, nmExists = fs.existsSync(nodeModulesDir);
|
|
214
|
-
if (!depsSame) reasons.push('依赖清单已变化');
|
|
215
|
-
if (!verifiedOk) reasons.push('上次构建未完成');
|
|
216
|
-
if (!envSame) reasons.push('运行环境已变化');
|
|
217
|
-
if (!nmExists) reasons.push('缓存目录缺失');
|
|
218
|
-
if (lastOptimized && !thisOptimize) reasons.push('优化已由开启转为关闭');
|
|
219
|
-
if (thisOptimize && !selfHashSame && lastOptimized) reasons.push('优化程序已更新');
|
|
220
|
-
if (!installerSame) reasons.push('安装器已更新');
|
|
221
|
-
if (reasons.length > 0) {
|
|
222
|
-
shouldInstall = true, shouldOptimize = thisOptimize;
|
|
223
|
-
console.log(chalk.gray(' ℹ️ ' + (thisOptimize ? '重新安装依赖+优化' : '重新安装依赖')
|
|
224
|
-
+ '(原因: ' + reasons.join(', ') + ')'));
|
|
225
|
-
}
|
|
226
|
-
else if (thisOptimize && !lastOptimized)
|
|
227
|
-
shouldInstall = false, shouldOptimize = true, console.log(chalk.gray(' ℹ️ 依赖与环境未变化,仅需优化'));
|
|
228
|
-
else {
|
|
229
|
-
shouldInstall = false, shouldOptimize = false, console.log(chalk.gray(' ✓ 依赖与环境未变化,跳过安装与优化'));
|
|
230
|
-
await clearVerified(buildDir);
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
} else
|
|
234
|
-
console.log(chalk.gray(' ℹ️ ' + (thisOptimize ? '重新安装依赖+优化' : '重新安装依赖') + '(原因: 缓存快照缺失)'));
|
|
235
|
-
|
|
236
|
-
if (shouldInstall) {
|
|
237
|
-
if (fs.existsSync(nodeModulesDir))
|
|
238
|
-
console.log(chalk.cyan(' 清理旧 node_modules...')), fs.rmSync(nodeModulesDir, { recursive: true, force: true });
|
|
239
|
-
|
|
240
|
-
try { fs.rmSync(snapshotPath, { force: true }); } catch { }
|
|
241
|
-
const env = { ...process.env };
|
|
242
|
-
if (hasNative) env.NODEJS_MOBILE_BUILD_NATIVE_MODULES = '1';
|
|
243
|
-
console.log(chalk.cyan(' 安装用户生产依赖(nodejs-assets/nodejs-project)...'));
|
|
244
|
-
run('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], { cwd: targetDir, env, quiet: true });
|
|
245
|
-
const snap = {
|
|
246
|
-
...snapshotData, platform: targetPlatform, abiFilters, runtimeMajor,
|
|
247
|
-
optimizeHash: SELF_HASH, installerHash, optimized: false, verified: false
|
|
248
|
-
};
|
|
249
|
-
fs.writeFileSync(snapshotPath, JSON.stringify(snap, null, 2));
|
|
250
|
-
console.log(chalk.green(' ✓ 用户依赖安装完成'));
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (hasNative && targetPlatform === 'android' && shouldInstall)
|
|
254
|
-
compileNativeModules(targetDir, userProjectDir, config, buildDir, abiFilters);
|
|
255
|
-
if (shouldOptimize) {
|
|
256
|
-
console.log(chalk.cyan(' [信息] 正在执行优化...'));
|
|
257
|
-
try {
|
|
258
|
-
await optimizeNodeModules(targetDir, {
|
|
259
|
-
targetPlatform, abiFilters, runtimeMajor, exclude: optimizeCfg.exclude || [], snapshotDir: buildDir
|
|
260
|
-
});
|
|
261
|
-
} catch (err) {
|
|
262
|
-
console.error(chalk.red(' [错误] node_modules 优化失败:' + err.message));
|
|
263
|
-
console.error(chalk.red(' [提示] 可在 mobileAppConfig.js 中设置 optimize: { enabled: false } 关闭优化,'));
|
|
264
|
-
console.error(chalk.red(' 或用 optimize: { exclude: ["包名"] } 排除问题包;')), process.exit(1);
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
},
|
|
268
|
-
/**
|
|
269
|
-
* 修改 @flun/nodejs-mobile-react-native 插件 build.gradle 的 abiFilters
|
|
270
|
-
*/
|
|
271
|
-
patchNodejsMobilePlugin = (userProjectDir, config) => {
|
|
272
|
-
const pluginDir = findPluginDir(userProjectDir);
|
|
273
|
-
if (!pluginDir) return console.warn(chalk.yellow(' ⚠️ 未找到 @flun/nodejs-mobile-react-native 插件,跳过 patch'));
|
|
274
|
-
|
|
275
|
-
const file = path.join(pluginDir, 'android', 'build.gradle'),
|
|
276
|
-
abiList = config.android.abiFilters.map((x) => `"${x}"`).join(', ');
|
|
277
|
-
|
|
278
|
-
let c = fs.readFileSync(file, 'utf-8');
|
|
279
|
-
const before = c;
|
|
280
|
-
c = c.replace(/abiFilters\s*=\s*\[[^\]]*\]/, `abiFilters = [${abiList}]`);
|
|
281
|
-
|
|
282
|
-
if (c !== before) fs.writeFileSync(file, c, 'utf-8'),
|
|
283
|
-
console.log(chalk.green(` ✓ 已设置插件 abiFilters = [${abiList}]`));
|
|
284
|
-
},
|
|
285
|
-
/**
|
|
286
|
-
* 让 react-native-webview 对 localhost 自签名证书放行
|
|
287
|
-
* Android: patch RNCWebViewClient.java
|
|
288
|
-
* iOS: patch RNCWebViewImpl.m
|
|
289
|
-
* 仅在 config.webview.allowSelfSignedCert === true 时应用
|
|
290
|
-
* 每次 test/build 都检查(npm 重装 webview 后会覆盖补丁)
|
|
291
|
-
*/
|
|
292
|
-
patchWebViewSsl = (userProjectDir, config) => {
|
|
293
|
-
if (!config.webview || config.webview.allowSelfSignedCert !== true) return;
|
|
294
|
-
|
|
295
|
-
const file = path.join(
|
|
296
|
-
userProjectDir, 'node_modules', 'react-native-webview', 'android', 'src', 'main', 'java', 'com',
|
|
297
|
-
'reactnativecommunity', 'webview', 'RNCWebViewClient.java'
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
if (fs.existsSync(file)) {
|
|
301
|
-
let c = fs.readFileSync(file, 'utf-8');
|
|
302
|
-
if (c.includes('LOCALHOST_SSL_BYPASS')) console.log(chalk.green(' ✓ Android WebView SSL 补丁已应用'));
|
|
303
|
-
else {
|
|
304
|
-
const old = 'handler.cancel();', methodIndex = c.indexOf('onReceivedSslError');
|
|
305
|
-
if (methodIndex !== -1) {
|
|
306
|
-
const cancelIndex = c.indexOf(old, methodIndex);
|
|
307
|
-
if (cancelIndex !== -1) {
|
|
308
|
-
const newCode = [
|
|
309
|
-
'// LOCALHOST_SSL_BYPASS: 对 localhost/127.0.0.1 放行自签名证书(仅本地测试用)',
|
|
310
|
-
' if (failingUrl != null',
|
|
311
|
-
' && (failingUrl.startsWith("https://127.0.0.1") || ' +
|
|
312
|
-
' failingUrl.startsWith("https://localhost"))) {',
|
|
313
|
-
' handler.proceed();',
|
|
314
|
-
' return;',
|
|
315
|
-
' }',
|
|
316
|
-
' handler.cancel();',
|
|
317
|
-
].join('\n');
|
|
318
|
-
c = c.substring(0, cancelIndex) + newCode + c.substring(cancelIndex + old.length);
|
|
319
|
-
fs.writeFileSync(file, c, 'utf-8');
|
|
320
|
-
console.log(chalk.green(' ✓ 已应用 Android WebView SSL 补丁(允许 localhost 自签证书)'));
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
patchWebViewSslIos(userProjectDir); // iOS 分支
|
|
326
|
-
};
|
|
327
|
-
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import { run } from './utils.js';
|
|
4
|
+
import { clearVerified, optimizeNodeModules, SELF_HASH } from './optimize-node-modules.js';
|
|
5
|
+
import { createRequire } from 'module';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { resolveSdkPath } from './config.js';
|
|
8
|
+
import { createHash } from 'crypto';
|
|
9
|
+
const
|
|
10
|
+
/**
|
|
11
|
+
* 内部:从 libnode 的 node_version.h 读取运行时 Node 主版本号
|
|
12
|
+
* 读不到直接报错退出(说明 libnode 目录不完整,继续执行必失败)
|
|
13
|
+
*/
|
|
14
|
+
resolveRuntimeMajor = (userProjectDir, targetPlatform) => {
|
|
15
|
+
const sub = targetPlatform === 'ios' ? 'ios' : 'android',
|
|
16
|
+
header = path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native',
|
|
17
|
+
sub, 'libnode', 'include', 'node', 'node_version.h');
|
|
18
|
+
if (!fs.existsSync(header)) {
|
|
19
|
+
console.error(chalk.red(' [错误] 未找到 node_version.h: ' + header));
|
|
20
|
+
console.error(chalk.red(' 说明 libnode 目录不完整,请重新安装依赖;')), process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
const m = fs.readFileSync(header, 'utf-8').match(/#define\s+NODE_MAJOR_VERSION\s+(\d+)/);
|
|
23
|
+
if (!m) console.error(chalk.red(' [错误] 无法解析 node_version.h 中的 NODE_MAJOR_VERSION')), process.exit(1);
|
|
24
|
+
return m[1];
|
|
25
|
+
},
|
|
26
|
+
/**
|
|
27
|
+
* 内部:判断用户项目的生产依赖里是否含原生模块(binding.gyp)
|
|
28
|
+
* 按用户根 package.json 的 dependencies 键名,去 userProjectDir/node_modules 下逐个查
|
|
29
|
+
* 不用扫 nodejs-project/node_modules:全新构建时它还不存在
|
|
30
|
+
*/
|
|
31
|
+
hasNativeModules = userProjectDir => {
|
|
32
|
+
const pkgPath = path.join(userProjectDir, 'package.json');
|
|
33
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
34
|
+
let deps = {};
|
|
35
|
+
try {
|
|
36
|
+
deps = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')).dependencies || {};
|
|
37
|
+
} catch { return false; }
|
|
38
|
+
|
|
39
|
+
const nodeModulesDir = path.join(userProjectDir, 'node_modules');
|
|
40
|
+
for (const name of Object.keys(deps))
|
|
41
|
+
if (fs.existsSync(path.join(nodeModulesDir, name, 'binding.gyp'))) return true;
|
|
42
|
+
return false;
|
|
43
|
+
},
|
|
44
|
+
/**
|
|
45
|
+
* 内部:定位 @flun/nodejs-mobile-react-native 插件目录
|
|
46
|
+
*/
|
|
47
|
+
findPluginDir = userProjectDir => {
|
|
48
|
+
const candidates = [path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native')];
|
|
49
|
+
for (const c of candidates) if (fs.existsSync(path.join(c, 'android', 'build.gradle'))) return c;
|
|
50
|
+
return null;
|
|
51
|
+
},
|
|
52
|
+
ABI_MAP = {
|
|
53
|
+
'armeabi-v7a': { arch: 'arm', prefix: 'armv7a-linux-androideabi' },
|
|
54
|
+
'arm64-v8a': { arch: 'arm64', prefix: 'aarch64-linux-android' },
|
|
55
|
+
'x86_64': { arch: 'x64', prefix: 'x86_64-linux-android' }
|
|
56
|
+
},
|
|
57
|
+
resolveNdkVersion = buildDir => {
|
|
58
|
+
const gradle = path.join(buildDir, 'android', 'build.gradle');
|
|
59
|
+
if (!fs.existsSync(gradle))
|
|
60
|
+
console.error(chalk.red(' [错误] 未找到 android/build.gradle: ' + gradle)), process.exit(1);
|
|
61
|
+
const m = fs.readFileSync(gradle, 'utf-8').match(/ndkVersion\s*=\s*"([^"]+)"/);
|
|
62
|
+
if (!m) console.error(chalk.red(' [错误] 无法从 android/build.gradle 解析 ndkVersion')), process.exit(1);
|
|
63
|
+
return m[1];
|
|
64
|
+
},
|
|
65
|
+
resolveCompileSdk = buildDir => {
|
|
66
|
+
const gradle = path.join(buildDir, 'android', 'build.gradle'),
|
|
67
|
+
m = fs.readFileSync(gradle, 'utf-8').match(/minSdkVersion\s*=\s*(\d+)/);
|
|
68
|
+
return Math.max(m ? +m[1] : 24, 24);
|
|
69
|
+
},
|
|
70
|
+
findNativePackages = targetDir => {
|
|
71
|
+
const nm = path.join(targetDir, 'node_modules'), result = [];
|
|
72
|
+
if (!fs.existsSync(nm)) return result;
|
|
73
|
+
for (const entry of fs.readdirSync(nm)) {
|
|
74
|
+
if (entry.startsWith('.')) continue;
|
|
75
|
+
const full = path.join(nm, entry);
|
|
76
|
+
if (!fs.statSync(full).isDirectory()) continue;
|
|
77
|
+
if (entry.startsWith('@')) {
|
|
78
|
+
for (const sub of fs.readdirSync(full)) {
|
|
79
|
+
const pkgDir = path.join(full, sub);
|
|
80
|
+
if (fs.statSync(pkgDir).isDirectory()
|
|
81
|
+
&& fs.existsSync(path.join(pkgDir, 'binding.gyp'))) result.push(entry + '/' + sub);
|
|
82
|
+
}
|
|
83
|
+
} else if (fs.existsSync(path.join(full, 'binding.gyp'))) result.push(entry);
|
|
84
|
+
}
|
|
85
|
+
return result;
|
|
86
|
+
},
|
|
87
|
+
compileNativeModules = (targetDir, userProjectDir, config, buildDir, abiFilters) => {
|
|
88
|
+
const sdkPath = resolveSdkPath(config);
|
|
89
|
+
if (!sdkPath) console.error(chalk.red(' [错误] 未找到 Android SDK,无法编译原生模块')), process.exit(1);
|
|
90
|
+
const ndkVersion = resolveNdkVersion(buildDir), compileSdk = resolveCompileSdk(buildDir),
|
|
91
|
+
ndkPath = path.join(sdkPath, 'ndk', ndkVersion);
|
|
92
|
+
if (!fs.existsSync(ndkPath))
|
|
93
|
+
console.error(chalk.red(' [错误] 未找到 NDK ' + ndkVersion + ': ' + ndkPath)), process.exit(1);
|
|
94
|
+
const pluginDir = findPluginDir(userProjectDir);
|
|
95
|
+
if (!pluginDir) console.error(chalk.red(' [错误] 未找到 @flun/nodejs-mobile-react-native 插件')), process.exit(1);
|
|
96
|
+
const libnodeDir = path.join(pluginDir, 'android', 'libnode'),
|
|
97
|
+
gypPath = createRequire(path.join(pluginDir, 'package.json')).resolve('@flun/nodejs-mobile-gyp/bin/node-gyp.js'),
|
|
98
|
+
hostTag = { win32: 'windows-x86_64', darwin: 'darwin-x86_64', linux: 'linux-x86_64' }[process.platform],
|
|
99
|
+
hostOs = process.platform === 'darwin' ? 'mac' : 'linux',
|
|
100
|
+
toolSuffix = process.platform === 'win32' ? '.cmd' : '',
|
|
101
|
+
binSuffix = process.platform === 'win32' ? '.exe' : '';
|
|
102
|
+
if (!hostTag) console.error(chalk.red(' [错误] 不支持的操作系统: ' + process.platform)), process.exit(1);
|
|
103
|
+
const toolchain = path.join(ndkPath, 'toolchains', 'llvm', 'prebuilt', hostTag),
|
|
104
|
+
assetsDir = path.dirname(targetDir), gradleProjectDir = path.join(buildDir, 'android');
|
|
105
|
+
const patchScript = path.join(pluginDir, 'scripts', 'patch-package.js');
|
|
106
|
+
if (fs.existsSync(patchScript)) run(process.execPath, [patchScript, path.join(targetDir, 'node_modules')],
|
|
107
|
+
{ cwd: targetDir, inherit: true });
|
|
108
|
+
for (const abi of abiFilters) {
|
|
109
|
+
const map = ABI_MAP[abi];
|
|
110
|
+
if (!map) console.error(chalk.red(' [错误] 不支持的 ABI: ' + abi)), process.exit(1);
|
|
111
|
+
const compilerPrefix = map.prefix + compileSdk,
|
|
112
|
+
env = {
|
|
113
|
+
...process.env,
|
|
114
|
+
npm_config_node_engine: 'v8', npm_config_nodedir: libnodeDir + path.sep,
|
|
115
|
+
npm_config_node_gyp: gypPath, npm_config_arch: map.arch,
|
|
116
|
+
npm_config_platform: 'android', npm_config_format: 'make-android',
|
|
117
|
+
PROJECT_DIR: gradleProjectDir, NODEJS_MOBILE_GYP: gypPath,
|
|
118
|
+
TOOLCHAIN: toolchain,
|
|
119
|
+
AR: path.join(toolchain, 'bin', 'llvm-ar' + binSuffix),
|
|
120
|
+
CC: path.join(toolchain, 'bin', compilerPrefix + '-clang' + toolSuffix),
|
|
121
|
+
CXX: path.join(toolchain, 'bin', compilerPrefix + '-clang++' + toolSuffix),
|
|
122
|
+
LINK: path.join(toolchain, 'bin', compilerPrefix + '-clang++' + toolSuffix),
|
|
123
|
+
RANLIB: path.join(toolchain, 'bin', 'llvm-ranlib' + binSuffix),
|
|
124
|
+
GYP_DEFINES: 'target_arch=' + map.arch + ' v8_target_arch=' + map.arch + ' android_target_arch=' + map.arch +
|
|
125
|
+
' host_os=' + hostOs + ' OS=android android_ndk_path=' + ndkPath.replace(/\\/g, '/'),
|
|
126
|
+
PATH: [path.join(ndkPath, 'prebuilt', hostTag, 'bin'), path.join(toolchain, 'bin'),
|
|
127
|
+
path.join(targetDir, 'node_modules', '.bin'), process.env.PATH].filter(Boolean).join(path.delimiter)
|
|
128
|
+
};
|
|
129
|
+
console.log(chalk.cyan(' ℹ️ 编译原生模块 (' + abi + ')...'));
|
|
130
|
+
try {
|
|
131
|
+
run('npm', ['rebuild', '--build-from-source', '--no-audit', '--no-fund'], { cwd: targetDir, env, quiet: true });
|
|
132
|
+
}
|
|
133
|
+
catch (e) { console.error(chalk.red(' [错误] 原生模块编译失败 (' + abi + '): ' + e.message)), process.exit(1); }
|
|
134
|
+
const missing = findNativePackages(targetDir).filter(name => {
|
|
135
|
+
const pkgDir = path.join(targetDir, 'node_modules', name),
|
|
136
|
+
prebuildDir = path.join(pkgDir, 'prebuilds', 'android-' + map.arch),
|
|
137
|
+
hasPrebuild = fs.existsSync(prebuildDir) && fs.readdirSync(prebuildDir).some(f => f.endsWith('.node')),
|
|
138
|
+
releaseDir = path.join(pkgDir, 'build', 'Release'),
|
|
139
|
+
hasBuild = fs.existsSync(releaseDir) && fs.readdirSync(releaseDir).some(f => f.endsWith('.node'));
|
|
140
|
+
return !hasPrebuild && !hasBuild;
|
|
141
|
+
});
|
|
142
|
+
if (missing.length > 0) {
|
|
143
|
+
console.error(chalk.red(' [错误] 以下原生模块编译后缺少 .node (' + abi + '):'));
|
|
144
|
+
for (const n of missing) console.error(chalk.red(' - ' + n));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
fs.writeFileSync(path.join(assetsDir, '.native-built-' + abi), new Date().toISOString());
|
|
148
|
+
console.log(chalk.green(' ✓ 原生模块编译完成 (' + abi + ')'));
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
/**
|
|
152
|
+
* 内部:iOS 的 WebView SSL 补丁
|
|
153
|
+
*/
|
|
154
|
+
patchWebViewSslIos = userProjectDir => {
|
|
155
|
+
const file = path.join(userProjectDir, 'node_modules', 'react-native-webview', 'apple', 'RNCWebViewImpl.m');
|
|
156
|
+
if (!fs.existsSync(file)) return;
|
|
157
|
+
|
|
158
|
+
let c = fs.readFileSync(file, 'utf-8');
|
|
159
|
+
if (c.includes('LOCALHOST_SSL_BYPASS_IOS')) return console.log(chalk.green(' ✓ iOS WebView SSL 补丁已应用'));
|
|
160
|
+
|
|
161
|
+
// 在 "NSString* host = nil;" 那一段之后插入 localhost 放行
|
|
162
|
+
const anchor =
|
|
163
|
+
' if ([[challenge protectionSpace] authenticationMethod] == NSURLAuthenticationMethodClientCertificate) {',
|
|
164
|
+
idx = c.indexOf(anchor);
|
|
165
|
+
if (idx === -1) return console.warn(chalk.yellow(' ⚠️ RNCWebViewImpl.m 里未找到锚点,跳过 iOS SSL 补丁'));
|
|
166
|
+
|
|
167
|
+
const injected = [
|
|
168
|
+
' // LOCALHOST_SSL_BYPASS_IOS: 对 localhost/127.0.0.1 放行自签名证书(仅本地测试用)',
|
|
169
|
+
' if (host != nil && ([host isEqualToString:@"127.0.0.1"] || [host isEqualToString:@"localhost"])) {',
|
|
170
|
+
' NSURLCredential *cred = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];',
|
|
171
|
+
' completionHandler(NSURLSessionAuthChallengeUseCredential, cred);',
|
|
172
|
+
' return;',
|
|
173
|
+
' }',
|
|
174
|
+
'',
|
|
175
|
+
].join('\n');
|
|
176
|
+
|
|
177
|
+
c = c.substring(0, idx) + injected + c.substring(idx);
|
|
178
|
+
fs.writeFileSync(file, c, 'utf-8');
|
|
179
|
+
console.log(chalk.green(' ✓ 已应用 iOS WebView SSL 补丁(允许 localhost 自签证书)'));
|
|
180
|
+
},
|
|
181
|
+
/**
|
|
182
|
+
* 装用户项目的生产依赖到 <buildDir>/nodejs-assets/nodejs-project/
|
|
183
|
+
* 只装 dependencies,不装 devDependencies
|
|
184
|
+
* 依赖 + 环境未变化且上次构建完成时跳过,否则按需重装 / 仅优化
|
|
185
|
+
* 快照记录在 buildDir/.deps-snapshot.json(与桌面版一致)
|
|
186
|
+
*/
|
|
187
|
+
installUserDeps = async (targetDir, userProjectDir, config, buildDir, targetPlatform) => {
|
|
188
|
+
const assetsDir = path.dirname(targetDir), bnmFile = path.join(assetsDir, 'BUILD_NATIVE_MODULES.txt'),
|
|
189
|
+
nodeModulesDir = path.join(targetDir, 'node_modules'), snapshotPath = path.join(buildDir, '.deps-snapshot.json'),
|
|
190
|
+
forceRebuild = process.env.NODE_MOBILE_FORCE_REBUILD === '1', hasNative = hasNativeModules(userProjectDir),
|
|
191
|
+
optimizeCfg = config.optimize || {}, thisOptimize = optimizeCfg.enabled !== false,
|
|
192
|
+
abiFilters = [...(config.android.abiFilters || [])].sort(),
|
|
193
|
+
runtimeMajor = resolveRuntimeMajor(userProjectDir, targetPlatform);
|
|
194
|
+
|
|
195
|
+
fs.writeFileSync(bnmFile, hasNative ? '1' : '0');
|
|
196
|
+
if (hasNative) console.log(chalk.gray(' ℹ️ 检测到原生模块(binding.gyp)'));
|
|
197
|
+
|
|
198
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(targetDir, 'package.json'), 'utf-8')), deps = pkg.dependencies || {},
|
|
199
|
+
sortedDeps = Object.keys(deps).sort().reduce((acc, k) => { acc[k] = deps[k]; return acc; }, {}),
|
|
200
|
+
installerHash = createHash('sha256').update(fs.readFileSync(new URL(import.meta.url), 'utf-8'))
|
|
201
|
+
.digest('hex').slice(0, 16),
|
|
202
|
+
snapshotData = { deps: sortedDeps, type: pkg.type || '', hasNative, forceRebuild };
|
|
203
|
+
|
|
204
|
+
let shouldInstall = true, shouldOptimize = thisOptimize, reasons = [];
|
|
205
|
+
if (fs.existsSync(snapshotPath)) {
|
|
206
|
+
const old = JSON.parse(fs.readFileSync(snapshotPath, 'utf-8')), verifiedOk = old.verified === true,
|
|
207
|
+
depsSame = JSON.stringify({
|
|
208
|
+
deps: old.deps, type: old.type, hasNative: old.hasNative, forceRebuild: old.forceRebuild
|
|
209
|
+
}) === JSON.stringify(snapshotData),
|
|
210
|
+
envSame = old.platform === targetPlatform && JSON.stringify(old.abiFilters) === JSON.stringify(abiFilters) &&
|
|
211
|
+
old.runtimeMajor === runtimeMajor,
|
|
212
|
+
lastOptimized = old.optimized !== false, selfHashSame = old.optimizeHash === SELF_HASH,
|
|
213
|
+
installerSame = old.installerHash === installerHash, nmExists = fs.existsSync(nodeModulesDir);
|
|
214
|
+
if (!depsSame) reasons.push('依赖清单已变化');
|
|
215
|
+
if (!verifiedOk) reasons.push('上次构建未完成');
|
|
216
|
+
if (!envSame) reasons.push('运行环境已变化');
|
|
217
|
+
if (!nmExists) reasons.push('缓存目录缺失');
|
|
218
|
+
if (lastOptimized && !thisOptimize) reasons.push('优化已由开启转为关闭');
|
|
219
|
+
if (thisOptimize && !selfHashSame && lastOptimized) reasons.push('优化程序已更新');
|
|
220
|
+
if (!installerSame) reasons.push('安装器已更新');
|
|
221
|
+
if (reasons.length > 0) {
|
|
222
|
+
shouldInstall = true, shouldOptimize = thisOptimize;
|
|
223
|
+
console.log(chalk.gray(' ℹ️ ' + (thisOptimize ? '重新安装依赖+优化' : '重新安装依赖')
|
|
224
|
+
+ '(原因: ' + reasons.join(', ') + ')'));
|
|
225
|
+
}
|
|
226
|
+
else if (thisOptimize && !lastOptimized)
|
|
227
|
+
shouldInstall = false, shouldOptimize = true, console.log(chalk.gray(' ℹ️ 依赖与环境未变化,仅需优化'));
|
|
228
|
+
else {
|
|
229
|
+
shouldInstall = false, shouldOptimize = false, console.log(chalk.gray(' ✓ 依赖与环境未变化,跳过安装与优化'));
|
|
230
|
+
await clearVerified(buildDir);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
} else
|
|
234
|
+
console.log(chalk.gray(' ℹ️ ' + (thisOptimize ? '重新安装依赖+优化' : '重新安装依赖') + '(原因: 缓存快照缺失)'));
|
|
235
|
+
|
|
236
|
+
if (shouldInstall) {
|
|
237
|
+
if (fs.existsSync(nodeModulesDir))
|
|
238
|
+
console.log(chalk.cyan(' 清理旧 node_modules...')), fs.rmSync(nodeModulesDir, { recursive: true, force: true });
|
|
239
|
+
|
|
240
|
+
try { fs.rmSync(snapshotPath, { force: true }); } catch { }
|
|
241
|
+
const env = { ...process.env };
|
|
242
|
+
if (hasNative) env.NODEJS_MOBILE_BUILD_NATIVE_MODULES = '1';
|
|
243
|
+
console.log(chalk.cyan(' 安装用户生产依赖(nodejs-assets/nodejs-project)...'));
|
|
244
|
+
run('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], { cwd: targetDir, env, quiet: true });
|
|
245
|
+
const snap = {
|
|
246
|
+
...snapshotData, platform: targetPlatform, abiFilters, runtimeMajor,
|
|
247
|
+
optimizeHash: SELF_HASH, installerHash, optimized: false, verified: false
|
|
248
|
+
};
|
|
249
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(snap, null, 2));
|
|
250
|
+
console.log(chalk.green(' ✓ 用户依赖安装完成'));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (hasNative && targetPlatform === 'android' && shouldInstall)
|
|
254
|
+
compileNativeModules(targetDir, userProjectDir, config, buildDir, abiFilters);
|
|
255
|
+
if (shouldOptimize) {
|
|
256
|
+
console.log(chalk.cyan(' [信息] 正在执行优化...'));
|
|
257
|
+
try {
|
|
258
|
+
await optimizeNodeModules(targetDir, {
|
|
259
|
+
targetPlatform, abiFilters, runtimeMajor, exclude: optimizeCfg.exclude || [], snapshotDir: buildDir
|
|
260
|
+
});
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error(chalk.red(' [错误] node_modules 优化失败:' + err.message));
|
|
263
|
+
console.error(chalk.red(' [提示] 可在 mobileAppConfig.js 中设置 optimize: { enabled: false } 关闭优化,'));
|
|
264
|
+
console.error(chalk.red(' 或用 optimize: { exclude: ["包名"] } 排除问题包;')), process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
268
|
+
/**
|
|
269
|
+
* 修改 @flun/nodejs-mobile-react-native 插件 build.gradle 的 abiFilters
|
|
270
|
+
*/
|
|
271
|
+
patchNodejsMobilePlugin = (userProjectDir, config) => {
|
|
272
|
+
const pluginDir = findPluginDir(userProjectDir);
|
|
273
|
+
if (!pluginDir) return console.warn(chalk.yellow(' ⚠️ 未找到 @flun/nodejs-mobile-react-native 插件,跳过 patch'));
|
|
274
|
+
|
|
275
|
+
const file = path.join(pluginDir, 'android', 'build.gradle'),
|
|
276
|
+
abiList = config.android.abiFilters.map((x) => `"${x}"`).join(', ');
|
|
277
|
+
|
|
278
|
+
let c = fs.readFileSync(file, 'utf-8');
|
|
279
|
+
const before = c;
|
|
280
|
+
c = c.replace(/abiFilters\s*=\s*\[[^\]]*\]/, `abiFilters = [${abiList}]`);
|
|
281
|
+
|
|
282
|
+
if (c !== before) fs.writeFileSync(file, c, 'utf-8'),
|
|
283
|
+
console.log(chalk.green(` ✓ 已设置插件 abiFilters = [${abiList}]`));
|
|
284
|
+
},
|
|
285
|
+
/**
|
|
286
|
+
* 让 react-native-webview 对 localhost 自签名证书放行
|
|
287
|
+
* Android: patch RNCWebViewClient.java
|
|
288
|
+
* iOS: patch RNCWebViewImpl.m
|
|
289
|
+
* 仅在 config.webview.allowSelfSignedCert === true 时应用
|
|
290
|
+
* 每次 test/build 都检查(npm 重装 webview 后会覆盖补丁)
|
|
291
|
+
*/
|
|
292
|
+
patchWebViewSsl = (userProjectDir, config) => {
|
|
293
|
+
if (!config.webview || config.webview.allowSelfSignedCert !== true) return;
|
|
294
|
+
|
|
295
|
+
const file = path.join(
|
|
296
|
+
userProjectDir, 'node_modules', 'react-native-webview', 'android', 'src', 'main', 'java', 'com',
|
|
297
|
+
'reactnativecommunity', 'webview', 'RNCWebViewClient.java'
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (fs.existsSync(file)) {
|
|
301
|
+
let c = fs.readFileSync(file, 'utf-8');
|
|
302
|
+
if (c.includes('LOCALHOST_SSL_BYPASS')) console.log(chalk.green(' ✓ Android WebView SSL 补丁已应用'));
|
|
303
|
+
else {
|
|
304
|
+
const old = 'handler.cancel();', methodIndex = c.indexOf('onReceivedSslError');
|
|
305
|
+
if (methodIndex !== -1) {
|
|
306
|
+
const cancelIndex = c.indexOf(old, methodIndex);
|
|
307
|
+
if (cancelIndex !== -1) {
|
|
308
|
+
const newCode = [
|
|
309
|
+
'// LOCALHOST_SSL_BYPASS: 对 localhost/127.0.0.1 放行自签名证书(仅本地测试用)',
|
|
310
|
+
' if (failingUrl != null',
|
|
311
|
+
' && (failingUrl.startsWith("https://127.0.0.1") || ' +
|
|
312
|
+
' failingUrl.startsWith("https://localhost"))) {',
|
|
313
|
+
' handler.proceed();',
|
|
314
|
+
' return;',
|
|
315
|
+
' }',
|
|
316
|
+
' handler.cancel();',
|
|
317
|
+
].join('\n');
|
|
318
|
+
c = c.substring(0, cancelIndex) + newCode + c.substring(cancelIndex + old.length);
|
|
319
|
+
fs.writeFileSync(file, c, 'utf-8');
|
|
320
|
+
console.log(chalk.green(' ✓ 已应用 Android WebView SSL 补丁(允许 localhost 自签证书)'));
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
patchWebViewSslIos(userProjectDir); // iOS 分支
|
|
326
|
+
};
|
|
327
|
+
|
|
328
328
|
export { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl };
|