@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/src/install.js CHANGED
@@ -25,7 +25,7 @@ const
25
25
  },
26
26
  /**
27
27
  * 内部:判断用户项目的生产依赖里是否含原生模块(binding.gyp)
28
- * 按用户根 package.json 的 dependencies 键名,去 userProjectDir/node_modules 下逐个查
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')], { cwd: targetDir, inherit: true });
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'), path.join(targetDir, 'node_modules', '.bin'), process.env.PATH].filter(Boolean).join(path.delimiter)
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 里未找到锚点,跳过 iOS SSL 补丁'));
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,不装 devDependencies
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) compileNativeModules(targetDir, userProjectDir, config, buildDir, abiFilters);
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 插件,跳过 patch'));
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 { fs.statSync(p).isDirectory() ? fs.rmSync(p, { recursive: true, force: true }) : fs.unlinkSync(p), removed++; } catch { }
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 (nmDir) => {
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' || f.name.endsWith('.orig') || MID_RE.test(f.name)) try { fs.unlinkSync(full), deleted++; } catch { }
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 读取,无法改为 UTF-8
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(去重,始终含 INTERNET)
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
- // 不用占位符,因为 AGP release 构建在 targetSdk>=28 时会强制 false
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,加 signingConfigs.release 块,release buildType 用它
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 项均为必填;缺项时提前报错,避免 Gradle 报"password incorrect"误导排查
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 已配置,但以下字段为空:' + _missing.join(', ') + '\n' +
172
- ' 启用签名后,keystore / storePassword / keyAlias / keyPassword 四项均为必填。\n' +
173
- ' 如仅需本地测试(不签名上架),可注释掉整个 signing 块——模板默认用 debug keystore 签名。'
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 不存在,跳过签名配置: ' + keystoreAbs));
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 读取,非 ASCII 字符必须转 \uXXXX 转义,
191
- // 否则中文路径(如「桌面」)会被读成乱码,导致 keystore 找不到。
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,并让 buildTypes.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 密度,覆盖各 mipmap 目录下的 ic_launcher 与 ic_launcher_round
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(' ⚠️ 图标文件不存在,保持默认: ' + iconAbs));
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 个尺寸,写入 AppIcon.appiconset
342
+ * iOS 应用图标:把用户提供的 PNG 缩放到 9 个尺寸,写入 AppIcon.appiconset
343
343
  * 同时改写 Contents.json 加 filename 字段
344
- * 未配置或文件不存在时保持默认(无图标,Xcode 会用系统占位)
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 图标文件不存在,保持默认: ' + iconAbs));
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,给每项加 filename
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
+ };
@@ -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,请安装 Xcode 并运行 xcode-select --install');
14
+ if (r.status !== 0) throw new Error('未找到 xcodebuild,请安装 Xcode 并运行 xcode-select --install');
15
15
  },
16
16
  /**
17
- * 执行 pod install(若 Pods/ 已存在且 Podfile.lock 未变,cocoapods 会自动跳过)
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,供 xcodebuild 签名用
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 签名证书不存在,跳过: ' + certPath));
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,导入临时 keychain 并让 xcodebuild 使用
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,导出 .ipa
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(去扩展名),写进 exportOptions.plist
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
- * 递归复制,exclude 支持 'node_modules/'、'*.log'、'.git/'
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
- * 内容不同才写文件,避免不必要的 mtime 变化触发下游(Gradle/Metro)重编
40
- * 返回 true 表示写入了,false 表示内容相同跳过
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,参数交由 Node 转义。
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,返回 ChildProcess
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 不存在,回退到自动查找: ' + configured));
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 编译器,用于把 JS bundle 编成字节码)
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 App = () => {
22
- const systemTheme = useColorScheme(),
23
- [url, setUrl] = useState<string | null>(null), [failedReason, setFailedReason] = useState<string | null>(null),
24
- [showDetail, setShowDetail] = useState(false),
25
- styles = StyleSheet.create({
26
- fill: { flex: 1 },
27
- loading: { flex: 1, justifyContent: 'center', alignItems: 'center' },
28
- errorContainer: {
29
- flex: 1,
30
- justifyContent: 'center',
31
- alignItems: 'center',
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
- const listener = (msg: unknown) => {
50
- if (typeof msg !== 'string') return;
51
- if (msg.startsWith('Node started on ')) {
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
- NodeMobile.channel.addListener('message', listener);
62
- return () => NodeMobile.channel.removeListener('message', listener);
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
- if (failedReason) {
67
- return (
68
- <View style={styles.errorContainer}>
69
- <Text style={styles.errorTitle}>应用启动失败</Text>
70
- <Text style={styles.errorHint}>请稍后重试,或联系应用发布者</Text>
71
- <Text
72
- style={styles.detailToggle}
73
- onPress={() => setShowDetail((v) => !v)}
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
- <View style={styles.loading}>
89
- <ActivityIndicator size="large" />
90
- <Text>{runtime.loadingText}</Text>
91
- </View>
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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * React Native 应用入口
3
- * 挂载根组件 App(见 App.tsx)到 AppRegistry,
3
+ * 挂载根组件 App(见 App.tsx)到 AppRegistry,
4
4
  * 组件名取自 app.json 的 name(由 CLI 根据 appName 自动生成)
5
5
  *
6
6
  * 用户一般不需要修改这个文件
@@ -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