@flun/node-mobile-app 4.0.6 → 4.0.8

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.
Files changed (49) hide show
  1. package/Android /346/236/204/345/273/272/346/214/207/345/215/227.md" +84 -76
  2. package/CHANGELOG.md +10 -8
  3. package/LICENSE +9 -4
  4. package/README.md +170 -77
  5. package/bin/cli.js +9 -9
  6. package/cli-assets/server.js +7 -4
  7. package/mobileAppConfig.js +32 -32
  8. package/package.json +5 -5
  9. package/scripts/copy-files.js +1 -1
  10. package/src/commands/build.js +9 -4
  11. package/src/commands/clean.js +3 -2
  12. package/src/commands/test.js +13 -9
  13. package/src/config.js +3 -3
  14. package/src/copy.js +41 -36
  15. package/src/fingerprint.js +4 -3
  16. package/src/generate.js +5 -3
  17. package/src/install.js +38 -27
  18. package/src/optimize-node-modules.js +10 -5
  19. package/src/patcher.js +37 -32
  20. package/src/platform-ios.js +6 -6
  21. package/src/utils.js +26 -22
  22. package/template/App.tsx +4 -4
  23. package/template/Gemfile +1 -1
  24. package/template/android/app/build.gradle +1 -1
  25. package/template/android/app/proguard-rules.pro +1 -1
  26. package/template/android/app/src/main/java/com/myapp/MainActivity.kt +1 -1
  27. package/template/android/app/src/main/java/com/myapp/MainApplication.kt +1 -1
  28. package/template/android/app/src/main/res/drawable/rn_edit_text_material.xml +6 -3
  29. package/template/android/app/src/main/res/raw/keep.xml +1 -1
  30. package/template/android/app/src/main/res/values/strings.xml +1 -1
  31. package/template/android/app/src/main/res/values/styles.xml +1 -1
  32. package/template/android/build.gradle +1 -1
  33. package/template/android/gradle.properties +1 -1
  34. package/template/android/gradlew +3 -2
  35. package/template/android/gradlew.bat +3 -2
  36. package/template/android/settings.gradle +1 -1
  37. package/template/app.json +1 -1
  38. package/template/index.js +2 -2
  39. package/template/ios/.xcode.env +1 -1
  40. package/template/ios/MyApp/Images.xcassets/AppIcon.appiconset/Contents.json +1 -1
  41. package/template/ios/MyApp/Images.xcassets/Contents.json +1 -1
  42. package/template/ios/MyApp/Info.plist +1 -1
  43. package/template/ios/MyApp/LaunchScreen.storyboard +30 -12
  44. package/template/ios/MyApp/PrivacyInfo.xcprivacy +1 -1
  45. package/template/ios/MyApp.xcodeproj/project.pbxproj +34 -15
  46. package/template/ios/MyApp.xcodeproj/xcshareddata/xcschemes/MyApp.xcscheme +1 -1
  47. package/template/ios/Podfile +1 -1
  48. package/template/package.json +1 -1
  49. package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +131 -98
@@ -400,7 +400,8 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
400
400
  if (cnt > 0) cleared += cnt, pkgItems.push(`${item}(${cnt})`);
401
401
  }
402
402
  }
403
- if (process.env.OPTIMIZE_DEBUG && pkgItems.length > 0) console.log(chalk.gray(` ✓ ${pkgRel}: ${pkgItems.join(', ')}`));
403
+ if (process.env.OPTIMIZE_DEBUG && pkgItems.length > 0)
404
+ console.log(chalk.gray(` ✓ ${pkgRel}: ${pkgItems.join(', ')}`));
404
405
  }
405
406
  return cleared;
406
407
  },
@@ -465,17 +466,20 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
465
466
  },
466
467
  delFile = p => {
467
468
  try {
468
- fs.statSync(p).isDirectory() ? fs.rmSync(p, { recursive: true, force: true }) : fs.unlinkSync(p), removed++;
469
+ fs.statSync(p).isDirectory() ? fs.rmSync(p, { recursive: true, force: true })
470
+ : fs.unlinkSync(p), removed++;
469
471
  } catch { }
470
472
  },
