@flun/node-mobile-app 4.1.2 → 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/Android/346/236/204/345/273/272/346/214/207/345/215/227.md +1 -1
- package/CHANGELOG.md +15 -22
- package/README.md +67 -13
- package/bin/cli.js +3 -3
- package/mobileAppConfig.js +17 -4
- package/package.json +1 -1
- package/scripts/copy-files.js +3 -2
- package/src/commands/build.js +105 -99
- package/src/commands/clean.js +12 -6
- package/src/commands/test.js +141 -133
- package/src/config.js +88 -52
- package/src/copy.js +302 -215
- package/src/fingerprint.js +96 -77
- package/src/generate.js +33 -21
- package/src/install.js +176 -162
- package/src/optimize-node-modules.js +5 -4
- package/src/patcher.js +438 -358
- package/src/platform-ios.js +122 -106
- package/src/utils.js +351 -252
- package/template/.prettierrc.js +8 -2
- package/template/App.tsx +98 -87
- package/template/babel.config.js +8 -2
- package/template/eslint.config.js +8 -2
- package/template/metro.config.js +9 -3
- package//350/207/252/345/256/232/344/271/211libnode/346/214/207/345/215/227.md +2 -2
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
|
-
|
|
7
|
-
|
|
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,61 +21,263 @@ 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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 => {
|
|
138
281
|
if (verbose) {
|
|
139
282
|
if (first) process.stdout.write(prefix), first = false;
|
|
140
283
|
process.stdout.write(chunk);
|
|
@@ -142,144 +285,100 @@ const ensureDir = dir => {
|
|
|
142
285
|
}
|
|
143
286
|
for (const line of chunk.toString().split(/\r?\n/)) {
|
|
144
287
|
if (!line.trim() || !KEY_RE.test(line)) continue;
|
|
145
|
-
process.stdout.write((first ? prefix : '') + line + '\n');
|
|
146
|
-
first = false;
|
|
288
|
+
process.stdout.write((first ? prefix : '') + line + '\n'), first = false;
|
|
147
289
|
}
|
|
148
290
|
};
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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)) {
|
|
202
350
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
* @param {string} appId 包名
|
|
223
|
-
* @param {'debug'|'release'} want 待装变体
|
|
224
|
-
* @param {string} buildDir 构建目录
|
|
225
|
-
* @param {string} [serial] 设备序列号(多设备时指定)
|
|
226
|
-
* @returns {void}
|
|
227
|
-
*/
|
|
228
|
-
ensureInstallable = (adb, appId, want, buildDir, serial) => {
|
|
229
|
-
const installed = getInstalledVariant(adb, appId, serial);
|
|
230
|
-
if (installed && installed !== want) {
|
|
231
|
-
console.log(chalk.yellow(` 已安装 ${installed} 包与待装 ${want} 包签名不同,先卸载`));
|
|
232
|
-
spawnCmd(adb, [...(serial ? ['-s', serial] : []), 'uninstall', appId], { encoding: 'utf-8' });
|
|
233
|
-
}
|
|
234
|
-
},
|
|
235
|
-
findHermesc = userProjectDir => {
|
|
236
|
-
const base = path.join(userProjectDir, 'node_modules', 'hermes-compiler', 'hermesc'), platform = process.platform;
|
|
237
|
-
let sub;
|
|
238
|
-
if (platform === 'win32') sub = ['win64-bin', 'hermesc.exe'];
|
|
239
|
-
else if (platform === 'darwin') sub = ['osx-bin', 'hermesc'];
|
|
240
|
-
else sub = ['linux64-bin', 'hermesc'];
|
|
241
|
-
const p = path.join(base, ...sub);
|
|
242
|
-
return fs.existsSync(p) ? p : null;
|
|
243
|
-
},
|
|
244
|
-
/**
|
|
245
|
-
* 文件名安全化:去除文件系统非法字符(/ \ : * ? " < > |)
|
|
246
|
-
* 空结果回退 'app'
|
|
247
|
-
*/
|
|
248
|
-
sanitizeFileName = s => {
|
|
249
|
-
const cleaned = String(s).replace(/[\\/:*?"<>|]/g, '_').trim();
|
|
250
|
-
return cleaned || 'app';
|
|
251
|
-
},
|
|
252
|
-
/**
|
|
253
|
-
* 生成 ESLint flat config 源码(替代 @react-native/eslint-config/flat)
|
|
254
|
-
*/
|
|
255
|
-
buildEslintConfigSource = () => `import js from '@eslint/js';
|
|
256
|
-
import tsParser from '@typescript-eslint/parser';
|
|
257
|
-
import tsPlugin from '@typescript-eslint/eslint-plugin';
|
|
258
|
-
import reactPlugin from '@eslint-react/eslint-plugin';
|
|
259
|
-
import reactHooks from 'eslint-plugin-react-hooks';
|
|
260
|
-
import prettier from 'eslint-config-prettier';
|
|
261
|
-
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
|
+
};
|
|
262
370
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
plugins: { ...reactPlugin.configs.recommended.plugins, 'react-hooks': reactHooks },
|
|
272
|
-
settings: { ...reactPlugin.configs.recommended.settings },
|
|
273
|
-
rules: {
|
|
274
|
-
...reactPlugin.configs.recommended.rules,
|
|
275
|
-
...reactHooks.configs.recommended.rules,
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
{ files: ['**/*.{ts,tsx}'], plugins: { '@typescript-eslint': tsPlugin },
|
|
279
|
-
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');
|
|
280
379
|
|
|
281
380
|
export {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
381
|
+
buildEslintConfigSource, copyDir, copyFileIfChanged, ensureDir, ensureInstallable, findAdb, findHermesc,
|
|
382
|
+
getInstalledVariant, isExcluded, readJson, removeDir, run, runQuiet, runSilent, runStream,
|
|
383
|
+
sanitizeFileName, spawnCmdAsync, warnGradleDownloadHint, writeIfChanged, writeInstallState, writeJson
|
|
285
384
|
};
|
package/template/.prettierrc.js
CHANGED