@flun/node-mobile-app 4.0.3 → 4.0.4

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/copy.js CHANGED
@@ -61,7 +61,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
61
61
  * 把 buildDir/package.json 的依赖版本同步为主包 dependencies 的版本
62
62
  * - 依赖名单来自 buildDir/package.json(模板声明需要哪些依赖)
63
63
  * - 版本号来自 pkgRoot/package.json 的 dependencies(单一数据源)
64
- * - 若主包缺少某个依赖,抛错中断(避免模板占位值被真正使用)
64
+ * - 若主包缺少某个依赖,抛错中断(避免模板占位值被真正使用)
65
65
  */
66
66
  syncTemplateDependencies = (pkgRoot, buildDir) => {
67
67
  const buildPkgPath = path.join(buildDir, 'package.json');
@@ -74,7 +74,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
74
74
  const mainDeps = mainPkg.dependencies || {}, missing = [];
75
75
  let depsUpdated = 0, needWrite = false;
76
76
 
77
- // 移除仅用于模板占位说明的 _comment 字段,避免带进最终 build 工程
77
+ // 移除仅用于模板占位说明的 _comment 字段,避免带进最终 build 工程
78
78
  if ('_comment' in buildPkg) delete buildPkg._comment, needWrite = true;
79
79
  for (const name of Object.keys(buildPkg.dependencies)) {
80
80
  const v = mainDeps[name];
@@ -91,27 +91,25 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
91
91
  throw new Error(
92
92
  'template/package.json 声明的以下依赖在主包 dependencies 中不存在:\n' +
93
93
  ' ' + missing.join(', ') + '\n' +
94
- ' 请在 @flun/node-mobile-app 的 package.json 中补上对应声明(版本号随意,\n' +
95
- ' 但必须存在,以便 syncTemplateDependencies 覆盖模板里的占位值)。'
94
+ ' 请在 @flun/node-mobile-app 的 package.json 中补上对应声明(版本号随意,\n' +
95
+ ' 但必须存在,以便 syncTemplateDependencies 覆盖模板里的占位值)。'
96
96
  );
97
97
  }
98
98
  },
99
99
  /**
100
- * 把主包 node_modules 里被 npm 嵌套(未提升到用户根)的依赖,按需链接到用户根 node_modules 下,
100
+ * 把主包 node_modules 里被 npm 嵌套(未提升到用户根)的依赖,按需链接到用户根 node_modules 下,
101
101
  * 让 RN CLI / Metro 从用户根也能解析到它们。
102
- * - 判据:从用户根视角解析不到,才建链接;能解析则跳过;
103
- * - 范围:遍历主包 node_modules 顶层全部条目(含 @scope/ 下每个子项),不列举任何包名;
104
- * - 落点:只在用户根 node_modules 缺失条目时建 symlink,不覆盖已有内容、不碰 package.json / package-lock.json;
105
- * - 建链接失败:中断报错,不静默。
102
+ * - 判据:从用户根视角解析不到,才建链接;能解析则跳过;
103
+ * - 范围:遍历主包 node_modules 顶层全部条目(含 @scope/ 下每个子项),不列举任何包名;
104
+ * - 落点:只在用户根 node_modules 缺失条目时建 symlink,不覆盖已有内容、不碰 package.json / package-lock.json;
105
+ * - 建链接失败:中断报错,不静默。
106
106
  */
