@flun/node-mobile-app 3.1.4 → 4.0.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/CHANGELOG.md +46 -15
- package/README.md +559 -545
- package/bin/cli.js +3 -2
- package/package.json +4 -3
- package/src/commands/build.js +115 -114
- package/src/commands/clean.js +4 -3
- package/src/commands/test.js +160 -109
- package/src/config.js +6 -5
- package/src/copy.js +320 -319
- package/src/fingerprint.js +123 -122
- package/src/generate.js +105 -104
- package/src/install.js +317 -215
- package/src/optimize-node-modules.js +57 -54
- package/src/patcher.js +389 -388
- package/src/platform-ios.js +6 -5
- package/src/utils.js +92 -5
package/src/platform-ios.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { run, ensureDir } from './utils.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
4
5
|
|
|
5
6
|
const SCHEME = 'MyApp',
|
|
6
7
|
/** iOS 相关命令只能在 macOS 上跑 */
|
|
@@ -16,7 +17,7 @@ const SCHEME = 'MyApp',
|
|
|
16
17
|
* 执行 pod install(若 Pods/ 已存在且 Podfile.lock 未变,cocoapods 会自动跳过)
|
|
17
18
|
*/
|
|
18
19
|
podInstall = iosDir => {
|
|
19
|
-
console.log(' pod install ...'), run('pod', ['install'], { cwd: iosDir, inherit: true });
|
|
20
|
+
console.log(chalk.cyan(' pod install ...')), run('pod', ['install'], { cwd: iosDir, inherit: true });
|
|
20
21
|
},
|
|
21
22
|
/**
|
|
22
23
|
* 把 config.ios.signing.certificate 导入临时 keychain,供 xcodebuild 签名用
|
|
@@ -28,7 +29,7 @@ const SCHEME = 'MyApp',
|
|
|
28
29
|
|
|
29
30
|
const certPath = path.resolve(userProjectDir, signing.certificate);
|
|
30
31
|
if (!fs.existsSync(certPath)) {
|
|
31
|
-
console.warn(' ⚠️ iOS 签名证书不存在,跳过:'
|
|
32
|
+
console.warn(chalk.yellow(' ⚠️ iOS 签名证书不存在,跳过: ' + certPath));
|
|
32
33
|
return null;
|
|
33
34
|
}
|
|
34
35
|
|
|
@@ -38,7 +39,7 @@ const SCHEME = 'MyApp',
|
|
|
38
39
|
|
|
39
40
|
// 清掉同名的旧 keychain(重复构建时)
|
|
40
41
|
if (fs.existsSync(keychainPath)) fs.rmSync(keychainPath, { force: true });
|
|
41
|
-
console.log(' 创建临时 keychain 并导入证书 ...');
|
|
42
|
+
console.log(chalk.cyan(' 创建临时 keychain 并导入证书 ...'));
|
|
42
43
|
run('security', ['create-keychain', '-p', keychainPassword, keychainPath], { quiet: true });
|
|
43
44
|
run('security', ['set-keychain-settings', '-lut', '3600', keychainPath], { quiet: true });
|
|
44
45
|
run('security', ['unlock-keychain', '-p', keychainPassword, keychainPath], { quiet: true });
|
|
@@ -84,7 +85,7 @@ const SCHEME = 'MyApp',
|
|
|
84
85
|
const keychain = setupSigningKeychain(buildDir, userProjectDir, config);
|
|
85
86
|
if (keychain) args.push('OTHER_CODE_SIGN_FLAGS=--keychain ' + keychain.keychainPath);
|
|
86
87
|
|
|
87
|
-
console.log(' xcodebuild archive ...'), run('xcodebuild', args, { cwd: iosDir, quiet: true });
|
|
88
|
+
console.log(chalk.cyan(' xcodebuild archive ...')), run('xcodebuild', args, { cwd: iosDir, quiet: true });
|
|
88
89
|
return archivePath;
|
|
89
90
|
},
|
|
90
91
|
/**
|
|
@@ -123,7 +124,7 @@ const SCHEME = 'MyApp',
|
|
|
123
124
|
].filter(Boolean).join('\n');
|
|
124
125
|
fs.writeFileSync(plistPath, plist, 'utf-8'); // 生成 exportOptions.plist
|
|
125
126
|
|
|
126
|
-
console.log(' xcodebuild -exportArchive ...');
|
|
127
|
+
console.log(chalk.cyan(' xcodebuild -exportArchive ...'));
|
|
127
128
|
run('xcodebuild', [
|
|
128
129
|
'-exportArchive',
|
|
129
130
|
'-archivePath', archivePath,
|
package/src/utils.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { spawnSync } from 'child_process';
|
|
3
|
+
import { spawnSync, spawn } from 'child_process';
|
|
4
|
+
import chalk from 'chalk';
|
|
4
5
|
|
|
5
6
|
const ensureDir = dir => {
|
|
6
7
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
@@ -80,6 +81,20 @@ const ensureDir = dir => {
|
|
|
80
81
|
}
|
|
81
82
|
return spawnSync(bin, args, { ...baseOpts, shell: false });
|
|
82
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
|
+
},
|
|
83
98
|
/**
|
|
84
99
|
* 执行命令:默认实时输出(inherit);quiet=true 时捕获输出,成功静默、失败才打印
|
|
85
100
|
*/
|
|
@@ -108,6 +123,30 @@ const ensureDir = dir => {
|
|
|
108
123
|
}
|
|
109
124
|
return r;
|
|
110
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
|
+
}),
|
|
111
150
|
/**
|
|
112
151
|
* 定位 adb(配置 > 环境变量 > 默认路径 > PATH)
|
|
113
152
|
*/
|
|
@@ -116,7 +155,7 @@ const ensureDir = dir => {
|
|
|
116
155
|
const configured = config?.android?.adbPath;
|
|
117
156
|
if (configured) {
|
|
118
157
|
if (fs.existsSync(configured)) return configured;
|
|
119
|
-
console.warn('⚠️ config.android.adbPath 不存在,回退到自动查找: ' + configured);
|
|
158
|
+
console.warn(chalk.yellow('⚠️ config.android.adbPath 不存在,回退到自动查找: ' + configured));
|
|
120
159
|
}
|
|
121
160
|
// 2. 环境变量 ANDROID_HOME / ANDROID_SDK_ROOT
|
|
122
161
|
for (const env of ['ANDROID_HOME', 'ANDROID_SDK_ROOT']) {
|
|
@@ -139,6 +178,53 @@ const ensureDir = dir => {
|
|
|
139
178
|
/**
|
|
140
179
|
* 定位 hermesc(Hermes 编译器,用于把 JS bundle 编成字节码)
|
|
141
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 { ensureDir(buildDir), fs.writeFileSync(path.join(buildDir, '.install-state.json'), JSON.stringify(state, null, 2)); } catch { }
|
|
199
|
+
},
|
|
200
|
+
/**
|
|
201
|
+
* 查询设备上已安装包的 DEBUGGABLE 标志
|
|
202
|
+
* @param {string} adb adb 路径
|
|
203
|
+
* @param {string} appId 包名
|
|
204
|
+
* @returns {'debug'|'release'|null} 变体或 null(未安装)
|
|
205
|
+
*/
|
|
206
|
+
getInstalledVariant = (adb, appId, serial) => {
|
|
207
|
+
const pre = serial ? ['-s', serial] : [],
|
|
208
|
+
r = spawnCmd(adb, [...pre, 'shell', 'dumpsys', 'package', appId], { encoding: 'utf-8' });
|
|
209
|
+
if (r.status !== 0 || !r.stdout || !r.stdout.includes('Package [')) return null;
|
|
210
|
+
return /\bDEBUGGABLE\b/.test(r.stdout) ? 'debug' : 'release';
|
|
211
|
+
},
|
|
212
|
+
/**
|
|
213
|
+
* 确保待装包可覆盖安装:标志不一致或变体不符时卸载已装包
|
|
214
|
+
* @param {string} adb adb 路径
|
|
215
|
+
* @param {string} appId 包名
|
|
216
|
+
* @param {'debug'|'release'} want 待装变体
|
|
217
|
+
* @param {string} buildDir 构建目录
|
|
218
|
+
* @param {string} [serial] 设备序列号(多设备时指定)
|
|
219
|
+
* @returns {void}
|
|
220
|
+
*/
|
|
221
|
+
ensureInstallable = (adb, appId, want, buildDir, serial) => {
|
|
222
|
+
const installed = getInstalledVariant(adb, appId, serial);
|
|
223
|
+
if (installed && installed !== want) {
|
|
224
|
+
console.log(chalk.yellow(` 已安装 ${installed} 包与待装 ${want} 包签名不同,先卸载`));
|
|
225
|
+
spawnCmd(adb, [...(serial ? ['-s', serial] : []), 'uninstall', appId], { encoding: 'utf-8' });
|
|
226
|
+
}
|
|
227
|
+
},
|
|
142
228
|
findHermesc = userProjectDir => {
|
|
143
229
|
const base = path.join(userProjectDir, 'node_modules', 'hermes-compiler', 'hermesc'), platform = process.platform;
|
|
144
230
|
let sub;
|
|
@@ -165,6 +251,7 @@ const ensureDir = dir => {
|
|
|
165
251
|
import reactPlugin from '@eslint-react/eslint-plugin';
|
|
166
252
|
import reactHooks from 'eslint-plugin-react-hooks';
|
|
167
253
|
import prettier from 'eslint-config-prettier';
|
|
254
|
+
import chalk from 'chalk';
|
|
168
255
|
|
|
169
256
|
export default [
|
|
170
257
|
js.configs.recommended,
|
|
@@ -185,6 +272,6 @@ const ensureDir = dir => {
|
|
|
185
272
|
rules: tsPlugin.configs.recommended.rules }, prettier];`;
|
|
186
273
|
|
|
187
274
|
export {
|
|
188
|
-
ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged, run, runQuiet,
|
|
189
|
-
runSilent, findAdb, findHermesc, sanitizeFileName, buildEslintConfigSource
|
|
190
|
-
};
|
|
275
|
+
ensureDir, removeDir, isExcluded, copyDir, readJson, writeJson, writeIfChanged, copyFileIfChanged, spawnCmdAsync, run, runQuiet, runStream,
|
|
276
|
+
runSilent, findAdb, readInstallState, writeInstallState, getInstalledVariant, ensureInstallable, findHermesc, sanitizeFileName, buildEslintConfigSource
|
|
277
|
+
};
|