Forráskód Böngészése

:children_crossing: puppeteer

Signed-off-by: LI JIAHAO <lijiahao99131@gmail.com>
LI JIAHAO 6 éve
szülő
commit
4df9fea6f2
8 módosított fájl, 322 hozzáadás és 182 törlés
  1. 1 1
      dist/main.js
  2. 4 1
      dist/twitter.js
  3. 127 108
      dist/webshot.js
  4. 1 0
      package.json
  5. 1 1
      src/main.ts
  6. 7 2
      src/twitter.ts
  7. 82 64
      src/webshot.ts
  8. 99 5
      yarn.lock

+ 1 - 1
dist/main.js

@@ -134,5 +134,5 @@ const worker = new twitter_1.default({
     bot: qq,
     webshotDelay: config.webshot_delay,
 });
-setTimeout(worker.work, config.work_interval * 1000);
+worker.launch();
 qq.connect();

+ 4 - 1
dist/twitter.js

@@ -9,6 +9,9 @@ const logger = log4js.getLogger('twitter');
 logger.level = global.loglevel;
 class default_1 {
     constructor(opt) {
+        this.launch = () => {
+            this.webshot = new webshot_1.default(() => setTimeout(this.work, this.workInterval * 1000));
+        };
         this.work = () => {
             const lock = this.lock;
             if (this.workInterval < 1)
@@ -94,7 +97,7 @@ class default_1 {
                 }
                 if (lock.threads[lock.feed[lock.workon]].offset === 0)
                     tweets.splice(1);
-                return webshot_1.default(tweets, msg => {
+                return this.webshot(tweets, msg => {
                     lock.threads[lock.feed[lock.workon]].subscribers.forEach(subscriber => {
                         logger.info(`pushing data of thread ${lock.feed[lock.workon]} to ${JSON.stringify(subscriber)}`);
                         this.bot.bot('send_msg', {

+ 127 - 108
dist/webshot.js

@@ -1,124 +1,143 @@
 "use strict";
 Object.defineProperty(exports, "__esModule", { value: true });
+const CallableInstance = require("callable-instance");
 const https = require("https");
 const log4js = require("log4js");
 const pngjs_1 = require("pngjs");
+const puppeteer = require("puppeteer");
 const read = require("read-all-stream");
-const webshot = require("webshot");
 const logger = log4js.getLogger('webshot');
 logger.level = global.loglevel;
-function renderWebshot(url, height, webshotDelay) {
-    const promise = new Promise(resolve => {
-        const options = {
-            windowSize: {
-                width: 1080,
-                height,
-            },
-            userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
-            renderDelay: webshotDelay,
-            quality: 100,
-            customCSS: 'html{zoom:2}header{display:none!important}',
+class Webshot extends CallableInstance {
+    constructor(onready) {
+        super('webshot');
+        this.renderWebshot = (url, height, webshotDelay) => {
+            const promise = new Promise(resolve => {
+                const width = 1080;
+                logger.info(`shooting ${width}*${height} webshot for ${url}`);
+                this.browser.newPage()
+                    .then(page => {
+                    page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36')
+                        .then(() => page.setViewport({
+                        width,
+                        height,
+                        isMobile: true,
+                    }))
+                        .then(() => page.goto(url))
+                        .then(() => page.addStyleTag({
+                        content: 'html{zoom:2}header{display:none!important}',
+                    }))
+                        .then(() => page.waitFor(webshotDelay))
+                        .then(() => page.screenshot())
+                        .then(screenshot => {
+                        new pngjs_1.PNG({
+                            filterType: 4,
+                        }).on('parsed', function () {
+                            let boundary = null;
+                            let x = 0;
+                            for (let y = 0; y < this.height; y++) {
+                                const idx = (this.width * y + x) << 2;
+                                if (this.data[idx] !== 255) {
+                                    boundary = y;
+                                    break;
+                                }
+                            }
+                            if (boundary !== null) {
+                                logger.info(`found boundary at ${boundary}, cropping image`);
+                                this.data = this.data.slice(0, (this.width * boundary) << 2);
+                                this.height = boundary;
+                                boundary = null;
+                                x = Math.floor(this.width / 2);
+                                for (let y = this.height - 1; y >= 0; y--) {
+                                    const idx = (this.width * y + x) << 2;
+                                    if (this.data[idx] !== 255) {
+                                        boundary = y;
+                                        break;
+                                    }
+                                }
+                                if (boundary != null) {
+                                    logger.info(`found boundary at ${boundary}, trimming image`);
+                                    this.data = this.data.slice(0, (this.width * boundary) << 2);
+                                    this.height = boundary;
+                                }
+                                read(this.pack(), 'base64').then(data => {
+                                    logger.info(`finished webshot for ${url}`);
+                                    resolve({ data, boundary });
+                                });
+                            }
+                            else if (height >= 8 * 1920) {
+                                logger.warn('too large, consider as a bug, returning');
+                                read(this.pack(), 'base64').then(data => {
+                                    logger.info(`finished webshot for ${url}`);
+                                    resolve({ data, boundary: 0 });
+                                });
+                            }
+                            else {
+                                logger.info('unable to found boundary, try shooting a larger image');
+                                resolve({ data: '', boundary });
+                            }
+                        }).parse(screenshot);
+                    })
+                        .then(() => page.close());
+                });
+            });
+            return promise.then(data => {
+                if (data.boundary === null)
+                    return this.renderWebshot(url, height + 1920, webshotDelay);
+                else
+                    return data.data;
+            });
         };
-        logger.info(`shooting ${options.windowSize.width}*${height} webshot for ${url}`);
-        webshot(url, options).pipe(new pngjs_1.PNG({
-            filterType: 4,
-        }))
-            .on('parsed', function () {
-            let boundary = null;
-            let x = 0;
-            for (let y = 0; y < this.height; y++) {
-                const idx = (this.width * y + x) << 2;
-                if (this.data[idx] !== 255) {
-                    boundary = y;
-                    break;
+        this.fetchImage = (url) => new Promise(resolve => {
+            logger.info(`fetching ${url}`);
+            https.get(url, res => {
+                if (res.statusCode === 200) {
+                    read(res, 'base64').then(data => {
+                        logger.info(`successfully fetched ${url}`);
+                        resolve(data);
+                    });
                 }
-            }
-            if (boundary !== null) {
-                logger.info(`found boundary at ${boundary}, cropping image`);
-                this.data = this.data.slice(0, (this.width * boundary) << 2);
-                this.height = boundary;
-                boundary = null;
-                x = Math.floor(this.width / 2);
-                for (let y = this.height - 1; y >= 0; y--) {
-                    const idx = (this.width * y + x) << 2;
-                    if (this.data[idx] !== 255) {
-                        boundary = y;
-                        break;
-                    }
-                }
-                if (boundary != null) {
-                    logger.info(`found boundary at ${boundary}, trimming image`);
-                    this.data = this.data.slice(0, (this.width * boundary) << 2);
-                    this.height = boundary;
+                else {
+                    logger.error(`failed to fetch ${url}: ${res.statusCode}`);
+                    resolve();
                 }
-                read(this.pack(), 'base64').then(data => {
-                    logger.info(`finished webshot for ${url}`);
-                    resolve({ data, boundary });
-                });
-            }
-            else if (height >= 8 * 1920) {
-                logger.warn('too large, consider as a bug, returning');
-                read(this.pack(), 'base64').then(data => {
-                    logger.info(`finished webshot for ${url}`);
-                    resolve({ data, boundary: 0 });
-                });
-            }
-            else {
-                logger.info('unable to found boundary, try shooting a larger image');
-                resolve({ data: '', boundary });
-            }
-        });
-    });
-    return promise.then(data => {
-        if (data.boundary === null)
-            return renderWebshot(url, height + 1920, webshotDelay);
-        else
-            return data.data;
-    });
-}
-function fetchImage(url) {
-    return new Promise(resolve => {
-        logger.info(`fetching ${url}`);
-        https.get(url, res => {
-            if (res.statusCode === 200) {
-                read(res, 'base64').then(data => {
-                    logger.info(`successfully fetched ${url}`);
-                    resolve(data);
-                });
-            }
-            else {
-                logger.error(`failed to fetch ${url}: ${res.statusCode}`);
+            }).on('error', (err) => {
+                logger.error(`failed to fetch ${url}: ${err.message}`);
                 resolve();
-            }
-        }).on('error', (err) => {
-            logger.error(`failed to fetch ${url}: ${err.message}`);
-            resolve();
+            });
         });
-    });
-}
-function default_1(tweets, callback, webshotDelay) {
-    let promise = new Promise(resolve => {
-        resolve();
-    });
-    tweets.forEach(twi => {
-        let cqstr = '';
-        const url = `https://mobile.twitter.com/${twi.user.screen_name}/status/${twi.id_str}`;
-        promise = promise.then(() => renderWebshot(url, 1920, webshotDelay))
-            .then(base64Webshot => {
-            if (base64Webshot)
-                cqstr += `[CQ:image,file=base64://${base64Webshot}]`;
+        puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox'] })
+            .then(browser => this.browser = browser)
+            .then(() => {
+            logger.info('launched puppeteer browser');
+            if (onready)
+                onready();
         });
-        if (twi.extended_entities) {
-            twi.extended_entities.media.forEach(media => {
-                promise = promise.then(() => fetchImage(media.media_url_https))
-                    .then(base64Image => {
-                    if (base64Image)
-                        cqstr += `[CQ:image,file=base64://${base64Image}]`;
-                });
+    }
+    webshot(tweets, callback, webshotDelay) {
+        let promise = new Promise(resolve => {
+            resolve();
+        });
+        tweets.forEach(twi => {
+            let cqstr = '';
+            const url = `https://mobile.twitter.com/${twi.user.screen_name}/status/${twi.id_str}`;
+            promise = promise.then(() => this.renderWebshot(url, 1920, webshotDelay))
+                .then(base64Webshot => {
+                if (base64Webshot)
+                    cqstr += `[CQ:image,file=base64://${base64Webshot}]`;
             });
-        }
-        promise.then(() => callback(cqstr));
-    });
-    return promise;
+            if (twi.extended_entities) {
+                twi.extended_entities.media.forEach(media => {
+                    promise = promise.then(() => this.fetchImage(media.media_url_https))
+                        .then(base64Image => {
+                        if (base64Image)
+                            cqstr += `[CQ:image,file=base64://${base64Image}]`;
+                    });
+                });
+            }
+            promise.then(() => callback(cqstr));
+        });
+        return promise;
+    }
 }
-exports.default = default_1;
+exports.default = Webshot;

+ 1 - 0
package.json

@@ -41,6 +41,7 @@
   "devDependencies": {
     "@types/node": "^10.5.1",
     "@types/pngjs": "^3.3.2",
+    "@types/puppeteer": "^1.5.0",
     "tslint": "^5.10.0",
     "tslint-config-prettier": "^1.13.0"
   }

+ 1 - 1
src/main.ts

@@ -143,6 +143,6 @@ const worker = new Worker({
   bot: qq,
   webshotDelay: config.webshot_delay,
 });
-setTimeout(worker.work, config.work_interval * 1000);
+worker.launch();
 
 qq.connect();

+ 7 - 2
src/twitter.ts

@@ -4,7 +4,7 @@ import * as path from 'path';
 import * as Twitter from 'twitter';
 
 import QQBot from './cqhttp';
-import webshot from './webshot';
+import Webshot from './webshot';
 
 interface IWorkerOption {
   lock: ILock;
@@ -29,6 +29,7 @@ export default class {
   private workInterval: number;
   private bot: QQBot;
   private webshotDelay: number;
+  private webshot: Webshot;
 
   constructor(opt: IWorkerOption) {
     this.client = new Twitter({
@@ -44,6 +45,10 @@ export default class {
     this.webshotDelay = opt.webshotDelay;
   }
 
+  public launch = () => {
+    this.webshot = new Webshot(() => setTimeout(this.work, this.workInterval * 1000));
+  }
+
   public work = () => {
     const lock = this.lock;
     if (this.workInterval < 1) this.workInterval = 1;
@@ -125,7 +130,7 @@ export default class {
         return;
       }
       if (lock.threads[lock.feed[lock.workon]].offset === 0) tweets.splice(1);
-      return webshot(tweets, msg => {
+      return (this.webshot as any)(tweets, msg => {
         lock.threads[lock.feed[lock.workon]].subscribers.forEach(subscriber => {
           logger.info(`pushing data of thread ${lock.feed[lock.workon]} to ${JSON.stringify(subscriber)}`);
           this.bot.bot('send_msg', {

+ 82 - 64
src/webshot.ts

@@ -1,77 +1,96 @@
+import * as CallableInstance from 'callable-instance';
 import * as https from 'https';
 import * as log4js from 'log4js';
 import { PNG } from 'pngjs';
+import * as puppeteer from 'puppeteer';
+import { Browser } from 'puppeteer';
 import * as read from 'read-all-stream';
-import * as CallableInstance from 'callable-instance';
 
 const logger = log4js.getLogger('webshot');
 logger.level = (global as any).loglevel;
 
 class Webshot extends CallableInstance {
-  constructor() {
+
+  private browser: Browser;
+
+  constructor(onready?: () => any) {
     super('webshot');
+    puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']})
+      .then(browser => this.browser = browser)
+      .then(() => {
+        logger.info('launched puppeteer browser');
+        if (onready) onready();
+      });
   }
 
-  private renderWebshot = (url: string, height: number, webshotDelay: number): Promise<string> =>  {
+  private renderWebshot = (url: string, height: number, webshotDelay: number): Promise<string> => {
     const promise = new Promise<{ data: string, boundary: null | number }>(resolve => {
-      const options = {
-        windowSize: {
-          width: 1080,
-          height,
-        },
-        userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36',
-        renderDelay: webshotDelay,
-        quality: 100,
-        customCSS: 'html{zoom:2}header{display:none!important}',
-      };
-      logger.info(`shooting ${options.windowSize.width}*${height} webshot for ${url}`);
-      webshot(url, options).pipe(new PNG({
-        filterType: 4,
-      }))
-        .on('parsed', function () {
-          let boundary = null;
-          let x = 0;
-          for (let y = 0; y < this.height; y++) {
-            const idx = (this.width * y + x) << 2;
-            if (this.data[idx] !== 255) {
-              boundary = y;
-              break;
-            }
-          }
-          if (boundary !== null) {
-            logger.info(`found boundary at ${boundary}, cropping image`);
-            this.data = this.data.slice(0, (this.width * boundary) << 2);
-            this.height = boundary;
+      const width = 1080;
+      logger.info(`shooting ${width}*${height} webshot for ${url}`);
+      this.browser.newPage()
+        .then(page => {
+          page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36')
+            .then(() => page.setViewport({
+              width,
+              height,
+              isMobile: true,
+            }))
+            .then(() => page.goto(url))
+            .then(() => page.addStyleTag({
+              content: 'html{zoom:2}header{display:none!important}',
+            }))
+            .then(() => page.waitFor(webshotDelay))
+            .then(() => page.screenshot())
+            .then(screenshot => {
+              new PNG({
+                filterType: 4,
+              }).on('parsed', function () {
+                let boundary = null;
+                let x = 0;
+                for (let y = 0; y < this.height; y++) {
+                  const idx = (this.width * y + x) << 2;
+                  if (this.data[idx] !== 255) {
+                    boundary = y;
+                    break;
+                  }
+                }
+                if (boundary !== null) {
+                  logger.info(`found boundary at ${boundary}, cropping image`);
+                  this.data = this.data.slice(0, (this.width * boundary) << 2);
+                  this.height = boundary;
 
-            boundary = null;
-            x = Math.floor(this.width / 2);
-            for (let y = this.height - 1; y >= 0; y--) {
-              const idx = (this.width * y + x) << 2;
-              if (this.data[idx] !== 255) {
-                boundary = y;
-                break;
-              }
-            }
-            if (boundary != null) {
-              logger.info(`found boundary at ${boundary}, trimming image`);
-              this.data = this.data.slice(0, (this.width * boundary) << 2);
-              this.height = boundary;
-            }
+                  boundary = null;
+                  x = Math.floor(this.width / 2);
+                  for (let y = this.height - 1; y >= 0; y--) {
+                    const idx = (this.width * y + x) << 2;
+                    if (this.data[idx] !== 255) {
+                      boundary = y;
+                      break;
+                    }
+                  }
+                  if (boundary != null) {
+                    logger.info(`found boundary at ${boundary}, trimming image`);
+                    this.data = this.data.slice(0, (this.width * boundary) << 2);
+                    this.height = boundary;
+                  }
 
-            read(this.pack(), 'base64').then(data => {
-              logger.info(`finished webshot for ${url}`);
-              resolve({data, boundary});
-            });
-          } else if (height >= 8 * 1920) {
-            logger.warn('too large, consider as a bug, returning');
-            read(this.pack(), 'base64').then(data => {
-              logger.info(`finished webshot for ${url}`);
-              resolve({data, boundary: 0});
-            });
-          } else {
-            logger.info('unable to found boundary, try shooting a larger image');
-            resolve({data: '', boundary});
-          }
+                  read(this.pack(), 'base64').then(data => {
+                    logger.info(`finished webshot for ${url}`);
+                    resolve({data, boundary});
+                  });
+                } else if (height >= 8 * 1920) {
+                  logger.warn('too large, consider as a bug, returning');
+                  read(this.pack(), 'base64').then(data => {
+                    logger.info(`finished webshot for ${url}`);
+                    resolve({data, boundary: 0});
+                  });
+                } else {
+                  logger.info('unable to found boundary, try shooting a larger image');
+                  resolve({data: '', boundary});
+                }
+              }).parse(screenshot);
+            })
+            .then(() => page.close());
         });
     });
     return promise.then(data => {
@@ -80,8 +99,8 @@ class Webshot extends CallableInstance {
     });
   }
 
-  private fetchImage = (url: string): Promise<string> => {
-    return new Promise<string>(resolve => {
+  private fetchImage = (url: string): Promise<string> =>
+    new Promise<string>(resolve => {
       logger.info(`fetching ${url}`);
       https.get(url, res => {
         if (res.statusCode === 200) {
@@ -97,10 +116,9 @@ class Webshot extends CallableInstance {
         logger.error(`failed to fetch ${url}: ${err.message}`);
         resolve();
       });
-    });
-  }
+    })
 
-  public webshot = (tweets, callback, webshotDelay: number): Promise<void> => {
+  public webshot(tweets, callback, webshotDelay: number): Promise<void> {
     let promise = new Promise<void>(resolve => {
       resolve();
     });

+ 99 - 5
yarn.lock

@@ -2,6 +2,10 @@
 # yarn lockfile v1
 
 
+"@types/events@*":
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
+
 "@types/node@*", "@types/node@^10.5.1":
   version "10.5.1"
   resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.1.tgz#d578446f4abff5c0b49ade9b4e5274f6badaadfc"
@@ -12,6 +16,13 @@
   dependencies:
     "@types/node" "*"
 
+"@types/puppeteer@^1.5.0":
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-1.5.0.tgz#95b6feff9522d3a054ed09b49798e7232f24d558"
+  dependencies:
+    "@types/events" "*"
+    "@types/node" "*"
+
 addressparser@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/addressparser/-/addressparser-1.0.1.tgz#47afbe1a2a9262191db6838e4fd1d39b40821746"
@@ -83,6 +94,10 @@ ast-types@0.x.x:
   version "0.11.5"
   resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.5.tgz#9890825d660c03c28339f315e9fa0a360e31ec28"
 
+async-limiter@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
+
 async@~2.6.0:
   version "2.6.1"
   resolved "https://registry.yarnpkg.com/async/-/async-2.6.1.tgz#b245a23ca71930044ec53fa46aa00a3e87c6a610"
@@ -158,6 +173,10 @@ brace-expansion@^1.1.7:
     balanced-match "^1.0.0"
     concat-map "0.0.1"
 
+buffer-from@^1.0.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04"
+
 buffer-more-ints@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-0.0.2.tgz#26b3885d10fa13db7fc01aae3aab870199e0124c"
@@ -182,6 +201,10 @@ bytes@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048"
 
+callable-instance@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/callable-instance/-/callable-instance-1.0.0.tgz#d6c7caaacafc598c02d44e2c6e20faae9634549b"
+
 caseless@~0.11.0:
   version "0.11.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
@@ -249,6 +272,15 @@ concat-map@0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
 
+concat-stream@1.6.2:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
+  dependencies:
+    buffer-from "^1.0.0"
+    inherits "^2.0.3"
+    readable-stream "^2.2.2"
+    typedarray "^0.0.6"
+
 core-util-is@1.0.2, core-util-is@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
@@ -280,7 +312,7 @@ date-format@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/date-format/-/date-format-1.2.0.tgz#615e828e233dd1ab9bb9ae0950e0ceccfa6ecad8"
 
-debug@2, debug@^2.2.0:
+debug@2, debug@2.6.9, debug@^2.2.0:
   version "2.6.9"
   resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
   dependencies:
@@ -379,6 +411,15 @@ extend@3, extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
 
+extract-zip@^1.6.6:
+  version "1.6.7"
+  resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9"
+  dependencies:
+    concat-stream "1.6.2"
+    debug "2.6.9"
+    mkdirp "0.5.1"
+    yauzl "2.4.1"
+
 extsprintf@1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -399,6 +440,12 @@ fast-levenshtein@~2.0.4:
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
 
+fd-slicer@~1.0.1:
+  version "1.0.1"
+  resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
+  dependencies:
+    pend "~1.2.0"
+
 file-uri-to-path@1:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@@ -467,7 +514,7 @@ getpass@^0.1.1:
   dependencies:
     assert-plus "^1.0.0"
 
-glob@^7.1.1:
+glob@^7.0.5, glob@^7.1.1:
   version "7.1.2"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
   dependencies:
@@ -603,7 +650,7 @@ inflight@^1.0.4:
     once "^1.3.0"
     wrappy "1"
 
-inherits@2, inherits@2.0.3, inherits@~2.0.1, inherits@~2.0.3:
+inherits@2, inherits@2.0.3, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
 
@@ -789,6 +836,10 @@ mime-types@^2.1.11, mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.7:
   dependencies:
     mime-db "~1.33.0"
 
+mime@^2.0.3:
+  version "2.3.1"
+  resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
+
 minimatch@^3.0.4:
   version "3.0.4"
   resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083"
@@ -799,7 +850,7 @@ minimist@0.0.8:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
 
-mkdirp@^0.5.1:
+mkdirp@0.5.1, mkdirp@^0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   dependencies:
@@ -928,6 +979,10 @@ path-proxy@~1.0.0:
   dependencies:
     inflection "~1.3.0"
 
+pend@~1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+
 performance-now@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -958,6 +1013,10 @@ process-nextick-args@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa"
 
+progress@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f"
+
 promisify-call@^2.0.2:
   version "2.0.4"
   resolved "https://registry.yarnpkg.com/promisify-call/-/promisify-call-2.0.4.tgz#d48c2d45652ccccd52801ddecbd533a6d4bd5fba"
@@ -989,6 +1048,19 @@ punycode@1.4.1, punycode@^1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
 
+puppeteer@^1.5.0:
+  version "1.5.0"
+  resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.5.0.tgz#e35db3f3ba3d41013feb65be02bdaa727ec7b8ec"
+  dependencies:
+    debug "^3.1.0"
+    extract-zip "^1.6.6"
+    https-proxy-agent "^2.2.1"
+    mime "^2.0.3"
+    progress "^2.0.0"
+    proxy-from-env "^1.0.0"
+    rimraf "^2.6.1"
+    ws "^5.1.1"
+
 qs@~6.2.0:
   version "6.2.3"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.3.tgz#1cfcb25c10a9b2b483053ff39f5dfc9233908cfe"
@@ -1022,7 +1094,7 @@ readable-stream@1.1.x, "readable-stream@1.x >=1.1.9":
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.3.0:
+readable-stream@2, readable-stream@^2.0.0, readable-stream@^2.2.2, readable-stream@^2.3.0:
   version "2.3.6"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
   dependencies:
@@ -1131,6 +1203,12 @@ resolve@^1.3.2:
   dependencies:
     path-parse "^1.0.5"
 
+rimraf@^2.6.1:
+  version "2.6.2"
+  resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
+  dependencies:
+    glob "^7.0.5"
+
 safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   version "5.1.2"
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
@@ -1349,6 +1427,10 @@ typedarray-to-buffer@^3.1.2:
   dependencies:
     is-typedarray "^1.0.0"
 
+typedarray@^0.0.6:
+  version "0.0.6"
+  resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+
 typescript@^2.9.2:
   version "2.9.2"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
@@ -1413,6 +1495,12 @@ wrappy@1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
 
+ws@^5.1.1:
+  version "5.2.1"
+  resolved "https://registry.yarnpkg.com/ws/-/ws-5.2.1.tgz#37827a0ba772d072a843c3615b0ad38bcdb354eb"
+  dependencies:
+    async-limiter "~1.0.0"
+
 xregexp@2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"
@@ -1428,3 +1516,9 @@ yaeti@^0.0.6:
 yallist@^2.1.2:
   version "2.1.2"
   resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52"
+
+yauzl@2.4.1:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"
+  dependencies:
+    fd-slicer "~1.0.1"