@flun/node-mobile-app 4.0.3 → 4.0.5
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" +347 -348
- package/CHANGELOG.md +9 -19
- package/bin/cli.js +5 -5
- package/mobileAppConfig.js +18 -18
- package/package.json +1 -1
- package/scripts/copy-files.js +1 -1
- package/src/commands/build.js +1 -1
- package/src/commands/test.js +21 -15
- package/src/config.js +8 -11
- package/src/copy.js +24 -28
- package/src/fingerprint.js +5 -5
- package/src/generate.js +7 -10
- package/src/install.js +13 -13
- package/src/optimize-node-modules.js +10 -8
- package/src/patcher.js +21 -21
- package/src/platform-ios.js +7 -7
- package/src/utils.js +13 -13
- package/template/App.tsx +83 -87
- package/template/index.js +1 -1
- package/template/metro.config.js +1 -3
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +444 -444
package/src/install.js
CHANGED
|
@@ -25,7 +25,7 @@ const
|
|
|
25
25
|
},
|
|
26
26
|
/**
|
|
27
27
|
* 内部:判断用户项目的生产依赖里是否含原生模块(binding.gyp)
|
|
28
|
-
* 按用户根 package.json 的 dependencies
|
|
28
|
+
* 按用户根 package.json 的 dependencies 键名,去 userProjectDir/node_modules 下逐个查
|
|
29
29
|
* 不用扫 nodejs-project/node_modules:全新构建时它还不存在
|
|
30
30
|
*/
|
|
31
31
|
hasNativeModules = userProjectDir => {
|
|
@@ -100,7 +100,8 @@ const
|
|
|
100
100
|
const toolchain = path.join(ndkPath, 'toolchains', 'llvm', 'prebuilt', hostTag),
|
|
101
101
|
assetsDir = path.dirname(targetDir), gradleProjectDir = path.join(buildDir, 'android');
|
|
102
102
|
const patchScript = path.join(pluginDir, 'scripts', 'patch-package.js');
|
|
103
|
-
if (fs.existsSync(patchScript)) run(process.execPath, [patchScript, path.join(targetDir, 'node_modules')],
|
|
103
|
+
if (fs.existsSync(patchScript)) run(process.execPath, [patchScript, path.join(targetDir, 'node_modules')],
|
|
104
|
+
{ cwd: targetDir, inherit: true });
|
|
104
105
|
for (const abi of abiFilters) {
|
|
105
106
|
const map = ABI_MAP[abi];
|
|
106
107
|
if (!map) console.error(chalk.red(' [错误] 不支持的 ABI: ' + abi)), process.exit(1);
|
|
@@ -119,7 +120,8 @@ const
|
|
|
119
120
|
RANLIB: path.join(toolchain, 'bin', 'llvm-ranlib' + binSuffix),
|
|
120
121
|
GYP_DEFINES: 'target_arch=' + map.arch + ' v8_target_arch=' + map.arch + ' android_target_arch=' + map.arch +
|
|
121
122
|
' host_os=' + hostOs + ' OS=android android_ndk_path=' + ndkPath.replace(/\\/g, '/'),
|
|
122
|
-
PATH: [path.join(ndkPath, 'prebuilt', hostTag, 'bin'), path.join(toolchain, 'bin'),
|
|
123
|
+
PATH: [path.join(ndkPath, 'prebuilt', hostTag, 'bin'), path.join(toolchain, 'bin'),
|
|
124
|
+
path.join(targetDir, 'node_modules', '.bin'), process.env.PATH].filter(Boolean).join(path.delimiter)
|
|
123
125
|
};
|
|
124
126
|
console.log(chalk.cyan(' ℹ️ 编译原生模块 (' + abi + ')...'));
|
|
125
127
|
try { run('npm', ['rebuild', '--build-from-source', '--no-audit', '--no-fund'], { cwd: targetDir, env, quiet: true }); }
|
|
@@ -154,7 +156,7 @@ const
|
|
|
154
156
|
// 在 "NSString* host = nil;" 那一段之后插入 localhost 放行
|
|
155
157
|
const anchor = ' if ([[challenge protectionSpace] authenticationMethod] == NSURLAuthenticationMethodClientCertificate) {',
|
|
156
158
|
idx = c.indexOf(anchor);
|
|
157
|
-
if (idx === -1) return console.warn(chalk.yellow(' ⚠️ RNCWebViewImpl.m
|
|
159
|
+
if (idx === -1) return console.warn(chalk.yellow(' ⚠️ RNCWebViewImpl.m 里未找到锚点,跳过 iOS SSL 补丁'));
|
|
158
160
|
|
|
159
161
|
const injected = [
|
|
160
162
|
' // LOCALHOST_SSL_BYPASS_IOS: 对 localhost/127.0.0.1 放行自签名证书(仅本地测试用)',
|
|
@@ -172,8 +174,8 @@ const
|
|
|
172
174
|
},
|
|
173
175
|
/**
|
|
174
176
|
* 装用户项目的生产依赖到 <buildDir>/nodejs-assets/nodejs-project/
|
|
175
|
-
* 只装 dependencies
|
|
176
|
-
* 依赖 +
|
|
177
|
+
* 只装 dependencies,不装 devDependencies
|
|
178
|
+
* 依赖 + 环境未变化且上次构建完成时跳过,否则按需重装 / 仅优化
|
|
177
179
|
* 快照记录在 buildDir/.deps-snapshot.json(与桌面版一致)
|
|
178
180
|
*/
|
|
179
181
|
installUserDeps = async (targetDir, userProjectDir, config, buildDir, targetPlatform) => {
|
|
@@ -201,8 +203,7 @@ const
|
|
|
201
203
|
envSame = old.platform === targetPlatform && JSON.stringify(old.abiFilters) === JSON.stringify(abiFilters) &&
|
|
202
204
|
old.runtimeMajor === runtimeMajor,
|
|
203
205
|
lastOptimized = old.optimized !== false, selfHashSame = old.optimizeHash === SELF_HASH,
|
|
204
|
-
installerSame = old.installerHash === installerHash,
|
|
205
|
-
nmExists = fs.existsSync(nodeModulesDir);
|
|
206
|
+
installerSame = old.installerHash === installerHash, nmExists = fs.existsSync(nodeModulesDir);
|
|
206
207
|
if (!depsSame) reasons.push('依赖清单已变化');
|
|
207
208
|
if (!verifiedOk) reasons.push('上次构建未完成');
|
|
208
209
|
if (!envSame) reasons.push('运行环境已变化');
|
|
@@ -240,8 +241,8 @@ const
|
|
|
240
241
|
console.log(chalk.green(' ✓ 用户依赖安装完成'));
|
|
241
242
|
}
|
|
242
243
|
|
|
243
|
-
if (hasNative && targetPlatform === 'android' && shouldInstall)
|
|
244
|
-
|
|
244
|
+
if (hasNative && targetPlatform === 'android' && shouldInstall)
|
|
245
|
+
compileNativeModules(targetDir, userProjectDir, config, buildDir, abiFilters);
|
|
245
246
|
if (shouldOptimize) {
|
|
246
247
|
console.log(chalk.cyan(' [信息] 正在执行优化...'));
|
|
247
248
|
try {
|
|
@@ -260,7 +261,7 @@ const
|
|
|
260
261
|
*/
|
|
261
262
|
patchNodejsMobilePlugin = (userProjectDir, config) => {
|
|
262
263
|
const pluginDir = findPluginDir(userProjectDir);
|
|
263
|
-
if (!pluginDir) return console.warn(chalk.yellow(' ⚠️ 未找到 @flun/nodejs-mobile-react-native
|
|
264
|
+
if (!pluginDir) return console.warn(chalk.yellow(' ⚠️ 未找到 @flun/nodejs-mobile-react-native 插件,跳过 patch'));
|
|
264
265
|
|
|
265
266
|
const file = path.join(pluginDir, 'android', 'build.gradle'),
|
|
266
267
|
abiList = config.android.abiFilters.map((x) => `"${x}"`).join(', ');
|
|
@@ -271,7 +272,6 @@ const
|
|
|
271
272
|
|
|
272
273
|
if (c !== before) fs.writeFileSync(file, c, 'utf-8'), console.log(chalk.green(` ✓ 已设置插件 abiFilters = [${abiList}]`));
|
|
273
274
|
},
|
|
274
|
-
|
|
275
275
|
/**
|
|
276
276
|
* 让 react-native-webview 对 localhost 自签名证书放行
|
|
277
277
|
* Android: patch RNCWebViewClient.java
|
|
@@ -314,4 +314,4 @@ const
|
|
|
314
314
|
patchWebViewSslIos(userProjectDir); // iOS 分支
|
|
315
315
|
};
|
|
316
316
|
|
|
317
|
-
export { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl };
|
|
317
|
+
export { installUserDeps, patchNodejsMobilePlugin, patchWebViewSsl };
|
|
@@ -464,15 +464,17 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
|
|
|
464
464
|
return null;
|
|
465
465
|
},
|
|
466
466
|
delFile = p => {
|
|
467
|
-
try {
|
|
467
|
+
try {
|
|
468
|
+
fs.statSync(p).isDirectory() ? fs.rmSync(p, { recursive: true, force: true }) : fs.unlinkSync(p), removed++;
|
|
469
|
+
} catch { }
|
|
468
470
|
},
|
|
469
471
|
processDir = d => {
|
|
470
472
|
let entries;
|
|
471
473
|
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
|
|
472
474
|
for (const e of entries) {
|
|
473
|
-
const full = path.join(d, e.name)
|
|
474
|
-
const expectedPrebuilds = targetPlatform === 'ios' ? ['darwin'] : ['android'],
|
|
475
|
+
const full = path.join(d, e.name), expectedPrebuilds = targetPlatform === 'ios' ? ['darwin'] : ['android'],
|
|
475
476
|
prebuildMatch = full.match(/[\\/]prebuilds[\\/](linux|darwin|win32|android)-/i);
|
|
477
|
+
|
|
476
478
|
if (prebuildMatch && !expectedPrebuilds.includes(prebuildMatch[1].toLowerCase())) { delFile(full); continue; }
|
|
477
479
|
if (e.isDirectory()) { processDir(full); continue; }
|
|
478
480
|
if (JS_RE.test(e.name)) continue;
|
|
@@ -496,7 +498,7 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
|
|
|
496
498
|
return removed;
|
|
497
499
|
},
|
|
498
500
|
// ==================== 清开发文件 ====================
|
|
499
|
-
pruneDevFiles = async
|
|
501
|
+
pruneDevFiles = async nmDir => {
|
|
500
502
|
let deleted = 0;
|
|
501
503
|
const PATTERNS = [
|
|
502
504
|
/(^|\/)tsconfig\.json$/,
|
|
@@ -521,8 +523,7 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
|
|
|
521
523
|
walk(full);
|
|
522
524
|
} else {
|
|
523
525
|
const rel = toPosix(path.relative(nmDir, full));
|
|
524
|
-
if (PATTERNS.some(re => re.test(rel)))
|
|
525
|
-
try { fs.unlinkSync(full), deleted++; } catch { }
|
|
526
|
+
if (PATTERNS.some(re => re.test(rel))) try { fs.unlinkSync(full), deleted++; } catch { }
|
|
526
527
|
}
|
|
527
528
|
}
|
|
528
529
|
};
|
|
@@ -536,7 +537,8 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
|
|
|
536
537
|
for (const f of fs.readdirSync(d, { withFileTypes: true })) {
|
|
537
538
|
const full = path.join(d, f.name);
|
|
538
539
|
if (f.isDirectory()) walk(full);
|
|
539
|
-
else if (full.includes(path.sep + 'obj.target' + path.sep) || f.name === 'Makefile' ||
|
|
540
|
+
else if (full.includes(path.sep + 'obj.target' + path.sep) || f.name === 'Makefile' ||
|
|
541
|
+
f.name.endsWith('.orig') || MID_RE.test(f.name)) try { fs.unlinkSync(full), deleted++; } catch { }
|
|
540
542
|
}
|
|
541
543
|
};
|
|
542
544
|
walk(nmDir);
|
|
@@ -702,4 +704,4 @@ const SELF_HASH = (() => {
|
|
|
702
704
|
} catch { return 'unknown'; }
|
|
703
705
|
})();
|
|
704
706
|
|
|
705
|
-
export { clearVerified, markVerified, optimizeNodeModules, SELF_HASH };
|
|
707
|
+
export { clearVerified, markVerified, optimizeNodeModules, SELF_HASH };
|
package/src/patcher.js
CHANGED
|
@@ -4,7 +4,7 @@ import { writeIfChanged } from './utils.js';
|
|
|
4
4
|
import chalk from 'chalk';
|
|
5
5
|
|
|
6
6
|
// 把字符串中所有非 ASCII 字符转成 \uXXXX(Java Properties 规范)
|
|
7
|
-
// gradle.properties 按 ISO-8859-1
|
|
7
|
+
// gradle.properties 按 ISO-8859-1 读取,无法改为 UTF-8
|
|
8
8
|
const escapeProps = s =>
|
|
9
9
|
String(s).replace(/[^\x00-\x7F]/g, ch => '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0')),
|
|
10
10
|
/**
|
|
@@ -97,7 +97,7 @@ const escapeProps = s =>
|
|
|
97
97
|
* 把 mobileAppConfig.js 里的 Android 编译配置写入 buildDir:
|
|
98
98
|
* - android/build.gradle 的 ext 块:minSdkVersion / targetSdkVersion / compileSdkVersion / buildToolsVersion
|
|
99
99
|
* - android/app/build.gradle 的 manifestPlaceholders:usesCleartextTraffic
|
|
100
|
-
* - AndroidManifest.xml:permissions
|
|
100
|
+
* - AndroidManifest.xml:permissions(去重,始终含 INTERNET)
|
|
101
101
|
*/
|
|
102
102
|
patchAndroidConfig = (buildDir, config) => {
|
|
103
103
|
const a = config.android || {}, rootGradle = path.join(buildDir, 'android', 'build.gradle'),
|
|
@@ -116,7 +116,7 @@ const escapeProps = s =>
|
|
|
116
116
|
writeIfChanged(rootGradle, c);
|
|
117
117
|
}
|
|
118
118
|
// 2. AndroidManifest.xml: 直接写死 usesCleartextTraffic + tools:replace
|
|
119
|
-
//
|
|
119
|
+
// 不用占位符,因为 AGP release 构建在 targetSdk>=28 时会强制 false
|
|
120
120
|
if (fs.existsSync(manifestFile)) {
|
|
121
121
|
let c = fs.readFileSync(manifestFile, 'utf-8');
|
|
122
122
|
const ct = a.usesCleartextTraffic !== false ? 'true' : 'false';
|
|
@@ -155,8 +155,8 @@ const escapeProps = s =>
|
|
|
155
155
|
},
|
|
156
156
|
/**
|
|
157
157
|
* 处理 Release 签名:
|
|
158
|
-
* - config.android.signing 有值 → 写 keystore 路径到 gradle.properties
|
|
159
|
-
* - 没值 → 不动(release 沿用 debug
|
|
158
|
+
* - config.android.signing 有值 → 写 keystore 路径到 gradle.properties,加 signingConfigs.release 块,release buildType 用它
|
|
159
|
+
* - 没值 → 不动(release 沿用 debug,可装但不可发布)
|
|
160
160
|
*
|
|
161
161
|
* keystore 路径以用户项目根为基准。
|
|
162
162
|
*/
|
|
@@ -164,18 +164,18 @@ const escapeProps = s =>
|
|
|
164
164
|
const signing = config.android && config.android.signing;
|
|
165
165
|
if (!signing || !signing.keystore) return; // 未配置签名 → 保持默认
|
|
166
166
|
|
|
167
|
-
// 启用签名后 4
|
|
167
|
+
// 启用签名后 4 项均为必填,缺项时提前报错,避免 Gradle 报"password incorrect"误导排查
|
|
168
168
|
const _missing = ['storePassword', 'keyAlias', 'keyPassword'].filter((k) => !signing[k]);
|
|
169
169
|
if (_missing.length > 0) {
|
|
170
170
|
throw new Error(
|
|
171
|
-
'android.signing.keystore
|
|
172
|
-
'
|
|
173
|
-
'
|
|
171
|
+
'android.signing.keystore 已配置,但以下字段为空:' + _missing.join(', ') + '\n' +
|
|
172
|
+
' 启用签名后,keystore / storePassword / keyAlias / keyPassword 四项均为必填。\n' +
|
|
173
|
+
' 如仅需本地测试(不签名上架),可注释掉整个 signing 块——模板默认用 debug keystore 签名。'
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
const keystoreAbs = path.resolve(userProjectDir, signing.keystore);
|
|
178
|
-
if (!fs.existsSync(keystoreAbs)) return console.warn(chalk.yellow(' ⚠️ 签名 keystore
|
|
178
|
+
if (!fs.existsSync(keystoreAbs)) return console.warn(chalk.yellow(' ⚠️ 签名 keystore 不存在,跳过签名配置: ' + keystoreAbs));
|
|
179
179
|
|
|
180
180
|
// 1. 写 gradle.properties(追加或覆盖 MYAPP_RELEASE_*)
|
|
181
181
|
const propsFile = path.join(buildDir, 'android', 'gradle.properties');
|
|
@@ -187,8 +187,8 @@ const escapeProps = s =>
|
|
|
187
187
|
c = c.replace(/^\s*MYAPP_RELEASE_KEY_ALIAS=.*\r?\n/gm, '');
|
|
188
188
|
c = c.replace(/^\s*MYAPP_RELEASE_KEY_PASSWORD=.*\r?\n/gm, '');
|
|
189
189
|
// 追加新的
|
|
190
|
-
// gradle.properties 按 ISO-8859-1
|
|
191
|
-
//
|
|
190
|
+
// gradle.properties 按 ISO-8859-1 读取,非 ASCII 字符必须转 \uXXXX 转义,
|
|
191
|
+
// 否则中文路径(如「桌面」)会被读成乱码,导致 keystore 找不到。
|
|
192
192
|
const esc = escapeProps(keystoreAbs.replace(/\\/g, '/'));
|
|
193
193
|
c += `\nMYAPP_RELEASE_STORE_FILE=${esc}\n`;
|
|
194
194
|
c += `MYAPP_RELEASE_STORE_PASSWORD=${escapeProps(signing.storePassword || '')}\n`;
|
|
@@ -197,12 +197,12 @@ const escapeProps = s =>
|
|
|
197
197
|
writeIfChanged(propsFile, c);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
// 2. app/build.gradle: 加 signingConfigs.release
|
|
200
|
+
// 2. app/build.gradle: 加 signingConfigs.release,并让 buildTypes.release 用它
|
|
201
201
|
const gradleFile = path.join(buildDir, 'android', 'app', 'build.gradle');
|
|
202
202
|
if (fs.existsSync(gradleFile)) {
|
|
203
203
|
let c = fs.readFileSync(gradleFile, 'utf-8');
|
|
204
204
|
|
|
205
|
-
// 加 release
|
|
205
|
+
// 加 release 签名块(先删旧的,避免重复)
|
|
206
206
|
c = c.replace(/\s*release\s*\{\s*storeFile[\s\S]*?\}\r?\n(\s*\}\s*\r?\n)/, '$1');
|
|
207
207
|
// 在 signingConfigs { 之后插入
|
|
208
208
|
c = c.replace(/(signingConfigs\s*\{)/,
|
|
@@ -224,7 +224,7 @@ const escapeProps = s =>
|
|
|
224
224
|
console.log(chalk.green(' ✓ Release 签名已配置: ' + path.basename(keystoreAbs)));
|
|
225
225
|
},
|
|
226
226
|
/**
|
|
227
|
-
* 应用图标:把用户提供的 PNG 缩放到 5 个 Android
|
|
227
|
+
* 应用图标:把用户提供的 PNG 缩放到 5 个 Android 密度,覆盖各 mipmap 目录下的 ic_launcher 与 ic_launcher_round
|
|
228
228
|
* 未配置或文件不存在时保持默认
|
|
229
229
|
*/
|
|
230
230
|
patchIcon = async (buildDir, userProjectDir, config) => {
|
|
@@ -232,7 +232,7 @@ const escapeProps = s =>
|
|
|
232
232
|
if (!iconPath) return;
|
|
233
233
|
|
|
234
234
|
const iconAbs = path.resolve(userProjectDir, iconPath);
|
|
235
|
-
if (!fs.existsSync(iconAbs)) return console.warn(chalk.yellow(' ⚠️
|
|
235
|
+
if (!fs.existsSync(iconAbs)) return console.warn(chalk.yellow(' ⚠️ 图标文件不存在,保持默认: ' + iconAbs));
|
|
236
236
|
|
|
237
237
|
const { Jimp } = await import('jimp'),
|
|
238
238
|
densities = [
|
|
@@ -339,16 +339,16 @@ const escapeProps = s =>
|
|
|
339
339
|
console.log(chalk.green(` ✓ iOS 配置:${appName} (${appId}) v${versionName}(${versionCode}), target ${deploymentTarget}`));
|
|
340
340
|
},
|
|
341
341
|
/**
|
|
342
|
-
* iOS 应用图标:把用户提供的 PNG 缩放到 9
|
|
342
|
+
* iOS 应用图标:把用户提供的 PNG 缩放到 9 个尺寸,写入 AppIcon.appiconset
|
|
343
343
|
* 同时改写 Contents.json 加 filename 字段
|
|
344
|
-
*
|
|
344
|
+
* 未配置或文件不存在时保持默认(无图标,Xcode 会用系统占位)
|
|
345
345
|
*/
|
|
346
346
|
patchIosIcon = async (buildDir, userProjectDir, config) => {
|
|
347
347
|
const iconPath = config.ios && config.ios.icon;
|
|
348
348
|
if (!iconPath) return;
|
|
349
349
|
|
|
350
350
|
const iconAbs = path.resolve(userProjectDir, iconPath);
|
|
351
|
-
if (!fs.existsSync(iconAbs)) return console.warn(chalk.yellow(' ⚠️ iOS
|
|
351
|
+
if (!fs.existsSync(iconAbs)) return console.warn(chalk.yellow(' ⚠️ iOS 图标文件不存在,保持默认: ' + iconAbs));
|
|
352
352
|
|
|
353
353
|
const iconDir = path.join(buildDir, 'ios', 'MyApp', 'Images.xcassets', 'AppIcon.appiconset');
|
|
354
354
|
if (!fs.existsSync(iconDir)) return;
|
|
@@ -378,7 +378,7 @@ const escapeProps = s =>
|
|
|
378
378
|
fs.writeFileSync(path.join(iconDir, v.name), buf);
|
|
379
379
|
}
|
|
380
380
|
|
|
381
|
-
// 改写 Contents.json
|
|
381
|
+
// 改写 Contents.json,给每项加 filename
|
|
382
382
|
writeIfChanged(contentsFile, JSON.stringify(contents, null, 2) + '\n');
|
|
383
383
|
console.log(chalk.green(` ✓ iOS 图标已应用: ${path.basename(iconAbs)}`));
|
|
384
384
|
}
|
|
@@ -386,4 +386,4 @@ const escapeProps = s =>
|
|
|
386
386
|
export {
|
|
387
387
|
resolveAppName, escapeKotlinString, escapeSwiftString, patchAppConfig, patchAndroidConfig, patchSigning,
|
|
388
388
|
patchIcon, patchIosConfig, patchIosIcon
|
|
389
|
-
};
|
|
389
|
+
};
|
package/src/platform-ios.js
CHANGED
|
@@ -11,16 +11,16 @@ const SCHEME = 'MyApp',
|
|
|
11
11
|
/** 检查 xcodebuild 是否存在 */
|
|
12
12
|
checkIosTools = () => {
|
|
13
13
|
const r = run('which', ['xcodebuild'], { stdio: 'pipe' });
|
|
14
|
-
if (r.status !== 0) throw new Error('未找到 xcodebuild
|
|
14
|
+
if (r.status !== 0) throw new Error('未找到 xcodebuild,请安装 Xcode 并运行 xcode-select --install');
|
|
15
15
|
},
|
|
16
16
|
/**
|
|
17
|
-
* 执行 pod install(若 Pods/ 已存在且 Podfile.lock
|
|
17
|
+
* 执行 pod install(若 Pods/ 已存在且 Podfile.lock 未变,cocoapods 会自动跳过)
|
|
18
18
|
*/
|
|
19
19
|
podInstall = iosDir => {
|
|
20
20
|
console.log(chalk.cyan(' pod install ...')), run('pod', ['install'], { cwd: iosDir, inherit: true });
|
|
21
21
|
},
|
|
22
22
|
/**
|
|
23
|
-
* 把 config.ios.signing.certificate 导入临时 keychain
|
|
23
|
+
* 把 config.ios.signing.certificate 导入临时 keychain,供 xcodebuild 签名用
|
|
24
24
|
* 返回 { keychainPath, keychainPassword } 或 null(未配置 / 文件缺失)
|
|
25
25
|
*/
|
|
26
26
|
setupSigningKeychain = (buildDir, userProjectDir, config) => {
|
|
@@ -29,7 +29,7 @@ const SCHEME = 'MyApp',
|
|
|
29
29
|
|
|
30
30
|
const certPath = path.resolve(userProjectDir, signing.certificate);
|
|
31
31
|
if (!fs.existsSync(certPath)) {
|
|
32
|
-
console.warn(chalk.yellow(' ⚠️ iOS
|
|
32
|
+
console.warn(chalk.yellow(' ⚠️ iOS 签名证书不存在,跳过: ' + certPath));
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -81,7 +81,7 @@ const SCHEME = 'MyApp',
|
|
|
81
81
|
|
|
82
82
|
if (teamId) args.push('DEVELOPMENT_TEAM=' + teamId);
|
|
83
83
|
|
|
84
|
-
// 若配置了 certificate
|
|
84
|
+
// 若配置了 certificate,导入临时 keychain 并让 xcodebuild 使用
|
|
85
85
|
const keychain = setupSigningKeychain(buildDir, userProjectDir, config);
|
|
86
86
|
if (keychain) args.push('OTHER_CODE_SIGN_FLAGS=--keychain ' + keychain.keychainPath);
|
|
87
87
|
|
|
@@ -89,7 +89,7 @@ const SCHEME = 'MyApp',
|
|
|
89
89
|
return archivePath;
|
|
90
90
|
},
|
|
91
91
|
/**
|
|
92
|
-
* xcodebuild -exportArchive
|
|
92
|
+
* xcodebuild -exportArchive,导出 .ipa
|
|
93
93
|
* 返回 .ipa 路径
|
|
94
94
|
*/
|
|
95
95
|
xcodebuildExport = (buildDir, config, release, outDir, userProjectDir) => {
|
|
@@ -97,7 +97,7 @@ const SCHEME = 'MyApp',
|
|
|
97
97
|
ensureDir(exportDir);
|
|
98
98
|
const archivePath = path.join(iosDir, 'build', SCHEME + '.xcarchive'), method = release ? 'ad-hoc' : 'development',
|
|
99
99
|
signing = (config.ios && config.ios.signing) || {}, teamId = signing.teamId || '',
|
|
100
|
-
// provisioningProfile 路径 → 取 basename
|
|
100
|
+
// provisioningProfile 路径 → 取 basename(去扩展名),写进 exportOptions.plist
|
|
101
101
|
profilePath = signing.provisioningProfile ? path.resolve(userProjectDir, signing.provisioningProfile) : '',
|
|
102
102
|
profileName = profilePath ? path.basename(profilePath).replace(/\.mobileprovision$/i, '') : '',
|
|
103
103
|
plistPath = path.join(iosDir, 'build', 'exportOptions.plist'),
|
package/src/utils.js
CHANGED
|
@@ -21,7 +21,7 @@ const ensureDir = dir => {
|
|
|
21
21
|
return false;
|
|
22
22
|
},
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* 递归复制,exclude 支持 'node_modules/'、'*.log'、'.git/'
|
|
25
25
|
*/
|
|
26
26
|
copyDir = (src, dest, exclude = []) => {
|
|
27
27
|
ensureDir(dest);
|
|
@@ -36,8 +36,8 @@ const ensureDir = dir => {
|
|
|
36
36
|
readJson = file => JSON.parse(fs.readFileSync(file, 'utf-8')),
|
|
37
37
|
writeJson = (file, obj) => fs.writeFileSync(file, JSON.stringify(obj, null, 2) + '\n', 'utf-8'),
|
|
38
38
|
/**
|
|
39
|
-
*
|
|
40
|
-
* 返回 true
|
|
39
|
+
* 内容不同才写文件,避免不必要的 mtime 变化触发下游(Gradle/Metro)重编
|
|
40
|
+
* 返回 true 表示写入了,false 表示内容相同跳过
|
|
41
41
|
*/
|
|
42
42
|
writeIfChanged = (file, content) => {
|
|
43
43
|
if (fs.existsSync(file)) {
|
|
@@ -49,7 +49,7 @@ const ensureDir = dir => {
|
|
|
49
49
|
return true;
|
|
50
50
|
},
|
|
51
51
|
/**
|
|
52
|
-
* 大小和 mtime
|
|
52
|
+
* 大小和 mtime 都一致才跳过复制,否则复制
|
|
53
53
|
* 用于用户项目 → nodejs-project 的增量复制
|
|
54
54
|
*/
|
|
55
55
|
copyFileIfChanged = (src, dst) => {
|
|
@@ -70,9 +70,9 @@ const ensureDir = dir => {
|
|
|
70
70
|
},
|
|
71
71
|
/**
|
|
72
72
|
* 统一的子进程启动:
|
|
73
|
-
* - Windows:.cmd / .bat 需经 shell
|
|
73
|
+
* - Windows:.cmd / .bat 需经 shell;把命令与参数拼成单字符串,
|
|
74
74
|
* 避免 spawnSync(bin, args, { shell: true }) 触发 DEP0190。
|
|
75
|
-
* - 其它平台:不用 shell
|
|
75
|
+
* - 其它平台:不用 shell,参数交由 Node 转义。
|
|
76
76
|
*/
|
|
77
77
|
spawnCmd = (bin, args, baseOpts = {}) => {
|
|
78
78
|
if (process.platform === 'win32') {
|
|
@@ -82,7 +82,7 @@ const ensureDir = dir => {
|
|
|
82
82
|
return spawnSync(bin, args, { ...baseOpts, shell: false });
|
|
83
83
|
},
|
|
84
84
|
/**
|
|
85
|
-
* 异步 spawn:参数同 spawnCmd
|
|
85
|
+
* 异步 spawn:参数同 spawnCmd,返回 ChildProcess
|
|
86
86
|
* @param {string} bin 命令
|
|
87
87
|
* @param {string[]} args 参数
|
|
88
88
|
* @param {object} [baseOpts] spawn 选项
|
|
@@ -96,7 +96,7 @@ const ensureDir = dir => {
|
|
|
96
96
|
return spawn(bin, args, { ...baseOpts, shell: false });
|
|
97
97
|
},
|
|
98
98
|
/**
|
|
99
|
-
* 执行命令:默认实时输出(inherit);quiet=true
|
|
99
|
+
* 执行命令:默认实时输出(inherit);quiet=true 时捕获输出,成功静默、失败才打印
|
|
100
100
|
*/
|
|
101
101
|
run = (bin, args, opts = {}) => {
|
|
102
102
|
const { quiet, ...restOpts } = opts;
|
|
@@ -124,7 +124,7 @@ const ensureDir = dir => {
|
|
|
124
124
|
return r;
|
|
125
125
|
},
|
|
126
126
|
/**
|
|
127
|
-
*
|
|
127
|
+
* 执行命令:实时输出,仅首行加缩进前缀,其余行原样
|
|
128
128
|
* @param {string} bin 命令
|
|
129
129
|
* @param {string[]} args 参数
|
|
130
130
|
* @param {object} [opts] spawn 选项 + prefix
|
|
@@ -155,7 +155,7 @@ const ensureDir = dir => {
|
|
|
155
155
|
const configured = config?.android?.adbPath;
|
|
156
156
|
if (configured) {
|
|
157
157
|
if (fs.existsSync(configured)) return configured;
|
|
158
|
-
console.warn(chalk.yellow('⚠️ config.android.adbPath
|
|
158
|
+
console.warn(chalk.yellow('⚠️ config.android.adbPath 不存在,回退到自动查找: ' + configured));
|
|
159
159
|
}
|
|
160
160
|
// 2. 环境变量 ANDROID_HOME / ANDROID_SDK_ROOT
|
|
161
161
|
for (const env of ['ANDROID_HOME', 'ANDROID_SDK_ROOT']) {
|
|
@@ -176,7 +176,7 @@ const ensureDir = dir => {
|
|
|
176
176
|
return 'adb';
|
|
177
177
|
},
|
|
178
178
|
/**
|
|
179
|
-
* 定位 hermesc(Hermes
|
|
179
|
+
* 定位 hermesc(Hermes 编译器,用于把 JS bundle 编成字节码)
|
|
180
180
|
*/
|
|
181
181
|
/**
|
|
182
182
|
* 读取安装状态标志(记录上次由本工具安装的包与变体)
|
|
@@ -221,7 +221,7 @@ const ensureDir = dir => {
|
|
|
221
221
|
ensureInstallable = (adb, appId, want, buildDir, serial) => {
|
|
222
222
|
const installed = getInstalledVariant(adb, appId, serial);
|
|
223
223
|
if (installed && installed !== want) {
|
|
224
|
-
console.log(chalk.yellow(` 已安装 ${installed} 包与待装 ${want}
|
|
224
|
+
console.log(chalk.yellow(` 已安装 ${installed} 包与待装 ${want} 包签名不同,先卸载`));
|
|
225
225
|
spawnCmd(adb, [...(serial ? ['-s', serial] : []), 'uninstall', appId], { encoding: 'utf-8' });
|
|
226
226
|
}
|
|
227
227
|
},
|
|
@@ -274,4 +274,4 @@ import chalk from 'chalk';
|
|
|
274
274
|
export {
|
|
275
275
|
ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged, spawnCmdAsync, run, runQuiet, runStream,
|
|
276
276
|
runSilent, findAdb, readInstallState, writeInstallState, getInstalledVariant, ensureInstallable, findHermesc, sanitizeFileName, buildEslintConfigSource
|
|
277
|
-
};
|
|
277
|
+
};
|
package/template/App.tsx
CHANGED
|
@@ -9,101 +9,97 @@ import runtime from './mobileApp.runtime';
|
|
|
9
9
|
// 'system' → 跟随系统(内部用默认浅/深色)
|
|
10
10
|
// { light: '#fff', dark: '#000' } → 按系统主题切换
|
|
11
11
|
const resolveBgColor = (bg: string | { light: string; dark: string }, theme: string | null | undefined): string => {
|
|
12
|
-
if (typeof bg === 'object' && bg !== null)
|
|
12
|
+
if (typeof bg === 'object' && bg !== null)
|
|
13
13
|
return (theme === 'dark' ? bg.dark : bg.light) || bg.light || '#ffffff';
|
|
14
|
-
|
|
15
|
-
if (bg === 'system') {
|
|
16
|
-
return theme === 'dark' ? '#121212' : '#ffffff';
|
|
17
|
-
}
|
|
14
|
+
if (bg === 'system') return theme === 'dark' ? '#121212' : '#ffffff';
|
|
18
15
|
return bg || '#ffffff';
|
|
19
|
-
}
|
|
16
|
+
},
|
|
17
|
+
App = () => {
|
|
18
|
+
const systemTheme = useColorScheme(),
|
|
19
|
+
[url, setUrl] = useState<string | null>(null), [failedReason, setFailedReason] = useState<string | null>(null),
|
|
20
|
+
[showDetail, setShowDetail] = useState(false),
|
|
21
|
+
styles = StyleSheet.create({
|
|
22
|
+
fill: { flex: 1 },
|
|
23
|
+
loading: { flex: 1, justifyContent: 'center', alignItems: 'center' },
|
|
24
|
+
errorContainer: {
|
|
25
|
+
flex: 1,
|
|
26
|
+
justifyContent: 'center',
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
padding: 24,
|
|
29
|
+
},
|
|
30
|
+
errorTitle: { fontSize: 20, fontWeight: '600', marginBottom: 16, color: '#c00' },
|
|
31
|
+
errorHint: { fontSize: 14, textAlign: 'center', color: '#888' },
|
|
32
|
+
detailToggle: { fontSize: 13, marginTop: 24, color: '#06c' },
|
|
33
|
+
detailText: {
|
|
34
|
+
fontSize: 12,
|
|
35
|
+
marginTop: 12,
|
|
36
|
+
paddingHorizontal: 16,
|
|
37
|
+
color: '#666',
|
|
38
|
+
fontFamily: 'monospace',
|
|
39
|
+
textAlign: 'center',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
NodeMobile.start('main.js');
|
|
20
44
|
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
padding: 24,
|
|
33
|
-
},
|
|
34
|
-
errorTitle: { fontSize: 20, fontWeight: '600', marginBottom: 16, color: '#c00' },
|
|
35
|
-
errorHint: { fontSize: 14, textAlign: 'center', color: '#888' },
|
|
36
|
-
detailToggle: { fontSize: 13, marginTop: 24, color: '#06c' },
|
|
37
|
-
detailText: {
|
|
38
|
-
fontSize: 12,
|
|
39
|
-
marginTop: 12,
|
|
40
|
-
paddingHorizontal: 16,
|
|
41
|
-
color: '#666',
|
|
42
|
-
fontFamily: 'monospace',
|
|
43
|
-
textAlign: 'center',
|
|
44
|
-
},
|
|
45
|
-
});
|
|
46
|
-
useEffect(() => {
|
|
47
|
-
NodeMobile.start('main.js');
|
|
45
|
+
const listener = (msg: unknown) => {
|
|
46
|
+
if (typeof msg !== 'string') return;
|
|
47
|
+
if (msg.startsWith('Node started on ')) {
|
|
48
|
+
const target = msg.slice('Node started on '.length).trim();
|
|
49
|
+
setFailedReason(null);
|
|
50
|
+
setUrl(target);
|
|
51
|
+
} else if (msg.startsWith('Node failed: ')) {
|
|
52
|
+
const reason = msg.slice('Node failed: '.length).trim();
|
|
53
|
+
setFailedReason(reason);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
48
56
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const target = msg.slice('Node started on '.length).trim();
|
|
53
|
-
setFailedReason(null);
|
|
54
|
-
setUrl(target);
|
|
55
|
-
} else if (msg.startsWith('Node failed: ')) {
|
|
56
|
-
const reason = msg.slice('Node failed: '.length).trim();
|
|
57
|
-
setFailedReason(reason);
|
|
58
|
-
}
|
|
59
|
-
};
|
|
57
|
+
NodeMobile.channel.addListener('message', listener);
|
|
58
|
+
return () => NodeMobile.channel.removeListener('message', listener);
|
|
59
|
+
}, []);
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
// 失败:显示错误页(用户看提示,开发者点开看详情)
|
|
62
|
+
if (failedReason) {
|
|
63
|
+
return (
|
|
64
|
+
<View style={styles.errorContainer}>
|
|
65
|
+
<Text style={styles.errorTitle}>应用启动失败</Text>
|
|
66
|
+
<Text style={styles.errorHint}>请稍后重试,或联系应用发布者</Text>
|
|
67
|
+
<Text
|
|
68
|
+
style={styles.detailToggle}
|
|
69
|
+
onPress={() => setShowDetail((v) => !v)}
|
|
70
|
+
>
|
|
71
|
+
{showDetail ? '▼ 收起详细信息' : '▶ 查看详细信息'}
|
|
72
|
+
</Text>
|
|
73
|
+
{showDetail && (
|
|
74
|
+
<Text style={styles.detailText}>{failedReason}</Text>
|
|
75
|
+
)}
|
|
76
|
+
</View>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
64
79
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
{showDetail ? '▼ 收起详细信息' : '▶ 查看详细信息'}
|
|
76
|
-
</Text>
|
|
77
|
-
{showDetail && (
|
|
78
|
-
<Text style={styles.detailText}>{failedReason}</Text>
|
|
79
|
-
)}
|
|
80
|
-
</View>
|
|
81
|
-
);
|
|
82
|
-
}
|
|
80
|
+
// 尚未就绪:loading
|
|
81
|
+
if (!url) {
|
|
82
|
+
if (!runtime.showLoading) return <View style={styles.fill} />;
|
|
83
|
+
return (
|
|
84
|
+
<View style={styles.loading}>
|
|
85
|
+
<ActivityIndicator size="large" />
|
|
86
|
+
<Text>{runtime.loadingText}</Text>
|
|
87
|
+
</View>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
83
90
|
|
|
84
|
-
// 尚未就绪:loading
|
|
85
|
-
if (!url) {
|
|
86
|
-
if (!runtime.showLoading) return <View style={styles.fill} />;
|
|
87
91
|
return (
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
+
<WebView
|
|
93
|
+
source={{ uri: url }}
|
|
94
|
+
style={[styles.fill, { backgroundColor: resolveBgColor(runtime.webview.backgroundColor, systemTheme) }]}
|
|
95
|
+
originWhitelist={['*']}
|
|
96
|
+
javaScriptEnabled={runtime.webview.enableJavaScript}
|
|
97
|
+
domStorageEnabled={runtime.webview.enableDomStorage}
|
|
98
|
+
allowFileAccess={runtime.webview.allowFileAccess}
|
|
99
|
+
allowUniversalAccessFromFileURLs={true}
|
|
100
|
+
mixedContentMode={runtime.webview.mixedContentMode}
|
|
101
|
+
/>
|
|
92
102
|
);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return (
|
|
96
|
-
<WebView
|
|
97
|
-
source={{ uri: url }}
|
|
98
|
-
style={[styles.fill, { backgroundColor: resolveBgColor(runtime.webview.backgroundColor, systemTheme) }]}
|
|
99
|
-
originWhitelist={['*']}
|
|
100
|
-
javaScriptEnabled={runtime.webview.enableJavaScript}
|
|
101
|
-
domStorageEnabled={runtime.webview.enableDomStorage}
|
|
102
|
-
allowFileAccess={runtime.webview.allowFileAccess}
|
|
103
|
-
allowUniversalAccessFromFileURLs={true}
|
|
104
|
-
mixedContentMode={runtime.webview.mixedContentMode}
|
|
105
|
-
/>
|
|
106
|
-
);
|
|
107
|
-
};
|
|
103
|
+
};
|
|
108
104
|
|
|
109
105
|
export { App };
|
package/template/index.js
CHANGED
package/template/metro.config.js
CHANGED
|
@@ -9,9 +9,7 @@ const exclusionList = exclusionListModule.default,
|
|
|
9
9
|
watchFolders: [__dirname, resolve(workspaceRoot, 'node_modules')],
|
|
10
10
|
resolver: {
|
|
11
11
|
nodeModulesPaths: [resolve(__dirname, 'node_modules'), resolve(workspaceRoot, 'node_modules')],
|
|
12
|
-
blockList: exclusionList([
|
|
13
|
-
/[/\\]nodejs-assets[/\\].*/
|
|
14
|
-
])
|
|
12
|
+
blockList: exclusionList([/[/\\]nodejs-assets[/\\].*/])
|
|
15
13
|
}
|
|
16
14
|
};
|
|
17
15
|
|