107
107
  linkNestedDeps = (pkgRoot, buildDir) => {
108
108
  const pkgNm = path.join(pkgRoot, 'node_modules');
109
109
  if (!fs.existsSync(pkgNm)) return;
110
110
 
111
- const userProjectDir = path.dirname(buildDir),
112
- userNm = path.join(userProjectDir, 'node_modules'),
113
- req = createRequire(path.join(userProjectDir, 'index.js')),
114
- candidates = [];
111
+ const userProjectDir = path.dirname(buildDir), userNm = path.join(userProjectDir, 'node_modules'),
112
+ req = createRequire(path.join(userProjectDir, 'index.js')), candidates = [];
115
113
  for (const entry of fs.readdirSync(pkgNm, { withFileTypes: true })) {
116
114
  if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
117
115
  if (entry.name.startsWith('@')) {
@@ -123,7 +121,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
123
121
 
124
122
  let linked = 0;
125
123
  for (const name of candidates) {
126
- try { req.resolve(name); continue; } catch { /* 解析不到,进入兜底 */ }
124
+ try { req.resolve(name); continue; } catch { /* 解析不到,进入兜底 */ }
127
125
  const src = path.join(pkgNm, name);
128
126
  if (!fs.existsSync(src)) continue;
129
127
  const realSrc = fs.realpathSync(src), dst = path.join(userNm, name);
@@ -132,8 +130,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
132
130
  if (occupied) continue;
133
131
  ensureDir(path.dirname(dst));
134
132
  try {
135
- fs.symlinkSync(realSrc, dst, 'junction');
136
- linked++;
133
+ fs.symlinkSync(realSrc, dst, 'junction'), linked++;
137
134
  } catch (err) {
138
135
  throw new Error(
139
136
  '建立兜底链接失败:' + dst + ' -> ' + realSrc + '\n' +
@@ -146,9 +143,9 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
146
143
  },
147
144
  /**
148
145
  * 把 CLI 包里的 template/ 逐文件覆盖到 build 目录
149
- * - 只覆盖模板里存在的文件,不删除 buildDir 里已有的其他内容(保留 Gradle 增量缓存)
146
+ * - 只覆盖模板里存在的文件,不删除 buildDir 里已有的其他内容(保留 Gradle 增量缓存)
150
147
  * - 模板文件被改写时自动同步;新增文件自动添加
151
- * - 若模板里删除了文件,旧副本残留由 CLI 版本指纹触发全量重建处理
148
+ * - 若模板里删除了文件,旧副本残留由 CLI 版本指纹触发全量重建处理
152
149
  */
153
150
  copyTemplate = (pkgRoot, buildDir, config) => {
154
151
  ensureDir(buildDir);
@@ -166,7 +163,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
166
163
  },
167
164
  /**
168
165
  * 复制用户 Node 项目到 <buildDir>/nodejs-assets/nodejs-project
169
- * 复制后重写 package.json:只保留 dependencies,剔除 devDependencies;用 config 的 allowScripts
166
+ * 复制后重写 package.json:只保留 dependencies,剔除 devDependencies;用 config 的 allowScripts
170
167
  */
171
168
  copyUserProject = (userProjectDir, buildDir, config) => {
172
169
  const targetDir = path.join(buildDir, 'nodejs-assets', 'nodejs-project');
@@ -177,7 +174,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
177
174
  'mobileAppConfig.js', 'mobileAppConfig.mjs', 'mobileAppConfig.cjs'
178
175
  ];
179
176
 
180
- // 增量复制:内容变化的文件才覆盖,保留 node_modules 和其他未变文件
177
+ // 增量复制:内容变化的文件才覆盖,保留 node_modules 和其他未变文件
181
178
  copyDirIncremental(userProjectDir, targetDir, exclude);
182
179
  // 黑名单式过滤:只删 devDependencies 和 allowScripts,其余字段原样保留
183
180
  const userPkgPath = path.join(userProjectDir, 'package.json');
@@ -198,7 +195,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
198
195
  */
199
196
  writeLocalProperties = (buildDir, config) => {
200
197
  const sdkPath = resolveSdkPath(config);
201
- if (!sdkPath) return console.warn(chalk.yellow(' ⚠️ 未找到 Android SDK,请在 mobileAppConfig.js 里配置 android.sdkPath'));
198
+ if (!sdkPath) return console.warn(chalk.yellow(' ⚠️ 未找到 Android SDK,请在 mobileAppConfig.js 里配置 android.sdkPath'));
202
199
 
203
200
  const localProps = path.join(buildDir, 'android', 'local.properties'),
204
201
  escaped = sdkPath.replace(/\\/g, '\\\\').replace(/:/g, '\\:');
@@ -243,12 +240,12 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
243
240
  * 按 config.android.abiFilters 修改 buildDir 里的 app/build.gradle:
244
241
  * - defaultConfig.ndk.abiFilters
245
242
  * - gradle.properties 的 reactNativeArchitectures
246
- * 插件 build.gradle 读的是 project(":app").android.defaultConfig.ndk.abiFilters,
247
- * 所以必须改这里,改插件自身的 abiFilters 无效。
243
+ * 插件 build.gradle 读的是 project(":app").android.defaultConfig.ndk.abiFilters,
244
+ * 所以必须改这里,改插件自身的 abiFilters 无效。
248
245
  */
249
246
  patchAndroidAbi = (buildDir, config) => {
250
247
  const file = path.join(buildDir, 'android', 'app', 'build.gradle');
251
- if (!fs.existsSync(file)) return console.warn(chalk.yellow(' ⚠️ 未找到 app/build.gradle,跳过 ABI patch'));
248
+ if (!fs.existsSync(file)) return console.warn(chalk.yellow(' ⚠️ 未找到 app/build.gradle,跳过 ABI patch'));
252
249
 
253
250
  const abiList = (config.android.abiFilters || []).map((x) => `"${x}"`).join(', ');
254
251
  let c = fs.readFileSync(file, 'utf-8');
@@ -256,7 +253,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
256
253
 
257
254
  // defaultConfig 里的 ndk { abiFilters ... }
258
255
  c = c.replace(/(defaultConfig\s*\{[\s\S]*?ndk\s*\{\s*abiFilters\s+)("[^"]*"(?:\s*,\s*"[^"]*")*)/, `$1${abiList}`);
259
- // 兜底:没有 ndk {} 块时,插到 defaultConfig 里
256
+ // 兜底:没有 ndk {} 块时,插到 defaultConfig 里
260
257
  if (!/ndk\s*\{\s*abiFilters/.test(c))
261
258
  c = c.replace(/(defaultConfig\s*\{)/, `$1\n ndk {\n abiFilters ${abiList}\n }`);
262
259
  // 不 patch splits.abi.include:AGP 禁止它和 ndk.abiFilters 同时存在
@@ -289,8 +286,7 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
289
286
  for (const name of extras) {
290
287
  const pkgJson = path.join(userProjectDir, 'node_modules', name, 'package.json');
291
288
  if (!fs.existsSync(pkgJson)) {
292
- missing.push(name);
293
- continue;
289
+ missing.push(name); continue;
294
290
  }
295
291
  try {
296
292
  const v = JSON.parse(fs.readFileSync(pkgJson, 'utf-8')).version;
@@ -317,4 +313,4 @@ const BUILD_DIR_NAME = 'node-mobile-app-build',
317
313
  export {
318
314
  getBuildDir, copyTemplate, copyUserProject, writeLocalProperties, ensureUserEntry, ensureUserConfig, patchAndroidAbi,
319
315
  patchExtraRnDependencies
320
- };
316
+ };
@@ -35,7 +35,7 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
35
35
  for (const e of entries) {
36
36
  if (e.name === 'node_modules' || e.name === '.git') continue;
37
37
  const full = path.join(d, e.name), rel = path.relative(templateDir, full).replace(/\\/g, '/');
38
- if (e.isDirectory()) { h.update('D:' + rel + '|'); walk(full); }
38
+ if (e.isDirectory()) h.update('D:' + rel + '|'), walk(full);
39
39
  else if (e.isFile()) {
40
40
  h.update('F:' + rel + '|');
41
41
  try { h.update(fs.readFileSync(full)); } catch { h.update('err'); }
@@ -108,7 +108,7 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
108
108
  if (templateChanged) reasons.push('模板文件');
109
109
  if (depsChanged) reasons.push('用户依赖');
110
110
  if (appInfoChanged) reasons.push('应用信息');
111
- console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 Gradle 缓存、CMake 缓存与依赖缓存'));
111
+ console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 Gradle 缓存、CMake 缓存与依赖缓存'));
112
112
  clearGradleCaches(buildDir, userProjectDir);
113
113
  removeDir(path.join(userProjectDir, 'node_modules', '@flun', 'nodejs-mobile-react-native', 'android', '.cxx'));
114
114
  clearDepsCaches(buildDir);
@@ -117,13 +117,13 @@ const FINGERPRINT_FILE = '.build-fingerprint.json',
117
117
  const reasons = [], targets = [];
118
118
  if (depsChanged) reasons.push('用户依赖'), targets.push('依赖缓存', 'Gradle 缓存');
119
119
  if (appInfoChanged) reasons.push('应用信息'), targets.push('Gradle 缓存');
120
- console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 ' + [...new Set(targets)].join('、')));
120
+ console.log(chalk.yellow(' ⚠️ 检测到变化(' + reasons.join('、') + '),清理 ' + [...new Set(targets)].join('、')));
121
121
  clearGradleCaches(buildDir, userProjectDir);
122
122
  if (depsChanged) clearDepsCaches(buildDir);
123
123
  }
124
- else console.log(chalk.gray(' ✓ 缓存有效,跳过清理'));
124
+ else console.log(chalk.gray(' ✓ 缓存有效,跳过清理'));
125
125
 
126
126
  writeFp(fpFile, newFp);
127
127
  };
128
128
 
129
- export { computeFingerprint, checkAndResetCache };
129
+ export { computeFingerprint, checkAndResetCache };
package/src/generate.js CHANGED
@@ -4,7 +4,7 @@ import { writeIfChanged } from './utils.js';
4
4
  import chalk from 'chalk';
5
5
 
6
6
  /**
7
- * 生成 main.js(桥接代码,用户不需要手写)
7
+ * 生成 main.js(桥接代码,用户不需要手写)
8
8
  *
9
9
  * 上报机制(通过 rn-bridge channel 发消息给 App.tsx):
10
10
  * - 'Node started on <URL>' server.js 成功调用 listen
@@ -14,9 +14,8 @@ const generateMainJs = (targetDir, config, moduleType) => {
14
14
  const entry = config.serverPath.startsWith('./') ? config.serverPath : './' + config.serverPath,
15
15
  entryLabel = config.serverPath, hb = config.advanced.heartbeatInterval,
16
16
  body = `(async () => {
17
- const rn_bridge = require('rn-bridge');
18
- const net = require('net'), https = require('https'), USER_ENTRY = ${JSON.stringify(entry)},
19
- ENTRY_LABEL = ${JSON.stringify(entryLabel)}, START_TIMEOUT_MS = 5000;
17
+ const rn_bridge = require('rn-bridge'), net = require('net'), https = require('https'),
18
+ USER_ENTRY = ${JSON.stringify(entry)}, ENTRY_LABEL = ${JSON.stringify(entryLabel)}, START_TIMEOUT_MS = 5000;
20
19
 
21
20
  let resolved = false;
22
21
  function emitUrl(url) {
@@ -44,7 +43,7 @@ const generateMainJs = (targetDir, config, moduleType) => {
44
43
  emitFailure('未处理的 Promise rejection', String(reason));
45
44
  });
46
45
 
47
- // 拦截 net.Server.prototype.listen,捕获协议和真实端口
46
+ // 拦截 net.Server.prototype.listen,捕获协议和真实端口
48
47
  const origListen = net.Server.prototype.listen;
49
48
  net.Server.prototype.listen = function (...args) {
50
49
  const server = this;
@@ -77,9 +76,7 @@ const generateMainJs = (targetDir, config, moduleType) => {
77
76
 
78
77
  ${config.advanced.keepAlive === false ? '// keepAlive: false — 不发送心跳' : "setInterval(() => rn_bridge.channel.send('heartbeat'), " + hb + ");"}
79
78
 
80
- rn_bridge.channel.on('message', (msg) => {
81
- rn_bridge.channel.send('Echo: ' + msg);
82
- });
79
+ rn_bridge.channel.on('message', msg => rn_bridge.channel.send('Echo: ' + msg));
83
80
  })();`,
84
81
  header = moduleType === 'module'
85
82
  ? `import { createRequire } from 'module';\nconst require = createRequire(import.meta.url);\n\n` : '',
@@ -97,9 +94,9 @@ const generateMainJs = (targetDir, config, moduleType) => {
97
94
  mixedContentMode: config.webview.mixedContentMode,
98
95
  backgroundColor: config.webview.backgroundColor
99
96
  },
100
- }, content = `// 自动生成,请勿手改\nexport default ${JSON.stringify(data, null, 2)} as const;\n`,
97
+ }, content = `// 自动生成,请勿手改\nexport default ${JSON.stringify(data, null, 2)} as const;\n`,
101
98
  changed = writeIfChanged(path.join(buildDir, 'mobileApp.runtime.ts'), content);
102
99
  console.log(chalk.green(changed ? ' ✓ 已生成 mobileApp.runtime.ts' : ' ✓ mobileApp.runtime.ts 无变化'));
103
100
  };
104
101
 
105
- export { generateMainJs, generateRuntime }
102
+ export { generateMainJs, generateRuntime }
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'),