@easyrn/erpush-cli 0.0.1-alpha
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.
Potentially problematic release.
This version of @easyrn/erpush-cli might be problematic. Click here for more details.
- package/cli.js +2 -0
- package/index.js +29 -0
- package/package.json +26 -0
- package/src/api.js +294 -0
- package/src/commands.js +281 -0
- package/src/exitsig.js +96 -0
- package/src/pack.js +325 -0
- package/src/patch.js +288 -0
- package/src/shell.js +251 -0
- package/src/utils.js +939 -0
package/src/exitsig.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 代码
|
|
3
|
+
* https://github.com/tapjs/signal-exit
|
|
4
|
+
*
|
|
5
|
+
* The ISC License
|
|
6
|
+
* Copyright (c) 2015-2022 Benjamin Coe, Isaac Z. Schlueter, and Contributors
|
|
7
|
+
*
|
|
8
|
+
* 为了降低与 ReactNative 依赖模块的版本冲突, 这里参照 signal-exit 写一个简易的监听函数
|
|
9
|
+
*/
|
|
10
|
+
const EE = require('events');
|
|
11
|
+
const signals = [];
|
|
12
|
+
const emitter = new EE();
|
|
13
|
+
let processOk = isWin = processBound = null;
|
|
14
|
+
|
|
15
|
+
function checkProcess(){
|
|
16
|
+
if (null === processOk) {
|
|
17
|
+
processOk = (process && typeof process === 'object'
|
|
18
|
+
&& typeof process.platform === 'string'
|
|
19
|
+
&& typeof process.on === 'function'
|
|
20
|
+
&& typeof process.kill === 'function'
|
|
21
|
+
&& typeof process.removeListener === 'function');
|
|
22
|
+
}
|
|
23
|
+
return processOk;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function initSignals(){
|
|
27
|
+
if (signals.length) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
signals.push('SIGINT', 'SIGHUP', 'SIGABRT', 'SIGALRM', 'SIGTERM');
|
|
31
|
+
if (process.platform !== 'win32') {
|
|
32
|
+
signals.push('SIGVTALRM', 'SIGXCPU', 'SIGXFSZ', 'SIGUSR2', 'SIGTRAP', 'SIGSYS', 'SIGQUIT', 'SIGIOT')
|
|
33
|
+
}
|
|
34
|
+
if (process.platform === 'linux') {
|
|
35
|
+
signals.push('SIGIO', 'SIGPOLL', 'SIGPWR', 'SIGSTKFLT', 'SIGUNUSED')
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function bindListeners(){
|
|
40
|
+
if (processBound !== null) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (null === isWin) {
|
|
44
|
+
isWin = /^win/i.test(process.platform);
|
|
45
|
+
}
|
|
46
|
+
processBound = {};
|
|
47
|
+
const bindOn = (ev, cb) => {
|
|
48
|
+
process.on(ev, cb)
|
|
49
|
+
processBound[ev] = cb
|
|
50
|
+
};
|
|
51
|
+
bindOn('exit', (code) => {
|
|
52
|
+
emitter.emit('exit', code, null);
|
|
53
|
+
unbindListeners();
|
|
54
|
+
});
|
|
55
|
+
initSignals();
|
|
56
|
+
signals.forEach(sig => {
|
|
57
|
+
try {
|
|
58
|
+
bindOn(sig, () => {
|
|
59
|
+
emitter.emit('exit', null, sig);
|
|
60
|
+
unbindListeners();
|
|
61
|
+
if (isWin && sig === 'SIGHUP') {
|
|
62
|
+
sig = 'SIGINT'
|
|
63
|
+
}
|
|
64
|
+
process.kill(process.pid, sig)
|
|
65
|
+
})
|
|
66
|
+
} catch {}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function unbindListeners(){
|
|
71
|
+
if (!processBound) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
for (let ev in processBound) {
|
|
75
|
+
process.removeListener(ev, processBound[ev])
|
|
76
|
+
}
|
|
77
|
+
processBound = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function onProcessExit(cb){
|
|
81
|
+
if (!checkProcess()) {
|
|
82
|
+
return () => {};
|
|
83
|
+
}
|
|
84
|
+
bindListeners();
|
|
85
|
+
const ev = 'exit';
|
|
86
|
+
const remove = function () {
|
|
87
|
+
emitter.removeListener(ev, cb)
|
|
88
|
+
if (emitter.listeners('exit').length === 0) {
|
|
89
|
+
unbindListeners();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
emitter.on(ev, cb)
|
|
93
|
+
return remove;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = onProcessExit;
|
package/src/pack.js
ADDED
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const {execSync} = require("child_process");
|
|
5
|
+
const {
|
|
6
|
+
CInfo, CError, errMsg,
|
|
7
|
+
fileExist, ensureDirSync, emptyDirSync,
|
|
8
|
+
packIpa, packZip, getRNVersion, execCommand
|
|
9
|
+
} = require('./utils');
|
|
10
|
+
|
|
11
|
+
// Hermes 平台类型
|
|
12
|
+
function getHermesOSBin() {
|
|
13
|
+
const plat = os.platform();
|
|
14
|
+
switch (plat) {
|
|
15
|
+
case 'win32':
|
|
16
|
+
return 'win64-bin';
|
|
17
|
+
case 'darwin':
|
|
18
|
+
return 'osx-bin';
|
|
19
|
+
case 'linux':
|
|
20
|
+
return 'linux64-bin';
|
|
21
|
+
default:
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 获取 Android gradle project.ext.react 配置
|
|
27
|
+
function gradleConfig(cwd) {
|
|
28
|
+
const gradleStr = fs.readFileSync(path.join(cwd, 'android/app/build.gradle')).toString();
|
|
29
|
+
const match = gradleStr.replace(
|
|
30
|
+
/\/\*[\s\S]*?\*\/|\/\/.*/g, ''
|
|
31
|
+
).match(
|
|
32
|
+
/project\.ext\.react\s*=\s*\[([^\]]*)\]/
|
|
33
|
+
);
|
|
34
|
+
return match ? match[1].split(',') : [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 确认是否启用了 Hermes 引擎
|
|
38
|
+
function isHermesEnable(cwd, platform) {
|
|
39
|
+
try {
|
|
40
|
+
if ('android' === platform) {
|
|
41
|
+
return gradleConfig(cwd).some(
|
|
42
|
+
line => /\benableHermes\s*:\s*true/.test(line)
|
|
43
|
+
);
|
|
44
|
+
} else if (platform === 'ios') {
|
|
45
|
+
const podPath = path.join(cwd, 'ios', 'Podfile');
|
|
46
|
+
const podStr = fileExist(podPath) ? fs.readFileSync(podPath).toString() : null;
|
|
47
|
+
return podStr && /\n[^#]*:?\bhermes_enabled\s*(=>|:)\s*true/.test(podStr);
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 获取自定义的 Hermes 引擎
|
|
54
|
+
function getGradleHermes(cwd, platform) {
|
|
55
|
+
if (platform && platform !== 'android') {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
let hermesCommand;
|
|
59
|
+
gradleConfig(cwd).some(line => {
|
|
60
|
+
const match = line.match(/\bhermesCommand\s*:\s*['|"](.*)['|"]/);
|
|
61
|
+
if (!match) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
hermesCommand = match[1];
|
|
65
|
+
return true;
|
|
66
|
+
});
|
|
67
|
+
if (!hermesCommand || hermesCommand.indexOf('%OS-BIN%') < 0) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return hermesCommand;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 获取 Hermes 执行文件位置, 参考 react 的代码逻辑
|
|
74
|
+
// @see https://github.com/facebook/react-native/blob/master/react.gradle
|
|
75
|
+
function getHermesCommond(cwd, platform) {
|
|
76
|
+
try {
|
|
77
|
+
const bin = getHermesOSBin();
|
|
78
|
+
if (!bin) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
const ext = 'win64-bin' === bin ? '.exe' : '';
|
|
82
|
+
// 自定义
|
|
83
|
+
const gradleHermes = getGradleHermes(cwd, platform);
|
|
84
|
+
if (gradleHermes) {
|
|
85
|
+
return path.join(cwd, 'android/app/', gradleHermes.replaceAll("%OS-BIN%", bin) + ext);
|
|
86
|
+
}
|
|
87
|
+
// react-native >= 0.69
|
|
88
|
+
let hermesPath = path.join(
|
|
89
|
+
cwd, "node_modules", "react-native", "sdks", "hermesc", bin, 'hermesc' + ext
|
|
90
|
+
);
|
|
91
|
+
if (fileExist(hermesPath)) {
|
|
92
|
+
return hermesPath;
|
|
93
|
+
}
|
|
94
|
+
// react-native >= 0.63
|
|
95
|
+
hermesPath = path.join(cwd, "node_modules", "hermes-engine", bin, 'hermesc' + ext);
|
|
96
|
+
if (fileExist(hermesPath)) {
|
|
97
|
+
return hermesPath;
|
|
98
|
+
}
|
|
99
|
+
// react-native >= 0.2
|
|
100
|
+
hermesPath = path.join(cwd, "node_modules", "hermes-engine", bin, 'hermes' + ext);
|
|
101
|
+
if (fileExist(hermesPath)) {
|
|
102
|
+
return hermesPath;
|
|
103
|
+
}
|
|
104
|
+
// react-native old
|
|
105
|
+
hermesPath = path.join(cwd, "node_modules", "hermesvm", bin, 'hermes' + ext);
|
|
106
|
+
if (fileExist(hermesPath)) {
|
|
107
|
+
return hermesPath;
|
|
108
|
+
}
|
|
109
|
+
return null
|
|
110
|
+
} catch(e) {}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
生成 android release apk
|
|
116
|
+
cwd: 运行目录
|
|
117
|
+
options: {target:"release", output:"filePath"}
|
|
118
|
+
target: 可指定编译类型
|
|
119
|
+
output: 相对于 cwd 的输出路径, 应包括 .apk 文件名
|
|
120
|
+
stdout: 信息输出的 stream
|
|
121
|
+
stderr: 异常输出的 stream
|
|
122
|
+
*/
|
|
123
|
+
async function makeApk(cwd, options, stdout, stderr) {
|
|
124
|
+
try {
|
|
125
|
+
const androidDir = path.join(cwd, 'android');
|
|
126
|
+
const gradlew = path.join(
|
|
127
|
+
androidDir,
|
|
128
|
+
process && process.platform && process.platform.startsWith('win') ? 'gradlew.bat' : './gradlew'
|
|
129
|
+
);
|
|
130
|
+
const {target='release', output} = options||{};
|
|
131
|
+
const args = ['assemble' + target.charAt(0).toUpperCase() + target.slice(1)];
|
|
132
|
+
await execCommand(gradlew, args, {
|
|
133
|
+
cwd:androidDir,
|
|
134
|
+
stdio:['pipe', stdout, stderr]
|
|
135
|
+
});
|
|
136
|
+
let apkFile = path.join(androidDir, `app/build/outputs/apk/${target}/app-${target}.apk`);
|
|
137
|
+
if(output) {
|
|
138
|
+
const destFile = path.join(cwd, output);
|
|
139
|
+
ensureDirSync(path.dirname(destFile));
|
|
140
|
+
fs.copyFileSync(apkFile, destFile);
|
|
141
|
+
apkFile = destFile;
|
|
142
|
+
}
|
|
143
|
+
stdout.write(CInfo + `saved to: ${apkFile}\n`);
|
|
144
|
+
return apkFile;
|
|
145
|
+
} catch(e) {
|
|
146
|
+
stderr.write(CError + errMsg(e) + "\n");
|
|
147
|
+
}
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
生成 ios release ipa
|
|
153
|
+
cwd: 运行目录
|
|
154
|
+
options: {target:"release", output:"filePath"}
|
|
155
|
+
target: 可指定编译类型
|
|
156
|
+
output: 相对于 cwd 的输出路径, 应包括 .ipa 文件名
|
|
157
|
+
stdout: 信息输出的 stream
|
|
158
|
+
stderr: 异常输出的 stream
|
|
159
|
+
*/
|
|
160
|
+
async function makeIpa(cwd, options, stdout, stderr) {
|
|
161
|
+
try {
|
|
162
|
+
const iosDir = path.join(cwd, 'ios');
|
|
163
|
+
const iosFiles = fs.readdirSync(iosDir);
|
|
164
|
+
let buildFile, isWorkspace;
|
|
165
|
+
for (let i = iosFiles.length - 1; i >= 0; i--) {
|
|
166
|
+
const fileName = iosFiles[i];
|
|
167
|
+
const ext = path.extname(fileName);
|
|
168
|
+
if (ext === '.xcworkspace') {
|
|
169
|
+
buildFile = fileName;
|
|
170
|
+
isWorkspace = true;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (ext === '.xcodeproj') {
|
|
174
|
+
buildFile = fileName;
|
|
175
|
+
isWorkspace = false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (!buildFile) {
|
|
179
|
+
throw new Error(`Could not find Xcode project files in "${iosDir}" folder`);
|
|
180
|
+
}
|
|
181
|
+
const {target='release', output='build/output/app-'+target+'.ipa'} = options||{};
|
|
182
|
+
const scheme = path.basename(buildFile, path.extname(buildFile));
|
|
183
|
+
const buildArgs = [
|
|
184
|
+
'build',
|
|
185
|
+
isWorkspace ? '-workspace' : '-project',
|
|
186
|
+
buildFile,
|
|
187
|
+
'-configuration',
|
|
188
|
+
target.charAt(0).toUpperCase() + target.slice(1),
|
|
189
|
+
'-scheme',
|
|
190
|
+
scheme,
|
|
191
|
+
'-sdk',
|
|
192
|
+
`iphoneos`,
|
|
193
|
+
];
|
|
194
|
+
// 获取 .app 文件的保存路径
|
|
195
|
+
let buildTargetPath;
|
|
196
|
+
const buildSettings = execSync(
|
|
197
|
+
'xcodebuild ' + buildArgs.concat(['-showBuildSettings', '-json']).join(' '),
|
|
198
|
+
{cwd:iosDir, encoding: 'utf8'}
|
|
199
|
+
);
|
|
200
|
+
const settings = JSON.parse(buildSettings);
|
|
201
|
+
for (const i in settings) {
|
|
202
|
+
const wrapperExtension = settings[i].buildSettings.WRAPPER_EXTENSION;
|
|
203
|
+
if (wrapperExtension === 'app') {
|
|
204
|
+
buildTargetPath = settings[i].buildSettings.TARGET_BUILD_DIR +
|
|
205
|
+
'/' + settings[i].buildSettings.EXECUTABLE_FOLDER_PATH;
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (!buildTargetPath) {
|
|
210
|
+
throw new Error('Failed to get the target build directory.');
|
|
211
|
+
}
|
|
212
|
+
await execCommand('xcodebuild', buildArgs, {
|
|
213
|
+
cwd: iosDir,
|
|
214
|
+
stdio:['pipe', stdout, stderr]
|
|
215
|
+
});
|
|
216
|
+
const releaseIpa = path.join(cwd, output);
|
|
217
|
+
ensureDirSync(path.dirname(releaseIpa));
|
|
218
|
+
await packIpa(buildTargetPath, releaseIpa);
|
|
219
|
+
stdout.write(CInfo + `saved to: ${releaseIpa}\n`);
|
|
220
|
+
return releaseIpa;
|
|
221
|
+
} catch(e) {
|
|
222
|
+
stderr.write(CError + errMsg(e) + "\n");
|
|
223
|
+
}
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
生成全量的 jsBundle
|
|
229
|
+
cwd: 运行目录
|
|
230
|
+
options: 与 react-native bundle 命令参数相同, 如
|
|
231
|
+
{platform:"android", output:"dirPath", "entry-file":"index.js", ...}
|
|
232
|
+
支持参数可参见: https://github.com/react-native-community/cli/blob/main/docs/commands.md#bundle
|
|
233
|
+
但此处对支持参数有所修改
|
|
234
|
+
1. 新增 output 参数, 设置相对于 cwd 的目录的输出目录
|
|
235
|
+
不能使用 bundle-output, assets-dest 参数了, 使用 output 统一指定目录
|
|
236
|
+
2. 新增 save-name 参数, 用于指定最终输出的文件名
|
|
237
|
+
stdout: 信息输出的 stream
|
|
238
|
+
stderr: 异常输出的 stream
|
|
239
|
+
*/
|
|
240
|
+
async function makeBundle(cwd, options, stdout, stderr) {
|
|
241
|
+
try {
|
|
242
|
+
if (!options.platform) {
|
|
243
|
+
throw new Error('platform unspecified');
|
|
244
|
+
}
|
|
245
|
+
if (!options['output']) {
|
|
246
|
+
options['output'] = 'build';
|
|
247
|
+
}
|
|
248
|
+
if (!options['entry-file']) {
|
|
249
|
+
options['entry-file'] = 'index.js'
|
|
250
|
+
}
|
|
251
|
+
if (!('dev' in options)) {
|
|
252
|
+
options['dev'] = false;
|
|
253
|
+
}
|
|
254
|
+
if (!('reset-cache' in options)) {
|
|
255
|
+
options['reset-cache'] = true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
let saveName = options['save-name'];
|
|
259
|
+
if (saveName) {
|
|
260
|
+
delete options['save-name'];
|
|
261
|
+
} else {
|
|
262
|
+
saveName = `${options.platform}.${Date.now()}.ppk`;
|
|
263
|
+
}
|
|
264
|
+
const output = options['output'];
|
|
265
|
+
const bundleName = 'index.bundlejs';
|
|
266
|
+
const outputFolder = path.join(output, 'bundle', options.platform);
|
|
267
|
+
delete options['output'];
|
|
268
|
+
|
|
269
|
+
options['assets-dest'] = outputFolder;
|
|
270
|
+
options['bundle-output'] = path.join(outputFolder, bundleName);
|
|
271
|
+
emptyDirSync(path.resolve(cwd, outputFolder));
|
|
272
|
+
|
|
273
|
+
// 准备生成 bundle 的参数
|
|
274
|
+
const version = getRNVersion(cwd).version;
|
|
275
|
+
const args = [
|
|
276
|
+
path.join("node_modules", "react-native", "local-cli", "cli.js"),
|
|
277
|
+
"bundle"
|
|
278
|
+
];
|
|
279
|
+
if ('_' in options) {
|
|
280
|
+
args.push(...options._);
|
|
281
|
+
delete options._;
|
|
282
|
+
}
|
|
283
|
+
for (let k in options) {
|
|
284
|
+
args.push('--'+k, options[k])
|
|
285
|
+
}
|
|
286
|
+
stdout.write("Bundling with React Native version: " + version + "\n");
|
|
287
|
+
stdout.write('Running bundle command: node ' + args.join(' ') + "\n");
|
|
288
|
+
|
|
289
|
+
// 编译 bundle 并根据配置编译为 hermes 字节码
|
|
290
|
+
const stdio = ['pipe', stdout, stderr];
|
|
291
|
+
await execCommand('node', args, {cwd, stdio});
|
|
292
|
+
if (isHermesEnable(cwd, options.platform)) {
|
|
293
|
+
const hermesCommond = getHermesCommond(cwd, options.platform);
|
|
294
|
+
if (!hermesCommond) {
|
|
295
|
+
stderr.write(CError + "Hermes enabled, but not find hermes-engine\n");
|
|
296
|
+
} else {
|
|
297
|
+
stdout.write(CInfo + "Hermes enabled, now compiling to hermes bytecode\n");
|
|
298
|
+
const jsFile = options['bundle-output'];
|
|
299
|
+
const commandArgs = ['-emit-binary', '-out', jsFile, jsFile, '-O'];
|
|
300
|
+
if ('win32' === os.platform()) {
|
|
301
|
+
commandArgs.unshift('/c');
|
|
302
|
+
}
|
|
303
|
+
await execCommand(hermesCommond, commandArgs, {cwd, stdio})
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 打包为 zip 格式的 ppk 文件
|
|
308
|
+
const ppkDir = path.resolve(cwd, output, 'output');
|
|
309
|
+
const ppkFile = path.join(ppkDir, saveName);
|
|
310
|
+
stdout.write(CInfo + "make bundle success, packing\n");
|
|
311
|
+
ensureDirSync(ppkDir);
|
|
312
|
+
await packZip(options['assets-dest'], ppkFile);
|
|
313
|
+
stdout.write(CInfo + `saved to: ${ppkFile}\n`);
|
|
314
|
+
return ppkFile;
|
|
315
|
+
} catch(e) {
|
|
316
|
+
stderr.write(CError + errMsg(e) + "\n");
|
|
317
|
+
}
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
makeApk,
|
|
323
|
+
makeIpa,
|
|
324
|
+
makeBundle
|
|
325
|
+
};
|
package/src/patch.js
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const {ZipFile} = require('yazl');
|
|
4
|
+
const {
|
|
5
|
+
CInfo, CError, errMsg, fileExist, ensureDirSync,
|
|
6
|
+
getDiff, enumZipEntries, readZipEntireBuffer, saveZipFile
|
|
7
|
+
} = require('./utils');
|
|
8
|
+
|
|
9
|
+
function basename(file) {
|
|
10
|
+
const m = /^(.+\/)[^\/]+\/?$/.exec(file);
|
|
11
|
+
return m && m[1];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function transformIosPath(file) {
|
|
15
|
+
const match = /^Payload\/[^/]+\/(.+)$/.exec(file);
|
|
16
|
+
return match && match[1];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// 处理命令行输入参数
|
|
20
|
+
function resolveOption(cwd, options, commond) {
|
|
21
|
+
let originName = 'origin_file', nextName = 'new_bundle';
|
|
22
|
+
switch(commond) {
|
|
23
|
+
case 'diffapk':
|
|
24
|
+
originName = 'origin_apk';
|
|
25
|
+
break;
|
|
26
|
+
case 'diffipa':
|
|
27
|
+
originName = 'origin_ipa';
|
|
28
|
+
break;
|
|
29
|
+
case 'diffbundle':
|
|
30
|
+
originName = 'origin_bundle';
|
|
31
|
+
break;
|
|
32
|
+
default:
|
|
33
|
+
nextName = 'new_file';
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
let {origin, next, output, cmd} = options;
|
|
37
|
+
if (!origin || !next) {
|
|
38
|
+
const err = cmd ? `erpush ${commond} <${originName}> <${nextName}> [--output save_name]` : 'Argumets error';
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
next = path.resolve(cwd, next);
|
|
42
|
+
origin = path.resolve(cwd, origin);
|
|
43
|
+
if (!fileExist(origin)) {
|
|
44
|
+
throw originName + ' file not exist';
|
|
45
|
+
}
|
|
46
|
+
if (!fileExist(next)) {
|
|
47
|
+
throw nextName + ' file not exist';
|
|
48
|
+
}
|
|
49
|
+
if (!output) {
|
|
50
|
+
if (commond === 'diff') {
|
|
51
|
+
output = 'build/diff/diff-'+Date.now()+'.patch';
|
|
52
|
+
} else {
|
|
53
|
+
output = 'build/output/diff-'+Date.now()+'.'+(commond === 'diff' ? 'diff' : (commond == 'diffipa' ? 'ipa' : 'apk'))+'-patch';
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
output = path.resolve(cwd, output);
|
|
57
|
+
return {origin, next, output}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
工具函数, 生成两个文件的 diff 补丁包, 文件路径为相对于 cwd 的路径
|
|
62
|
+
cwd: 运行目录
|
|
63
|
+
options: {origin:"原始文件路径", next:"新文件路径", output:"patch生成路径"}
|
|
64
|
+
stdout: 信息输出的 stream
|
|
65
|
+
stderr: 异常输出的 stream
|
|
66
|
+
*/
|
|
67
|
+
async function diff(cwd, options, stdout, stderr) {
|
|
68
|
+
try {
|
|
69
|
+
const {origin, next, output} = resolveOption(cwd, options, 'diff');
|
|
70
|
+
ensureDirSync(path.dirname(output));
|
|
71
|
+
fs.writeFileSync(output, getDiff(
|
|
72
|
+
fs.readFileSync(origin),
|
|
73
|
+
fs.readFileSync(next)
|
|
74
|
+
));
|
|
75
|
+
stdout.write(CInfo + `saved to: ${output}\n`);
|
|
76
|
+
return output;
|
|
77
|
+
} catch (e) {
|
|
78
|
+
stderr.write(CError + errMsg(e) + "\n");
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
生成 bundle 相对于 apk/ipa 的全量更新包, 文件路径为相对于 cwd 的路径
|
|
85
|
+
cwd: 运行目录
|
|
86
|
+
options: {origin:"apk|ipa 路径", next:"新 bundle 路径", output:"patch保存路径"}
|
|
87
|
+
stdout: 信息输出的 stream
|
|
88
|
+
stderr: 异常输出的 stream
|
|
89
|
+
ios: 是否为 ios
|
|
90
|
+
客户端更新流程: 解压全量更新包 -> 根据 __diff.json 中的 copies 字段, 从安装包复制相关文件
|
|
91
|
+
*/
|
|
92
|
+
async function diffPackage(cwd, options, stdout, stderr, ios) {
|
|
93
|
+
try {
|
|
94
|
+
const originBundleName = ios ? 'main.jsbundle' : 'assets/index.android.bundle';
|
|
95
|
+
const {origin, next, output} = resolveOption(cwd, options, ios ? 'diffipa' : 'diffapk');
|
|
96
|
+
|
|
97
|
+
// 读取 apk 或 ipa 文件
|
|
98
|
+
let originSource;
|
|
99
|
+
const originMap = {};
|
|
100
|
+
const originEntries = {};
|
|
101
|
+
await enumZipEntries(origin, (entry, zip) => {
|
|
102
|
+
if (!entry.isDirectory) {
|
|
103
|
+
const file = ios ? transformIosPath(entry.fileName) : entry.fileName;
|
|
104
|
+
if (!file) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
// isFile
|
|
108
|
+
originEntries[file] = entry.hash;
|
|
109
|
+
originMap[entry.hash] = file;
|
|
110
|
+
// js bundle file
|
|
111
|
+
if (file === originBundleName) {
|
|
112
|
+
return readZipEntireBuffer(entry, zip).then(v => (originSource = v));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
originSource = originSource || Buffer.alloc(0);
|
|
117
|
+
|
|
118
|
+
// 读取 ppk 文件, 提取与 apk|ipa 的不同
|
|
119
|
+
const copies = {};
|
|
120
|
+
const zipfile = new ZipFile();
|
|
121
|
+
await enumZipEntries(next, (entry, nextZipfile) => {
|
|
122
|
+
// Directory
|
|
123
|
+
if (entry.isDirectory) {
|
|
124
|
+
zipfile.addEmptyDirectory(entry.fileName);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
// Bundle file
|
|
128
|
+
if (entry.fileName === 'index.bundlejs') {
|
|
129
|
+
stdout.write(CInfo + "Found bundle\n");
|
|
130
|
+
return readZipEntireBuffer(entry, nextZipfile).then(newSource => {
|
|
131
|
+
zipfile.addBuffer(getDiff(originSource, newSource), 'index.bundlejs.patch');
|
|
132
|
+
stdout.write(CInfo + "Make diff bundle success\n");
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// 该文件在 apk|ipa 中存在 && 路径一致, 无需打包, 标记为 copy, 从安装包中复制
|
|
136
|
+
if (originEntries[entry.fileName] === entry.hash) {
|
|
137
|
+
copies[entry.fileName] = '';
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
// 该文件在 apk|ipa 中存在 && 路径不一致, 无需打包, 标记 copy 文件在安装包的原路径
|
|
141
|
+
if (originMap[entry.hash]) {
|
|
142
|
+
copies[entry.fileName] = originMap[entry.hash];
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
// 新增的文件
|
|
146
|
+
return new Promise((resolve, reject) => {
|
|
147
|
+
nextZipfile.openReadStream(entry, function(err, readStream) {
|
|
148
|
+
if (err) {
|
|
149
|
+
return reject(err);
|
|
150
|
+
}
|
|
151
|
+
zipfile.addReadStream(readStream, entry.fileName);
|
|
152
|
+
resolve();
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
zipfile.addBuffer(Buffer.from(JSON.stringify({copies})), '__diff.json');
|
|
157
|
+
zipfile.end();
|
|
158
|
+
|
|
159
|
+
// save patch
|
|
160
|
+
ensureDirSync(path.dirname(output));
|
|
161
|
+
await saveZipFile(zipfile, output)
|
|
162
|
+
stdout.write(CInfo + `saved to: ${output}\n`);
|
|
163
|
+
return output;
|
|
164
|
+
} catch (e) {
|
|
165
|
+
stderr.write(CError + errMsg(e) + "\n");
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
生 新bundle 相对于 旧bundle 的增量更新包, 文件路径为相对于 cwd 的路径
|
|
172
|
+
cwd: 运行目录
|
|
173
|
+
options: {origin:"旧 bundle 路径", next:"新 bundle 路径", output:"patch保存路径"}
|
|
174
|
+
stdout: 信息输出的 stream
|
|
175
|
+
stderr: 异常输出的 stream
|
|
176
|
+
客户端更新时: 根据 copies 复制文件 -> 复制上一个除 deletes 之外的文件 -> 将新版本合并进去
|
|
177
|
+
*/
|
|
178
|
+
async function diffBundle(cwd, options, stdout, stderr) {
|
|
179
|
+
try {
|
|
180
|
+
const {origin, next, output} = resolveOption(cwd, options, 'diff');
|
|
181
|
+
|
|
182
|
+
// 读取 旧bundle 文件
|
|
183
|
+
let originSource;
|
|
184
|
+
const originMap = {};
|
|
185
|
+
const originEntries = {};
|
|
186
|
+
stdout.write(CInfo + `Read origin bundle: ${options.origin}\n`);
|
|
187
|
+
await enumZipEntries(origin, (entry, zipFile) => {
|
|
188
|
+
originEntries[entry.fileName] = entry;
|
|
189
|
+
if (entry.isDirectory) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
originMap[entry.hash] = entry.fileName;
|
|
193
|
+
// js bundle file
|
|
194
|
+
if (entry.fileName === 'index.bundlejs') {
|
|
195
|
+
return readZipEntireBuffer(entry, zipFile).then(v => (originSource = v));
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
originSource = originSource || Buffer.alloc(0);
|
|
199
|
+
|
|
200
|
+
// 读取 新Bundle 文件, 提取与 旧bundle 的不同
|
|
201
|
+
const copies = {};
|
|
202
|
+
const addedEntry = {};
|
|
203
|
+
const newEntries = {};
|
|
204
|
+
const dirsAdded = [];
|
|
205
|
+
const zipfile = new ZipFile();
|
|
206
|
+
const addEntry = (file) => {
|
|
207
|
+
if (!file || addedEntry[file]) {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const base = basename(file);
|
|
211
|
+
if (base) {
|
|
212
|
+
addEntry(base);
|
|
213
|
+
}
|
|
214
|
+
if (!dirsAdded.includes(file)) {
|
|
215
|
+
zipfile.addEmptyDirectory(file);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
stdout.write(CInfo + `Read next bundle: ${options.next}\n`);
|
|
219
|
+
await enumZipEntries(next, (entry, nextZipfile) => {
|
|
220
|
+
newEntries[entry.fileName] = entry;
|
|
221
|
+
// Directory
|
|
222
|
+
if (entry.isDirectory) {
|
|
223
|
+
if (!originEntries[entry.fileName]) {
|
|
224
|
+
addEntry(entry.fileName);
|
|
225
|
+
}
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
// Bundle file
|
|
229
|
+
if (entry.fileName === 'index.bundlejs') {
|
|
230
|
+
stdout.write(CInfo + "Found bundle\n");
|
|
231
|
+
return readZipEntireBuffer(entry, nextZipfile).then(newSource => {
|
|
232
|
+
zipfile.addBuffer(getDiff(originSource, newSource), 'index.bundlejs.patch');
|
|
233
|
+
stdout.write(CInfo + "Make diff bundle success\n");
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const originEntry = originEntries[entry.fileName];
|
|
237
|
+
// 旧版存在相同 hash 文件, 且路径相同, 跳过
|
|
238
|
+
if (originEntry && originEntry.hash === entry.hash) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
// 旧版存在相同 hash 文件, 但路径不同, 需复制
|
|
242
|
+
if (originMap[entry.hash]) {
|
|
243
|
+
const base = basename(entry.fileName);
|
|
244
|
+
if (!originEntries[base]) {
|
|
245
|
+
addEntry(base);
|
|
246
|
+
}
|
|
247
|
+
copies[entry.fileName] = originMap[entry.hash];
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// 新增的文件, 先添加文件夹, 再添加文件
|
|
251
|
+
addEntry(basename(entry.fileName));
|
|
252
|
+
return new Promise((resolve, reject) => {
|
|
253
|
+
nextZipfile.openReadStream(entry, function(err, readStream) {
|
|
254
|
+
if (err) {
|
|
255
|
+
return reject(err);
|
|
256
|
+
}
|
|
257
|
+
zipfile.addReadStream(readStream, entry.fileName);
|
|
258
|
+
resolve();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// 在 旧bundle 中存在, 而 新Bundel 中不存在的文件标记为 deletes
|
|
264
|
+
const deletes = {};
|
|
265
|
+
for (var k in originEntries) {
|
|
266
|
+
if (!newEntries[k]) {
|
|
267
|
+
deletes[k] = 1;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
zipfile.addBuffer(Buffer.from(JSON.stringify({ copies, deletes })), '__diff.json');
|
|
271
|
+
zipfile.end();
|
|
272
|
+
|
|
273
|
+
// save patch
|
|
274
|
+
ensureDirSync(path.dirname(output));
|
|
275
|
+
await saveZipFile(zipfile, output)
|
|
276
|
+
stdout.write(CInfo + `saved to: ${output}\n`);
|
|
277
|
+
return output;
|
|
278
|
+
} catch (e) {
|
|
279
|
+
stderr.write(CError + errMsg(e) + "\n");
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module.exports = {
|
|
285
|
+
diff,
|
|
286
|
+
diffPackage,
|
|
287
|
+
diffBundle,
|
|
288
|
+
};
|