@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/utils.js
ADDED
|
@@ -0,0 +1,939 @@
|
|
|
1
|
+
const os = require('os');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const yazl = require('yazl');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
const wcwidth = require('wcwidth');
|
|
7
|
+
const tough = require("tough-cookie");
|
|
8
|
+
const minimist = require('minimist');
|
|
9
|
+
const FormData = require('form-data');
|
|
10
|
+
const nodeFetch = require("node-fetch");
|
|
11
|
+
const {spawn} = require('child_process');
|
|
12
|
+
const onProcessExit = require('./exitsig');
|
|
13
|
+
const {open:openZipFile} = require('yauzl');
|
|
14
|
+
|
|
15
|
+
const runtimeCache = {};
|
|
16
|
+
const supportPlatforms = ['android', 'ios'];
|
|
17
|
+
const MakeDiff = (() => {
|
|
18
|
+
try {
|
|
19
|
+
return require('erpush').diff;
|
|
20
|
+
} catch (e) {
|
|
21
|
+
return e;
|
|
22
|
+
}
|
|
23
|
+
})();
|
|
24
|
+
|
|
25
|
+
// 判断 版本号 a 是否大于 版本号 b
|
|
26
|
+
function compareVersion(a, b){
|
|
27
|
+
a = a.split('.').map(x => parseInt(x, 10))
|
|
28
|
+
b = b.split('.').map(x => parseInt(x, 10))
|
|
29
|
+
return a[0] > b[0] || (a[0] === b[0] && (a[1] > b[1] || (a[1] === b[1] && a[2] >= b[2])))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
class SimpleSpinner {
|
|
33
|
+
constructor({stream, text, frames, color='cyan', interval=100} = {}){
|
|
34
|
+
if (!stream && process) {
|
|
35
|
+
stream = process.stdout;
|
|
36
|
+
}
|
|
37
|
+
this._text = text;
|
|
38
|
+
this._stream = stream;
|
|
39
|
+
this._isTTY = isTtyStream(stream);
|
|
40
|
+
this._cursorHide = false;
|
|
41
|
+
this._restoreListend = false;
|
|
42
|
+
this._linesToClear = 0;
|
|
43
|
+
this._spinning = false;
|
|
44
|
+
this._timerId = null;
|
|
45
|
+
this._frameIndex = 0;
|
|
46
|
+
this._frameColor = color;
|
|
47
|
+
this._interval = interval;
|
|
48
|
+
this._spinnerFrames = frames;
|
|
49
|
+
}
|
|
50
|
+
intLineCount() {
|
|
51
|
+
let lineCount = 0;
|
|
52
|
+
const columns = this._stream.columns || 80;
|
|
53
|
+
for (const line of ('--' + removeStrAnsi(this._text)).split('\n')) {
|
|
54
|
+
lineCount += Math.max(1, Math.ceil(wcwidth(line) / columns));
|
|
55
|
+
}
|
|
56
|
+
this._lineCount = lineCount;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
toggleCursor(hide){
|
|
60
|
+
const self = this;
|
|
61
|
+
hide = Boolean(hide);
|
|
62
|
+
if (hide !== self._cursorHide && self._isTTY) {
|
|
63
|
+
self._cursorHide = hide;
|
|
64
|
+
self._stream.write(hide ? '\u001B[?25l' : '\u001B[?25h');
|
|
65
|
+
// 隐藏光标后, 为防止因异常退出, 在进程结束后显示光标
|
|
66
|
+
if (hide && !self._restoreListend) {
|
|
67
|
+
self._restoreListend = true;
|
|
68
|
+
onProcessExit(() => {
|
|
69
|
+
self.toggleCursor(false)
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return this;
|
|
74
|
+
}
|
|
75
|
+
start(){
|
|
76
|
+
if (!this._isTTY) {
|
|
77
|
+
if (this._text && this._stream) {
|
|
78
|
+
this._stream.write(`- ${this._text}\n`);
|
|
79
|
+
}
|
|
80
|
+
return this;
|
|
81
|
+
}
|
|
82
|
+
if (this._spinning) {
|
|
83
|
+
return this;
|
|
84
|
+
}
|
|
85
|
+
this._spinning = true;
|
|
86
|
+
this.intLineCount().toggleCursor(true).render();
|
|
87
|
+
this._timerId = setInterval(this.render.bind(this), this._interval);
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
90
|
+
stop(){
|
|
91
|
+
if (!this._spinning) {
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
if (this._timerId) {
|
|
95
|
+
clearInterval(this._timerId);
|
|
96
|
+
this._timerId = null;
|
|
97
|
+
}
|
|
98
|
+
this._frameIndex = 0;
|
|
99
|
+
return this.clear().toggleCursor(false);
|
|
100
|
+
}
|
|
101
|
+
render(){
|
|
102
|
+
this.clear();
|
|
103
|
+
this._stream.write(this.frame());
|
|
104
|
+
this._linesToClear = this._lineCount;
|
|
105
|
+
return this;
|
|
106
|
+
}
|
|
107
|
+
frame(){
|
|
108
|
+
if (!this._spinnerFrames) {
|
|
109
|
+
this._spinnerFrames = supportUnicode()
|
|
110
|
+
? ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
111
|
+
: ["-", "\\", "|", "/"]
|
|
112
|
+
}
|
|
113
|
+
let frame = this._spinnerFrames[this._frameIndex];
|
|
114
|
+
if (this._frameColor) {
|
|
115
|
+
frame = color(frame, this._frameColor)
|
|
116
|
+
}
|
|
117
|
+
this._frameIndex = ++this._frameIndex % this._spinnerFrames.length;
|
|
118
|
+
return frame + ' ' + this._text;
|
|
119
|
+
}
|
|
120
|
+
clear(){
|
|
121
|
+
if (this._linesToClear > 0) {
|
|
122
|
+
this._stream.cursorTo(0);
|
|
123
|
+
for (let index = 0; index < this._linesToClear; index++) {
|
|
124
|
+
if (index > 0) {
|
|
125
|
+
this._stream.moveCursor(0, -1);
|
|
126
|
+
}
|
|
127
|
+
this._stream.clearLine(1);
|
|
128
|
+
}
|
|
129
|
+
this._linesToClear = 0;
|
|
130
|
+
}
|
|
131
|
+
return this;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 解析 process 参数
|
|
136
|
+
function parseProcess(p){
|
|
137
|
+
const _ = p.env._||null;
|
|
138
|
+
const npx = _ && _.endsWith('/npx');
|
|
139
|
+
const options = minimist(p.argv.slice(2));
|
|
140
|
+
const args = options._;
|
|
141
|
+
const name = args.shift();
|
|
142
|
+
delete options._;
|
|
143
|
+
return {npx, name, args, options};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// stream 是否可交互
|
|
147
|
+
function isTtyStream(stream){
|
|
148
|
+
return Boolean(
|
|
149
|
+
stream && stream.isTTY && (!process || !process.env || (
|
|
150
|
+
process.env.TERM !== 'dumb' && !('CI' in process.env)
|
|
151
|
+
))
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 当前是否支持 ansi 字符 (彩色字体)
|
|
156
|
+
// https://github.com/sindresorhus/is-unicode-supported/blob/main/index.js
|
|
157
|
+
function supportUnicode() {
|
|
158
|
+
if (runtimeCache._supportUnicode_ === undefined) {
|
|
159
|
+
runtimeCache._supportUnicode_ = process ? (() => {
|
|
160
|
+
const {env = {}} = process;
|
|
161
|
+
return process.platform === 'win32'
|
|
162
|
+
? Boolean(env.CI)
|
|
163
|
+
|| Boolean(env.WT_SESSION) // Windows Terminal
|
|
164
|
+
|| Boolean(env.TERMINUS_SUBLIME) // Terminus (<0.2.27)
|
|
165
|
+
|| env.ConEmuTask === '{cmd::Cmder}' // ConEmu and cmder
|
|
166
|
+
|| env.TERM_PROGRAM === 'Terminus-Sublime'
|
|
167
|
+
|| env.TERM_PROGRAM === 'vscode'
|
|
168
|
+
|| env.TERM === 'xterm-256color'
|
|
169
|
+
|| env.TERM === 'alacritty'
|
|
170
|
+
|| env.TERMINAL_EMULATOR === 'JetBrains-JediTerm'
|
|
171
|
+
: env.TERM !== 'linux';
|
|
172
|
+
})() : false;
|
|
173
|
+
}
|
|
174
|
+
return runtimeCache._supportUnicode_;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ansi 正则 https://github.com/chalk/ansi-regex/blob/main/index.js
|
|
178
|
+
function ansiRegex({onlyFirst = false} = {}) {
|
|
179
|
+
const key = '_ansiRegexExp_' + (onlyFirst ? 'f' : 'g');
|
|
180
|
+
if (!runtimeCache[key]) {
|
|
181
|
+
const pattern = [
|
|
182
|
+
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
|
|
183
|
+
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))'
|
|
184
|
+
].join('|');
|
|
185
|
+
runtimeCache[key] = new RegExp(pattern, onlyFirst ? undefined : 'g');
|
|
186
|
+
}
|
|
187
|
+
return runtimeCache[key];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 去除字符串中所有 Ansi 字符
|
|
191
|
+
function removeStrAnsi(str){
|
|
192
|
+
return str.replace(ansiRegex(), '')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 美化文字
|
|
196
|
+
* color(str, 'red', 1)
|
|
197
|
+
* decorated(str, {
|
|
198
|
+
* color:'red',
|
|
199
|
+
* bright:0,
|
|
200
|
+
* bg:'green',
|
|
201
|
+
* bgBright:1,
|
|
202
|
+
* bold:1,
|
|
203
|
+
* underline:1,
|
|
204
|
+
* })
|
|
205
|
+
*/
|
|
206
|
+
const _decoratedColors = [
|
|
207
|
+
'black', 'red', 'green', 'yellow',
|
|
208
|
+
'blue', 'magenta', 'cyan', 'white'
|
|
209
|
+
];
|
|
210
|
+
const _decoratedModifier = {
|
|
211
|
+
'bold': [1, 22],
|
|
212
|
+
'dim': [2, 22],
|
|
213
|
+
'italic': [3, 23],
|
|
214
|
+
'underline': [4, 24],
|
|
215
|
+
'blinking': [5, 25],
|
|
216
|
+
'strikethrough': [9, 29],
|
|
217
|
+
};
|
|
218
|
+
const CInfo = color('Info:', 'cyan', true) + ' ';
|
|
219
|
+
const CWarning = color('Warning:', 'magenta', true) + ' ';
|
|
220
|
+
const CError = color('Error:', 'red', true) + ' ';
|
|
221
|
+
|
|
222
|
+
function color(str, color, bold){
|
|
223
|
+
return decorated(str, {color, bold})
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function decorated(str, options){
|
|
227
|
+
const {color, bg, bright=0, bgBright=0, ...modifier} = options||{};
|
|
228
|
+
if (!supportUnicode()) {
|
|
229
|
+
return str;
|
|
230
|
+
}
|
|
231
|
+
const set = [], unset = [];
|
|
232
|
+
const updateColor = (colorCode, isBright, isBg) => {
|
|
233
|
+
const colorType = typeof colorCode;
|
|
234
|
+
if (colorType !== 'number') {
|
|
235
|
+
const colorIndex = 'string' === colorType
|
|
236
|
+
? _decoratedColors.indexOf(colorCode.toLowerCase()) : -1;
|
|
237
|
+
if (colorIndex > -1) {
|
|
238
|
+
colorCode = colorIndex + (isBright ? 90 : 30) + (isBg ? 10 : 0);
|
|
239
|
+
} else {
|
|
240
|
+
colorCode = null;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (colorCode) {
|
|
244
|
+
set.push(colorCode);
|
|
245
|
+
unset.push(isBg ? 49 : 39);
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
for (let k in modifier) {
|
|
249
|
+
if (k in _decoratedModifier && modifier[k]) {
|
|
250
|
+
const [open, close] = _decoratedModifier[k];
|
|
251
|
+
set.push(open);
|
|
252
|
+
unset.push(close);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
updateColor(color, bright, false);
|
|
256
|
+
updateColor(bg, bgBright, true);
|
|
257
|
+
if (!set.length) {
|
|
258
|
+
return str;
|
|
259
|
+
}
|
|
260
|
+
return "\x1b[" +(set.join(';'))+ "m" +str+ "\x1b[" +(unset.join(';'))+ "m";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// error 转 string
|
|
264
|
+
function errMsg(e){
|
|
265
|
+
if (typeof e === 'object' && 'message' in e) {
|
|
266
|
+
return e.message
|
|
267
|
+
}
|
|
268
|
+
return e;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// 创建 SimpleSpinner 对象
|
|
272
|
+
function makeSpinner(options) {
|
|
273
|
+
return new SimpleSpinner(options)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// 转 Array 为 String 表格, 自动对齐
|
|
277
|
+
function makeTable(data) {
|
|
278
|
+
const rows = [];
|
|
279
|
+
const rowWidth = [];
|
|
280
|
+
data.forEach(item => {
|
|
281
|
+
if (!Array.isArray(item)) {
|
|
282
|
+
rows.push(null)
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const row = [];
|
|
286
|
+
item.forEach((str, index) => {
|
|
287
|
+
const width = wcwidth(String(removeStrAnsi(str)));
|
|
288
|
+
row.push(width);
|
|
289
|
+
if (!rowWidth[index] || rowWidth[index] < width) {
|
|
290
|
+
rowWidth[index] = width;
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
rows.push(row)
|
|
294
|
+
})
|
|
295
|
+
const txts = [];
|
|
296
|
+
const split = '-'.repeat(rowWidth.reduce((a, b) => a + b) + rowWidth.length * 2);
|
|
297
|
+
data.forEach((item, n) => {
|
|
298
|
+
if (!Array.isArray(item)) {
|
|
299
|
+
txts.push(split)
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
let line = '';
|
|
303
|
+
const widths = rows[n];
|
|
304
|
+
item.forEach((str, index) => {
|
|
305
|
+
line += String(str) + ' '.repeat(rowWidth[index] - widths[index] + 2)
|
|
306
|
+
});
|
|
307
|
+
txts.push(line);
|
|
308
|
+
})
|
|
309
|
+
return txts.join("\n")
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// 文件操作
|
|
313
|
+
function fileExist(path, dir){
|
|
314
|
+
try {
|
|
315
|
+
const f = fs.lstatSync(path);
|
|
316
|
+
return dir ? f.isDirectory() : f.isFile()
|
|
317
|
+
} catch(e) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function dirExist(path){
|
|
323
|
+
return fileExist(path, true)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function ensureDirSync(dir, options){
|
|
327
|
+
if (runtimeCache._nodeVersion10_12_0 === undefined) {
|
|
328
|
+
runtimeCache._nodeVersion10_12_0 = compareVersion(process.versions.node, '10.12.0');
|
|
329
|
+
}
|
|
330
|
+
dir = path.resolve(dir);
|
|
331
|
+
options = {mode: 0o777, ...options};
|
|
332
|
+
if (runtimeCache._nodeVersion10_12_0) {
|
|
333
|
+
fs.mkdirSync(dir, {
|
|
334
|
+
mode: options.mode,
|
|
335
|
+
recursive: true
|
|
336
|
+
})
|
|
337
|
+
} else {
|
|
338
|
+
mkraf(dir, options.mode);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function mkraf(dir, mode){
|
|
343
|
+
try {
|
|
344
|
+
fs.mkdirSync(dir, mode);
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (error.code === 'EPERM') {
|
|
347
|
+
throw error
|
|
348
|
+
}
|
|
349
|
+
if (error.code === 'ENOENT') {
|
|
350
|
+
const dirname = path.dirname(dir);
|
|
351
|
+
if (dirname === dir) {
|
|
352
|
+
const error = new Error(`operation not permitted, mkdir '${dir}'`)
|
|
353
|
+
error.code = 'EPERM'
|
|
354
|
+
error.errno = -4048
|
|
355
|
+
error.path = dir
|
|
356
|
+
error.syscall = 'mkdir'
|
|
357
|
+
throw error
|
|
358
|
+
}
|
|
359
|
+
if (error.message.includes('null bytes')) {
|
|
360
|
+
throw error
|
|
361
|
+
}
|
|
362
|
+
mkraf(dirname)
|
|
363
|
+
return mkraf(dir)
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
if (!fs.statSync(dir).isDirectory()) {
|
|
367
|
+
throw new Error('The path is not a directory')
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
throw error
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function emptyDirSync(dir){
|
|
376
|
+
let items
|
|
377
|
+
try {
|
|
378
|
+
items = fs.readdirSync(dir)
|
|
379
|
+
} catch {
|
|
380
|
+
return ensureDirSync(dir)
|
|
381
|
+
}
|
|
382
|
+
items.forEach(item => {
|
|
383
|
+
removeDirSync(path.join(dir, item));
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function removeDirSync(dir){
|
|
388
|
+
return fs.rmSync
|
|
389
|
+
? fs.rmSync(dir, { recursive: true, force: true }) : rimraf(dir)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function rimraf(dir) {
|
|
393
|
+
if (!dirExist(dir)) {
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
fs.readdirSync(dir).forEach(function(entry) {
|
|
397
|
+
var subpath = path.join(dir, entry);
|
|
398
|
+
if (fs.lstatSync(subpath).isDirectory()) {
|
|
399
|
+
rimraf(subpath);
|
|
400
|
+
} else {
|
|
401
|
+
fs.unlinkSync(subpath);
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
fs.rmdirSync(dir);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function fileMd5(filename) {
|
|
408
|
+
let fd;
|
|
409
|
+
try {
|
|
410
|
+
fd = fs.openSync(filename, 'r')
|
|
411
|
+
} catch (e) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
const BUFFER_SIZE = 8192;
|
|
415
|
+
const hash = crypto.createHash('md5')
|
|
416
|
+
const buffer = Buffer.alloc(BUFFER_SIZE)
|
|
417
|
+
try {
|
|
418
|
+
let bytesRead
|
|
419
|
+
do {
|
|
420
|
+
bytesRead = fs.readSync(fd, buffer, 0, BUFFER_SIZE)
|
|
421
|
+
hash.update(buffer.subarray(0, bytesRead))
|
|
422
|
+
} while (bytesRead === BUFFER_SIZE)
|
|
423
|
+
} finally {
|
|
424
|
+
fs.closeSync(fd)
|
|
425
|
+
}
|
|
426
|
+
return hash.digest('hex')
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
function getCacheDir() {
|
|
430
|
+
const dir = path.join(os.homedir(), '.config', 'erpush');
|
|
431
|
+
ensureDirSync(dir);
|
|
432
|
+
return dir;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 获取 oldBuf, newBuf 的 diff buffer
|
|
436
|
+
function getDiff(oldBuf, newBuf) {
|
|
437
|
+
if (typeof MakeDiff !== 'function') {
|
|
438
|
+
const message = 'Load "erpush" module failed.';
|
|
439
|
+
if (MakeDiff instanceof Error) {
|
|
440
|
+
MakeDiff.message = message + "\n" + MakeDiff.message;
|
|
441
|
+
throw MakeDiff;
|
|
442
|
+
}
|
|
443
|
+
throw new Error(message);
|
|
444
|
+
}
|
|
445
|
+
return MakeDiff(oldBuf, newBuf);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// 打包 xcode 编译的 .app 为 .ipa 文件
|
|
449
|
+
function packIpa(source, dest){
|
|
450
|
+
return packDirToZip(source, dest, true);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// 打包 dir 为 zip 文件, 保存到 save 路径
|
|
454
|
+
function packZip(dir, save) {
|
|
455
|
+
return packDirToZip(dir, save);
|
|
456
|
+
}
|
|
457
|
+
function packDirToZip(dir, save, ipa){
|
|
458
|
+
return new Promise(function (resolve, reject) {
|
|
459
|
+
const zip = new yazl.ZipFile();
|
|
460
|
+
let rel = '';
|
|
461
|
+
if (ipa) {
|
|
462
|
+
const appName = path.basename(dir);
|
|
463
|
+
rel = 'Payload/' + appName;
|
|
464
|
+
}
|
|
465
|
+
addRecursive(zip, dir, rel);
|
|
466
|
+
zip.end();
|
|
467
|
+
zip.on('error', function (err) {
|
|
468
|
+
fs.unlinkSync(save)
|
|
469
|
+
reject(err);
|
|
470
|
+
});
|
|
471
|
+
zip.outputStream.pipe(fs.createWriteStream(save)).on('close', function () {
|
|
472
|
+
resolve();
|
|
473
|
+
});
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function addRecursive(zip, root, rel) {
|
|
477
|
+
if (rel) {
|
|
478
|
+
rel += '/';
|
|
479
|
+
zip.addEmptyDirectory(rel);
|
|
480
|
+
}
|
|
481
|
+
const childs = fs.readdirSync(root);
|
|
482
|
+
for (const name of childs) {
|
|
483
|
+
if (name === '.' || name === '..') {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const fullPath = path.join(root, name);
|
|
487
|
+
const stat = fs.statSync(fullPath);
|
|
488
|
+
if (stat.isFile()) {
|
|
489
|
+
zip.addFile(fullPath, rel + name);
|
|
490
|
+
} else if (stat.isDirectory()) {
|
|
491
|
+
addRecursive(zip, fullPath, rel + name);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** 枚举 Zip 内所有文件, callback(entry, zipfile), 若不指定 basic 为 true
|
|
497
|
+
* 文件属性 entry 会新增 isDirectory/hash 两个字段, 原 entry 内有一个 crc32 的 hash 值
|
|
498
|
+
* 但考虑到 crc32 的碰撞概率略大, 所以此处额外计算一个新的 hash 值用于校验
|
|
499
|
+
*/
|
|
500
|
+
function enumZipEntries(zipFn, callback, basic) {
|
|
501
|
+
return new Promise((resolve, reject) => {
|
|
502
|
+
openZipFile(zipFn, {lazyEntries: true}, (err, zipfile) => {
|
|
503
|
+
if (err) {
|
|
504
|
+
reject(err);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
zipfile.on('end', resolve);
|
|
508
|
+
zipfile.on('error', reject);
|
|
509
|
+
zipfile.on('entry', entry => {
|
|
510
|
+
getZipEntryHash(zipfile, entry, basic).then(entryPlus => {
|
|
511
|
+
return Promise.resolve(callback(entryPlus, zipfile))
|
|
512
|
+
}).then(() => zipfile.readEntry())
|
|
513
|
+
});
|
|
514
|
+
zipfile.readEntry();
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
function getZipEntryHash(zipfile, entry, basic) {
|
|
519
|
+
return new Promise((resolve, reject) => {
|
|
520
|
+
if (basic) {
|
|
521
|
+
resolve(entry);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
entry.isDirectory = /\/$/.test(entry.fileName);
|
|
525
|
+
if (entry.isDirectory) {
|
|
526
|
+
entry.hash = null;
|
|
527
|
+
resolve(entry);
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
zipfile.openReadStream(entry, function(err, readStream) {
|
|
531
|
+
if (err) {
|
|
532
|
+
reject(err);
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
const hash = crypto.createHash('md5').setEncoding('hex');
|
|
536
|
+
readStream.on("end", function() {
|
|
537
|
+
hash.end();
|
|
538
|
+
entry.hash = hash.read();
|
|
539
|
+
resolve(entry);
|
|
540
|
+
});
|
|
541
|
+
readStream.pipe(hash);
|
|
542
|
+
});
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 获取 enumZipEntries 枚举的单个文件 buffer
|
|
547
|
+
function readZipEntireBuffer(entry, zipfile) {
|
|
548
|
+
const buffers = [];
|
|
549
|
+
return new Promise((resolve, reject) => {
|
|
550
|
+
zipfile.openReadStream(entry, (err, stream) => {
|
|
551
|
+
if (err) {
|
|
552
|
+
reject(err);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
stream.pipe({
|
|
556
|
+
write(chunk) {
|
|
557
|
+
buffers.push(chunk);
|
|
558
|
+
},
|
|
559
|
+
end() {
|
|
560
|
+
resolve(Buffer.concat(buffers));
|
|
561
|
+
},
|
|
562
|
+
prependListener() {},
|
|
563
|
+
on() {},
|
|
564
|
+
once() {},
|
|
565
|
+
emit() {},
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// 保存 ZipFile 对象为文件
|
|
572
|
+
function saveZipFile(zipfile, output) {
|
|
573
|
+
ensureDirSync(path.dirname(output));
|
|
574
|
+
return new Promise(function (resolve, reject) {
|
|
575
|
+
zipfile.on('error', err => {
|
|
576
|
+
fs.unlinkSync(output)
|
|
577
|
+
reject(err);
|
|
578
|
+
});
|
|
579
|
+
zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function() {
|
|
580
|
+
resolve();
|
|
581
|
+
});
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// 获取字符串共同前缀
|
|
586
|
+
// https://www.geeksforgeeks.org/longest-common-prefix-using-binary-search/
|
|
587
|
+
function getCommonPrefix(arr) {
|
|
588
|
+
let low = 0, high = 0;
|
|
589
|
+
arr.forEach(s => {
|
|
590
|
+
if (!high || s.length < high) {
|
|
591
|
+
high = s.length
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
let prefix = '';
|
|
595
|
+
const first = arr[0];
|
|
596
|
+
while (low <= high) {
|
|
597
|
+
const mid = Math.floor(low + (high - low) / 2);
|
|
598
|
+
const interrupt = arr.some(r => {
|
|
599
|
+
for (let i = low; i <= mid; i++) {
|
|
600
|
+
if (r[i] !== first[i]) {
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
if (interrupt) {
|
|
606
|
+
high = mid - 1;
|
|
607
|
+
} else {
|
|
608
|
+
prefix += first.substr(low, mid-low+1);
|
|
609
|
+
low = mid + 1;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
return prefix;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// 在 rootDir 目录查找有共同前缀 prefix 的文件(夹)
|
|
616
|
+
function getCommonPath(rootDir, prefix) {
|
|
617
|
+
const dash = prefix.lastIndexOf('/');
|
|
618
|
+
const curPad = prefix.substr(dash + 1);
|
|
619
|
+
const curDir = dash !== -1 ? prefix.substring(0, dash + 1) : '';
|
|
620
|
+
let completions;
|
|
621
|
+
try {
|
|
622
|
+
completions = fs.readdirSync(
|
|
623
|
+
path.join(rootDir, curDir),
|
|
624
|
+
{withFileTypes:true}
|
|
625
|
+
).map(r =>
|
|
626
|
+
(curPad ? '' : prefix) + r.name + (r.isDirectory() ? '/' : '')
|
|
627
|
+
);
|
|
628
|
+
} catch(e) {
|
|
629
|
+
completions = [];
|
|
630
|
+
}
|
|
631
|
+
// 若 prefix 为全路径, 如 /foo/, 直接返回该目录下所有列表即可
|
|
632
|
+
if (!curPad) {
|
|
633
|
+
return [completions, prefix]
|
|
634
|
+
}
|
|
635
|
+
// 若 prefix 为 /foo/ba, 获取 /foo/ 目录下 ba 开头的文件列表
|
|
636
|
+
let hits = [];
|
|
637
|
+
completions.forEach(r => {
|
|
638
|
+
if (r.startsWith(curPad)) {
|
|
639
|
+
hits.push(r);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
// 获取 ba 开头文件的共同前缀, 如 hits 为 [bara, barb], 得到 bar
|
|
643
|
+
if (hits.length > 1) {
|
|
644
|
+
const prefix = getCommonPrefix(hits);
|
|
645
|
+
if (prefix !== curPad) {
|
|
646
|
+
hits = [prefix];
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
// 给列表文件重新加上 /foo/ 前缀
|
|
650
|
+
if (curDir != '') {
|
|
651
|
+
hits = hits.map(v => curDir + v);
|
|
652
|
+
}
|
|
653
|
+
return [hits, prefix];
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// 执行一个 shell 命令
|
|
657
|
+
function execCommand(command, args, options){
|
|
658
|
+
return new Promise(function (resolve, reject) {
|
|
659
|
+
const child = spawn(command, args, options);
|
|
660
|
+
child.on('close', function (code) {
|
|
661
|
+
if (code) {
|
|
662
|
+
reject(`"react-native bundle" command exited with code ${code}.`);
|
|
663
|
+
} else {
|
|
664
|
+
resolve();
|
|
665
|
+
}
|
|
666
|
+
})
|
|
667
|
+
})
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** 下载 url 指定的文件, 可指定 md5 进行校验
|
|
671
|
+
* download(url, md5).then(rs => {
|
|
672
|
+
* rs: {code:Int, message:String, file:String}
|
|
673
|
+
* })
|
|
674
|
+
*/
|
|
675
|
+
async function download(url, md5) {
|
|
676
|
+
return new Promise(resolve => {
|
|
677
|
+
if (!url) {
|
|
678
|
+
resolve({code:1, message:'download url unavailable'})
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
let localFile;
|
|
682
|
+
if (md5) {
|
|
683
|
+
localFile = path.join(getCacheDir(), md5);
|
|
684
|
+
}
|
|
685
|
+
const tmpFile = localFile
|
|
686
|
+
? localFile + "_tmp"
|
|
687
|
+
: path.join(getCacheDir(), crypto.randomBytes(8).toString("hex"));
|
|
688
|
+
const stream = fs.createWriteStream(tmpFile);
|
|
689
|
+
nodeFetch(url, {
|
|
690
|
+
headers: {
|
|
691
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
|
|
692
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
693
|
+
'Accept-Language': 'en-US,en;q=0.9,fr;q=0.8,ro;q=0.7,ru;q=0.6,la;q=0.5,pt;q=0.4,de;q=0.3',
|
|
694
|
+
'Cache-Control': 'max-age=0',
|
|
695
|
+
'Connection': 'keep-alive',
|
|
696
|
+
'Upgrade-Insecure-Requests': '1',
|
|
697
|
+
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36'
|
|
698
|
+
}
|
|
699
|
+
}).then(res => {
|
|
700
|
+
res.body.pipe(stream);
|
|
701
|
+
res.body.on("error", (error) => {
|
|
702
|
+
resolve({code:1, message: errMsg(error)})
|
|
703
|
+
});
|
|
704
|
+
stream.on("finish", () => {
|
|
705
|
+
const checkMd5 = fileMd5(tmpFile);
|
|
706
|
+
if (md5 && checkMd5 !== md5) {
|
|
707
|
+
fs.unlinkSync(tmpFile);
|
|
708
|
+
resolve({code:1, message:'check download file md5 failed'})
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (!localFile) {
|
|
712
|
+
localFile = path.join(getCacheDir(), checkMd5);
|
|
713
|
+
}
|
|
714
|
+
fs.renameSync(tmpFile, localFile)
|
|
715
|
+
resolve({code:0, file:localFile})
|
|
716
|
+
});
|
|
717
|
+
}).catch(error => {
|
|
718
|
+
resolve({code:1, message: errMsg(error)})
|
|
719
|
+
})
|
|
720
|
+
})
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// 获取 RN 版本
|
|
724
|
+
function getRNVersion(projectDir) {
|
|
725
|
+
if (!runtimeCache._rnVersion) {
|
|
726
|
+
const version = JSON.parse(fs.readFileSync(path.resolve(projectDir||'', 'node_modules/react-native/package.json'))).version;
|
|
727
|
+
const match = /^(\d+)\.(\d+)(\.(\d+))?/.exec(version);
|
|
728
|
+
runtimeCache._rnVersion = {
|
|
729
|
+
version,
|
|
730
|
+
major: match[1] | 0,
|
|
731
|
+
minor: match[2] | 0,
|
|
732
|
+
patch: match[4] | 0
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
return runtimeCache._rnVersion;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// 获取 erpush 版本
|
|
739
|
+
function getPushVersion() {
|
|
740
|
+
if (!runtimeCache._eyVersion) {
|
|
741
|
+
runtimeCache._eyVersion = JSON.parse(fs.readFileSync(
|
|
742
|
+
path.resolve(__dirname, './../package.json')
|
|
743
|
+
)).version;
|
|
744
|
+
}
|
|
745
|
+
return runtimeCache._eyVersion;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// 设置 erpush 配置信息
|
|
749
|
+
function setConfig(projectDir, config){
|
|
750
|
+
const file = path.join(projectDir, 'erpush.json');
|
|
751
|
+
let now = {};
|
|
752
|
+
try {
|
|
753
|
+
const content = fs.readFileSync(file);
|
|
754
|
+
now = JSON.parse(content);
|
|
755
|
+
} catch {}
|
|
756
|
+
config = {...now, ...config};
|
|
757
|
+
config = JSON.stringify(config, null, 2);
|
|
758
|
+
fs.writeFileSync(file, config);
|
|
759
|
+
return file;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// 获取 erpush 配置信息
|
|
763
|
+
function getConfig(projectDir){
|
|
764
|
+
let now = {};
|
|
765
|
+
try {
|
|
766
|
+
const content = fs.readFileSync(path.join(projectDir, 'erpush.json'));
|
|
767
|
+
now = JSON.parse(content);
|
|
768
|
+
} catch {}
|
|
769
|
+
return now;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// 获取项目的 App id
|
|
773
|
+
function getAppId(projectDir, platform, fallbackId){
|
|
774
|
+
if (supportPlatforms.indexOf(platform) == -1) {
|
|
775
|
+
return {code:-1, message:'platform not support'}
|
|
776
|
+
}
|
|
777
|
+
if (fallbackId) {
|
|
778
|
+
return {code:0, message:fallbackId}
|
|
779
|
+
}
|
|
780
|
+
const config = getConfig(projectDir);
|
|
781
|
+
if (!(platform in config)) {
|
|
782
|
+
return {code:-3, message: "Unbound app, please run `erpush app bind` first"}
|
|
783
|
+
}
|
|
784
|
+
return {code:0, message:config[platform]}
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// 发送 API 请求: 以 cookie 做为凭证, 服务端可以此来鉴权, 返回 json
|
|
788
|
+
async function requestAPI(projectDir, uri, payload, asForm) {
|
|
789
|
+
let {baseUrl} = getConfig(projectDir);
|
|
790
|
+
if (uri && !/^[a-zA-Z]+:\/\//.test(uri)) {
|
|
791
|
+
if (!baseUrl) {
|
|
792
|
+
uri = null;
|
|
793
|
+
} else {
|
|
794
|
+
// trim baseUrl right /
|
|
795
|
+
while(baseUrl.charAt(baseUrl.length-1) === '/') {
|
|
796
|
+
baseUrl = baseUrl.substring(0, baseUrl.length-1);
|
|
797
|
+
}
|
|
798
|
+
// trim uri left /
|
|
799
|
+
while(uri.charAt(0) === '/') {
|
|
800
|
+
uri = uri.substring(1);
|
|
801
|
+
}
|
|
802
|
+
uri = baseUrl + '/' + uri;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (!uri || !/^https?:\/\//i.test(uri)) {
|
|
806
|
+
return {
|
|
807
|
+
code:-2,
|
|
808
|
+
message: "request url incorrect"
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const options = {
|
|
812
|
+
headers:{
|
|
813
|
+
'User-Agent': "erpush-cli/" + getPushVersion(),
|
|
814
|
+
}
|
|
815
|
+
};
|
|
816
|
+
if (payload) {
|
|
817
|
+
options.method = 'POST';
|
|
818
|
+
if (asForm) {
|
|
819
|
+
const form = new FormData();
|
|
820
|
+
for (let key in payload) {
|
|
821
|
+
form.append(key, payload[key]);
|
|
822
|
+
}
|
|
823
|
+
options.body = form;
|
|
824
|
+
} else {
|
|
825
|
+
options.body = JSON.stringify(payload);
|
|
826
|
+
}
|
|
827
|
+
} else {
|
|
828
|
+
options.method = 'GET';
|
|
829
|
+
}
|
|
830
|
+
// 在进程结束时保存 cookie 为文件
|
|
831
|
+
if (!runtimeCache.jar) {
|
|
832
|
+
runtimeCache.store = new tough.MemoryCookieStore();
|
|
833
|
+
runtimeCache.jarFile = path.join(getCacheDir(), '.cookiejar');
|
|
834
|
+
try {
|
|
835
|
+
if (!fileExist(runtimeCache.jarFile)) {
|
|
836
|
+
throw '';
|
|
837
|
+
}
|
|
838
|
+
runtimeCache.jar = tough.CookieJar.deserializeSync(
|
|
839
|
+
fs.readFileSync(runtimeCache.jarFile).toString(),
|
|
840
|
+
runtimeCache.store
|
|
841
|
+
);
|
|
842
|
+
}catch(e){
|
|
843
|
+
runtimeCache.jar = new tough.CookieJar(runtimeCache.store);
|
|
844
|
+
}
|
|
845
|
+
onProcessExit(() => {
|
|
846
|
+
if (!runtimeCache.changed) {
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
// 仅保存持久化的, 未设置过期时间的仅在当前进程有效
|
|
850
|
+
const cookieLists = [];
|
|
851
|
+
const Store = runtimeCache.store;
|
|
852
|
+
Store.getAllCookies((err, cookies) => {
|
|
853
|
+
if (err) {
|
|
854
|
+
throw err;
|
|
855
|
+
}
|
|
856
|
+
cookies.forEach(cookie => {
|
|
857
|
+
if (cookie.isPersistent()) {
|
|
858
|
+
cookie = cookie instanceof tough.Cookie ? cookie.toJSON() : cookie;
|
|
859
|
+
delete cookie.creationIndex;
|
|
860
|
+
cookieLists.push(cookie)
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
const serialized = {
|
|
865
|
+
rejectPublicSuffixes: !!runtimeCache.jar.rejectPublicSuffixes,
|
|
866
|
+
enableLooseMode: !!runtimeCache.jar.enableLooseMode,
|
|
867
|
+
allowSpecialUseDomain: !!runtimeCache.jar.allowSpecialUseDomain,
|
|
868
|
+
prefixSecurity: runtimeCache.jar.prefixSecurity,
|
|
869
|
+
cookies: cookieLists
|
|
870
|
+
};
|
|
871
|
+
fs.writeFileSync(runtimeCache.jarFile, JSON.stringify(serialized));
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
// 设置请求 cookie
|
|
875
|
+
const Jar = runtimeCache.jar;
|
|
876
|
+
const cookies = await Jar.getCookieString(uri);
|
|
877
|
+
if (cookies) {
|
|
878
|
+
options.headers['cookie'] = cookies;
|
|
879
|
+
}
|
|
880
|
+
const res = await nodeFetch(uri, options);
|
|
881
|
+
const resCookies = res.headers.raw()['set-cookie'];
|
|
882
|
+
if (resCookies) {
|
|
883
|
+
if (!runtimeCache.changed) {
|
|
884
|
+
runtimeCache.changed = true;
|
|
885
|
+
}
|
|
886
|
+
(Array.isArray(resCookies) ? resCookies : [resCookies]).forEach(cookie => {
|
|
887
|
+
Jar.setCookieSync(cookie, uri)
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
return res;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
module.exports = {
|
|
894
|
+
wcwidth,
|
|
895
|
+
minimist,
|
|
896
|
+
FormData,
|
|
897
|
+
nodeFetch,
|
|
898
|
+
CInfo,
|
|
899
|
+
CWarning,
|
|
900
|
+
CError,
|
|
901
|
+
supportPlatforms,
|
|
902
|
+
compareVersion,
|
|
903
|
+
onProcessExit,
|
|
904
|
+
parseProcess,
|
|
905
|
+
isTtyStream,
|
|
906
|
+
supportUnicode,
|
|
907
|
+
ansiRegex,
|
|
908
|
+
removeStrAnsi,
|
|
909
|
+
color,
|
|
910
|
+
decorated,
|
|
911
|
+
errMsg,
|
|
912
|
+
makeSpinner,
|
|
913
|
+
makeTable,
|
|
914
|
+
|
|
915
|
+
fileExist,
|
|
916
|
+
dirExist,
|
|
917
|
+
ensureDirSync,
|
|
918
|
+
emptyDirSync,
|
|
919
|
+
removeDirSync,
|
|
920
|
+
fileMd5,
|
|
921
|
+
getCacheDir,
|
|
922
|
+
getDiff,
|
|
923
|
+
packIpa,
|
|
924
|
+
packZip,
|
|
925
|
+
enumZipEntries,
|
|
926
|
+
readZipEntireBuffer,
|
|
927
|
+
saveZipFile,
|
|
928
|
+
getCommonPrefix,
|
|
929
|
+
getCommonPath,
|
|
930
|
+
execCommand,
|
|
931
|
+
download,
|
|
932
|
+
|
|
933
|
+
getRNVersion,
|
|
934
|
+
getPushVersion,
|
|
935
|
+
setConfig,
|
|
936
|
+
getConfig,
|
|
937
|
+
getAppId,
|
|
938
|
+
requestAPI,
|
|
939
|
+
};
|