@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/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
+ };