@flun/node-mobile-app 4.1.1 → 4.2.0

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/utils.js CHANGED
@@ -1,73 +1,14 @@
1
+ import chalk from 'chalk';
2
+ import { spawn, spawnSync } from 'child_process';
1
3
  import fs from 'fs';
2
4
  import path from 'path';
3
- import { spawnSync, spawn } from 'child_process';
4
- import chalk from 'chalk';
5
5
 
6
- const ensureDir = dir => {
7
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
6
+ // 把单个参数转成适合 Windows cmd.exe 的形式:含空白或 shell 元字符时加双引号;
7
+ const quoteShellArg = (s) => {
8
+ const str = String(s);
9
+ if (str === '') return '""';
10
+ return /[\s"&|<>^()]/.test(str) ? '"' + str.replace(/"/g, '""') + '"' : str;
8
11
  },
9
- removeDir = dir => {
10
- if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
11
- },
12
- isExcluded = (name, patterns) => {
13
- for (let p of patterns) {
14
- p = p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '');
15
- if (p === name) return true;
16
- if (p.includes('*')) {
17
- const escaped = p.replace(/[.+?^${}()|[\]\\/]/g, '\\$&').replace(/\*/g, '.*');
18
- if (new RegExp('^' + escaped + '$').test(name)) return true;
19
- }
20
- }
21
- return false;
22
- },
23
- /**
24
- * 递归复制,exclude 支持 'node_modules/'、'*.log'、'.git/'
25
- */
26
- copyDir = (src, dest, exclude = []) => {
27
- ensureDir(dest);
28
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
29
- const name = entry.name;
30
- if (isExcluded(name, exclude)) continue;
31
- const s = path.join(src, name), d = path.join(dest, name);
32
- if (entry.isDirectory()) copyDir(s, d, exclude);
33
- else if (entry.isFile()) fs.copyFileSync(s, d);
34
- }
35
- },
36
- readJson = file => JSON.parse(fs.readFileSync(file, 'utf-8')),
37
- writeJson = (file, obj) => fs.writeFileSync(file, JSON.stringify(obj, null, 2) + '\n', 'utf-8'),
38
- /**
39
- * 内容不同才写文件,避免不必要的 mtime 变化触发下游(Gradle/Metro)重编
40
- * 返回 true 表示写入了,false 表示内容相同跳过
41
- */
42
- writeIfChanged = (file, content) => {
43
- if (fs.existsSync(file)) {
44
- try {
45
- if (fs.readFileSync(file, 'utf-8') === content) return false;
46
- } catch { /* 读失败就当需要写 */ }
47
- }
48
- fs.writeFileSync(file, content, 'utf-8');
49
- return true;
50
- },
51
- /**
52
- * 大小和 mtime 都一致才跳过复制,否则复制
53
- * 用于用户项目 → nodejs-project 的增量复制
54
- */
55
- copyFileIfChanged = (src, dst) => {
56
- if (fs.existsSync(dst)) {
57
- try {
58
- const s = fs.statSync(src), d = fs.statSync(dst);
59
- if (s.size === d.size && Math.floor(s.mtimeMs) <= Math.floor(d.mtimeMs)) return false;
60
- } catch { /* 出错就复制 */ }
61
- }
62
- fs.copyFileSync(src, dst);
63
- return true;
64
- },
65
- // 把单个参数转成适合 Windows cmd.exe 的形式:含空白或 shell 元字符时加双引号;
66
- quoteShellArg = (s) => {
67
- const str = String(s);
68
- if (str === '') return '""';
69
- return /[\s"&|<>^()]/.test(str) ? '"' + str.replace(/"/g, '""') + '"' : str;
70
- },
71
12
  /**
72
13
  * 统一的子进程启动:
73
14
  * - Windows:.cmd / .bat 需经 shell;把命令与参数拼成单字符串,
@@ -80,202 +21,364 @@ const ensureDir = dir => {
80
21
  return spawnSync(cmd, { ...baseOpts, shell: true });
81
22
  }
82
23
  return spawnSync(bin, args, { ...baseOpts, shell: false });
83
- },
84
- /**
85
- * 异步 spawn:参数同 spawnCmd,返回 ChildProcess
86
- * @param {string} bin 命令
87
- * @param {string[]} args 参数
88
- * @param {object} [baseOpts] spawn 选项
89
- * @returns {import('child_process').ChildProcess}
90
- */
91
- spawnCmdAsync = (bin, args, baseOpts = {}) => {
92
- if (process.platform === 'win32') {
93
- const cmd = [bin, ...args].map(quoteShellArg).join(' ');
94
- return spawn(cmd, { ...baseOpts, shell: true });
95
- }
96
- return spawn(bin, args, { ...baseOpts, shell: false });
97
- },
98
- /**
99
- * 执行命令:默认实时输出(inherit);quiet=true 时捕获输出,成功静默、失败才打印
100
- */
101
- run = (bin, args, opts = {}) => {
102
- const { quiet, ...restOpts } = opts;
103
- if (!quiet || process.env.OPTIMIZE_DEBUG) {
104
- const r = spawnCmd(bin, args, { stdio: 'inherit', ...restOpts });
105
- if (r.status !== 0) throw new Error(`${bin} ${args.join(' ')} 退出码 ${r.status}`);
106
- return r;
107
- }
108
- const r = spawnCmd(bin, args, { encoding: 'utf-8', ...restOpts });
109
- if (r.status !== 0) {
110
- if (r.stdout) process.stderr.write(r.stdout);
111
- if (r.stderr) process.stderr.write(r.stderr);
112
- throw new Error(`${bin} ${args.join(' ')} 退出码 ${r.status}`);
24
+ };
25
+
26
+ /**
27
+ * 生成 ESLint flat config 源码,替代 @react-native/eslint-config/flat
28
+ * >查看定义:@see {@link buildEslintConfigSource}
29
+ * @returns {string} ESLint 配置源码
30
+ */
31
+ const buildEslintConfigSource = () => `import js from '@eslint/js';
32
+ import reactPlugin from '@eslint-react/eslint-plugin';
33
+ import tsParser from '@typescript-eslint/parser';
34
+ import tsPlugin from '@typescript-eslint/eslint-plugin';
35
+ import chalk from 'chalk';
36
+ import reactHooks from 'eslint-plugin-react-hooks';
37
+ import prettier from 'eslint-config-prettier';
38
+
39
+ export default [
40
+ js.configs.recommended,
41
+ {
42
+ files: ['**/*.{js,jsx,ts,tsx}'],
43
+ languageOptions: {parser: tsParser, parserOptions: { ecmaFeatures: { jsx: true }, sourceType: 'module' }},
44
+ plugins: { ...reactPlugin.configs.recommended.plugins, 'react-hooks': reactHooks },
45
+ settings: { ...reactPlugin.configs.recommended.settings },
46
+ rules: { ...reactPlugin.configs.recommended.rules, ...reactHooks.configs.recommended.rules }
47
+ },
48
+ { files: ['**/*.{ts,tsx}'], plugins: { '@typescript-eslint': tsPlugin }, rules: tsPlugin.configs.recommended.rules },
49
+ prettier];`;
50
+
51
+ /**
52
+ * 递归复制目录,exclude 支持 'node_modules/', '*.log', '.git/'
53
+ * >查看定义:@see {@link copyDir}
54
+ * @param {string} src 源目录
55
+ * @param {string} dest 目标目录
56
+ * @param {string[]} [exclude] 排除模式列表
57
+ * @returns {void}
58
+ */
59
+ const copyDir = (src, dest, exclude = []) => {
60
+ ensureDir(dest);
61
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
62
+ const name = entry.name;
63
+ if (isExcluded(name, exclude)) continue;
64
+ const s = path.join(src, name), d = path.join(dest, name);
65
+ if (entry.isDirectory()) copyDir(s, d, exclude);
66
+ else if (entry.isFile()) fs.copyFileSync(s, d);
67
+ }
68
+ };
69
+
70
+ /**
71
+ * 大小和 mtime 都一致才跳过复制,否则复制
72
+ * >查看定义:@see {@link copyFileIfChanged}
73
+ * @param {string} src 源文件
74
+ * @param {string} dst 目标文件
75
+ * @returns {boolean} 是否执行了复制
76
+ */
77
+ const copyFileIfChanged = (src, dst) => {
78
+ if (fs.existsSync(dst)) {
79
+ try {
80
+ const s = fs.statSync(src), d = fs.statSync(dst);
81
+ if (s.size === d.size && Math.floor(s.mtimeMs) <= Math.floor(d.mtimeMs)) return false;
82
+ } catch { /* 出错就复制 */ }
83
+ }
84
+ fs.copyFileSync(src, dst);
85
+ return true;
86
+ };
87
+
88
+ /**
89
+ * 目录不存在则递归创建
90
+ * >查看定义:@see {@link ensureDir}
91
+ * @param {string} dir 目录路径
92
+ * @returns {void}
93
+ */
94
+ const ensureDir = dir => {
95
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
96
+ };
97
+
98
+ /**
99
+ * 确保待装包可覆盖安装:标志不一致或变体不符时卸载已装包
100
+ * >查看定义:@see {@link ensureInstallable}
101
+ * @param {string} adb adb 路径
102
+ * @param {string} appId 包名
103
+ * @param {string} want 待装变体(debug 或 release)
104
+ * @param {string} [serial] 设备序列号
105
+ * @returns {void}
106
+ */
107
+ const ensureInstallable = (adb, appId, want, serial) => {
108
+ const installed = getInstalledVariant(adb, appId, serial);
109
+ if (installed && installed !== want) {
110
+ console.log(chalk.yellow(` 已安装 ${installed} 包与待装 ${want} 包签名不同,先卸载`));
111
+ spawnCmd(adb, [...(serial ? ['-s', serial] : []), 'uninstall', appId], { encoding: 'utf-8' });
112
+ }
113
+ };
114
+
115
+ /**
116
+ * 定位 adb:配置 > 环境变量 > 默认路径 > PATH
117
+ * >查看定义:@see {@link findAdb}
118
+ * @param {object} config 用户配置
119
+ * @returns {string} adb 路径
120
+ */
121
+ const findAdb = config => {
122
+ // 1. 用户显式配置的路径优先
123
+ const configured = config?.android?.adbPath;
124
+ if (configured) {
125
+ if (fs.existsSync(configured)) return configured;
126
+ console.warn(chalk.yellow('⚠️ config.android.adbPath 不存在,回退到自动查找: ' + configured));
127
+ }
128
+ // 2. 环境变量 ANDROID_HOME / ANDROID_SDK_ROOT
129
+ for (const env of ['ANDROID_HOME', 'ANDROID_SDK_ROOT']) {
130
+ const base = process.env[env];
131
+ if (base) {
132
+ const p = path.join(base, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
133
+ if (fs.existsSync(p)) return p;
113
134
  }
114
- return r;
115
- },
116
- runQuiet = (bin, args, opts = {}) => spawnCmd(bin, args, { encoding: 'utf-8', ...opts }),
117
- runSilent = (bin, args, opts = {}) => {
118
- const r = spawnCmd(bin, args, { encoding: 'utf-8', ...opts });
119
- if (r.status !== 0) {
120
- if (r.stdout) process.stderr.write(r.stdout);
121
- if (r.stderr) process.stderr.write(r.stderr);
122
- throw new Error(`${bin} ${args.join(' ')} 退出码 ${r.status}`);
135
+ }
136
+ // 3. 默认路径猜测(Windows / macOS / Linux)
137
+ const home = process.env.USERPROFILE || process.env.HOME || '', localAppData = process.env.LOCALAPPDATA || '',
138
+ candidates = [
139
+ path.join(localAppData, 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
140
+ path.join(home, 'Library', 'Android', 'sdk', 'platform-tools', 'adb'),
141
+ path.join(home, 'Android', 'Sdk', 'platform-tools', 'adb')
142
+ ];
143
+ for (const c of candidates) if (c && fs.existsSync(c)) return c;
144
+ return 'adb';
145
+ };
146
+
147
+ /**
148
+ * 定位 hermesc(Hermes 编译器,用于把 JS bundle 编成字节码)
149
+ * >查看定义:@see {@link findHermesc}
150
+ * @param {string} userProjectDir 用户项目目录
151
+ * @returns {string|null} hermesc 路径,未找到返回 null
152
+ */
153
+ const findHermesc = userProjectDir => {
154
+ const base = path.join(userProjectDir, 'node_modules', 'hermes-compiler', 'hermesc'), platform = process.platform;
155
+ let sub;
156
+ if (platform === 'win32') sub = ['win64-bin', 'hermesc.exe'];
157
+ else if (platform === 'darwin') sub = ['osx-bin', 'hermesc'];
158
+ else sub = ['linux64-bin', 'hermesc'];
159
+ const p = path.join(base, ...sub);
160
+ return fs.existsSync(p) ? p : null;
161
+ };
162
+
163
+ /**
164
+ * 查询设备上已安装包的 DEBUGGABLE 标志
165
+ * >查看定义:@see {@link getInstalledVariant}
166
+ * @param {string} adb adb 路径
167
+ * @param {string} appId 包名
168
+ * @param {string} [serial] 设备序列号
169
+ * @returns {string|null} 变体或 null(未安装)
170
+ */
171
+ const getInstalledVariant = (adb, appId, serial) => {
172
+ const pre = serial ? ['-s', serial] : [],
173
+ r = spawnCmd(adb, [...pre, 'shell', 'dumpsys', 'package', appId], { encoding: 'utf-8' });
174
+ if (r.status !== 0 || !r.stdout || !r.stdout.includes('Package [')) return null;
175
+ return /\bDEBUGGABLE\b/.test(r.stdout) ? 'debug' : 'release';
176
+ };
177
+
178
+ /**
179
+ * 判断文件名是否命中排除模式
180
+ * >查看定义:@see {@link isExcluded}
181
+ * @param {string} name 文件名
182
+ * @param {string[]} patterns 模式列表
183
+ * @returns {boolean} 是否命中
184
+ */
185
+ const isExcluded = (name, patterns) => {
186
+ for (let p of patterns) {
187
+ p = p.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/$/, '');
188
+ if (p === name) return true;
189
+ if (p.includes('*')) {
190
+ const escaped = p.replace(/[.+?^${}()|[\]\\/]/g, '\\$&').replace(/\*/g, '.*');
191
+ if (new RegExp('^' + escaped + '$').test(name)) return true;
123
192
  }
193
+ }
194
+ return false;
195
+ };
196
+
197
+
198
+ /**
199
+ * 读取并解析 JSON 文件
200
+ * >查看定义:@see {@link readJson}
201
+ * @param {string} file 文件路径
202
+ * @returns {*} 解析结果
203
+ */
204
+ const readJson = file => JSON.parse(fs.readFileSync(file, 'utf-8'));
205
+
206
+ /**
207
+ * 目录存在则递归删除
208
+ * >查看定义:@see {@link removeDir}
209
+ * @param {string} dir 目录路径
210
+ * @returns {void}
211
+ */
212
+ const removeDir = dir => {
213
+ if (fs.existsSync(dir)) fs.rmSync(dir, { recursive: true, force: true });
214
+ };
215
+
216
+ /**
217
+ * 执行命令:默认实时输出;quiet=true 时捕获输出,成功静默、失败才打印
218
+ * >查看定义:@see {@link run}
219
+ * @param {string} bin 命令
220
+ * @param {string[]} args 参数
221
+ * @param {object} [opts] spawn 选项,支持 quiet
222
+ * @returns {object} spawnSync 结果
223
+ */
224
+ const run = (bin, args, opts = {}) => {
225
+ const { quiet, ...restOpts } = opts;
226
+ if (!quiet || process.env.OPTIMIZE_DEBUG) {
227
+ const r = spawnCmd(bin, args, { stdio: 'inherit', ...restOpts });
228
+ if (r.status !== 0) throw new Error(`${bin} ${args.join(' ')} 退出码 ${r.status}`);
124
229
  return r;
125
- },
126
- /**
127
- * 执行命令:实时输出,仅首行加缩进前缀,其余行原样
128
- * @param {string} bin 命令
129
- * @param {string[]} args 参数
130
- * @param {object} [opts] spawn 选项 + prefix
131
- * @returns {Promise<void>}
132
- */
133
- runStream = (bin, args, opts = {}) => new Promise((resolve, reject) => {
134
- const { prefix = ' ', quiet = false, ...restOpts } = opts, child = spawnCmdAsync(bin, args, restOpts),
135
- verbose = !quiet || process.env.OPTIMIZE_DEBUG, KEY_RE = /Installing|Installed|FAILED|error|Error|失败|退出码/;
136
- let first = true;
137
- const write = chunk => {
230
+ }
231
+ const r = spawnCmd(bin, args, { encoding: 'utf-8', ...restOpts });
232
+ if (r.status !== 0) {
233
+ if (r.stdout) process.stderr.write(r.stdout);
234
+ if (r.stderr) process.stderr.write(r.stderr);
235
+ throw new Error(`${bin} ${args.join(' ')} 退出码 ${r.status}`);
236
+ }
237
+ return r;
238
+ };
239
+
240
+ /**
241
+ * 执行命令并捕获输出,不打印
242
+ * >查看定义:@see {@link runQuiet}
243
+ * @param {string} bin 命令
244
+ * @param {string[]} args 参数
245
+ * @param {object} [opts] spawn 选项
246
+ * @returns {object} spawnSync 结果
247
+ */
248
+ const runQuiet = (bin, args, opts = {}) => spawnCmd(bin, args, { encoding: 'utf-8', ...opts });
249
+
250
+ /**
251
+ * 执行命令,成功静默、失败打印输出并抛错
252
+ * >查看定义:@see {@link runSilent}
253
+ * @param {string} bin 命令
254
+ * @param {string[]} args 参数
255
+ * @param {object} [opts] spawn 选项
256
+ * @returns {object} spawnSync 结果
257
+ */
258
+ const runSilent = (bin, args, opts = {}) => {
259
+ const r = spawnCmd(bin, args, { encoding: 'utf-8', ...opts });
260
+ if (r.status !== 0) {
261
+ if (r.stdout) process.stderr.write(r.stdout);
262
+ if (r.stderr) process.stderr.write(r.stderr);
263
+ throw new Error(`${bin} ${args.join(' ')} 退出码 ${r.status}`);
264
+ }
265
+ return r;
266
+ };
267
+
268
+ /**
269
+ * 执行命令并实时输出,仅首行加缩进前缀
270
+ * >查看定义:@see {@link runStream}
271
+ * @param {string} bin 命令
272
+ * @param {string[]} args 参数
273
+ * @param {object} [opts] spawn 选项,支持 prefix 与 quiet
274
+ * @returns {Promise<void>}
275
+ */
276
+ const runStream = (bin, args, opts = {}) => new Promise((resolve, reject) => {
277
+ let first = true;
278
+ const { prefix = ' ', quiet = false, ...restOpts } = opts, child = spawnCmdAsync(bin, args, restOpts),
279
+ verbose = !quiet || process.env.OPTIMIZE_DEBUG, KEY_RE = /Installing|Installed|FAILED|error|Error|失败|退出码/,
280
+ write = chunk => {
281
+ if (verbose) {
282
+ if (first) process.stdout.write(prefix), first = false;
283
+ process.stdout.write(chunk);
284
+ return;
285
+ }
138
286
  for (const line of chunk.toString().split(/\r?\n/)) {
139
- if (!line.trim()) continue;
140
- if (!verbose && !KEY_RE.test(line)) continue;
141
- process.stdout.write((first ? prefix : '') + line + '\n');
142
- first = false;
287
+ if (!line.trim() || !KEY_RE.test(line)) continue;
288
+ process.stdout.write((first ? prefix : '') + line + '\n'), first = false;
143
289
  }
144
290
  };
145
- if (child.stdout) child.stdout.on('data', write);
146
- if (child.stderr) child.stderr.on('data', write);
147
- child.on('error', reject);
148
- child.on('close', code => code === 0 ? resolve() : reject(new Error(`${bin} ${args.join(' ')} 退出码 ${code}`)));
149
- }),
150
- /**
151
- * 定位 adb(配置 > 环境变量 > 默认路径 > PATH)
152
- */
153
- findAdb = config => {
154
- // 1. 用户显式配置的路径优先
155
- const configured = config?.android?.adbPath;
156
- if (configured) {
157
- if (fs.existsSync(configured)) return configured;
158
- console.warn(chalk.yellow('⚠️ config.android.adbPath 不存在,回退到自动查找: ' + configured));
159
- }
160
- // 2. 环境变量 ANDROID_HOME / ANDROID_SDK_ROOT
161
- for (const env of ['ANDROID_HOME', 'ANDROID_SDK_ROOT']) {
162
- const base = process.env[env];
163
- if (base) {
164
- const p = path.join(base, 'platform-tools', process.platform === 'win32' ? 'adb.exe' : 'adb');
165
- if (fs.existsSync(p)) return p;
166
- }
167
- }
168
- // 3. 默认路径猜测(Windows / macOS / Linux)
169
- const home = process.env.USERPROFILE || process.env.HOME || '', localAppData = process.env.LOCALAPPDATA || '',
170
- candidates = [
171
- path.join(localAppData, 'Android', 'Sdk', 'platform-tools', 'adb.exe'),
172
- path.join(home, 'Library', 'Android', 'sdk', 'platform-tools', 'adb'),
173
- path.join(home, 'Android', 'Sdk', 'platform-tools', 'adb')
174
- ];
175
- for (const c of candidates) if (c && fs.existsSync(c)) return c;
176
- return 'adb';
177
- },
178
- /**
179
- * 定位 hermesc(Hermes 编译器,用于把 JS bundle 编成字节码)
180
- */
181
- /**
182
- * 读取安装状态标志(记录上次由本工具安装的包与变体)
183
- * @param {string} buildDir 构建目录
184
- * @returns {{appId:string, variant:'debug'|'release'}|null} 状态或 null
185
- */
186
- readInstallState = buildDir => {
187
- const f = path.join(buildDir, '.install-state.json');
188
- if (!fs.existsSync(f)) return null;
189
- try { return JSON.parse(fs.readFileSync(f, 'utf-8')); } catch { return null; }
190
- },
191
- /**
192
- * 写入安装状态标志(安装成功后调用)
193
- * @param {string} buildDir 构建目录
194
- * @param {object} state 状态
195
- * @returns {void}
196
- */
197
- writeInstallState = (buildDir, state) => {
291
+ if (child.stdout) child.stdout.on('data', write);
292
+ if (child.stderr) child.stderr.on('data', write);
293
+ child.on('error', reject);
294
+ child.on('close', code => code === 0 ? resolve() : reject(new Error(`${bin} ${args.join(' ')} 退出码 ${code}`)));
295
+ });
296
+
297
+ /**
298
+ * 文件名安全化:去除文件系统非法字符,空结果回退 app
299
+ * >查看定义:@see {@link sanitizeFileName}
300
+ * @param {string} s 原始文件名
301
+ * @returns {string} 安全文件名
302
+ */
303
+ const sanitizeFileName = s => {
304
+ const cleaned = String(s).replace(/[\\/:*?"<>|]/g, '_').trim();
305
+ return cleaned || 'app';
306
+ };
307
+
308
+ /**
309
+ * 异步 spawn:参数同 spawnCmd,返回 ChildProcess
310
+ * >查看定义:@see {@link spawnCmdAsync}
311
+ * @param {string} bin 命令
312
+ * @param {string[]} args 参数
313
+ * @param {object} [baseOpts] spawn 选项
314
+ * @returns {import('child_process').ChildProcess}
315
+ */
316
+ const spawnCmdAsync = (bin, args, baseOpts = {}) => {
317
+ if (process.platform === 'win32') {
318
+ const cmd = [bin, ...args].map(quoteShellArg).join(' ');
319
+ return spawn(cmd, { ...baseOpts, shell: true });
320
+ }
321
+ return spawn(bin, args, { ...baseOpts, shell: false });
322
+ };
323
+
324
+ /**
325
+ * Gradle 构建失败时,若用户配置了自定义下载地址或 Maven 仓库,提示改回自动探测
326
+ * >查看定义:@see {@link warnGradleDownloadHint}
327
+ * @param {object} config 用户配置
328
+ * @returns {void}
329
+ */
330
+ const warnGradleDownloadHint = config => {
331
+ const a = config && config.android;
332
+ if (!a) return;
333
+ if (a.gradleDistributionUrl)
334
+ console.error(chalk.yellow(' 提示: android.gradleDistributionUrl 已配置但下载或构建失败,' +
335
+ '可将该字段改回 null 让工具自动探测,或配置正确可达地址'));
336
+ if (Array.isArray(a.mavenRepositories) && a.mavenRepositories.length > 0)
337
+ console.error(chalk.yellow(' 提示: android.mavenRepositories 已配置但依赖解析失败,' +
338
+ '可将该字段改回 null 使用内置默认仓库,或配置正确可达的仓库地址'));
339
+ };
340
+
341
+ /**
342
+ * 内容不同才写文件,避免无谓的 mtime 变化
343
+ * >查看定义:@see {@link writeIfChanged}
344
+ * @param {string} file 文件路径
345
+ * @param {string} content 内容
346
+ * @returns {boolean} 是否写入
347
+ */
348
+ const writeIfChanged = (file, content) => {
349
+ if (fs.existsSync(file)) {
198
350
  try {
199
- ensureDir(buildDir),
200
- fs.writeFileSync(path.join(buildDir, '.install-state.json'), JSON.stringify(state, null, 2));
201
- } catch { }
202
- },
203
- /**
204
- * 查询设备上已安装包的 DEBUGGABLE 标志
205
- * @param {string} adb adb 路径
206
- * @param {string} appId 包名
207
- * @returns {'debug'|'release'|null} 变体或 null(未安装)
208
- */
209
- getInstalledVariant = (adb, appId, serial) => {
210
- const pre = serial ? ['-s', serial] : [],
211
- r = spawnCmd(adb, [...pre, 'shell', 'dumpsys', 'package', appId], { encoding: 'utf-8' });
212
- if (r.status !== 0 || !r.stdout || !r.stdout.includes('Package [')) return null;
213
- return /\bDEBUGGABLE\b/.test(r.stdout) ? 'debug' : 'release';
214
- },
215
- /**
216
- * 确保待装包可覆盖安装:标志不一致或变体不符时卸载已装包
217
- * @param {string} adb adb 路径
218
- * @param {string} appId 包名
219
- * @param {'debug'|'release'} want 待装变体
220
- * @param {string} buildDir 构建目录
221
- * @param {string} [serial] 设备序列号(多设备时指定)
222
- * @returns {void}
223
- */
224
- ensureInstallable = (adb, appId, want, buildDir, serial) => {
225
- const installed = getInstalledVariant(adb, appId, serial);
226
- if (installed && installed !== want) {
227
- console.log(chalk.yellow(` 已安装 ${installed} 包与待装 ${want} 包签名不同,先卸载`));
228
- spawnCmd(adb, [...(serial ? ['-s', serial] : []), 'uninstall', appId], { encoding: 'utf-8' });
229
- }
230
- },
231
- findHermesc = userProjectDir => {
232
- const base = path.join(userProjectDir, 'node_modules', 'hermes-compiler', 'hermesc'), platform = process.platform;
233
- let sub;
234
- if (platform === 'win32') sub = ['win64-bin', 'hermesc.exe'];
235
- else if (platform === 'darwin') sub = ['osx-bin', 'hermesc'];
236
- else sub = ['linux64-bin', 'hermesc'];
237
- const p = path.join(base, ...sub);
238
- return fs.existsSync(p) ? p : null;
239
- },
240
- /**
241
- * 文件名安全化:去除文件系统非法字符(/ \ : * ? " < > |)
242
- * 空结果回退 'app'
243
- */
244
- sanitizeFileName = s => {
245
- const cleaned = String(s).replace(/[\\/:*?"<>|]/g, '_').trim();
246
- return cleaned || 'app';
247
- },
248
- /**
249
- * 生成 ESLint flat config 源码(替代 @react-native/eslint-config/flat)
250
- */
251
- buildEslintConfigSource = () => `import js from '@eslint/js';
252
- import tsParser from '@typescript-eslint/parser';
253
- import tsPlugin from '@typescript-eslint/eslint-plugin';
254
- import reactPlugin from '@eslint-react/eslint-plugin';
255
- import reactHooks from 'eslint-plugin-react-hooks';
256
- import prettier from 'eslint-config-prettier';
257
- import chalk from 'chalk';
351
+ if (fs.readFileSync(file, 'utf-8') === content) return false;
352
+ } catch { /* 读失败就当需要写 */ }
353
+ }
354
+ fs.writeFileSync(file, content, 'utf-8');
355
+ return true;
356
+ };
357
+
358
+ /**
359
+ * 写入安装状态标志,安装成功后调用
360
+ * >查看定义:@see {@link writeInstallState}
361
+ * @param {string} buildDir 构建目录
362
+ * @param {object} state 状态对象
363
+ * @returns {void}
364
+ */
365
+ const writeInstallState = (buildDir, state) => {
366
+ try {
367
+ ensureDir(buildDir), fs.writeFileSync(path.join(buildDir, '.install-state.json'), JSON.stringify(state, null, 2));
368
+ } catch { }
369
+ };
258
370
 
259
- export default [
260
- js.configs.recommended,
261
- {
262
- files: ['**/*.{js,jsx,ts,tsx}'],
263
- languageOptions: {
264
- parser: tsParser,
265
- parserOptions: { ecmaFeatures: { jsx: true }, sourceType: 'module' },
266
- },
267
- plugins: { ...reactPlugin.configs.recommended.plugins, 'react-hooks': reactHooks },
268
- settings: { ...reactPlugin.configs.recommended.settings },
269
- rules: {
270
- ...reactPlugin.configs.recommended.rules,
271
- ...reactHooks.configs.recommended.rules,
272
- },
273
- },
274
- { files: ['**/*.{ts,tsx}'], plugins: { '@typescript-eslint': tsPlugin },
275
- rules: tsPlugin.configs.recommended.rules }, prettier];`;
371
+ /**
372
+ * 把对象写入 JSON 文件
373
+ * >查看定义:@see {@link writeJson}
374
+ * @param {string} file 文件路径
375
+ * @param {*} obj 待写入对象
376
+ * @returns {void}
377
+ */
378
+ const writeJson = (file, obj) => fs.writeFileSync(file, JSON.stringify(obj, null, 2) + '\n', 'utf-8');
276
379
 
277
380
  export {
278
- ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged, spawnCmdAsync, run,
279
- runQuiet, runStream, runSilent, findAdb, readInstallState, writeInstallState, getInstalledVariant, ensureInstallable,
280
- findHermesc, sanitizeFileName, buildEslintConfigSource
381
+ buildEslintConfigSource, copyDir, copyFileIfChanged, ensureDir, ensureInstallable, findAdb, findHermesc,
382
+ getInstalledVariant, isExcluded, readJson, removeDir, run, runQuiet, runSilent, runStream,
383
+ sanitizeFileName, spawnCmdAsync, warnGradleDownloadHint, writeIfChanged, writeInstallState, writeJson
281
384
  };
@@ -1,5 +1,11 @@
1
- export default {
1
+ /**
2
+ * Prettier 格式化配置
3
+ * >查看定义:@see {@link prettierConfig}
4
+ */
5
+ const prettierConfig = {
2
6
  arrowParens: 'avoid',
3
7
  singleQuote: true,
4
8
  trailingComma: 'all',
5
- };
9
+ };
10
+
11
+ export default prettierConfig;