erpush-cli 0.0.1-prepayload

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 erpush-cli might be problematic. Click here for more details.

package/src/utils.js ADDED
@@ -0,0 +1,625 @@
1
+ const os = require('os');
2
+ const yazl = require('yazl');
3
+ const path = require('path');
4
+ const fs = require('fs-extra');
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 {open:openZipFile} = require('yauzl');
13
+
14
+ const runtimeCache = {};
15
+ const supportPlatforms = ['android', 'ios'];
16
+ const MakeDiff = (() => {
17
+ try {
18
+ return require('erpush').diff;
19
+ } catch (e) {
20
+ return e;
21
+ }
22
+ })();
23
+
24
+ // 解析 process 参数
25
+ function parseProcess(p){
26
+ const _ = p.env._||null;
27
+ const npx = _ && _.endsWith('/npx');
28
+ const options = minimist(p.argv.slice(2));
29
+ const args = options._;
30
+ const name = args.shift();
31
+ delete options._;
32
+ return {npx, name, args, options};
33
+ }
34
+
35
+ // 消息相关
36
+ const CInfo = color('Info:', 36, true) + ' ';
37
+ const CWarning = color('Warning:', 35, true) + ' ';
38
+ const CError = color('Error:', 31, true) + ' ';
39
+
40
+ function color(str, code, bold){
41
+ return (bold ? '\033[1m' : '')
42
+ + (code ? "\x1b["+code+"m" : '')
43
+ + str
44
+ + (code ? "\x1b" : '')
45
+ + (bold ? "\033" : '')
46
+ + "[0m";
47
+ }
48
+
49
+ function rmColor(str) {
50
+ return typeof str === 'string' ? str.replace(
51
+ /[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g,
52
+ ''
53
+ ) : str;
54
+ }
55
+
56
+ function errMsg(e){
57
+ if (typeof e === 'object' && 'message' in e) {
58
+ return e.message
59
+ }
60
+ return e;
61
+ }
62
+
63
+ // 转 Array 为 String 表格, 自动对齐
64
+ function makeTable(data) {
65
+ const rows = [];
66
+ const rowWidth = [];
67
+ data.forEach(item => {
68
+ if (!Array.isArray(item)) {
69
+ rows.push(null)
70
+ return;
71
+ }
72
+ const row = [];
73
+ item.forEach((str, index) => {
74
+ const width = wcwidth(String(rmColor(str)));
75
+ row.push(width);
76
+ if (!rowWidth[index] || rowWidth[index] < width) {
77
+ rowWidth[index] = width;
78
+ }
79
+ })
80
+ rows.push(row)
81
+ })
82
+ const txts = [];
83
+ const split = '-'.repeat(rowWidth.reduce((a, b) => a + b) + rowWidth.length * 2);
84
+ data.forEach((item, n) => {
85
+ if (!Array.isArray(item)) {
86
+ txts.push(split)
87
+ return;
88
+ }
89
+ let line = '';
90
+ const widths = rows[n];
91
+ item.forEach((str, index) => {
92
+ line += String(str) + ' '.repeat(rowWidth[index] - widths[index] + 2)
93
+ });
94
+ txts.push(line);
95
+ })
96
+ return txts.join("\n")
97
+ }
98
+
99
+ // 文件操作
100
+ function getCacheDir() {
101
+ const dir = path.join(os.homedir(), '.easypush');
102
+ fs.ensureDirSync(dir);
103
+ return dir;
104
+ }
105
+
106
+ function dirExist(path){
107
+ return fileExist(path, true)
108
+ }
109
+
110
+ function fileExist(path, dir){
111
+ try {
112
+ const f = fs.lstatSync(path)
113
+ return dir ? f.isDirectory() : f.isFile()
114
+ } catch(e) {
115
+ return false;
116
+ }
117
+ }
118
+
119
+ function fileMd5(filename) {
120
+ let fd;
121
+ try {
122
+ fd = fs.openSync(filename, 'r')
123
+ } catch (e) {
124
+ return false;
125
+ }
126
+ const BUFFER_SIZE = 8192;
127
+ const hash = crypto.createHash('md5')
128
+ const buffer = Buffer.alloc(BUFFER_SIZE)
129
+ try {
130
+ let bytesRead
131
+ do {
132
+ bytesRead = fs.readSync(fd, buffer, 0, BUFFER_SIZE)
133
+ hash.update(buffer.subarray(0, bytesRead))
134
+ } while (bytesRead === BUFFER_SIZE)
135
+ } finally {
136
+ fs.closeSync(fd)
137
+ }
138
+ return hash.digest('hex')
139
+ }
140
+
141
+ // 获取 oldBuf, newBuf 的 diff buffer
142
+ function getDiff(oldBuf, newBuf) {
143
+ if (typeof MakeDiff !== 'function') {
144
+ const message = 'Load "easypush" module failed.';
145
+ if (MakeDiff instanceof Error) {
146
+ MakeDiff.message = message + "\n" + MakeDiff.message;
147
+ throw MakeDiff;
148
+ }
149
+ throw new Error(message);
150
+ }
151
+ return MakeDiff(oldBuf, newBuf);
152
+ }
153
+
154
+ // 打包 xcode 编译的 .app 为 .ipa 文件
155
+ function packIpa(source, dest){
156
+ return packDirToZip(source, dest, true);
157
+ }
158
+
159
+ // 打包 dir 为 zip 文件, 保存到 save 路径
160
+ function packZip(dir, save) {
161
+ return packDirToZip(dir, save);
162
+ }
163
+ function packDirToZip(dir, save, ipa){
164
+ return new Promise(function (resolve, reject) {
165
+ const zip = new yazl.ZipFile();
166
+ let rel = '';
167
+ if (ipa) {
168
+ const appName = path.basename(dir);
169
+ rel = 'Payload/' + appName;
170
+ }
171
+ addRecursive(zip, dir, rel);
172
+ zip.end();
173
+ zip.on('error', function (err) {
174
+ fs.removeSync(save)
175
+ reject(err);
176
+ });
177
+ zip.outputStream.pipe(fs.createWriteStream(save)).on('close', function () {
178
+ resolve();
179
+ });
180
+ });
181
+ }
182
+ function addRecursive(zip, root, rel) {
183
+ if (rel) {
184
+ rel += '/';
185
+ zip.addEmptyDirectory(rel);
186
+ }
187
+ const childs = fs.readdirSync(root);
188
+ for (const name of childs) {
189
+ if (name === '.' || name === '..') {
190
+ continue;
191
+ }
192
+ const fullPath = path.join(root, name);
193
+ const stat = fs.statSync(fullPath);
194
+ if (stat.isFile()) {
195
+ zip.addFile(fullPath, rel + name);
196
+ } else if (stat.isDirectory()) {
197
+ addRecursive(zip, fullPath, rel + name);
198
+ }
199
+ }
200
+ }
201
+
202
+ // 枚举 Zip 内所有文件, callback(entry, zipfile), 若不指定 basic 为 true
203
+ // 文件属性 entry 会新增 isDirectory/hash 两个字段, 原 entry 内有一个 crc32 的 hash 值
204
+ // 但考虑到 crc32 的碰撞概率略大, 所以此处额外计算一个新的 hash 值用于校验
205
+ function enumZipEntries(zipFn, callback, basic) {
206
+ return new Promise((resolve, reject) => {
207
+ openZipFile(zipFn, {lazyEntries: true}, (err, zipfile) => {
208
+ if (err) {
209
+ reject(err);
210
+ return;
211
+ }
212
+ zipfile.on('end', resolve);
213
+ zipfile.on('error', reject);
214
+ zipfile.on('entry', entry => {
215
+ getZipEntryHash(zipfile, entry, basic).then(entryPlus => {
216
+ return Promise.resolve(callback(entryPlus, zipfile))
217
+ }).then(() => zipfile.readEntry())
218
+ });
219
+ zipfile.readEntry();
220
+ });
221
+ });
222
+ }
223
+ function getZipEntryHash(zipfile, entry, basic) {
224
+ return new Promise((resolve, reject) => {
225
+ if (basic) {
226
+ resolve(entry);
227
+ return;
228
+ }
229
+ entry.isDirectory = /\/$/.test(entry.fileName);
230
+ if (entry.isDirectory) {
231
+ entry.hash = null;
232
+ resolve(entry);
233
+ return;
234
+ }
235
+ zipfile.openReadStream(entry, function(err, readStream) {
236
+ if (err) {
237
+ reject(err);
238
+ return;
239
+ }
240
+ const hash = crypto.createHash('md5').setEncoding('hex');
241
+ readStream.on("end", function() {
242
+ hash.end();
243
+ entry.hash = hash.read();
244
+ resolve(entry);
245
+ });
246
+ readStream.pipe(hash);
247
+ });
248
+ })
249
+ }
250
+
251
+ // 获取 enumZipEntries 枚举的单个文件 buffer
252
+ function readZipEntireBuffer(entry, zipfile) {
253
+ const buffers = [];
254
+ return new Promise((resolve, reject) => {
255
+ zipfile.openReadStream(entry, (err, stream) => {
256
+ if (err) {
257
+ reject(err);
258
+ return;
259
+ }
260
+ stream.pipe({
261
+ write(chunk) {
262
+ buffers.push(chunk);
263
+ },
264
+ end() {
265
+ resolve(Buffer.concat(buffers));
266
+ },
267
+ prependListener() {},
268
+ on() {},
269
+ once() {},
270
+ emit() {},
271
+ });
272
+ });
273
+ });
274
+ }
275
+
276
+ // 保存 ZipFile 对象为文件
277
+ function saveZipFile(zipfile, output) {
278
+ fs.ensureDirSync(path.dirname(output));
279
+ return new Promise(function (resolve, reject) {
280
+ zipfile.on('error', err => {
281
+ fs.removeSync(output)
282
+ reject(err);
283
+ });
284
+ zipfile.outputStream.pipe(fs.createWriteStream(output)).on('close', function() {
285
+ resolve();
286
+ });
287
+ })
288
+ }
289
+
290
+ // 获取字符串共同前缀
291
+ // https://www.geeksforgeeks.org/longest-common-prefix-using-binary-search/
292
+ function getCommonPrefix(arr) {
293
+ let low = 0, high = 0;
294
+ arr.forEach(s => {
295
+ if (!high || s.length < high) {
296
+ high = s.length
297
+ }
298
+ });
299
+ let prefix = '';
300
+ const first = arr[0];
301
+ while (low <= high) {
302
+ const mid = Math.floor(low + (high - low) / 2);
303
+ const interrupt = arr.some(r => {
304
+ for (let i = low; i <= mid; i++) {
305
+ if (r[i] !== first[i]) {
306
+ return true;
307
+ }
308
+ }
309
+ });
310
+ if (interrupt) {
311
+ high = mid - 1;
312
+ } else {
313
+ prefix += first.substr(low, mid-low+1);
314
+ low = mid + 1;
315
+ }
316
+ }
317
+ return prefix;
318
+ }
319
+
320
+ // 在 rootDir 目录查找有共同前缀 prefix 的文件(夹)
321
+ function getCommonPath(rootDir, prefix) {
322
+ const dash = prefix.lastIndexOf('/');
323
+ const curPad = prefix.substr(dash + 1);
324
+ const curDir = dash !== -1 ? prefix.substring(0, dash + 1) : '';
325
+ let completions;
326
+ try {
327
+ completions = fs.readdirSync(
328
+ path.join(rootDir, curDir),
329
+ {withFileTypes:true}
330
+ ).map(r =>
331
+ (curPad ? '' : prefix) + r.name + (r.isDirectory() ? '/' : '')
332
+ );
333
+ } catch(e) {
334
+ completions = [];
335
+ }
336
+ // 若 prefix 为全路径, 如 /foo/, 直接返回该目录下所有列表即可
337
+ if (!curPad) {
338
+ return [completions, prefix]
339
+ }
340
+ // 若 prefix 为 /foo/ba, 获取 /foo/ 目录下 ba 开头的文件列表
341
+ let hits = [];
342
+ completions.forEach(r => {
343
+ if (r.startsWith(curPad)) {
344
+ hits.push(r);
345
+ }
346
+ });
347
+ // 获取 ba 开头文件的共同前缀, 如 hits 为 [bara, barb], 得到 bar
348
+ if (hits.length > 1) {
349
+ const prefix = getCommonPrefix(hits);
350
+ if (prefix !== curPad) {
351
+ hits = [prefix];
352
+ }
353
+ }
354
+ // 给列表文件重新加上 /foo/ 前缀
355
+ if (curDir != '') {
356
+ hits = hits.map(v => curDir + v);
357
+ }
358
+ return [hits, prefix];
359
+ }
360
+
361
+ // 获取 RN 版本
362
+ function getRNVersion(projectDir) {
363
+ if (!runtimeCache.rnVersion) {
364
+ const version = JSON.parse(fs.readFileSync(path.resolve(projectDir||'', 'node_modules/react-native/package.json'))).version;
365
+ const match = /^(\d+)\.(\d+)(\.(\d+))?/.exec(version);
366
+ runtimeCache.rnVersion = {
367
+ version,
368
+ major: match[1] | 0,
369
+ minor: match[2] | 0,
370
+ patch: match[4] | 0
371
+ };
372
+ }
373
+ return runtimeCache.rnVersion;
374
+ }
375
+
376
+ // 获取 EasyPush 版本
377
+ function getEasyVersion() {
378
+ if (!runtimeCache.eyVersion) {
379
+ runtimeCache.eyVersion = JSON.parse(fs.readFileSync(
380
+ path.resolve(__dirname, './../package.json')
381
+ )).version;
382
+ }
383
+ return runtimeCache.eyVersion;
384
+ }
385
+
386
+ // 设置 easypush 配置信息
387
+ function setConfig(projectDir, config){
388
+ const file = path.join(projectDir, 'easypush.json');
389
+ const now = fs.readJsonSync(file, { throws: false })||{};
390
+ config = {...now, ...config};
391
+ fs.writeJsonSync(file, config, {spaces:2});
392
+ return file;
393
+ }
394
+
395
+ // 获取 easypush 配置信息
396
+ function getConfig(projectDir){
397
+ return fs.readJsonSync(
398
+ path.join(projectDir, 'easypush.json'),
399
+ { throws: false }
400
+ )||{};
401
+ }
402
+
403
+ // 获取项目的 App id
404
+ function getAppId(projectDir, platform, fallbackId){
405
+ if (supportPlatforms.indexOf(platform) == -1) {
406
+ return {code:-1, message:'platform not support'}
407
+ }
408
+ if (fallbackId) {
409
+ return {code:0, message:fallbackId}
410
+ }
411
+ const config = getConfig(projectDir);
412
+ if (!(platform in config)) {
413
+ return {code:-3, message: "Unbound app, please run `easypush app bind` first"}
414
+ }
415
+ return {code:0, message:config[platform]}
416
+ }
417
+
418
+ // 发送 API 请求: 以 cookie 做为凭证, 服务端可以此来鉴权, 返回 json
419
+ async function requestAPI(projectDir, uri, payload, asForm) {
420
+ let {baseUrl} = getConfig(projectDir);
421
+ if (uri && !/^[a-zA-Z]+:\/\//.test(uri)) {
422
+ if (!baseUrl) {
423
+ uri = null;
424
+ } else {
425
+ // trim baseUrl right /
426
+ while(baseUrl.charAt(baseUrl.length-1) === '/') {
427
+ baseUrl = baseUrl.substring(0, baseUrl.length-1);
428
+ }
429
+ // trim uri left /
430
+ while(uri.charAt(0) === '/') {
431
+ uri = uri.substring(1);
432
+ }
433
+ uri = baseUrl + '/' + uri;
434
+ }
435
+ }
436
+ if (!uri || !/^https?:\/\//i.test(uri)) {
437
+ return {
438
+ code:-2,
439
+ message: "request url incorrect"
440
+ }
441
+ }
442
+ const options = {
443
+ headers:{
444
+ 'User-Agent': "easypush-client/" + getEasyVersion(),
445
+ }
446
+ };
447
+ if (payload) {
448
+ options.method = 'POST';
449
+ if (asForm) {
450
+ const form = new FormData();
451
+ for (let key in payload) {
452
+ form.append(key, payload[key]);
453
+ }
454
+ options.body = form;
455
+ } else {
456
+ options.body = JSON.stringify(payload);
457
+ }
458
+ } else {
459
+ options.method = 'GET';
460
+ }
461
+ // 在进程结束时保存 cookie 为文件
462
+ if (!runtimeCache.jar) {
463
+ runtimeCache.store = new tough.MemoryCookieStore();
464
+ runtimeCache.jarFile = path.join(getCacheDir(), '.cookiejar');
465
+ try {
466
+ if (!fileExist(runtimeCache.jarFile)) {
467
+ throw '';
468
+ }
469
+ runtimeCache.jar = tough.CookieJar.deserializeSync(
470
+ fs.readFileSync(runtimeCache.jarFile).toString(),
471
+ runtimeCache.store
472
+ );
473
+ }catch(e){
474
+ runtimeCache.jar = new tough.CookieJar(runtimeCache.store);
475
+ }
476
+ process.on('exit', () => {
477
+ if (!runtimeCache.changed) {
478
+ return;
479
+ }
480
+ // 仅保存持久化的, 未设置过期时间的仅在当前进程有效
481
+ const cookieLists = [];
482
+ const Store = runtimeCache.store;
483
+ Store.getAllCookies((err, cookies) => {
484
+ if (err) {
485
+ throw err;
486
+ }
487
+ cookies.forEach(cookie => {
488
+ if (cookie.isPersistent()) {
489
+ cookie = cookie instanceof tough.Cookie ? cookie.toJSON() : cookie;
490
+ delete cookie.creationIndex;
491
+ cookieLists.push(cookie)
492
+ }
493
+ });
494
+ });
495
+ const serialized = {
496
+ rejectPublicSuffixes: !!runtimeCache.jar.rejectPublicSuffixes,
497
+ enableLooseMode: !!runtimeCache.jar.enableLooseMode,
498
+ allowSpecialUseDomain: !!runtimeCache.jar.allowSpecialUseDomain,
499
+ prefixSecurity: runtimeCache.jar.prefixSecurity,
500
+ cookies: cookieLists
501
+ };
502
+ fs.writeFileSync(runtimeCache.jarFile, JSON.stringify(serialized));
503
+ });
504
+ }
505
+ // 设置请求 cookie
506
+ const Jar = runtimeCache.jar;
507
+ const cookies = await Jar.getCookieString(uri);
508
+ if (cookies) {
509
+ options.headers['cookie'] = cookies;
510
+ }
511
+ const res = await nodeFetch(uri, options);
512
+ const resCookies = res.headers.raw()['set-cookie'];
513
+ if (resCookies) {
514
+ if (!runtimeCache.changed) {
515
+ runtimeCache.changed = true;
516
+ }
517
+ (Array.isArray(resCookies) ? resCookies : [resCookies]).forEach(cookie => {
518
+ Jar.setCookieSync(cookie, uri)
519
+ });
520
+ }
521
+ return res;
522
+ }
523
+
524
+ /**
525
+ * 下载 url 指定的文件, 可指定 md5 进行校验
526
+ * download(url, md5).then(rs => {
527
+ * rs: {code:Int, message:String, file:String}
528
+ * })
529
+ */
530
+ async function download(url, md5) {
531
+ return new Promise(resolve => {
532
+ if (!url) {
533
+ resolve({code:1, message:'download url unavailable'})
534
+ return;
535
+ }
536
+ let localFile;
537
+ if (md5) {
538
+ localFile = path.join(getCacheDir(), md5);
539
+ }
540
+ const tmpFile = localFile
541
+ ? localFile + "_tmp"
542
+ : path.join(getCacheDir(), crypto.randomBytes(8).toString("hex"));
543
+ const stream = fs.createWriteStream(tmpFile);
544
+ nodeFetch(url, {
545
+ headers: {
546
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
547
+ 'Accept-Encoding': 'gzip, deflate, br',
548
+ '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',
549
+ 'Cache-Control': 'max-age=0',
550
+ 'Connection': 'keep-alive',
551
+ 'Upgrade-Insecure-Requests': '1',
552
+ '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'
553
+ }
554
+ }).then(res => {
555
+ res.body.pipe(stream);
556
+ res.body.on("error", (error) => {
557
+ resolve({code:1, message: errMsg(error)})
558
+ });
559
+ stream.on("finish", () => {
560
+ const checkMd5 = fileMd5(tmpFile);
561
+ if (md5 && checkMd5 !== md5) {
562
+ fs.removeSync(tmpFile);
563
+ resolve({code:1, message:'check download file md5 failed'})
564
+ return;
565
+ }
566
+ if (!localFile) {
567
+ localFile = path.join(getCacheDir(), checkMd5);
568
+ }
569
+ fs.moveSync(tmpFile, localFile, {overwrite:true})
570
+ resolve({code:0, file:localFile})
571
+ });
572
+ }).catch(error => {
573
+ resolve({code:1, message: errMsg(error)})
574
+ })
575
+ })
576
+ }
577
+
578
+ // 执行命令
579
+ function execCommand(command, args, options){
580
+ return new Promise(function (resolve, reject) {
581
+ const child = spawn(command, args, options);
582
+ child.on('close', function (code) {
583
+ if (code) {
584
+ reject(`"react-native bundle" command exited with code ${code}.`);
585
+ } else {
586
+ resolve();
587
+ }
588
+ })
589
+ })
590
+ }
591
+
592
+ module.exports = {
593
+ supportPlatforms,
594
+ parseProcess,
595
+ CInfo,
596
+ CWarning,
597
+ CError,
598
+ color,
599
+ rmColor,
600
+ errMsg,
601
+ wcwidth,
602
+ makeTable,
603
+
604
+ getCacheDir,
605
+ dirExist,
606
+ fileExist,
607
+ fileMd5,
608
+ getDiff,
609
+ packIpa,
610
+ packZip,
611
+ enumZipEntries,
612
+ readZipEntireBuffer,
613
+ saveZipFile,
614
+ getCommonPrefix,
615
+ getCommonPath,
616
+
617
+ getRNVersion,
618
+ getEasyVersion,
619
+ setConfig,
620
+ getConfig,
621
+ getAppId,
622
+ download,
623
+ requestAPI,
624
+ execCommand,
625
+ };