@flun/node-mobile-app 4.0.8 → 4.0.10

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,281 +1,281 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import { spawnSync, spawn } from 'child_process';
4
- import chalk from 'chalk';
5
-
6
- const ensureDir = dir => {
7
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
8
- },
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
- /**
72
- * 统一的子进程启动:
73
- * - Windows:.cmd / .bat 需经 shell;把命令与参数拼成单字符串,
74
- * 避免 spawnSync(bin, args, { shell: true }) 触发 DEP0190;
75
- * - 其它平台:不用 shell,参数交由 Node 转义;
76
- */
77
- spawnCmd = (bin, args, baseOpts = {}) => {
78
- if (process.platform === 'win32') {
79
- const cmd = [bin, ...args].map(quoteShellArg).join(' ');
80
- return spawnSync(cmd, { ...baseOpts, shell: true });
81
- }
82
- 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}`);
113
- }
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}`);
123
- }
124
- 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 => {
138
- 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;
143
- }
144
- };
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) => {
198
- 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';
258
-
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];`;
276
-
277
- 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
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawnSync, spawn } from 'child_process';
4
+ import chalk from 'chalk';
5
+
6
+ const ensureDir = dir => {
7
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
8
+ },
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
+ /**
72
+ * 统一的子进程启动:
73
+ * - Windows:.cmd / .bat 需经 shell;把命令与参数拼成单字符串,
74
+ * 避免 spawnSync(bin, args, { shell: true }) 触发 DEP0190;
75
+ * - 其它平台:不用 shell,参数交由 Node 转义;
76
+ */
77
+ spawnCmd = (bin, args, baseOpts = {}) => {
78
+ if (process.platform === 'win32') {
79
+ const cmd = [bin, ...args].map(quoteShellArg).join(' ');
80
+ return spawnSync(cmd, { ...baseOpts, shell: true });
81
+ }
82
+ 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}`);
113
+ }
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}`);
123
+ }
124
+ 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 => {
138
+ 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;
143
+ }
144
+ };
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) => {
198
+ 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';
258
+
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];`;
276
+
277
+ 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
281
281
  };