471
473
  processDir = d => {
472
474
  let entries;
473
475
  try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { return; }
474
476
  for (const e of entries) {
475
- const full = path.join(d, e.name), expectedPrebuilds = targetPlatform === 'ios' ? ['darwin'] : ['android'],
477
+ const full = path.join(d, e.name),
478
+ expectedPrebuilds = targetPlatform === 'ios' ? ['darwin'] : ['android'],
476
479
  prebuildMatch = full.match(/[\\/]prebuilds[\\/](linux|darwin|win32|android)-/i);
477
480
 
478
- if (prebuildMatch && !expectedPrebuilds.includes(prebuildMatch[1].toLowerCase())) { delFile(full); continue; }
481
+ if (prebuildMatch && !expectedPrebuilds.includes(prebuildMatch[1].toLowerCase()))
482
+ { delFile(full); continue; }
479
483
  if (e.isDirectory()) { processDir(full); continue; }
480
484
  if (JS_RE.test(e.name)) continue;
481
485
  let plat = suffixPlat(e.name), arch = null;
@@ -532,7 +536,8 @@ const require = createRequire(import.meta.url), subpath = (...parts) => './' + p
532
536
  },
533
537
  pruneIntermediates = async nmDir => {
534
538
  let deleted = 0;
535
- const MID_RE = /\.(o|d|mk|stamp|Makefile|iobj|ipdb|pdb|exp|lib|obj|vcxproj|tlog|recipe|lastbuildstate|nativecodeanalysis\.xml)$/i,
539
+ const MID_RE =
540
+ /\.(o|d|mk|stamp|Makefile|iobj|ipdb|pdb|exp|lib|obj|vcxproj|tlog|recipe|lastbuildstate|nativecodeanalysis\.xml)$/i,
536
541
  walk = d => {
537
542
  for (const f of fs.readdirSync(d, { withFileTypes: true })) {
538
543
  const full = path.join(d, f.name);
package/src/patcher.js CHANGED
@@ -3,12 +3,12 @@ import fs from 'fs';
3
3
  import { writeIfChanged } from './utils.js';
4
4
  import chalk from 'chalk';
5
5
 
6
- // 把字符串中所有非 ASCII 字符转成 \uXXXX(Java Properties 规范)
6
+ // 把字符串中所有非 ASCII 字符转成 \uXXXX(Java Properties 规范)
7
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
  /**
11
- * 解析最终 appName:
11
+ * 解析最终 appName:
12
12
  * 1. config.appName
13
13
  * 2. 用户项目 package.json 的 name
14
14
  * 3. 'MyApp'
@@ -24,16 +24,16 @@ const escapeProps = s =>
24
24
  }
25
25
  return 'MyApp';
26
26
  },
27
- /** 通用字符串转义基础:\\, ", \n, \r, \t */
27
+ /** 通用字符串转义基础:\\, ", \n, \r, \t */
28
28
  escapeStringBase = s => String(s)
29
29
  .replace(/\\/g, '\\\\')
30
30
  .replace(/"/g, '\\"')
31
31
  .replace(/\n/g, '\\n')
32
32
  .replace(/\r/g, '\\r')
33
33
  .replace(/\t/g, '\\t'),
34
- /** Kotlin 字符串字面量转义:基础 + 转义 $ */
34
+ /** Kotlin 字符串字面量转义:基础 + 转义 $ */
35
35
  escapeKotlinString = s => escapeStringBase(s).replace(/\$/g, '\\$'),
36
- /** Swift 字符串字面量转义:基础 + 转义 \( */
36
+ /** Swift 字符串字面量转义:基础 + 转义 \( */
37
37
  escapeSwiftString = s => escapeStringBase(s).replace(/\(/g, '\\('),
38
38
  escapeXml = s => {
39
39
  return String(s)
@@ -91,13 +91,13 @@ const escapeProps = s =>
91
91
  writeIfChanged(f, c);
92
92
  }
93
93
 
94
- console.log(chalk.green(` ✓ 应用信息:${appName} (${appId}) v${versionName}(${versionCode})`));
94
+ console.log(chalk.green(` ✓ 应用信息:${appName} (${appId}) v${versionName}(${versionCode})`));
95
95
  },
96
96
  /**
97
- * 把 mobileAppConfig.js 里的 Android 编译配置写入 buildDir:
98
- * - android/build.gradle 的 ext 块:minSdkVersion / targetSdkVersion / compileSdkVersion / buildToolsVersion
99
- * - android/app/build.gradle 的 manifestPlaceholders:usesCleartextTraffic
100
- * - AndroidManifest.xml:permissions(去重,始终含 INTERNET)
97
+ * 把 mobileAppConfig.js 里的 Android 编译配置写入 buildDir:
98
+ * - android/build.gradle 的 ext 块:minSdkVersion / targetSdkVersion / compileSdkVersion / buildToolsVersion
99
+ * - android/app/build.gradle 的 manifestPlaceholders:usesCleartextTraffic
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'),
@@ -125,7 +125,8 @@ const escapeProps = s =>
125
125
  if (!c.includes('xmlns:tools=')) {
126
126
  c = c.replace(
127
127
  /<manifest\s+xmlns:android="[^"]*"/,
128
- '<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"'
128
+ '<manifest xmlns:android="http://schemas.android.com/apk/res/android" ' +
129
+ 'xmlns:tools="http://schemas.android.com/tools"'
129
130
  );
130
131
  }
131
132
  // 替换占位符值为实际值
@@ -150,15 +151,16 @@ const escapeProps = s =>
150
151
  writeIfChanged(manifestFile, c);
151
152
  }
152
153
 
153
- console.log(chalk.green(` ✓ Android 配置:SDK ${a.minSdkVersion}/${a.targetSdkVersion}/${a.compileSdkVersion},
154
+ console.log(chalk.green(` ✓ Android 配置:SDK ${a.minSdkVersion}/${a.targetSdkVersion}/${a.compileSdkVersion},
154
155
  权限 ${new Set(['android.permission.INTERNET', ...(a.permissions || [])]).size} 项`));
155
156
  },
156
157
  /**
157
- * 处理 Release 签名:
158
- * - config.android.signing 有值 → 写 keystore 路径到 gradle.properties,加 signingConfigs.release 块,release buildType 用它
159
- * - 没值 → 不动(release 沿用 debug,可装但不可发布)
158
+ * 处理 Release 签名:
159
+ * - config.android.signing 有值 → 写 keystore 路径到 gradle.properties,
160
+ * 加 signingConfigs.release 块,release buildType 用它
161
+ * - 没值 → 不动(release 沿用 debug,可装但不可发布)
160
162
  *
161
- * keystore 路径以用户项目根为基准。
163
+ * keystore 路径以用户项目根为基准;
162
164
  */
163
165
  patchSigning = (buildDir, userProjectDir, config) => {
164
166
  const signing = config.android && config.android.signing;
@@ -168,16 +170,17 @@ const escapeProps = s =>
168
170
  const _missing = ['storePassword', 'keyAlias', 'keyPassword'].filter((k) => !signing[k]);
169
171
  if (_missing.length > 0) {
170
172
  throw new Error(
171
- 'android.signing.keystore 已配置,但以下字段为空:' + _missing.join(', ') + '\n' +
172
- ' 启用签名后,keystore / storePassword / keyAlias / keyPassword 四项均为必填。\n' +
173
- ' 如仅需本地测试(不签名上架),可注释掉整个 signing 块——模板默认用 debug keystore 签名。'
173
+ 'android.signing.keystore 已配置,但以下字段为空:' + _missing.join(', ') + '\n' +
174
+ ' 启用签名后,keystore / storePassword / keyAlias / keyPassword 四项均为必填;\n' +
175
+ ' 如仅需本地测试(不签名上架),可注释掉整个 signing 块--模板默认用 debug keystore 签名;'
174
176
  );
175
177
  }
176
178
 
177
179
  const keystoreAbs = path.resolve(userProjectDir, signing.keystore);
178
- if (!fs.existsSync(keystoreAbs)) return console.warn(chalk.yellow(' ⚠️ 签名 keystore 不存在,跳过签名配置: ' + keystoreAbs));
180
+ if (!fs.existsSync(keystoreAbs))
181
+ return console.warn(chalk.yellow(' ⚠️ 签名 keystore 不存在,跳过签名配置: ' + keystoreAbs));
179
182
 
180
- // 1. 写 gradle.properties(追加或覆盖 MYAPP_RELEASE_*)
183
+ // 1. 写 gradle.properties(追加或覆盖 MYAPP_RELEASE_*)
181
184
  const propsFile = path.join(buildDir, 'android', 'gradle.properties');
182
185
  if (fs.existsSync(propsFile)) {
183
186
  let c = fs.readFileSync(propsFile, 'utf-8');
@@ -188,7 +191,7 @@ const escapeProps = s =>
188
191
  c = c.replace(/^\s*MYAPP_RELEASE_KEY_PASSWORD=.*\r?\n/gm, '');
189
192
  // 追加新的
190
193
  // gradle.properties 按 ISO-8859-1 读取,非 ASCII 字符必须转 \uXXXX 转义,
191
- // 否则中文路径(如「桌面」)会被读成乱码,导致 keystore 找不到。
194
+ // 否则中文路径(如"桌面")会被读成乱码,导致 keystore 找不到;
192
195
  const esc = escapeProps(keystoreAbs.replace(/\\/g, '/'));
193
196
  c += `\nMYAPP_RELEASE_STORE_FILE=${esc}\n`;
194
197
  c += `MYAPP_RELEASE_STORE_PASSWORD=${escapeProps(signing.storePassword || '')}\n`;
@@ -202,7 +205,7 @@ const escapeProps = s =>
202
205
  if (fs.existsSync(gradleFile)) {
203
206
  let c = fs.readFileSync(gradleFile, 'utf-8');
204
207
 
205
- // 加 release 签名块(先删旧的,避免重复)
208
+ // 加 release 签名块(先删旧的,避免重复)
206
209
  c = c.replace(/\s*release\s*\{\s*storeFile[\s\S]*?\}\r?\n(\s*\}\s*\r?\n)/, '$1');
207
210
  // 在 signingConfigs { 之后插入
208
211
  c = c.replace(/(signingConfigs\s*\{)/,
@@ -224,7 +227,7 @@ const escapeProps = s =>
224
227
  console.log(chalk.green(' ✓ Release 签名已配置: ' + path.basename(keystoreAbs)));
225
228
  },
226
229
  /**
227
- * 应用图标:把用户提供的 PNG 缩放到 5 个 Android 密度,覆盖各 mipmap 目录下的 ic_launcher 与 ic_launcher_round
230
+ * 应用图标:把用户提供的 PNG 缩放到 5 个 Android 密度,覆盖各 mipmap 目录下的 ic_launcher 与 ic_launcher_round
228
231
  * 未配置或文件不存在时保持默认
229
232
  */
230
233
  patchIcon = async (buildDir, userProjectDir, config) => {
@@ -258,10 +261,11 @@ const escapeProps = s =>
258
261
  console.log(chalk.green(` ✓ 图标已应用: ${path.basename(iconAbs)}`));
259
262
  },
260
263
  /**
261
- * 写入 iOS 工程配置:
264
+ * 写入 iOS 工程配置:
262
265
  * - Info.plist: CFBundleDisplayName
263
- * - project.pbxproj: PRODUCT_BUNDLE_IDENTIFIER / MARKETING_VERSION / CURRENT_PROJECT_VERSION / IPHONEOS_DEPLOYMENT_TARGET
264
- * 只在 buildDir 里存在 ios/ 时执行(其余平台跳过)。
266
+ * - project.pbxproj: PRODUCT_BUNDLE_IDENTIFIER /
267
+ * MARKETING_VERSION / CURRENT_PROJECT_VERSION / IPHONEOS_DEPLOYMENT_TARGET
268
+ * 只在 buildDir 里存在 ios/ 时执行(其余平台跳过);
265
269
  */
266
270
  patchIosConfig = (buildDir, userProjectDir, config) => {
267
271
  const iosDir = path.join(buildDir, 'ios');
@@ -271,7 +275,7 @@ const escapeProps = s =>
271
275
  versionCode = config.versionCode, deploymentTarget = (config.ios?.deploymentTarget) || '15.1',
272
276
  plist = path.join(iosDir, 'MyApp', 'Info.plist'), pbx = path.join(iosDir, 'MyApp.xcodeproj', 'project.pbxproj'),
273
277
  appDelegate = path.join(iosDir, 'MyApp', 'AppDelegate.swift');
274
- // 1. Info.plist:CFBundleDisplayName + ATS (对应 Android 的 usesCleartextTraffic)
278
+ // 1. Info.plist:CFBundleDisplayName + ATS (对应 Android 的 usesCleartextTraffic)
275
279
  if (fs.existsSync(plist)) {
276
280
  let c = fs.readFileSync(plist, 'utf-8');
277
281
 
@@ -336,12 +340,13 @@ const escapeProps = s =>
336
340
  writeIfChanged(appDelegate, c);
337
341
  }
338
342
 
339
- console.log(chalk.green(` ✓ iOS 配置:${appName} (${appId}) v${versionName}(${versionCode}), target ${deploymentTarget}`));
343
+ console.log(chalk.green(` ✓ iOS 配置:${appName} (${appId}) v${versionName}(${versionCode})` +
344
+ `, target ${deploymentTarget}`));
340
345
  },
341
346
  /**
342
- * iOS 应用图标:把用户提供的 PNG 缩放到 9 个尺寸,写入 AppIcon.appiconset
347
+ * iOS 应用图标:把用户提供的 PNG 缩放到 9 个尺寸,写入 AppIcon.appiconset
343
348
  * 同时改写 Contents.json 加 filename 字段
344
- * 未配置或文件不存在时保持默认(无图标,Xcode 会用系统占位)
349
+ * 未配置或文件不存在时保持默认(无图标,Xcode 会用系统占位)
345
350
  */
346
351
  patchIosIcon = async (buildDir, userProjectDir, config) => {
347
352
  const iconPath = config.ios && config.ios.icon;
@@ -354,7 +359,7 @@ const escapeProps = s =>
354
359
  if (!fs.existsSync(iconDir)) return;
355
360
 
356
361
  const { Jimp } = await import('jimp'), source = await Jimp.read(iconAbs),
357
- // Apple 规范:每项 (idiom, size, scale) 对应的像素尺寸与文件名
362
+ // Apple 规范:每项 (idiom, size, scale) 对应的像素尺寸与文件名
358
363
  variants = [
359
364
  { idiom: 'iphone', size: '20x20', scale: '2x', px: 40, name: 'Icon-App-20x20@2x.png' },
360
365
  { idiom: 'iphone', size: '20x20', scale: '3x', px: 60, name: 'Icon-App-20x20@3x.png' },
@@ -6,7 +6,7 @@ import chalk from 'chalk';
6
6
  const SCHEME = 'MyApp',
7
7
  /** iOS 相关命令只能在 macOS 上跑 */
8
8
  ensureMac = () => {
9
- if (process.platform !== 'darwin') throw new Error('iOS 构建只能在 macOS 上执行(当前平台: ' + process.platform + ')');
9
+ if (process.platform !== 'darwin') throw new Error('iOS 构建只能在 macOS 上执行(当前平台: ' + process.platform + ')');
10
10
  },
11
11
  /** 检查 xcodebuild 是否存在 */
12
12
  checkIosTools = () => {
@@ -14,14 +14,14 @@ const SCHEME = 'MyApp',
14
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
23
  * 把 config.ios.signing.certificate 导入临时 keychain,供 xcodebuild 签名用
24
- * 返回 { keychainPath, keychainPassword } 或 null(未配置 / 文件缺失)
24
+ * 返回 { keychainPath, keychainPassword } 或 null(未配置 / 文件缺失)
25
25
  */
26
26
  setupSigningKeychain = (buildDir, userProjectDir, config) => {
27
27
  const signing = config.ios && config.ios.signing;
@@ -37,7 +37,7 @@ const SCHEME = 'MyApp',
37
37
  ensureDir(keychainDir);
38
38
  const keychainPath = path.join(keychainDir, 'build.keychain-db'), keychainPassword = 'nmapp' + Date.now();
39
39
 
40
- // 清掉同名的旧 keychain(重复构建时)
40
+ // 清掉同名的旧 keychain(重复构建时)
41
41
  if (fs.existsSync(keychainPath)) fs.rmSync(keychainPath, { force: true });
42
42
  console.log(chalk.cyan(' 创建临时 keychain 并导入证书 ...'));
43
43
  run('security', ['create-keychain', '-p', keychainPassword, keychainPath], { quiet: true });
@@ -64,7 +64,7 @@ const SCHEME = 'MyApp',
64
64
  */
65
65
  xcodebuildArchive = (buildDir, config, release, userProjectDir) => {
66
66
  const iosDir = path.join(buildDir, 'ios'), workspace = path.join(iosDir, SCHEME + '.xcworkspace');
67
- if (!fs.existsSync(workspace)) throw new Error('未找到 ' + workspace + '(pod install 是否成功?)');
67
+ if (!fs.existsSync(workspace)) throw new Error('未找到 ' + workspace + '(pod install 是否成功?)');
68
68
 
69
69
  const archiveDir = path.join(iosDir, 'build');
70
70
  ensureDir(archiveDir);
@@ -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
@@ -36,7 +36,7 @@ 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)重编
39
+ * 内容不同才写文件,避免不必要的 mtime 变化触发下游(Gradle/Metro)重编
40
40
  * 返回 true 表示写入了,false 表示内容相同跳过
41
41
  */
42
42
  writeIfChanged = (file, content) => {
@@ -62,17 +62,17 @@ const ensureDir = dir => {
62
62
  fs.copyFileSync(src, dst);
63
63
  return true;
64
64
  },
65
- // 把单个参数转成适合 Windows cmd.exe 的形式:含空白或 shell 元字符时加双引号。
65
+ // 把单个参数转成适合 Windows cmd.exe 的形式:含空白或 shell 元字符时加双引号;
66
66
  quoteShellArg = (s) => {
67
67
  const str = String(s);
68
68
  if (str === '') return '""';
69
69
  return /[\s"&|<>^()]/.test(str) ? '"' + str.replace(/"/g, '""') + '"' : str;
70
70
  },
71
71
  /**
72
- * 统一的子进程启动:
73
- * - Windows:.cmd / .bat 需经 shell;把命令与参数拼成单字符串,
74
- * 避免 spawnSync(bin, args, { shell: true }) 触发 DEP0190。
75
- * - 其它平台:不用 shell,参数交由 Node 转义。
72
+ * 统一的子进程启动:
73
+ * - Windows:.cmd / .bat 需经 shell;把命令与参数拼成单字符串,
74
+ * 避免 spawnSync(bin, args, { shell: true }) 触发 DEP0190;
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
@@ -148,7 +148,7 @@ const ensureDir = dir => {
148
148
  child.on('close', code => code === 0 ? resolve() : reject(new Error(`${bin} ${args.join(' ')} 退出码 ${code}`)));
149
149
  }),
150
150
  /**
151
- * 定位 adb(配置 > 环境变量 > 默认路径 > PATH)
151
+ * 定位 adb(配置 > 环境变量 > 默认路径 > PATH)
152
152
  */
153
153
  findAdb = config => {
154
154
  // 1. 用户显式配置的路径优先
@@ -165,7 +165,7 @@ const ensureDir = dir => {
165
165
  if (fs.existsSync(p)) return p;
166
166
  }
167
167
  }
168
- // 3. 默认路径猜测(Windows / macOS / Linux)
168
+ // 3. 默认路径猜测(Windows / macOS / Linux)
169
169
  const home = process.env.USERPROFILE || process.env.HOME || '', localAppData = process.env.LOCALAPPDATA || '',
170
170
  candidates = [
171
171
  path.join(localAppData, 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
@@ -176,10 +176,10 @@ 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
+ * 读取安装状态标志(记录上次由本工具安装的包与变体)
183
183
  * @param {string} buildDir 构建目录
184
184
  * @returns {{appId:string, variant:'debug'|'release'}|null} 状态或 null
185
185
  */
@@ -189,19 +189,22 @@ const ensureDir = dir => {
189
189
  try { return JSON.parse(fs.readFileSync(f, 'utf-8')); } catch { return null; }
190
190
  },
191
191
  /**
192
- * 写入安装状态标志(安装成功后调用)
192
+ * 写入安装状态标志(安装成功后调用)
193
193
  * @param {string} buildDir 构建目录
194
194
  * @param {object} state 状态
195
195
  * @returns {void}
196
196
  */
197
197
  writeInstallState = (buildDir, state) => {
198
- try { ensureDir(buildDir), fs.writeFileSync(path.join(buildDir, '.install-state.json'), JSON.stringify(state, null, 2)); } catch { }
198
+ try {
199
+ ensureDir(buildDir),
200
+ fs.writeFileSync(path.join(buildDir, '.install-state.json'), JSON.stringify(state, null, 2));
201
+ } catch { }
199
202
  },
200
203
  /**
201
204
  * 查询设备上已安装包的 DEBUGGABLE 标志
202
205
  * @param {string} adb adb 路径
203
206
  * @param {string} appId 包名
204
- * @returns {'debug'|'release'|null} 变体或 null(未安装)
207
+ * @returns {'debug'|'release'|null} 变体或 null(未安装)
205
208
  */
206
209
  getInstalledVariant = (adb, appId, serial) => {
207
210
  const pre = serial ? ['-s', serial] : [],
@@ -210,12 +213,12 @@ const ensureDir = dir => {
210
213
  return /\bDEBUGGABLE\b/.test(r.stdout) ? 'debug' : 'release';
211
214
  },
212
215
  /**
213
- * 确保待装包可覆盖安装:标志不一致或变体不符时卸载已装包
216
+ * 确保待装包可覆盖安装:标志不一致或变体不符时卸载已装包
214
217
  * @param {string} adb adb 路径
215
218
  * @param {string} appId 包名
216
219
  * @param {'debug'|'release'} want 待装变体
217
220
  * @param {string} buildDir 构建目录
218
- * @param {string} [serial] 设备序列号(多设备时指定)
221
+ * @param {string} [serial] 设备序列号(多设备时指定)
219
222
  * @returns {void}
220
223
  */
221
224
  ensureInstallable = (adb, appId, want, buildDir, serial) => {
@@ -235,7 +238,7 @@ const ensureDir = dir => {
235
238
  return fs.existsSync(p) ? p : null;
236
239
  },
237
240
  /**
238
- * 文件名安全化:去除文件系统非法字符(/ \ : * ? " < > |)
241
+ * 文件名安全化:去除文件系统非法字符(/ \ : * ? " < > |)
239
242
  * 空结果回退 'app'
240
243
  */
241
244
  sanitizeFileName = s => {
@@ -243,7 +246,7 @@ const ensureDir = dir => {
243
246
  return cleaned || 'app';
244
247
  },
245
248
  /**
246
- * 生成 ESLint flat config 源码(替代 @react-native/eslint-config/flat)
249
+ * 生成 ESLint flat config 源码(替代 @react-native/eslint-config/flat)
247
250
  */
248
251
  buildEslintConfigSource = () => `import js from '@eslint/js';
249
252
  import tsParser from '@typescript-eslint/parser';
@@ -272,6 +275,7 @@ import chalk from 'chalk';
272
275
  rules: tsPlugin.configs.recommended.rules }, prettier];`;
273
276
 
274
277
  export {
275
- ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged, spawnCmdAsync, run, runQuiet, runStream,
276
- runSilent, findAdb, readInstallState, writeInstallState, getInstalledVariant, ensureInstallable, findHermesc, sanitizeFileName, buildEslintConfigSource
278
+ ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged, spawnCmdAsync, run,
279
+ runQuiet, runStream, runSilent, findAdb, readInstallState, writeInstallState, getInstalledVariant, ensureInstallable,
280
+ findHermesc, sanitizeFileName, buildEslintConfigSource
277
281
  };
package/template/App.tsx CHANGED
@@ -4,9 +4,9 @@ import { WebView } from 'react-native-webview';
4
4
  import * as NodeMobile from '@flun/nodejs-mobile-react-native';
5
5
  import runtime from './mobileApp.runtime';
6
6
 
7
- // 背景色解析:支持以下三种配置
7
+ // 背景色解析:支持以下三种配置
8
8
  // '#ffffff' / 'white' 等字符串 → 固定色
9
- // 'system' → 跟随系统(内部用默认浅/深色)
9
+ // 'system' → 跟随系统(内部用默认浅/深色)
10
10
  // { light: '#fff', dark: '#000' } → 按系统主题切换
11
11
  const resolveBgColor = (bg: string | { light: string; dark: string }, theme: string | null | undefined): string => {
12
12
  if (typeof bg === 'object' && bg !== null)
@@ -58,7 +58,7 @@ const resolveBgColor = (bg: string | { light: string; dark: string }, theme: str
58
58
  return () => NodeMobile.channel.removeListener('message', listener);
59
59
  }, []);
60
60
 
61
- // 失败:显示错误页(用户看提示,开发者点开看详情)
61
+ // 失败:显示错误页(用户看提示,开发者点开看详情)
62
62
  if (failedReason) {
63
63
  return (
64
64
  <View style={styles.errorContainer}>
@@ -77,7 +77,7 @@ const resolveBgColor = (bg: string | { light: string; dark: string }, theme: str
77
77
  );
78
78
  }
79
79
 
80
- // 尚未就绪:loading
80
+ // 尚未就绪:loading
81
81
  if (!url) {
82
82
  if (!runtime.showLoading) return <View style={styles.fill} />;
83
83
  return (
package/template/Gemfile CHANGED
@@ -14,4 +14,4 @@ gem 'bigdecimal'
14
14
  gem 'logger'
15
15
  gem 'benchmark'
16
16
  gem 'mutex_m'
17
- gem 'nkf'
17
+ gem 'nkf'
@@ -124,4 +124,4 @@ dependencies {
124
124
  } else {
125
125
  implementation jscFlavor
126
126
  }
127
- }
127
+ }
@@ -7,4 +7,4 @@
7
7
  # For more details, see
8
8
  # http://developer.android.com/guide/developing/tools/proguard.html
9
9
 
10
- # Add any project specific keep options here:
10
+ # Add any project specific keep options here:
@@ -19,4 +19,4 @@ class MainActivity : ReactActivity() {
19
19
  */
20
20
  override fun createReactActivityDelegate(): ReactActivityDelegate =
21
21
  DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
22
- }
22
+ }
@@ -24,4 +24,4 @@ class MainApplication : Application(), ReactApplication {
24
24
  super.onCreate()
25
25
  loadReactNative(this)
26
26
  }
27
- }
27
+ }
@@ -24,9 +24,12 @@
24
24
  <!--
25
25
  This file is a copy of abc_edit_text_material (https://bit.ly/3k8fX7I).
26
26
  The item below with state_pressed="false" and state_focused="false" causes a NullPointerException.
27
- NullPointerException:tempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(android.content.res.Resources)'
27
+ NullPointerException:tempt to invoke virtual method
28
+ 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable(
29
+ android.content.res.Resources)'
28
30
 
29
- <item android:state_pressed="false" android:state_focused="false" android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
31
+ <item android:state_pressed="false" android:state_focused="false"
32
+ android:drawable="@drawable/abc_textfield_default_mtrl_alpha"/>
30
33
 
31
34
  For more info, see https://bit.ly/3CdLStv (react-native/pull/29452) and https://bit.ly/3nxOMoR.
32
35
  -->
@@ -34,4 +37,4 @@
34
37
  <item android:drawable="@drawable/abc_textfield_activated_mtrl_alpha"/>
35
38
  </selector>
36
39
 
37
- </inset>
40
+ </inset>
@@ -1 +1 @@
1
- <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@drawable/node_modules_reactnative_libraries_logbox_ui_logboximages_close,@drawable/node_modules_reactnative_libraries_logbox_ui_logboximages_alerttriangle,@drawable/node_modules_reactnative_libraries_logbox_ui_logboximages_loader,@drawable/node_modules_reactnative_libraries_logbox_ui_logboximages_chevronleft,@drawable/node_modules_reactnative_libraries_logbox_ui_logboximages_chevronright" />
1
+ <resources xmlns:tools="http://schemas.android.com/tools" tools:keep="@drawable/node_modules_reactnative_libraries_logbox_ui_logboximages_close,@drawable/node_modules_reactnative_libraries_logbox_ui_logboximages_alerttriangle,@drawable/node_modules_reactnative_libraries_logbox_ui_logboximages_loader,@drawable/node_modules_reactnative_libraries_logbox_ui_logboximages_chevronleft,@drawable/node_modules_reactnative_libraries_logbox_ui_logboximages_chevronright" />
@@ -1,3 +1,3 @@
1
1
  <resources>
2
2
  <string name="app_name">MyApp</string>
3
- </resources>
3
+ </resources>
@@ -6,4 +6,4 @@
6
6
  <item name="android:editTextBackground">@drawable/rn_edit_text_material</item>
7
7
  </style>
8
8
 
9
- </resources>
9
+ </resources>
@@ -20,4 +20,4 @@ buildscript {
20
20
  }
21
21
  }
22
22
 
23
- apply plugin: "com.facebook.react.rootproject"
23
+ apply plugin: "com.facebook.react.rootproject"
@@ -48,4 +48,4 @@ edgeToEdgeEnabled=true
48
48
  android.builtInKotlin=false
49
49
  android.newDsl=false
50
50
  # 允许非 ASCII 路径(如中文路径)构建;AGP 默认拒绝
51
- android.overridePathCheck=true
51
+ android.overridePathCheck=true
@@ -57,7 +57,8 @@
57
57
  # Darwin, MinGW, and NonStop.
58
58
  #
59
59
  # (3) This script is generated from the Groovy template
60
- # https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
60
+ # https://github.com/gradle/gradle/blob/2d6327017519d23b96af35865dc997fcb544fb40/
61
+ # platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61
62
  # within the Gradle project.
62
63
  #
63
64
  # You can find Gradle at https://github.com/gradle/gradle/.
@@ -245,4 +246,4 @@ eval "set -- $(
245
246
  tr '\n' ' '
246
247
  )" '"$@"'
247
248
 
248
- exec "$JAVACMD" "$@"
249
+ exec "$JAVACMD" "$@"
@@ -78,7 +78,8 @@ goto fail
78
78
 
79
79
 
80
80
  @rem Execute Gradle
81
- "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
81
+ "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% ^
82
+ "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
82
83
 
83
84
  :end
84
85
  @rem End local scope for the variables with windows NT shell
@@ -95,4 +96,4 @@ exit /b %EXIT_CODE%
95
96
  :mainEnd
96
97
  if "%OS%"=="Windows_NT" endlocal
97
98
 
98
- :omega
99
+ :omega
@@ -12,4 +12,4 @@ plugins { id("com.facebook.react.settings") }
12
12
  extensions.configure(com.facebook.react.ReactSettingsExtension){ ex -> ex.autolinkLibrariesFromCommand() }
13
13
  rootProject.name = 'MyApp'
14
14
  include ':app'
15
- includeBuild('../../node_modules/@react-native/gradle-plugin')
15
+ includeBuild('../../node_modules/@react-native/gradle-plugin')
package/template/app.json CHANGED
@@ -1,4 +1,4 @@
1
1
  {
2
2
  "name": "MyApp",
3
3
  "displayName": "MyApp"
4
- }
4
+ }
package/template/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * React Native 应用入口
3
- * 挂载根组件 App(见 App.tsx)到 AppRegistry,
4
- * 组件名取自 app.json 的 name(由 CLI 根据 appName 自动生成)
3
+ * 挂载根组件 App(见 App.tsx)到 AppRegistry,
4
+ * 组件名取自 app.json 的 name(由 CLI 根据 appName 自动生成)
5
5
  *
6
6
  * 用户一般不需要修改这个文件
7
7
  */
@@ -8,4 +8,4 @@
8
8
  # Customize the NODE_BINARY variable here.
9
9
  # For example, to use nvm with brew, add the following line
10
10
  # . "$(brew --prefix nvm)/nvm.sh" --no-use
11
- export NODE_BINARY=$(command -v node)
11
+ export NODE_BINARY=$(command -v node)
@@ -50,4 +50,4 @@
50
50
  "author" : "xcode",
51
51
  "version" : 1
52
52
  }
53
- }
53
+ }
@@ -3,4 +3,4 @@
3
3
  "version" : 1,
4
4
  "author" : "xcode"
5
5
  }
6
- }
6
+ }