├── .eslintrc.js ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── builder ├── .dockerignore ├── Dockerfile ├── Dockerfile.nonheadless ├── README.md ├── app.yaml ├── chromeuser-script_nonheadless.sh ├── docker_build.sh ├── docker_run.sh ├── entrypoint.sh ├── entrypoint_nonheadless.sh ├── package.json ├── server.js └── yarn.lock ├── deploy.sh ├── frontend ├── .gcloudignore ├── README.md ├── app.yaml ├── lighthouse-ci.js ├── package.json ├── public │ ├── app.js │ ├── logo-nolight.png │ ├── logo.svg │ ├── styles.css │ └── try.html ├── server.js └── yarn.lock ├── package.json ├── runlighthouse.js └── yarn.lock /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // start with google standard style 3 | // https://github.com/google/eslint-config-google/blob/master/index.js 4 | "extends": ["eslint:recommended", "google"], 5 | "env": { 6 | "node": true, 7 | "es6": true, 8 | "browser": true, 9 | }, 10 | "parserOptions": { 11 | "ecmaVersion": 8, 12 | "ecmaFeatures": { 13 | "jsx": false, 14 | "experimentalObjectRestSpread": false 15 | }, 16 | "sourceType": "script" 17 | }, 18 | "rules": { 19 | // 2 == error, 1 == warning, 0 == off 20 | "indent": [2, 2, { 21 | "SwitchCase": 1, 22 | "VariableDeclarator": 2 23 | }], 24 | "max-len": [2, 100, { 25 | "ignoreComments": true, 26 | "ignoreUrls": true, 27 | "tabWidth": 2 28 | }], 29 | "no-empty": [2, { 30 | "allowEmptyCatch": true 31 | }], 32 | "no-implicit-coercion": [2, { 33 | "boolean": false, 34 | "number": true, 35 | "string": true 36 | }], 37 | "no-unused-expressions": [2, { 38 | "allowShortCircuit": true, 39 | "allowTernary": false 40 | }], 41 | "no-unused-vars": [2, { 42 | "vars": "all", 43 | "args": "after-used", 44 | "argsIgnorePattern": "(^reject$|^_$)", 45 | "varsIgnorePattern": "(^_$)" 46 | }], 47 | "quotes": [2, "single"], 48 | "strict": [2, "global"], 49 | "prefer-const": 2, 50 | 51 | // Disabled rules 52 | "require-jsdoc": 0, 53 | "valid-jsdoc": 0, 54 | "comma-dangle": 0, 55 | "arrow-parens": 0, 56 | "no-console": 0 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | npm-debug.log 4 | .vscode 5 | 6 | frontend/.oauth_token 7 | frontend/app.yaml -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Folders 2 | .vscode/ 3 | node_modules/ 4 | builder/ 5 | frontend/ 6 | 7 | # Dev files 8 | deploy.sh 9 | .editorconfig 10 | .eslintignore 11 | .eslintrc.js 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2014 Google Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lighthouse Bot (deprecated) 2 | 3 | **Update:** LighthouseBot has been deprecated and we now recommend using the official [Lighthouse CI](https://github.com/GoogleChrome/lighthouse-ci) project to automate running Lighthouse for every commit, view the changes, and prevent regressions 4 | 5 | ## Historical README below 6 | 7 | This repo contained the frontend and backend for running Lighthouse in CI and integration with Github Pull Requests. An example web service is hosted for demo purposes. 8 | 9 | ## Auditing GitHub Pull Requests 10 | 11 | > Please note: This drop in service is considered **Beta**. There are no SLAs or uptime guarantees. If you're interested in running your own CI server in a Docker container, check out [Running your own CI server](#running-your-own-ci-server). 12 | 13 | Lighthouse can be setup as part of your CI on **Travis only**. As new pull requests come in, the **Lighthouse Bot tests the changes and reports back the new score**. 14 | 15 | Run Lighthouse on Github PRs 16 | 17 | To audit pull requests, do the following: 18 | 19 | ### 1. Initial setup 20 | 21 | #### Add the lighthousebot to your repo 22 | 23 | First, add [lighthousebot](https://github.com/lighthousebot) as a collaborator on your repo. Lighthouse CI uses an OAuth token scoped to the `repo` permission in order to update the status of your PRs and post comments on the issue as the little Lighthouse icon. 24 | 25 | _* Until Lighthousebot accepts your invitation to collaborate, which is currently a lengthy manual process, it does not have permission to update the status of your PRs. However, it will post a comment on your PR._ 26 | 27 | #### Get an API Key 28 | 29 | [Request an API Key](https://goo.gl/forms/9BzzhHd1sKzsvyC52). API keys will eventually be 30 | enforced and are necessary so we can contact you when there are changes to the CI system. 31 | 32 | Once you have a key, update Travis settings by adding an `LIGHTHOUSE_API_KEY` environment variables with your key: 33 | 34 | Travis LIGHTHOUSE_API_KEY env variable 35 | 36 | The `lighthousebot` script will include your key in requests made to the CI server. 37 | 38 | ### 2. Deploy the PR 39 | 40 | We recommend deploying your PR to a real staging server instead of running a local server on Travis. 41 | A staging environment will produce realistic performance numbers that are 42 | more representative of your production setup. The Lighthouse report will be more accurate. 43 | 44 | In `.travis.yml`, add an `after_success` that **deploys the PR's changes to a staging server**. 45 | 46 | ```bash 47 | after_success: 48 | - ./deploy.sh # TODO(you): deploy the PR changes to your staging server. 49 | ``` 50 | 51 | Since every hosting environment has different deployment setups, the implementation of `deploy.sh` is left to the reader. 52 | 53 | > **Tip:** Using Google App Engine? Check out [`deploy_pr_gae.sh`](https://github.com/GoogleChrome/chromium-dashboard/blob/master/travis/deploy_pr_gae.sh) which shows how to install the GAE SDK and deploy PR changes programmatically. 54 | 55 | ### 3. Call lighthousebot 56 | 57 | Install the script: 58 | 59 | npm i --save-dev https://github.com/GoogleChromeLabs/lighthousebot 60 | 61 | Add an NPM script to your `package.json`: 62 | 63 | ```js 64 | "scripts": { 65 | "lh": "lighthousebot" 66 | } 67 | ``` 68 | 69 | Next, in `.travis.yml` call [`npm run lh`][runlighthouse-link] as the last step in `after_success`: 70 | 71 | ```yml 72 | install: 73 | - npm install # make sure to install the deps when Travis runs. 74 | after_success: 75 | - ./deploy.sh # TODO(you): deploy the PR changes to your staging server. 76 | - npm run lh -- https://staging.example.com 77 | ``` 78 | 79 | When Lighthouse is done auditing the URL, the bot will post a comment to the pull 80 | request containing the updated scores: 81 | 82 | Lighthouse Github comment 83 | 84 | You can also opt-out of the comment by using the `--no-comment` flag. 85 | 86 | #### Failing a PR when it drops your Lighthouse score 87 | 88 | Lighthouse CI can prevent PRs from being merged when one of the scores falls 89 | below a specified value. Just include one or more of `--pwa`, `--perf`, `--seo`, 90 | `--a11y`, or `--bp`: 91 | 92 | ```yml 93 | after_success: 94 | - ./deploy.sh # TODO(you): deploy the PR changes to your staging server. 95 | - npm run lh -- --perf=96 --pwa=100 https://staging.example.com 96 | ``` 97 | 98 | 99 | 100 | #### Options 101 | 102 | ```bash 103 | $ lighthouse-ci -h 104 | 105 | Usage: 106 | runlighthouse.js [--perf,pwa,seo,a11y,bp=] [--no-comment] [--runner=chrome,wpt] 107 | 108 | Options: 109 | Minimum score values can be passed per category as a way to fail the PR if 110 | the thresholds are not met. If you don't provide thresholds, the PR will 111 | be mergeable no matter what the scores. 112 | 113 | --pwa Minimum PWA score for the PR to be considered "passing". [Number] 114 | --perf Minimum performance score for the PR to be considered "passing". [Number] 115 | --seo Minimum seo score for the PR to be considered "passing". [Number] 116 | --a11y Minimum accessibility score for the PR to be considered "passing". [Number] 117 | --bp Minimum best practices score for the PR to be considered "passing". [Number] 118 | 119 | --no-comment Doesn't post a comment to the PR issue summarizing the Lighthouse results. [Boolean] 120 | 121 | --runner Selects Lighthouse running on Chrome or WebPageTest. [--runner=chrome,wpt] 122 | 123 | --help Prints help. 124 | 125 | Examples: 126 | 127 | Runs Lighthouse and posts a summary of the results. 128 | runlighthouse.js https://example.com 129 | 130 | Fails the PR if the performance score drops below 93. Posts the summary comment. 131 | runlighthouse.js --perf=93 https://example.com 132 | 133 | Fails the PR if perf score drops below 93 or the PWA score drops below 100. Posts the summary comment. 134 | runlighthouse.js --perf=93 --pwa=100 https://example.com 135 | 136 | Runs Lighthouse on WebPageTest. Fails the PR if the perf score drops below 93. 137 | runlighthouse.js --perf=93 --runner=wpt --no-comment https://example.com 138 | ``` 139 | 140 | ## Running on WebPageTest instead of Chrome 141 | 142 | By default, `lighthousebot` runs your PRs through Lighthouse hosted in the cloud. As an alternative, you can test on real devices using the WebPageTest integration: 143 | 144 | ```bash 145 | lighthousebot --perf=96 --runner=wpt https://staging.example.com 146 | ``` 147 | 148 | At the end of testing, your PR will be updated with a link to the WebPageTest results containing the Lighthouse report! 149 | 150 | ## Running your own CI server 151 | 152 | Want to setup your own Lighthouse instance in a Docker container? 153 | 154 | The good news is Docker does most of the work for us! The bulk of getting started is in [Development](#development). That will take you through initial setup and show how to run the CI frontend. 155 | 156 | For the backend, see [builder/README.md](https://github.com/GoogleChromeLabs/lighthousebot/blob/master/builder/README.md) for building and running the Docker container. 157 | 158 | Other changes, to the "Development" section: 159 | 160 | - Create a personal OAuth token in https://github.com/settings/tokens. Drop it in `frontend/.oauth_token`. 161 | - Add a `LIGHTHOUSE_CI_HOST` env variable to Travis settings that points to your own URL. The one where you deploy the Docker container. 162 | 163 | ## Development 164 | 165 | Initial setup: 166 | 167 | 1. Ask an existing dev for the oauth2 token. If you need to regenerate one, see below. 168 | - Create `frontend/.oauth_token` and copy in the token value. 169 | 170 | Run the dev server: 171 | 172 | cd frontend 173 | npm run start 174 | 175 | This will start a web server and use the token in `.oauth_token`. The token is used to update PR status in Github. 176 | 177 | In your test repo: 178 | 179 | - Run `npm i --save-dev https://github.com/GoogleChromeLabs/lighthousebot` 180 | - Follow the steps in [Auditing Github Pull Requests](#auditing-github-pull-requests) for setting up 181 | your repo. 182 | 183 | Notes: 184 | 185 | - If you want to make changes to the builder, you'll need [Docker](https://www.docker.com/) and the [GAE Node SDK](https://cloud.google.com/appengine/docs/flexible/nodejs/download). 186 | - To make changes to the CI server, you'll probably want to run [ngrok](https://ngrok.com/) so you can test against a local server instead of deploying for each change. In Travis settings, 187 | add a `LIGHTHOUSE_CI_HOST` env variable that points to your ngrok instance. 188 | 189 | ##### Generating a new OAuth2 token 190 | 191 | If you need to generate a new OAuth token: 192 | 193 | 1. Sign in to the [lighthousebot](https://github.com/lighthousebot) Github account. (Admins: the credentials are in the usual password tool). 194 | 2. Visit personal access tokens: https://github.com/settings/tokens. 195 | 3. Regenerate the token. **Important**: this invalidates the existing token so other developers will need to be informed. 196 | 4. Update token in `frontend/.oauth_token`. 197 | 198 | #### Deploy 199 | 200 | By default, these scripts deploy to [Google App Engine Flexible containers](https://cloud.google.com/appengine/docs/flexible/nodejs/) (Node). If you're running your own CI server, use your own setup :) 201 | 202 | Deploy the frontend: 203 | 204 | npm run deploy YYYY-MM-DD frontend 205 | 206 | Deploy the CI builder backend: 207 | 208 | npm run deploy YYYY-MM-DD builder 209 | 210 | ## Source & Components 211 | 212 | This repo contains several different pieces for the Lighthouse Bot: a backend, frontend, and frontend UI. 213 | 214 | ### UI Frontend 215 | > Quick way to try Lighthouse: https://lighthouse-ci.appspot.com/try 216 | 217 | Relevant source: 218 | 219 | - `frontend/public/` - UI for https://lighthouse-ci.appspot.com/try. 220 | 221 | ### Bot CI server (frontend) 222 | > Server that responds to requests from Travis. 223 | 224 | REST endpoints: 225 | - `https://lighthouse-ci.appspot.com/run_on_chrome` 226 | - `https://lighthouse-ci.appspot.com/run_on_wpt` 227 | 228 | #### Example 229 | 230 | **Note:** `lighthousebot` does this for you. 231 | 232 | ``` 233 | POST https://lighthouse-ci.appspot.com/run_on_chrome 234 | Content-Type: application/json 235 | X-API-KEY: 236 | 237 | { 238 | testUrl: "https://staging.example.com", 239 | thresholds: { 240 | pwa: 100, 241 | perf: 96, 242 | }, 243 | addComment: true, 244 | repo: { 245 | owner: "", 246 | name: "" 247 | }, 248 | pr: { 249 | number: , 250 | sha: "" 251 | } 252 | } 253 | ``` 254 | 255 | Relevant source: 256 | 257 | - [`frontend/server.js`](https://github.com/GoogleChromeLabs/lighthousebot/blob/master/frontend/server.js) - server which accepts Github pull requests and updates the status of your PR. 258 | 259 | ### CI backend (builder) 260 | > Server that runs Lighthouse against a URL, using Chrome. 261 | 262 | REST endpoints: 263 | - `https://lighthouse-ci.appspot.com/ci` 264 | 265 | #### Example 266 | 267 | **Note:** `lighthousebot` does this for you. 268 | 269 | ```bash 270 | curl -X POST \ 271 | -H "Content-Type: application/json" \ 272 | -H "X-API-KEY: " \ 273 | --data '{"output": "json", "url": "https://staging.example.com"}' \ 274 | https://builder-dot-lighthouse-ci.appspot.com/ci 275 | ``` 276 | 277 | ## FAQ 278 | 279 | ##### Why not deployment events? 280 | 281 | Github's [Deployment API](https://developer.github.com/v3/repos/deployments/) would 282 | be ideal, but it has some downsides: 283 | 284 | - Github Deployments happen __after__ a pull is merged. We want to support blocking PR 285 | merges based on a LH score. 286 | - We want to be able to audit changes as they're add to the PR. `pull_request`/`push` events are more appropriate for that. 287 | 288 | ##### Why not a Github Webhook? 289 | 290 | The main downside of a Github webhook is that there's no way to include custom 291 | data in the payload Github sends to the webhook handler. For example, how would 292 | Lighthouse know what url to test? With a webhook, the user also has to setup it 293 | up and configure it properly. 294 | 295 | Future work: Lighthouse Bot could define a file that developer includes in their 296 | repo. The bot's endpoint could pull a `.lighthouse_ci` file that includes meta 297 | data `{minLighthouseScore: 96, testUrl: 'https://staging.example.com'}`. However, 298 | this requires work from the developer. 299 | 300 | [runlighthouse-link]: https://github.com/GoogleChromeLabs/lighthousebot/blob/master/runlighthouse.js 301 | -------------------------------------------------------------------------------- /builder/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | .dockerignore 4 | Dockerfile* 5 | *-debug.log 6 | *-error.log 7 | .git 8 | .hg 9 | .svn 10 | README.md 11 | -------------------------------------------------------------------------------- /builder/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-slim 2 | 3 | LABEL maintainer="Eric Bidelman " 4 | 5 | # Install utilities 6 | RUN apt-get update --fix-missing && apt-get -y upgrade 7 | 8 | # Install latest chrome dev package. 9 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 10 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 11 | && apt-get update \ 12 | && apt-get install -y google-chrome-unstable --no-install-recommends \ 13 | && rm -rf /var/lib/apt/lists/* \ 14 | && rm -rf /src/*.deb 15 | 16 | ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init 17 | RUN chmod +x /usr/local/bin/dumb-init 18 | 19 | # Download latest Lighthouse from npm. 20 | # cache bust so we always get the latest version of LH when building the image. 21 | ARG CACHEBUST=1 22 | RUN npm i lighthouse -g 23 | 24 | # Install express. 25 | COPY package.json . 26 | RUN npm i --production 27 | 28 | # Add the simple server. 29 | COPY server.js / 30 | RUN chmod +x /server.js 31 | 32 | COPY entrypoint.sh / 33 | RUN chmod +x /entrypoint.sh 34 | 35 | # Add a chrome user and setup home dir. 36 | RUN groupadd --system chrome && \ 37 | useradd --system --create-home --gid chrome --groups audio,video chrome && \ 38 | mkdir --parents /home/chrome/reports && \ 39 | chown --recursive chrome:chrome /home/chrome 40 | 41 | USER chrome 42 | 43 | #VOLUME /home/chrome/reports 44 | #WORKDIR /home/chrome/reports 45 | 46 | # Disable Lighthouse error reporting to prevent prompt. 47 | ENV CI=true 48 | 49 | EXPOSE 8080 50 | 51 | ENTRYPOINT ["dumb-init", "--", "/entrypoint.sh"] 52 | #CMD ["lighthouse", "--help"] 53 | -------------------------------------------------------------------------------- /builder/Dockerfile.nonheadless: -------------------------------------------------------------------------------- 1 | FROM node:8-slim 2 | 3 | LABEL maintainer="Eric Bidelman " 4 | 5 | # Install utilities, Xvfb and dbus for X11 6 | RUN apt-get update --fix-missing && apt-get -y upgrade 7 | RUN apt-get install -y sudo xvfb dbus-x11 --no-install-recommends 8 | 9 | # Install latest chrome dev package. 10 | RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 11 | && sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list' \ 12 | && apt-get update \ 13 | && apt-get install -y google-chrome-unstable --no-install-recommends \ 14 | && rm -rf /var/lib/apt/lists/* \ 15 | && rm -rf /src/*.deb 16 | 17 | ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.0/dumb-init_1.2.0_amd64 /usr/local/bin/dumb-init 18 | RUN chmod +x /usr/local/bin/dumb-init 19 | 20 | # Download latest Lighthouse from npm. 21 | # cache bust so we always get the latest version of LH when building the image. 22 | ARG CACHEBUST=1 23 | RUN npm i lighthouse -g 24 | 25 | # Install express. 26 | COPY package.json . 27 | RUN npm i --production 28 | 29 | # Add the simple server. 30 | COPY server.js / 31 | RUN chmod +x /server.js 32 | 33 | # Copy the chrome-user script used to start Chrome as non-root 34 | COPY chromeuser-script_nonheadless.sh / 35 | RUN chmod +x /chromeuser-script_nonheadless.sh 36 | 37 | # Set the entrypoint 38 | COPY entrypoint_nonheadless.sh / 39 | RUN chmod +x /entrypoint_nonheadless.sh 40 | 41 | # Add a user and make it a sudo user. 42 | RUN groupadd -r chrome && useradd -r -m -g chrome -G audio,video chrome && \ 43 | mkdir -p /home/chrome/reports && \ 44 | chown -R chrome:chrome /home/chrome && \ 45 | sudo adduser chrome sudo 46 | 47 | # Disable Lighthouse error reporting to prevent prompt. 48 | ENV CI=true 49 | 50 | EXPOSE 8080 51 | 52 | ENTRYPOINT ["dumb-init", "--", "/entrypoint_nonheadless.sh"] 53 | #ENTRYPOINT ["dumb-init", "--"] 54 | 55 | #CMD ["/entrypoint_nonheadless.sh"] 56 | #CMD ["/bin/bash"] 57 | #CMD ["node", "server.js"] 58 | -------------------------------------------------------------------------------- /builder/README.md: -------------------------------------------------------------------------------- 1 | # Lighthouse in Docker 2 | 3 | > Run Lighthouse in a Docker container (as a CLI or a web service) 4 | 5 | This folder repo example Dockerfiles for running Lighthouse using [Headless Chrome](https://developers.google.com/web/updates/2017/04/headless-chrome) and full Chrome and can 6 | be used in cloud environments like [Google App Engine Flex](https://cloud.google.com/appengine/docs/flexible/nodejs/) (Node). 7 | 8 | Main source files: 9 | 10 | - [`Dockerfile`](https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile) - Dockerfile for running Lighthouse using headless Chrome. 11 | - [`Dockerfile.nonheadless`](https://github.com/ebidel/lighthouse-ci/blob/master/builder/Dockerfile.nonheadless) - Dockerfile for running Lighthouse using full Chrome. 12 | - `server.js` - The server implementation for the `/ci` endpoint. See [Using the container as a web service](#using-the-container-as-a-cli). 13 | 14 | ## Build it 15 | 16 | Fire up Docker, then run: 17 | 18 | ```bash 19 | yarn build 20 | ``` 21 | 22 | **Image size: ~690MB.** 23 | 24 | ## Running the container 25 | 26 | There are two ways to run the container. One is directly from the command line. 27 | The other option starts a server and allows you to run Lighthouse as a web service (LaaS). 28 | 29 | ### Using the container as a CLI 30 | 31 | The container can be from the the CLI just like using the Lighthouse npm module. See 32 | Lighthouse docs for [CLI options](https://github.com/GoogleChrome/lighthouse#cli-options). 33 | 34 | ```bash 35 | # Audit example.com. Lighthouse results are printed to stdout. 36 | docker run -it --rm --cap-add=SYS_ADMIN lighthouse_ci https://example.com 37 | 38 | 39 | # Audits example.com and saves HTML report to a file. 40 | docker run -it --rm --cap-add=SYS_ADMIN lighthouse_ci https://example.com --quiet > report.html 41 | 42 | # Audits example.com and saves JSON results to a file. 43 | docker run -it --rm --cap-add=SYS_ADMIN lighthouse_ci https://example.com --quiet --output=json > report.json 44 | 45 | # Print Lighthouse version used in the container. 46 | docker run -it --rm --cap-add=SYS_ADMIN lighthouse_ci --version 47 | ``` 48 | 49 | ### Using the container as a web service (LaaS) 50 | 51 | The container also ships with a web service that supports a REST API. You can 52 | use it to run Lighthouse in the cloud and return scores. 53 | 54 | To run the web server, invoke `docker run` without any arguments: 55 | 56 | ```bash 57 | docker run -dit -p 8080:8080 --rm --name lighthouse_ci --cap-add=SYS_ADMIN lighthouse_ci 58 | 59 | # or 60 | yarn serve 61 | 62 | # or 63 | # handy for building + restarting the container 64 | yarn restart 65 | ``` 66 | 67 | This starts a server on `8080` and exposes a REST endpoint at `http://localhost:8080/ci`. 68 | By default, requests ask for an API key to help prevent abuse and associate 69 | requests with users. However, uou don't have to use one in your own server. 70 | If you don't to require keys from users, simply include the parameter but use a 71 | fake value (e.g. "abc123"). 72 | 73 | **Examples** 74 | 75 | `GET` requests will stream logs from Lighthouse until the report is ready. Once 76 | ready, the page redirects to the final output: 77 | 78 | 79 | ```bash 80 | curl http://localhost:8080/ci?key=&url=https://example.com&output=html 81 | ``` 82 | 83 | The endpoint also supports `POST` requests. Instead of query params, send JSON 84 | with the same parameter names: 85 | 86 | ```bash 87 | curl -X POST \ 88 | -H "Content-Type: application/json" \ 89 | -H "X-API-KEY: " \ 90 | --data '{"output": "json", "url": "https://example.com"}' \ 91 | http://localhost:8080/ci 92 | ``` 93 | 94 | where `output` is `json` or `html`. 95 | 96 | ## Using full Chrome instead of headless Chrome 97 | 98 | By default, the Dockerfile launches headless Chrome to run Lighthouse. If you 99 | want to to use "headlful" Chrome, build the image using `Dockerfile.nonheadless`: 100 | 101 | ```bash 102 | docker build -f Dockerfile.nonheadless -t lighthouse_ci . --build-arg CACHEBUST=$(date +%d) 103 | ``` 104 | 105 | Everything else should remain the same. 106 | 107 | ## Deploy to Google App Engine Flex (Node) 108 | 109 | First, get yourself the [Google Cloud SDK](https://cloud.google.com/sdk/). 110 | 111 | When you're ready to deploy the app, run `gcloud deploy` with your app id and version: 112 | 113 | ``` 114 | gcloud app deploy app.yaml --project YOUR_PROJECT_ID --version 2017-10-16 115 | ``` 116 | -------------------------------------------------------------------------------- /builder/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: custom 2 | env: flex 3 | service: builder 4 | 5 | health_check: 6 | enable_health_check: False 7 | 8 | automatic_scaling: 9 | min_num_instances: 1 10 | max_num_instances: 5 11 | 12 | resources: 13 | cpu: 4 14 | memory_gb: 16 15 | disk_size_gb: 10 16 | -------------------------------------------------------------------------------- /builder/chromeuser-script_nonheadless.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run full chrome with a bunch of stuff turned off. 4 | nohup google-chrome \ 5 | --no-first-run \ 6 | --disable-gpu \ 7 | --disable-translate \ 8 | --disable-default-apps \ 9 | --disable-extensions \ 10 | --disable-background-networking \ 11 | --disable-sync \ 12 | --metrics-recording-only \ 13 | --safebrowsing-disable-auto-update \ 14 | --disable-setuid-sandbox \ 15 | --user-data-dir=${TMP_PROFILE_DIR} \ 16 | --remote-debugging-port=9222 'about:blank' & 17 | -------------------------------------------------------------------------------- /builder/docker_build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Switch if you want to Lighthouse with full Chrome instead of headless. 4 | # docker build -f Dockerfile.nonheadless -t lighthouse_ci . --build-arg CACHEBUST=$(date +%d) 5 | 6 | docker build -t lighthouse_ci . --build-arg CACHEBUST=$(date +%d) 7 | -------------------------------------------------------------------------------- /builder/docker_run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker kill lighthouse_ci 4 | docker run -dit -p 8080:8080 --rm --name lighthouse_ci --cap-add=SYS_ADMIN lighthouse_ci 5 | -------------------------------------------------------------------------------- /builder/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ -z "$1" ]; then 4 | npm run start 5 | else 6 | lighthouse --port=9222 --chrome-flags="--headless" --output-path=stdout $@ 7 | fi 8 | -------------------------------------------------------------------------------- /builder/entrypoint_nonheadless.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Using full Chrome in Docker requires us to start xvfb and launch our own instance. 4 | 5 | /etc/init.d/dbus start 6 | 7 | Xvfb :99 -ac -screen 0 1280x1024x24 -nolisten tcp & 8 | xvfb=$! 9 | export DISPLAY=:99 10 | 11 | TMP_PROFILE_DIR=$(mktemp -d -t lighthouse.XXXXXXXXXX) 12 | 13 | su chrome /chromeuser-script_nonheadless.sh 14 | 15 | if [ -z "$1" ]; then 16 | npm run start 17 | else 18 | lighthouse --port=9222 --output-path=stdout $@ 19 | fi 20 | -------------------------------------------------------------------------------- /builder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lighthouse_ci", 3 | "description": "Lighthouse in Docker", 4 | "author": "Eric Bidelman , Cedric Bellet", 5 | "license": "Apache-2.0", 6 | "main": "server.js", 7 | "scripts": { 8 | "start": "node server.js", 9 | "build": "./docker_build.sh", 10 | "restart": "yarn build && yarn serve", 11 | "serve": "./docker_run.sh", 12 | "chrome": "docker run -it --rm --cap-add=SYS_ADMIN lighthouse_ci https://example.com --fast --quiet --output=json | node -e \"let f = ''; process.stdin.on('data', d => f += d); process.stdin.on('close', () => console.log(JSON.parse(f).userAgent));\"" 13 | }, 14 | "dependencies": { 15 | "body-parser": "^1.18.3", 16 | "express": "^4.16.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /builder/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const express = require('express'); 5 | const spawn = require('child_process').spawn; 6 | const bodyParser = require('body-parser'); 7 | 8 | const API_KEY_HEADER = 'X-API-KEY'; 9 | const PORT = 8080; 10 | const REPORTS_DIR = './home/chrome/reports'; 11 | 12 | function validURL(url, res) { 13 | if (!url) { 14 | res.status(400).send('Please provide a URL.'); 15 | return false; 16 | } 17 | 18 | if (!url.startsWith('http')) { 19 | res.status(400).send('URL must start with http.'); 20 | return false; 21 | } 22 | 23 | return true; 24 | } 25 | 26 | function getDefaultArgs(outputPath, format) { 27 | return [ 28 | `--output-path=${outputPath}`, 29 | `--output=${format}`, 30 | // Dicey to use port=0 to launch a new instance of Chrome per invocation 31 | // of LH. On Linux, eventually Chrome Launcher begins to fail. 32 | // Root is https://github.com/GoogleChrome/chrome-launcher/issues/6. 33 | // '--port=9222', 34 | '--port=0', // choose random port every time so we launch a new instance of Chrome. 35 | // Note: this is a noop when using Dockerfile.nonheadless b/c Chrome is already launched. 36 | '--chrome-flags="--headless"', 37 | ]; 38 | } 39 | 40 | // Handler for CI. 41 | function runLH(params, req, res, next) { 42 | const url = params.url; 43 | const format = params.output || params.format || 'html'; 44 | const log = params.log || req.method === 'GET'; 45 | 46 | if (!validURL(url, res)) { 47 | return; 48 | } 49 | 50 | const fileName = `report.${Date.now()}.${format}`; 51 | const outputPath = `${REPORTS_DIR}/${fileName}`; 52 | const args = getDefaultArgs(outputPath, format); 53 | 54 | const child = spawn('lighthouse', [...args, url]); 55 | child.stderr.pipe(process.stderr); 56 | child.stdout.pipe(process.stdout); 57 | 58 | if (log) { 59 | res.writeHead(200, { 60 | 'Content-Type': 'text/html', 61 | 'Cache-Control': 'no-cache', 62 | 'Connection': 'keep-alive', 63 | 'X-Accel-Buffering': 'no' // Forces Flex App Engine to keep connection open for streaming. 64 | }); 65 | 66 | res.write(` 67 | 76 | '); 90 | res.write(``); 91 | res.end(); 92 | } else { 93 | res.sendFile(`/${outputPath}`, {}, err => { 94 | if (err) { 95 | next(err); 96 | } 97 | // delete report 98 | fs.unlink(outputPath, err => { 99 | if (err) { 100 | next(err); 101 | } 102 | }); 103 | }); 104 | } 105 | }); 106 | } 107 | 108 | // Serve sent event handler for https://lighthouse-ci.appspot.com/try. 109 | function runLighthouseAsEventStream(req, res, next) { 110 | const url = req.query.url; 111 | const format = req.query.output || req.query.format || 'html'; 112 | 113 | if (!validURL(url, res)) { 114 | return; 115 | } 116 | 117 | // Send headers for event-stream connection. 118 | res.writeHead(200, { 119 | 'Content-Type': 'text/event-stream', 120 | 'Cache-Control': 'no-cache', 121 | 'Connection': 'keep-alive', 122 | 'Access-Control-Allow-Origin': '*', 123 | 'X-Accel-Buffering': 'no' // Forces Flex App Engine to keep connection open for SSE. 124 | }); 125 | 126 | const fileName = `report.${Date.now()}.${format}`; 127 | const outputPath = `./${REPORTS_DIR}/${fileName}`; 128 | const args = getDefaultArgs(outputPath, format); 129 | 130 | const child = spawn('lighthouse', [...args, url]); 131 | // console.log('pid', child.pid); 132 | 133 | child.stderr.pipe(process.stderr); 134 | child.stdout.pipe(process.stdout); 135 | 136 | let log = ''; 137 | 138 | // child.on('exit', (statusCode, signal) => { 139 | // console.log(statusCode, signal); 140 | // }); 141 | 142 | child.stderr.on('data', data => { 143 | const str = data.toString(); 144 | res.write(`data: ${str}\n\n`); 145 | log += str; 146 | }); 147 | 148 | child.on('close', statusCode => { 149 | if (log.match(/Error: /gm)) { 150 | res.write(`data: ERROR\n\n`); 151 | } else { 152 | const serverOrigin = `https://${req.hostname}/`; 153 | res.write(`data: done ${serverOrigin + fileName}\n\n`); 154 | } 155 | 156 | res.status(410).end(); 157 | log = ''; 158 | }); 159 | } 160 | 161 | const app = express(); 162 | app.use(bodyParser.json()); 163 | 164 | app.use(function enableCors(req, res, next) { 165 | res.set('Access-Control-Allow-Origin', '*'); 166 | 167 | // Record GA hit. 168 | // const visitor = ua(GA_ACCOUNT, {https: true}); 169 | // visitor.pageview(req.originalUrl).send(); 170 | 171 | next(); 172 | }); 173 | 174 | app.use(express.static(REPORTS_DIR)); 175 | 176 | app.get('/ci', (req, res, next) => { 177 | const apiKey = req.query.key; 178 | // Require API for get requests. 179 | if (!apiKey) { 180 | res.status(403).send('Missing API key. Please include the key parameter'); 181 | return; 182 | } 183 | console.log(`${API_KEY_HEADER}: ${apiKey}`); 184 | runLH(req.query, req, res, next); 185 | }); 186 | 187 | app.post('/ci', (req, res, next) => { 188 | // // Require an API key from users. 189 | // if (!req.get(API_KEY_HEADER)) { 190 | // const msg = `${API_KEY_HEADER} is missing`; 191 | // const err = new Error(msg); 192 | // res.status(403).json(err.message); 193 | // return; 194 | // } 195 | 196 | console.log(`${API_KEY_HEADER}: ${req.get(API_KEY_HEADER)}`); 197 | 198 | runLH(req.body, req, res, next); 199 | }); 200 | 201 | app.get('/stream', (req, res, next) => { 202 | runLighthouseAsEventStream(req, res, next); 203 | }); 204 | 205 | app.listen(PORT); 206 | console.log(`Running on http://localhost:${PORT}`); 207 | -------------------------------------------------------------------------------- /builder/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.5: 6 | version "1.3.5" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" 8 | dependencies: 9 | mime-types "~2.1.18" 10 | negotiator "0.6.1" 11 | 12 | array-flatten@1.1.1: 13 | version "1.1.1" 14 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 15 | 16 | body-parser@1.18.2: 17 | version "1.18.2" 18 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" 19 | dependencies: 20 | bytes "3.0.0" 21 | content-type "~1.0.4" 22 | debug "2.6.9" 23 | depd "~1.1.1" 24 | http-errors "~1.6.2" 25 | iconv-lite "0.4.19" 26 | on-finished "~2.3.0" 27 | qs "6.5.1" 28 | raw-body "2.3.2" 29 | type-is "~1.6.15" 30 | 31 | body-parser@^1.18.3: 32 | version "1.18.3" 33 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" 34 | dependencies: 35 | bytes "3.0.0" 36 | content-type "~1.0.4" 37 | debug "2.6.9" 38 | depd "~1.1.2" 39 | http-errors "~1.6.3" 40 | iconv-lite "0.4.23" 41 | on-finished "~2.3.0" 42 | qs "6.5.2" 43 | raw-body "2.3.3" 44 | type-is "~1.6.16" 45 | 46 | bytes@3.0.0: 47 | version "3.0.0" 48 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 49 | 50 | content-disposition@0.5.2: 51 | version "0.5.2" 52 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 53 | 54 | content-type@~1.0.4: 55 | version "1.0.4" 56 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 57 | 58 | cookie-signature@1.0.6: 59 | version "1.0.6" 60 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 61 | 62 | cookie@0.3.1: 63 | version "0.3.1" 64 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 65 | 66 | debug@2.6.9: 67 | version "2.6.9" 68 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 69 | dependencies: 70 | ms "2.0.0" 71 | 72 | depd@1.1.1, depd@~1.1.1: 73 | version "1.1.1" 74 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" 75 | 76 | depd@~1.1.2: 77 | version "1.1.2" 78 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 79 | 80 | destroy@~1.0.4: 81 | version "1.0.4" 82 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 83 | 84 | ee-first@1.1.1: 85 | version "1.1.1" 86 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 87 | 88 | encodeurl@~1.0.2: 89 | version "1.0.2" 90 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 91 | 92 | escape-html@~1.0.3: 93 | version "1.0.3" 94 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 95 | 96 | etag@~1.8.1: 97 | version "1.8.1" 98 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 99 | 100 | express@^4.16.3: 101 | version "4.16.3" 102 | resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" 103 | dependencies: 104 | accepts "~1.3.5" 105 | array-flatten "1.1.1" 106 | body-parser "1.18.2" 107 | content-disposition "0.5.2" 108 | content-type "~1.0.4" 109 | cookie "0.3.1" 110 | cookie-signature "1.0.6" 111 | debug "2.6.9" 112 | depd "~1.1.2" 113 | encodeurl "~1.0.2" 114 | escape-html "~1.0.3" 115 | etag "~1.8.1" 116 | finalhandler "1.1.1" 117 | fresh "0.5.2" 118 | merge-descriptors "1.0.1" 119 | methods "~1.1.2" 120 | on-finished "~2.3.0" 121 | parseurl "~1.3.2" 122 | path-to-regexp "0.1.7" 123 | proxy-addr "~2.0.3" 124 | qs "6.5.1" 125 | range-parser "~1.2.0" 126 | safe-buffer "5.1.1" 127 | send "0.16.2" 128 | serve-static "1.13.2" 129 | setprototypeof "1.1.0" 130 | statuses "~1.4.0" 131 | type-is "~1.6.16" 132 | utils-merge "1.0.1" 133 | vary "~1.1.2" 134 | 135 | finalhandler@1.1.1: 136 | version "1.1.1" 137 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" 138 | dependencies: 139 | debug "2.6.9" 140 | encodeurl "~1.0.2" 141 | escape-html "~1.0.3" 142 | on-finished "~2.3.0" 143 | parseurl "~1.3.2" 144 | statuses "~1.4.0" 145 | unpipe "~1.0.0" 146 | 147 | forwarded@~0.1.2: 148 | version "0.1.2" 149 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 150 | 151 | fresh@0.5.2: 152 | version "0.5.2" 153 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 154 | 155 | http-errors@1.6.2, http-errors@~1.6.2: 156 | version "1.6.2" 157 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" 158 | dependencies: 159 | depd "1.1.1" 160 | inherits "2.0.3" 161 | setprototypeof "1.0.3" 162 | statuses ">= 1.3.1 < 2" 163 | 164 | http-errors@1.6.3, http-errors@~1.6.3: 165 | version "1.6.3" 166 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" 167 | dependencies: 168 | depd "~1.1.2" 169 | inherits "2.0.3" 170 | setprototypeof "1.1.0" 171 | statuses ">= 1.4.0 < 2" 172 | 173 | iconv-lite@0.4.19: 174 | version "0.4.19" 175 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 176 | 177 | iconv-lite@0.4.23: 178 | version "0.4.23" 179 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" 180 | dependencies: 181 | safer-buffer ">= 2.1.2 < 3" 182 | 183 | inherits@2.0.3: 184 | version "2.0.3" 185 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 186 | 187 | ipaddr.js@1.8.0: 188 | version "1.8.0" 189 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" 190 | 191 | media-typer@0.3.0: 192 | version "0.3.0" 193 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 194 | 195 | merge-descriptors@1.0.1: 196 | version "1.0.1" 197 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 198 | 199 | methods@~1.1.2: 200 | version "1.1.2" 201 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 202 | 203 | mime-db@~1.30.0: 204 | version "1.30.0" 205 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" 206 | 207 | mime-db@~1.36.0: 208 | version "1.36.0" 209 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" 210 | 211 | mime-types@~2.1.15: 212 | version "2.1.17" 213 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" 214 | dependencies: 215 | mime-db "~1.30.0" 216 | 217 | mime-types@~2.1.18: 218 | version "2.1.20" 219 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" 220 | dependencies: 221 | mime-db "~1.36.0" 222 | 223 | mime@1.4.1: 224 | version "1.4.1" 225 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" 226 | 227 | ms@2.0.0: 228 | version "2.0.0" 229 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 230 | 231 | negotiator@0.6.1: 232 | version "0.6.1" 233 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 234 | 235 | on-finished@~2.3.0: 236 | version "2.3.0" 237 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 238 | dependencies: 239 | ee-first "1.1.1" 240 | 241 | parseurl@~1.3.2: 242 | version "1.3.2" 243 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 244 | 245 | path-to-regexp@0.1.7: 246 | version "0.1.7" 247 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 248 | 249 | proxy-addr@~2.0.3: 250 | version "2.0.4" 251 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" 252 | dependencies: 253 | forwarded "~0.1.2" 254 | ipaddr.js "1.8.0" 255 | 256 | qs@6.5.1: 257 | version "6.5.1" 258 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 259 | 260 | qs@6.5.2: 261 | version "6.5.2" 262 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 263 | 264 | range-parser@~1.2.0: 265 | version "1.2.0" 266 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 267 | 268 | raw-body@2.3.2: 269 | version "2.3.2" 270 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" 271 | dependencies: 272 | bytes "3.0.0" 273 | http-errors "1.6.2" 274 | iconv-lite "0.4.19" 275 | unpipe "1.0.0" 276 | 277 | raw-body@2.3.3: 278 | version "2.3.3" 279 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" 280 | dependencies: 281 | bytes "3.0.0" 282 | http-errors "1.6.3" 283 | iconv-lite "0.4.23" 284 | unpipe "1.0.0" 285 | 286 | safe-buffer@5.1.1: 287 | version "5.1.1" 288 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 289 | 290 | "safer-buffer@>= 2.1.2 < 3": 291 | version "2.1.2" 292 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 293 | 294 | send@0.16.2: 295 | version "0.16.2" 296 | resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" 297 | dependencies: 298 | debug "2.6.9" 299 | depd "~1.1.2" 300 | destroy "~1.0.4" 301 | encodeurl "~1.0.2" 302 | escape-html "~1.0.3" 303 | etag "~1.8.1" 304 | fresh "0.5.2" 305 | http-errors "~1.6.2" 306 | mime "1.4.1" 307 | ms "2.0.0" 308 | on-finished "~2.3.0" 309 | range-parser "~1.2.0" 310 | statuses "~1.4.0" 311 | 312 | serve-static@1.13.2: 313 | version "1.13.2" 314 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" 315 | dependencies: 316 | encodeurl "~1.0.2" 317 | escape-html "~1.0.3" 318 | parseurl "~1.3.2" 319 | send "0.16.2" 320 | 321 | setprototypeof@1.0.3: 322 | version "1.0.3" 323 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 324 | 325 | setprototypeof@1.1.0: 326 | version "1.1.0" 327 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 328 | 329 | "statuses@>= 1.3.1 < 2": 330 | version "1.3.1" 331 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 332 | 333 | "statuses@>= 1.4.0 < 2": 334 | version "1.5.0" 335 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 336 | 337 | statuses@~1.4.0: 338 | version "1.4.0" 339 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" 340 | 341 | type-is@~1.6.15: 342 | version "1.6.15" 343 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" 344 | dependencies: 345 | media-typer "0.3.0" 346 | mime-types "~2.1.15" 347 | 348 | type-is@~1.6.16: 349 | version "1.6.16" 350 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" 351 | dependencies: 352 | media-typer "0.3.0" 353 | mime-types "~2.1.18" 354 | 355 | unpipe@1.0.0, unpipe@~1.0.0: 356 | version "1.0.0" 357 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 358 | 359 | utils-merge@1.0.1: 360 | version "1.0.1" 361 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 362 | 363 | vary@~1.1.2: 364 | version "1.1.2" 365 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 366 | -------------------------------------------------------------------------------- /deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | deployVersion=$1 4 | app=$2 5 | usage="Usage: deploy.sh `date +%Y-%m-%d` builder|frontend " 6 | #readonly APPDIR=$(dirname $BASH_SOURCE) 7 | 8 | if [ -z "$deployVersion" ] 9 | then 10 | echo "App version not specified." 11 | echo $usage 12 | exit 0 13 | fi 14 | 15 | if [ -z "$app" ] 16 | then 17 | echo 'Please specify "builder" or "frontend" target to deploy.' 18 | echo $usage 19 | exit 0 20 | fi 21 | 22 | echo "Deploying $app version: $deployVersion" 23 | 24 | if [ $app == "builder" ] 25 | then 26 | gcloud app deploy builder/app.yaml \ 27 | --project lighthouse-ci --version $deployVersion 28 | elif [ $app == "frontend" ] 29 | then 30 | gcloud app deploy frontend/app.yaml \ 31 | --project lighthouse-ci --version $deployVersion 32 | else 33 | echo $usage 34 | exit 0 35 | fi 36 | -------------------------------------------------------------------------------- /frontend/.gcloudignore: -------------------------------------------------------------------------------- 1 | # This file specifies files that are *not* uploaded to Google Cloud Platform 2 | # using gcloud. It follows the same syntax as .gitignore, with the addition of 3 | # "#!include" directives (which insert the entries of the given .gitignore-style 4 | # file at that point). 5 | # 6 | # For more information, run: 7 | # $ gcloud topic gcloudignore 8 | # 9 | .gcloudignore 10 | # If you would like to upload your .git directory, .gitignore file or files 11 | # from your .gitignore file, remove the corresponding line 12 | # below: 13 | .git 14 | .gitignore 15 | 16 | # Node.js dependencies: 17 | node_modules/ -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | Frontend that shows how to communicate with the Lighthhouse CI builder backend. 2 | 3 | Try it: https://lighthouse-ci.appspot.com/try 4 | -------------------------------------------------------------------------------- /frontend/app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs8 2 | #env: flex 3 | 4 | #automatic_scaling: 5 | # min_num_instances: 1 6 | # max_num_instances: 1 7 | -------------------------------------------------------------------------------- /frontend/lighthouse-ci.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | const fetch = require('node-fetch'); // polyfill 19 | const Github = require('@octokit/rest'); 20 | const URL = require('url').URL; 21 | const URLSearchParams = require('url').URLSearchParams; 22 | 23 | class LighthouseCI { 24 | /** 25 | * @param {!string} token Github OAuth token that has repo:status access. 26 | */ 27 | constructor(token) { 28 | this.github = new Github({debug: false}); 29 | this.github.authenticate({type: 'oauth', token}); 30 | } 31 | 32 | handleError(err, prInfo) { 33 | console.error(err); 34 | this.updateGithubStatus(Object.assign({ 35 | state: 'error', 36 | description: `Error. ${err.message}` 37 | }, prInfo)); 38 | } 39 | 40 | testOnHeadlessChrome(body, headers) { 41 | headers = Object.assign(headers, { 42 | 'Content-Type': 'application/json' 43 | }); 44 | 45 | // POST https://builder-dot-lighthouse-ci.appspot.com/ci 46 | // '{"output": "json", "url": }"' 47 | return fetch('https://builder-dot-lighthouse-ci.appspot.com/ci', { 48 | method: 'POST', 49 | body: JSON.stringify(body), 50 | headers 51 | }).then(resp => resp.json()) 52 | .catch(err => { 53 | throw err; 54 | }); 55 | } 56 | 57 | /** 58 | * Uses WebPageTest's Rest API to run Lighthouse and score a URL. 59 | * See https://sites.google.com/a/webpagetest.org/docs/advanced-features/webpagetest-restful-apis 60 | * @param {!string} apiKey 61 | * @param {!string} testUrl URL to audit. 62 | * @param {!string} pingback URL for WPT to ping when result is ready. 63 | * @return {!Promise} json response from starting a WPT run. 64 | */ 65 | startOnWebpageTest(apiKey, testUrl, pingback) { 66 | const params = new URLSearchParams(); 67 | params.set('k', apiKey); 68 | params.set('f', 'json'); 69 | params.set('pingback', pingback); // The pingback is passed an "id" parameter of the test. 70 | // TODO: match emulation to LH settings. 71 | params.set('location', 'Dulles_Nexus5:Nexus 5 - Chrome Beta.3G_EM'); 72 | // For native: Dulles_Linux:Chrome.Native 73 | // params.set('location', 'Dulles_MotoG4:Moto G4 - Chrome Beta.3GFast'); 74 | params.set('mobile', 1); // Emulate mobile (for desktop cases). 75 | params.set('type', 'lighthouse'); // LH-only run. 76 | params.set('lighthouse', 1); 77 | params.set('url', testUrl); 78 | 79 | const wptUrl = new URL('https://www.webpagetest.org/runtest.php'); 80 | wptUrl.search = params; 81 | 82 | return fetch(wptUrl.toString()) 83 | .then(resp => resp.json()) 84 | .catch(err => { 85 | throw err; 86 | }); 87 | } 88 | 89 | /** 90 | * Returns the scores for each category. 91 | * @param {!Object} lhr Lighthouse results object. 92 | * @return {!Object>} 93 | */ 94 | static getOverallScores(lhr) { 95 | const cats = Object.keys(lhr.categories); 96 | const obj = {}; 97 | for (const cat of cats) { 98 | obj[cat] = lhr.categories[cat].score * 100; 99 | } 100 | return obj; 101 | } 102 | 103 | /** 104 | * Updates associated PR status. 105 | * @param {!Object=} opts Options to set the status with. 106 | * @return {!Promise} Status object from Github API. 107 | */ 108 | updateGithubStatus(opts={}) { 109 | const statusObj = Object.assign({context: 'Lighthouse'}, opts); 110 | 111 | return this.github.repos.createStatus(statusObj).then(status => { 112 | console.log(status.data.description); 113 | return status; 114 | }); 115 | } 116 | 117 | /** 118 | * Updates pass/fail state of PR. 119 | * @param {!Object} lhr Lighthouse results object. 120 | * @param {!Object} thresholds Minimum scores per category. 121 | * @param {!Object} opts Options to set the status with. 122 | * @return {!Promise} Overall lighthouse score. 123 | */ 124 | assignPassFailToPR(lhr, thresholds, opts) { 125 | const scores = LighthouseCI.getOverallScores(lhr); 126 | 127 | let passing = true; 128 | const scoresStr = []; 129 | 130 | for (const [cat, score] of Object.entries(scores)) { 131 | if (cat in thresholds) { 132 | const minScore = thresholds[cat]; 133 | if (minScore > Math.round(score)) { 134 | passing = false; 135 | } 136 | scoresStr.push(`${cat}:${minScore}`); 137 | } 138 | } 139 | 140 | let description = `Failed. Required scores: ${scoresStr.join(',')}.`; 141 | if (passing) { 142 | description = 'Passed. Lighthouse scores meet thresholds.'; 143 | } 144 | 145 | const status = Object.assign({ 146 | description, 147 | state: passing ? 'success' : 'failure' 148 | }, opts); 149 | 150 | // eslint-disable-next-line no-unused-vars 151 | return this.updateGithubStatus(status).then(status => scores); 152 | } 153 | 154 | /** 155 | * Posts a comment to the PR with the latest Lighthouse scores. 156 | * @param {!{owner: string, repo: string, number: number}} prInfo 157 | * @param {!Object} lhr Lighthouse results object. 158 | * @param {!Object} thresholds Minimum scores per category. 159 | * @return {!Promise} Lighthouse scores. 160 | */ 161 | postLighthouseComment(prInfo, lhr, thresholds) { 162 | let rows = ''; 163 | Object.values(lhr.categories).forEach(cat => { 164 | const threshold = thresholds[cat.id] || '-'; 165 | rows += `| ${cat.title} | ${cat.score * 100} | ${threshold} \n`; 166 | }); 167 | 168 | const body = ` 169 | Updated [Lighthouse](https://developers.google.com/web/tools/lighthouse/) report for the changes in this PR: 170 | 171 | | Category | New score | Required threshold | 172 | | ------------- | ------------- | ------------- | 173 | ${rows} 174 | 175 | _Tested with Lighthouse version: ${lhr.lighthouseVersion}_`; 176 | 177 | const scores = LighthouseCI.getOverallScores(lhr); 178 | 179 | // eslint-disable-next-line no-unused-vars 180 | return this.github.issues.createComment(Object.assign({body}, prInfo)).then(status => scores); 181 | } 182 | } 183 | 184 | module.exports = LighthouseCI; 185 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "description": "Lighthouse CI", 4 | "main": "index.js", 5 | "license": "Apache-2.0", 6 | "scripts": { 7 | "serve": "OAUTH_TOKEN=`cat .oauth_token` PORT=8081 node server.js", 8 | "start": "OAUTH_TOKEN=`cat .oauth_token` node server.js", 9 | "lint": "npx eslint ." 10 | }, 11 | "engines": { 12 | "node": ">=8" 13 | }, 14 | "dependencies": { 15 | "@octokit/rest": "^15.13.0", 16 | "body-parser": "^1.18.3", 17 | "eslint": "^5.6.1", 18 | "eslint-config-google": "^0.10.0", 19 | "express": "^4.16.3", 20 | "node-fetch": "1.7.3" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/public/app.js: -------------------------------------------------------------------------------- 1 | /* global ga */ 2 | 3 | 'use strict'; 4 | 5 | (function() { 6 | const input = document.querySelector('#url'); 7 | const logger = document.querySelector('#logger'); 8 | const scoreEl = document.querySelector('#lighthouse-score'); 9 | const reportLink = document.querySelector('#reportLink'); 10 | const background = document.querySelector('#background'); 11 | const logo = document.querySelector('.logo-section'); 12 | const searchArrow = document.querySelector('.search-arrow'); 13 | const startOver = document.querySelector('#startover'); 14 | 15 | // const params = new URLSearchParams(location.search); 16 | // let setTimeoutId_; 17 | const ENDPOINT_ORIGIN = location.hostname === 'localhost' ? 18 | 'http://localhost:8080' : 'https://builder-dot-lighthouse-ci.appspot.com'; 19 | 20 | // /** 21 | // * @param {number} score 22 | // * @return {string} 23 | // */ 24 | // function calculateRating(score) { 25 | // let rating = 'poor'; 26 | // if (score > 45) { 27 | // rating = 'average'; 28 | // } 29 | // if (score > 75) { 30 | // rating = 'good'; 31 | // } 32 | // return rating; 33 | // } 34 | 35 | // /** 36 | // * @param {(number|string)} score 37 | // */ 38 | // function setScore(score) { 39 | // score = Number(score); 40 | 41 | // const rating = calculateRating(score); 42 | 43 | // scoreEl.textContent = score; 44 | // scoreEl.classList.add(rating); 45 | // document.body.classList.add(rating, 'done'); 46 | // document.body.classList.remove('running'); 47 | // } 48 | 49 | function startNewRun() { 50 | resetUI(false); 51 | document.body.classList.add('running'); 52 | } 53 | 54 | /** 55 | * @param {string} url 56 | */ 57 | function finalizeRun(url) { 58 | // const match = logger.value.match(/.*LIGHTHOUSE SCORE:\s+(.*)/); 59 | // if (match) { 60 | // let score = Number(match[1]); 61 | // score = score.toLocaleString(undefined, {maximumFractionDigits: 1}); 62 | 63 | // setScore(score); 64 | startOver.tabIndex = 0; 65 | reportLink.tabIndex = 0; 66 | reportLink.href = url; 67 | // reportLink.setAttribute('href', url); 68 | reportLink.focus(); 69 | 70 | ga('send', 'event', 'Lighthouse', 'finish run'); 71 | // } else { 72 | // const split = logger.value.split('\n'); 73 | // ga('send', 'event', 'Lighthouse', 'error', split[split.length - 2]); 74 | // } 75 | 76 | document.body.classList.remove('running'); 77 | document.body.classList.add('done'); 78 | } 79 | 80 | function updateLog(data) { 81 | logger.value += data.replace(/.*GMT\s/, '') + '\n'; 82 | logger.scrollTop = logger.scrollHeight; 83 | } 84 | 85 | function resetUI(clearInput=true) { 86 | if (clearInput) { 87 | input.value = null; 88 | } 89 | logger.value = ''; 90 | document.body.className = ''; 91 | reportLink.tabIndex = -1; 92 | reportLink.href = '#'; 93 | startOver.tabIndex = -1; 94 | scoreEl.textContent = ''; 95 | scoreEl.className = ''; 96 | } 97 | 98 | /** 99 | * @param {string} url URL to test in Lighthouse. 100 | */ 101 | function runLighthouse(url = '') { 102 | // If user inputs domain, make it a full URL. 103 | if (!url.match(/^https?:\/\//) && url.length) { 104 | url = `http://${url}`; 105 | input.value = url; 106 | } 107 | 108 | if (!input.validity.valid) { 109 | alert('URL is not valid. Please make sure to use a full URL (e.g. https://www.example.com).'); 110 | return; 111 | } 112 | 113 | let endpoint = `${ENDPOINT_ORIGIN}/stream?url=${url}`; 114 | if (document.querySelector('#useheadless').checked) { 115 | endpoint += '&headless=true'; 116 | } 117 | 118 | const source = new EventSource(endpoint); 119 | 120 | source.addEventListener('message', e => { 121 | if (e.data.startsWith('ERROR')) { 122 | source.close(); 123 | } else if (e.data.startsWith('done')) { 124 | source.close(); 125 | 126 | let url = e.data.split(' ')[1]; 127 | if (url.includes('localhost')) { 128 | url = url.replace('https://localhost', ENDPOINT_ORIGIN); 129 | } 130 | finalizeRun(url); 131 | return; 132 | } 133 | 134 | updateLog(e.data); 135 | }); 136 | 137 | source.addEventListener('open', e => { 138 | startNewRun(); 139 | ga('send', 'event', 'Lighthouse', 'start run'); 140 | }); 141 | 142 | source.addEventListener('error', e => { 143 | if (e.readyState === EventSource.CLOSED) { 144 | source.close(); 145 | } 146 | }); 147 | } 148 | 149 | function attachEventListeners() { 150 | input.addEventListener('keydown', e => { 151 | if (e.keyCode === 13) { // Enter 152 | runLighthouse(e.target.value); 153 | } 154 | }); 155 | 156 | document.addEventListener('click', e => input.focus()); 157 | 158 | logo.addEventListener('click', e => { 159 | if (document.body.classList.contains('done')) { 160 | fetch('/reset'); 161 | resetUI(); 162 | document.querySelector('#useheadless').checked = false; 163 | } 164 | }); 165 | 166 | searchArrow.addEventListener('click', e => { 167 | runLighthouse(input.value); 168 | }); 169 | 170 | startOver.addEventListener('click', e => { 171 | e.preventDefault(); 172 | resetUI(); 173 | document.querySelector('#useheadless').checked = false; 174 | }); 175 | 176 | reportLink.addEventListener('click', e => { 177 | ga('send', 'event', 'Lighthouse', 'open report'); 178 | }); 179 | } 180 | 181 | attachEventListeners(); 182 | input.focus(); 183 | })(); 184 | -------------------------------------------------------------------------------- /frontend/public/logo-nolight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GoogleChromeLabs/lighthousebot/a4bfc0857741c1cd6bde9ded967971fd27254ed6/frontend/public/logo-nolight.png -------------------------------------------------------------------------------- /frontend/public/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /frontend/public/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --grey: #aaa; 3 | --color-poor: #e53935; 4 | --color-average: #ef6c00; 5 | --color-good: #43a047; 6 | } 7 | @keyframes blink { 8 | 0% { 9 | opacity: 0; 10 | } 11 | 33% { 12 | opacity: 1; 13 | } 14 | 100% { 15 | opacity: 1; 16 | } 17 | } 18 | * { 19 | box-sizing: border-box; 20 | } 21 | html, body { 22 | height: 100vh; 23 | margin: 0; 24 | } 25 | .hidden { 26 | display: none !important; 27 | } 28 | body { 29 | line-height: 1.6; 30 | font-family: 'Product Sans', Arial, sans-serif; 31 | font-weight: 300; 32 | display: flex; 33 | justify-content: center; 34 | align-items: center; 35 | flex-direction: column; 36 | background: #fafafa; 37 | -webkit-font-smoothing: antialiased; 38 | } 39 | main { 40 | display: flex; 41 | flex-direction: column; 42 | } 43 | #background { 44 | transition: opacity 300ms ease-in-out; 45 | z-index: -2; 46 | pointer-events: none; 47 | position: absolute; 48 | top: 0; 49 | left: 0; 50 | height: 100%; 51 | width: 100%; 52 | opacity: 0; 53 | } 54 | body.good #background { 55 | opacity: 1; 56 | box-shadow: 0px 0px 100px var(--color-good) inset; 57 | } 58 | body.average #background { 59 | opacity: 1; 60 | box-shadow: 0px 0px 100px var(--color-average) inset; 61 | } 62 | body.poor #background { 63 | opacity: 1; 64 | box-shadow: 0px 0px 100px var(--color-poor) inset; 65 | } 66 | body.running .logo-nolight { 67 | animation-name: blink; 68 | } 69 | body.done #lighthouse-report { 70 | pointer-events: initial; 71 | transform: translateY(250%); 72 | opacity: 1; 73 | } 74 | h1, h2, h3 { 75 | font-weight: inherit; 76 | } 77 | a { 78 | text-decoration: none; 79 | color: currentColor; 80 | } 81 | main { 82 | width: 50vw; 83 | } 84 | footer { 85 | position: absolute; 86 | left: 0; 87 | right: 0; 88 | bottom: 16px; 89 | text-align: center; 90 | color: var(--grey); 91 | transition: opacity 300ms ease-in-out; 92 | } 93 | .logo-section { 94 | display: flex; 95 | justify-content: center; 96 | margin-top: -250px; 97 | margin-bottom: -30px; 98 | margin-left: -16px; 99 | flex-shrink: 0; 100 | background: radial-gradient(ellipse at 50% 50%, #fafafa 25%, transparent); 101 | position: relative; 102 | } 103 | .logo-section h1 { 104 | display: none; 105 | } 106 | .logo { 107 | cursor: pointer; 108 | top: 0; 109 | } 110 | #url { 111 | --shadow: 0 2px 2px 0 rgba(0,0,0,0.16), 0 0 0 1px rgba(0,0,0,0.08); 112 | --hover-shadow: 0 3px 8px 0 rgba(0,0,0,0.2), 0 0 0 1px rgba(0,0,0,0.08); 113 | border: none; 114 | height: var(--url-section-height); 115 | width: 100%; 116 | outline: none; 117 | border-radius: var(--url-section-border-radius); 118 | box-shadow: var(--shadow); 119 | transition: box-shadow 200ms cubic-bezier(0.4, 0.0, 0.2, 1); 120 | padding: 0 16px; 121 | padding-right: calc(8px * 12); 122 | font: inherit; 123 | z-index: 1; 124 | } 125 | #url:hover { 126 | box-shadow: var(--hover-shadow); 127 | } 128 | #url:-webkit-autofill { 129 | box-shadow: 0 0 0px 1000px white inset, var(--shadow); 130 | } 131 | #url:hover:-webkit-autofill { 132 | box-shadow: 0 0 0px 1000px white inset, var(--hover-shadow); 133 | } 134 | #url::-moz-input-placeholder { 135 | color: var(--grey); 136 | } 137 | #url::-webkit-input-placeholder { 138 | color: var(--grey); 139 | } 140 | .logo { 141 | height: 300px; 142 | } 143 | .logo-nolight { 144 | position: absolute; 145 | opacity: 0; 146 | animation-duration: 2.5s; 147 | animation-direction: alternate; 148 | animation-iteration-count: infinite; 149 | animation-timing-function: ease-in-out; 150 | } 151 | #logger { 152 | width: 100%; 153 | border: none; 154 | background: transparent; 155 | position: absolute; 156 | z-index: -1; 157 | top: 0; 158 | left: 0; 159 | bottom: 30px; 160 | padding: 0 50px 50px 50px; 161 | margin-top: 40px; 162 | color: #bbb; 163 | resize: none; 164 | font-size: 14px; 165 | } 166 | .url-section { 167 | --url-section-height: 50px; 168 | --url-section-border-radius: 2px; 169 | display: flex; 170 | flex-direction: column; 171 | position: relative; 172 | } 173 | .search-arrow { 174 | background: linear-gradient(to bottom, transparent 60%, #eee); 175 | border-left: 1px solid #ddd; 176 | cursor: pointer; 177 | color: #555; 178 | border-bottom-left-radius: var(--url-section-border-radius); 179 | border-bottom-right-radius: var(--url-section-border-radius); 180 | font-size: 30px; 181 | height: var(--url-section-height); 182 | position: absolute; 183 | display: flex; 184 | align-items: center; 185 | justify-content: center; 186 | align-self: flex-end; 187 | padding: 8px; 188 | z-index: 2; 189 | } 190 | .search-arrow:hover { 191 | box-shadow: -3px 0px 3px #eee; 192 | } 193 | #lighthouse-score { 194 | font-size: 24px; 195 | position: absolute; 196 | right: calc(8px * 4); 197 | display: flex; 198 | align-items: center; 199 | align-self: flex-end; 200 | padding: 8px; 201 | z-index: 1; 202 | pointer-events: none; 203 | } 204 | #lighthouse-score.good { 205 | color: var(--color-good); 206 | } 207 | #lighthouse-score.average { 208 | color: var(--color-average); 209 | } 210 | #lighthouse-score.poor { 211 | color: var(--color-poor); 212 | } 213 | #lighthouse-report { 214 | transition: all 400ms cubic-bezier(0.18, 0.89, 0.32, 1.28) 500ms; 215 | position: absolute; 216 | display: flex; 217 | align-self: center; 218 | pointer-events: none; 219 | width: 270px; 220 | opacity: 0; 221 | display: flex; 222 | justify-content: space-between; 223 | } 224 | #lighthouse-report .button { 225 | padding: 8px 16px; 226 | background: rgb(50,109,255); 227 | color: #fff; 228 | border-radius: 4px; 229 | } 230 | #lighthouse-report .button:hover { 231 | font-weight: 600; 232 | } 233 | .useheadless-setting { 234 | align-self: flex-end; 235 | margin-top: 12px; 236 | color: var(--grey); 237 | } 238 | .useheadless-setting label { 239 | display: flex; 240 | align-items: center; 241 | cursor: pointer; 242 | padding-right: 4px; 243 | } 244 | .useheadless-setting label:hover { 245 | color: #777; 246 | } 247 | input[type="checkbox"] { 248 | -webkit-appearance: none; 249 | height: 20px; 250 | width: 20px; 251 | border: 1px solid var(--grey); 252 | position: relative; 253 | border-radius: 50%; 254 | overflow: hidden; 255 | cursor: pointer; 256 | margin-right: 6px; 257 | background: #fafafa; 258 | } 259 | input[type="checkbox"]:checked { 260 | background-color: var(--color-good); 261 | border-color: darkgreen; 262 | } 263 | input[type="checkbox"]:not(:checked):hover { 264 | border: 1px solid #777; 265 | } 266 | input[type="checkbox"]:before { 267 | position: absolute; 268 | width: 100%; 269 | height: 100%; 270 | display: flex; 271 | align-items: center; 272 | justify-content: center; 273 | color: #fff; 274 | line-height: 0; 275 | } 276 | input[type="checkbox"]:checked:before { 277 | content: '✔'; 278 | } 279 | /*input[type="checkbox"]:hover:before { 280 | background: darkgreen; 281 | }*/ 282 | 283 | html.kiosk { 284 | pointer-events: none; 285 | } 286 | .kiosk #lighthouse-score { 287 | top: 100px; 288 | left: 0; 289 | right: 0; 290 | width: 100%; 291 | text-align: center; 292 | display: block; 293 | font-size: 100px; 294 | } 295 | .kiosk #lighthouse-report, 296 | .kiosk .useheadless-setting, 297 | .kiosk .search-arrow { 298 | display: none; 299 | } 300 | .kiosk .logo-section h1 { 301 | font-size: 80px; 302 | color: rgb(23,27,162); 303 | display: block; 304 | margin: -140px -26px 100px 0; 305 | text-transform: uppercase; 306 | } 307 | .kiosk .logo-section { 308 | flex-direction: column; 309 | align-items: center; 310 | margin-top: -500px; 311 | margin-bottom: -60px; 312 | margin-left: -25px; 313 | } 314 | .kiosk .logo { 315 | height: 600px; 316 | width: 600px; 317 | } 318 | .kiosk .url-section { 319 | font-size: 35px; 320 | background: radial-gradient(ellipse at 50% 50%, #fafafa 25%, transparent); 321 | } 322 | .kiosk .url-section::before { 323 | content: 'Testing...'; 324 | color: var(--grey); 325 | display: flex; 326 | justify-content: center; 327 | opacity: 0; 328 | transition: opacity 400ms ease-in-out; 329 | font-size: 24px; 330 | } 331 | .kiosk .running .url-section::before { 332 | opacity: 1; 333 | } 334 | .kiosk #url { 335 | background: none; 336 | text-align: center; 337 | padding: 0; 338 | box-shadow: none; 339 | } 340 | .kiosk #logger { 341 | font-size: 20px; 342 | } 343 | -------------------------------------------------------------------------------- /frontend/public/try.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Run Lighthouse 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 | 16 | 17 |

Lighthouse

18 |
19 |
20 | 21 |
22 |
23 | 27 | 30 |
31 |
32 | 33 | 34 | 35 | 38 | 39 |
40 | 41 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /frontend/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | 18 | const bodyParser = require('body-parser'); 19 | const express = require('express'); 20 | const fetch = require('node-fetch'); // polyfill 21 | const LighthouseCI = require('./lighthouse-ci'); 22 | 23 | const WPT_API_KEY = 'A.04c7244ba25a5d6d717b0343a821aa59'; 24 | const WPT_PR_MAP = new Map(); 25 | 26 | const GITHUB_PENDING_STATUS = { 27 | state: 'pending', 28 | description: 'Auditing PR changes...' 29 | }; 30 | 31 | const CI = new LighthouseCI(process.env.OAUTH_TOKEN); 32 | const API_KEY_HEADER = 'X-API-KEY'; 33 | 34 | const app = express(); 35 | app.use(bodyParser.urlencoded({extended: true})); 36 | app.use(bodyParser.json()); 37 | app.use(express.static('public', { 38 | extensions: ['html', 'htm'], 39 | })); 40 | 41 | app.get('/', (req, res) => { 42 | res.redirect('https://github.com/ebidel/lighthouse-ci'); 43 | }); 44 | 45 | // Handler pingback result from webpagetest. 46 | app.get('/wpt_ping', async (req, res) => { 47 | const wptTestId = req.query.id; 48 | 49 | if (!WPT_PR_MAP.has(wptTestId)) { 50 | res.status(404).send('Unknown WebPageTest id.'); 51 | return; 52 | } 53 | 54 | const {prInfo, config} = WPT_PR_MAP.get(wptTestId); 55 | 56 | const resp = await fetch(`https://www.webpagetest.org/jsonResult.php?test=${wptTestId}`); 57 | 58 | try { 59 | const json = await resp.json(); 60 | 61 | if (!json.data || !json.data.lighthouse) { 62 | console.log(json); 63 | throw new Error('Lighthouse results were not found in WebPageTest results.'); 64 | } 65 | 66 | const lhr = json.data.lighthouse; 67 | const targetUrl = `https://www.webpagetest.org/lighthouse.php?test=${wptTestId}`; 68 | 69 | if (Object.keys(config.thresholds).length) { 70 | await CI.assignPassFailToPR(lhr, config.thresholds, Object.assign({ 71 | target_url: targetUrl 72 | }, prInfo)); 73 | } else { 74 | await CI.updateGithubStatus(Object.assign({ 75 | description: 'Auditing complete. See scores above.', 76 | state: 'success', 77 | target_url: targetUrl 78 | }, prInfo)); 79 | } 80 | 81 | // Post comment on issue with updated LH scores. 82 | if (config.addComment) { 83 | try { 84 | await CI.postLighthouseComment(prInfo, lhr, config.thresholds); 85 | } catch (err) { 86 | res.json('Error posting Lighthouse comment to PR.'); 87 | } 88 | } 89 | 90 | WPT_PR_MAP.delete(wptTestId); // cleanup 91 | 92 | const scores = LighthouseCI.getOverallScores(lhr); 93 | res.status(200).send(scores); 94 | } catch (err) { 95 | CI.handleError(err, prInfo); 96 | res.json(err); 97 | } 98 | }); 99 | 100 | // Handler to start Lighthouse run on webpagetest. 101 | app.post('/run_on_wpt', async (req, res) => { 102 | const config = Object.assign({ 103 | pingbackUrl: `${req.protocol}://${req.get('host')}/wpt_ping` 104 | }, req.body); 105 | 106 | const prInfo = { 107 | repo: config.repo.name, 108 | owner: config.repo.owner, 109 | number: config.pr.number, 110 | sha: config.pr.sha 111 | }; 112 | 113 | console.log(`${API_KEY_HEADER}: ${req.get(API_KEY_HEADER)}`); 114 | 115 | try { 116 | const json = await CI.startOnWebpageTest(WPT_API_KEY, config.testUrl, config.pingbackUrl); 117 | 118 | if (!json.data || !json.data.testId) { 119 | throw new Error( 120 | 'Lighthouse results were not found in WebPageTest results.'); 121 | } 122 | 123 | // Stash wpt id -> github pr sha mapping. 124 | WPT_PR_MAP.set(json.data.testId, {prInfo, config}); 125 | 126 | const result = await CI.updateGithubStatus(Object.assign({ 127 | target_url: json.data.userUrl 128 | }, prInfo, GITHUB_PENDING_STATUS)); 129 | 130 | res.status(200).send(result); 131 | } catch (err) { 132 | CI.handleError(err, prInfo); 133 | res.status(500).send(err.message); 134 | } 135 | }); 136 | 137 | // Handler to start Lighthouse run on Chrome. 138 | app.post('/run_on_chrome', async (req, res) => { 139 | const config = Object.assign({}, req.body); 140 | 141 | const prInfo = { 142 | repo: config.repo.name, 143 | owner: config.repo.owner, 144 | number: config.pr.number, 145 | sha: config.pr.sha 146 | }; 147 | 148 | // // Require an API key from users. 149 | // if (!req.get(API_KEY_HEADER)) { 150 | // const msg = `${API_KEY_HEADER} is missing`; 151 | // const err = new Error(msg); 152 | // CI.handleError(err, prInfo); 153 | // res.status(403).json(err.message); 154 | // return; 155 | // } 156 | 157 | // Update GH status: inform user auditing has started. 158 | try { 159 | const status = Object.assign({}, prInfo, GITHUB_PENDING_STATUS); 160 | await CI.updateGithubStatus(status); 161 | } catch (err) { 162 | CI.handleError(err, prInfo); 163 | } 164 | 165 | console.log(`${API_KEY_HEADER}: ${req.get(API_KEY_HEADER)}`); 166 | 167 | // Run Lighthouse CI against the PR changes. 168 | let lhr; 169 | try { 170 | const headers = {[API_KEY_HEADER]: req.get(API_KEY_HEADER)}; 171 | lhr = await CI.testOnHeadlessChrome( 172 | {output: config.output, url: config.testUrl}, headers); 173 | } catch (err) { 174 | CI.handleError(err, prInfo); 175 | res.json(`Error from CI backend. ${err.message}`); 176 | return; // Treat a LH error as fatal. Do not proceed. 177 | } 178 | 179 | try { 180 | // Show deprecation message for anyone still using --score flag. 181 | if ('minPassScore' in config) { 182 | CI.handleError(new Error('--score flag is not longer supported. See docs.'), prInfo); 183 | res.json(`--score flag has been removed in favor of a threshold score for each category. 184 | Please see https://github.com/ebidel/lighthouse-ci#failing-a-pr-when-it-drops-your-lighthouse-score`); 185 | return; 186 | } 187 | 188 | // Assign pass/fail to PR if a min score is provided. 189 | if (Object.keys(config.thresholds).length) { 190 | await CI.assignPassFailToPR(lhr, config.thresholds, Object.assign({ 191 | target_url: config.testUrl 192 | }, prInfo)); 193 | } else { 194 | await CI.updateGithubStatus(Object.assign({ 195 | description: 'Auditing complete. See scores above.', 196 | state: 'success' 197 | }, prInfo)); 198 | } 199 | } catch (err) { 200 | CI.handleError(err, prInfo); 201 | } 202 | 203 | // Post comment on issue with updated LH scores. 204 | if (config.addComment) { 205 | try { 206 | await CI.postLighthouseComment(prInfo, lhr, config.thresholds); 207 | } catch (err) { 208 | res.json('Error posting Lighthouse comment to PR.'); 209 | } 210 | } 211 | 212 | const scores = LighthouseCI.getOverallScores(lhr); 213 | res.status(200).send(scores); 214 | }); 215 | 216 | // app.post('/github_webhook', async (req, res) => { 217 | // if (!('x-github-event' in req.headers)) { 218 | // res.status(400).send('Not a request from Github.'); 219 | // return; 220 | // } 221 | 222 | // // Ignore non-pull request events. 223 | // if (req.headers['x-github-event'] !== 'pull_request') { 224 | // res.status(200).send('Not a pull request event.'); 225 | // return; 226 | // } 227 | 228 | // if (['opened', 'reopened', 'synchronize'].includes(req.body.action)) { 229 | // const prInfo = { 230 | // owner: req.body.repository.full_name.split('/')[0], 231 | // repo: req.body.repository.full_name.split('/')[1], 232 | // number: req.body.number, 233 | // sha: req.body.pull_request.head.sha 234 | // }; 235 | // } else { 236 | // res.status(200).send(''); 237 | // } 238 | // }); 239 | 240 | // app.get('/test_wpt', (req, res) => { 241 | // const pingbackUrl = 'https://68002859.ngrok.io/wpt_ping'; 242 | // const testUrl = 'https://www.chromestatus.com/features'; 243 | 244 | // return CI.startOnWebpageTest(testUrl, pingbackUrl) 245 | // .then(json => { 246 | // // stash wpt id -> github pr sha mapping. 247 | // WPT_PR_MAP.set(json.data.testId, {prInfo: {}, config: {}}); 248 | 249 | // res.status(200).send(json.data.userUrl); 250 | // }) 251 | // .catch(err => { 252 | // res.status(500).send(err.message); 253 | // }); 254 | // }); 255 | 256 | const PORT = process.env.PORT || 8080; 257 | app.listen(PORT, () => { 258 | console.log(`App listening on port ${PORT}`); 259 | console.log('Press Ctrl+C to quit.'); 260 | }); 261 | -------------------------------------------------------------------------------- /frontend/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/code-frame@^7.0.0": 6 | version "7.0.0" 7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" 8 | integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== 9 | dependencies: 10 | "@babel/highlight" "^7.0.0" 11 | 12 | "@babel/highlight@^7.0.0": 13 | version "7.0.0" 14 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" 15 | integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== 16 | dependencies: 17 | chalk "^2.0.0" 18 | esutils "^2.0.2" 19 | js-tokens "^4.0.0" 20 | 21 | "@octokit/rest@^15.13.0": 22 | version "15.13.0" 23 | resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-15.13.0.tgz#078262de2f1d1b02ead7a00c4a3870f517528bcb" 24 | integrity sha512-zgsrqMCLcv4XqpT0QGUykHTvKo33aCVzXP86Bq6HmeKuwY6hEWJ+AVCeL/m3bXk1JBpLyBgzjJDfWEfZcqsR6g== 25 | dependencies: 26 | before-after-hook "^1.1.0" 27 | btoa-lite "^1.0.0" 28 | debug "^3.1.0" 29 | http-proxy-agent "^2.1.0" 30 | https-proxy-agent "^2.2.0" 31 | lodash "^4.17.4" 32 | node-fetch "^2.1.1" 33 | universal-user-agent "^2.0.0" 34 | url-template "^2.0.8" 35 | 36 | accepts@~1.3.5: 37 | version "1.3.5" 38 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" 39 | integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= 40 | dependencies: 41 | mime-types "~2.1.18" 42 | negotiator "0.6.1" 43 | 44 | acorn-jsx@^4.1.1: 45 | version "4.1.1" 46 | resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e" 47 | integrity sha512-JY+iV6r+cO21KtntVvFkD+iqjtdpRUpGqKWgfkCdZq1R+kbreEl8EcdcJR4SmiIgsIQT33s6QzheQ9a275Q8xw== 48 | dependencies: 49 | acorn "^5.0.3" 50 | 51 | acorn@^5.0.3, acorn@^5.6.0: 52 | version "5.7.3" 53 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" 54 | integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== 55 | 56 | agent-base@4, agent-base@^4.1.0: 57 | version "4.2.1" 58 | resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" 59 | integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== 60 | dependencies: 61 | es6-promisify "^5.0.0" 62 | 63 | ajv-keywords@^3.0.0: 64 | version "3.2.0" 65 | resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" 66 | integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= 67 | 68 | ajv@^6.0.1, ajv@^6.5.3: 69 | version "6.5.4" 70 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.4.tgz#247d5274110db653706b550fcc2b797ca28cfc59" 71 | integrity sha512-4Wyjt8+t6YszqaXnLDfMmG/8AlO5Zbcsy3ATHncCzjW/NoPzAId8AK6749Ybjmdt+kUY1gP60fCu46oDxPv/mg== 72 | dependencies: 73 | fast-deep-equal "^2.0.1" 74 | fast-json-stable-stringify "^2.0.0" 75 | json-schema-traverse "^0.4.1" 76 | uri-js "^4.2.2" 77 | 78 | ansi-escapes@^3.0.0: 79 | version "3.1.0" 80 | resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" 81 | integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw== 82 | 83 | ansi-regex@^3.0.0: 84 | version "3.0.0" 85 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" 86 | integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= 87 | 88 | ansi-styles@^3.2.1: 89 | version "3.2.1" 90 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 91 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 92 | dependencies: 93 | color-convert "^1.9.0" 94 | 95 | argparse@^1.0.7: 96 | version "1.0.9" 97 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" 98 | integrity sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY= 99 | dependencies: 100 | sprintf-js "~1.0.2" 101 | 102 | array-flatten@1.1.1: 103 | version "1.1.1" 104 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 105 | integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= 106 | 107 | array-union@^1.0.1: 108 | version "1.0.2" 109 | resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" 110 | integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= 111 | dependencies: 112 | array-uniq "^1.0.1" 113 | 114 | array-uniq@^1.0.1: 115 | version "1.0.3" 116 | resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" 117 | integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= 118 | 119 | arrify@^1.0.0: 120 | version "1.0.1" 121 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" 122 | integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= 123 | 124 | balanced-match@^0.4.1: 125 | version "0.4.2" 126 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" 127 | integrity sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg= 128 | 129 | before-after-hook@^1.1.0: 130 | version "1.1.0" 131 | resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.1.0.tgz#83165e15a59460d13702cb8febd6a1807896db5a" 132 | integrity sha512-VOMDtYPwLbIncTxNoSzRyvaMxtXmLWLUqr8k5AfC1BzLk34HvBXaQX8snOwQZ4c0aX8aSERqtJSiI9/m2u5kuA== 133 | 134 | body-parser@1.18.2: 135 | version "1.18.2" 136 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" 137 | integrity sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ= 138 | dependencies: 139 | bytes "3.0.0" 140 | content-type "~1.0.4" 141 | debug "2.6.9" 142 | depd "~1.1.1" 143 | http-errors "~1.6.2" 144 | iconv-lite "0.4.19" 145 | on-finished "~2.3.0" 146 | qs "6.5.1" 147 | raw-body "2.3.2" 148 | type-is "~1.6.15" 149 | 150 | body-parser@^1.18.3: 151 | version "1.18.3" 152 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4" 153 | integrity sha1-WykhmP/dVTs6DyDe0FkrlWlVyLQ= 154 | dependencies: 155 | bytes "3.0.0" 156 | content-type "~1.0.4" 157 | debug "2.6.9" 158 | depd "~1.1.2" 159 | http-errors "~1.6.3" 160 | iconv-lite "0.4.23" 161 | on-finished "~2.3.0" 162 | qs "6.5.2" 163 | raw-body "2.3.3" 164 | type-is "~1.6.16" 165 | 166 | brace-expansion@^1.1.7: 167 | version "1.1.7" 168 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.7.tgz#3effc3c50e000531fb720eaff80f0ae8ef23cf59" 169 | integrity sha1-Pv/DxQ4ABTH7cg6v+A8K6O8jz1k= 170 | dependencies: 171 | balanced-match "^0.4.1" 172 | concat-map "0.0.1" 173 | 174 | btoa-lite@^1.0.0: 175 | version "1.0.0" 176 | resolved "https://registry.yarnpkg.com/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" 177 | integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= 178 | 179 | bytes@3.0.0: 180 | version "3.0.0" 181 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" 182 | integrity sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg= 183 | 184 | caller-path@^0.1.0: 185 | version "0.1.0" 186 | resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" 187 | integrity sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8= 188 | dependencies: 189 | callsites "^0.2.0" 190 | 191 | callsites@^0.2.0: 192 | version "0.2.0" 193 | resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" 194 | integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo= 195 | 196 | chalk@^2.0.0, chalk@^2.1.0: 197 | version "2.4.1" 198 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" 199 | integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== 200 | dependencies: 201 | ansi-styles "^3.2.1" 202 | escape-string-regexp "^1.0.5" 203 | supports-color "^5.3.0" 204 | 205 | chardet@^0.7.0: 206 | version "0.7.0" 207 | resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" 208 | integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== 209 | 210 | circular-json@^0.3.1: 211 | version "0.3.1" 212 | resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" 213 | integrity sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0= 214 | 215 | cli-cursor@^2.1.0: 216 | version "2.1.0" 217 | resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" 218 | integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= 219 | dependencies: 220 | restore-cursor "^2.0.0" 221 | 222 | cli-width@^2.0.0: 223 | version "2.1.0" 224 | resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" 225 | integrity sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao= 226 | 227 | color-convert@^1.9.0: 228 | version "1.9.3" 229 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 230 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 231 | dependencies: 232 | color-name "1.1.3" 233 | 234 | color-name@1.1.3: 235 | version "1.1.3" 236 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 237 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 238 | 239 | concat-map@0.0.1: 240 | version "0.0.1" 241 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 242 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 243 | 244 | content-disposition@0.5.2: 245 | version "0.5.2" 246 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 247 | integrity sha1-DPaLud318r55YcOoUXjLhdunjLQ= 248 | 249 | content-type@~1.0.4: 250 | version "1.0.4" 251 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" 252 | integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== 253 | 254 | cookie-signature@1.0.6: 255 | version "1.0.6" 256 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 257 | integrity sha1-4wOogrNCzD7oylE6eZmXNNqzriw= 258 | 259 | cookie@0.3.1: 260 | version "0.3.1" 261 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 262 | integrity sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s= 263 | 264 | cross-spawn@^6.0.5: 265 | version "6.0.5" 266 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" 267 | integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== 268 | dependencies: 269 | nice-try "^1.0.4" 270 | path-key "^2.0.1" 271 | semver "^5.5.0" 272 | shebang-command "^1.2.0" 273 | which "^1.2.9" 274 | 275 | debug@2.6.9: 276 | version "2.6.9" 277 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 278 | integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== 279 | dependencies: 280 | ms "2.0.0" 281 | 282 | debug@3.1.0: 283 | version "3.1.0" 284 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" 285 | integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== 286 | dependencies: 287 | ms "2.0.0" 288 | 289 | debug@^3.1.0: 290 | version "3.2.5" 291 | resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.5.tgz#c2418fbfd7a29f4d4f70ff4cea604d4b64c46407" 292 | integrity sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg== 293 | dependencies: 294 | ms "^2.1.1" 295 | 296 | debug@^4.0.1: 297 | version "4.0.1" 298 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.0.1.tgz#f9bb36d439b8d1f0dd52d8fb6b46e4ebb8c1cd5b" 299 | integrity sha512-K23FHJ/Mt404FSlp6gSZCevIbTMLX0j3fmHhUEhQ3Wq0FMODW3+cUSoLdy1Gx4polAf4t/lphhmHH35BB8cLYw== 300 | dependencies: 301 | ms "^2.1.1" 302 | 303 | deep-is@~0.1.3: 304 | version "0.1.3" 305 | resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" 306 | integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= 307 | 308 | del@^2.0.2: 309 | version "2.2.2" 310 | resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" 311 | integrity sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag= 312 | dependencies: 313 | globby "^5.0.0" 314 | is-path-cwd "^1.0.0" 315 | is-path-in-cwd "^1.0.0" 316 | object-assign "^4.0.1" 317 | pify "^2.0.0" 318 | pinkie-promise "^2.0.0" 319 | rimraf "^2.2.8" 320 | 321 | depd@1.1.1: 322 | version "1.1.1" 323 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" 324 | integrity sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k= 325 | 326 | depd@~1.1.1, depd@~1.1.2: 327 | version "1.1.2" 328 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" 329 | integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= 330 | 331 | destroy@~1.0.4: 332 | version "1.0.4" 333 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 334 | integrity sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA= 335 | 336 | doctrine@^2.1.0: 337 | version "2.1.0" 338 | resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" 339 | integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== 340 | dependencies: 341 | esutils "^2.0.2" 342 | 343 | ee-first@1.1.1: 344 | version "1.1.1" 345 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 346 | integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= 347 | 348 | encodeurl@~1.0.2: 349 | version "1.0.2" 350 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" 351 | integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= 352 | 353 | encoding@^0.1.11: 354 | version "0.1.12" 355 | resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" 356 | integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= 357 | dependencies: 358 | iconv-lite "~0.4.13" 359 | 360 | es6-promise@^4.0.3: 361 | version "4.2.5" 362 | resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.5.tgz#da6d0d5692efb461e082c14817fe2427d8f5d054" 363 | integrity sha512-n6wvpdE43VFtJq+lUDYDBFUwV8TZbuGXLV4D6wKafg13ldznKsyEvatubnmUe31zcvelSzOHF+XbaT+Bl9ObDg== 364 | 365 | es6-promisify@^5.0.0: 366 | version "5.0.0" 367 | resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203" 368 | integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM= 369 | dependencies: 370 | es6-promise "^4.0.3" 371 | 372 | escape-html@~1.0.3: 373 | version "1.0.3" 374 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 375 | integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= 376 | 377 | escape-string-regexp@^1.0.5: 378 | version "1.0.5" 379 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 380 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 381 | 382 | eslint-config-google@^0.10.0: 383 | version "0.10.0" 384 | resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.10.0.tgz#09adb5b1514aa99618ad0515e0cce59b8ca82d90" 385 | integrity sha512-PGlMufI13kljog4HlDkwtyqJ7ZZFOcl0ppEvhDoE1lq+8+nMe0lQs0WIZrXpQJhwxhii3SZuCHW2g/weS6Xpyw== 386 | 387 | eslint-scope@^4.0.0: 388 | version "4.0.0" 389 | resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" 390 | integrity sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA== 391 | dependencies: 392 | esrecurse "^4.1.0" 393 | estraverse "^4.1.1" 394 | 395 | eslint-utils@^1.3.1: 396 | version "1.3.1" 397 | resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512" 398 | integrity sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q== 399 | 400 | eslint-visitor-keys@^1.0.0: 401 | version "1.0.0" 402 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" 403 | integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ== 404 | 405 | eslint@^5.6.1: 406 | version "5.6.1" 407 | resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.6.1.tgz#348134e32ccc09abb2df1bf282b3f6eed8c7b480" 408 | integrity sha512-hgrDtGWz368b7Wqf+v1Z69O3ZebNR0+GA7PtDdbmuz4rInFVUV9uw7whjZEiWyLzCjVb5Rs5WRN1TAS6eo7AYA== 409 | dependencies: 410 | "@babel/code-frame" "^7.0.0" 411 | ajv "^6.5.3" 412 | chalk "^2.1.0" 413 | cross-spawn "^6.0.5" 414 | debug "^4.0.1" 415 | doctrine "^2.1.0" 416 | eslint-scope "^4.0.0" 417 | eslint-utils "^1.3.1" 418 | eslint-visitor-keys "^1.0.0" 419 | espree "^4.0.0" 420 | esquery "^1.0.1" 421 | esutils "^2.0.2" 422 | file-entry-cache "^2.0.0" 423 | functional-red-black-tree "^1.0.1" 424 | glob "^7.1.2" 425 | globals "^11.7.0" 426 | ignore "^4.0.6" 427 | imurmurhash "^0.1.4" 428 | inquirer "^6.1.0" 429 | is-resolvable "^1.1.0" 430 | js-yaml "^3.12.0" 431 | json-stable-stringify-without-jsonify "^1.0.1" 432 | levn "^0.3.0" 433 | lodash "^4.17.5" 434 | minimatch "^3.0.4" 435 | mkdirp "^0.5.1" 436 | natural-compare "^1.4.0" 437 | optionator "^0.8.2" 438 | path-is-inside "^1.0.2" 439 | pluralize "^7.0.0" 440 | progress "^2.0.0" 441 | regexpp "^2.0.0" 442 | require-uncached "^1.0.3" 443 | semver "^5.5.1" 444 | strip-ansi "^4.0.0" 445 | strip-json-comments "^2.0.1" 446 | table "^4.0.3" 447 | text-table "^0.2.0" 448 | 449 | espree@^4.0.0: 450 | version "4.0.0" 451 | resolved "https://registry.yarnpkg.com/espree/-/espree-4.0.0.tgz#253998f20a0f82db5d866385799d912a83a36634" 452 | integrity sha512-kapdTCt1bjmspxStVKX6huolXVV5ZfyZguY1lcfhVVZstce3bqxH9mcLzNn3/mlgW6wQ732+0fuG9v7h0ZQoKg== 453 | dependencies: 454 | acorn "^5.6.0" 455 | acorn-jsx "^4.1.1" 456 | 457 | esprima@^4.0.0: 458 | version "4.0.1" 459 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" 460 | integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== 461 | 462 | esquery@^1.0.1: 463 | version "1.0.1" 464 | resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" 465 | integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== 466 | dependencies: 467 | estraverse "^4.0.0" 468 | 469 | esrecurse@^4.1.0: 470 | version "4.1.0" 471 | resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" 472 | integrity sha1-RxO2U2rffyrE8yfVWed1a/9kgiA= 473 | dependencies: 474 | estraverse "~4.1.0" 475 | object-assign "^4.0.1" 476 | 477 | estraverse@^4.0.0, estraverse@^4.1.1: 478 | version "4.2.0" 479 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" 480 | integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= 481 | 482 | estraverse@~4.1.0: 483 | version "4.1.1" 484 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" 485 | integrity sha1-9srKcokzqFDvkGYdDheYK6RxEaI= 486 | 487 | esutils@^2.0.2: 488 | version "2.0.2" 489 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" 490 | integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= 491 | 492 | etag@~1.8.1: 493 | version "1.8.1" 494 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" 495 | integrity sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc= 496 | 497 | express@^4.16.3: 498 | version "4.16.3" 499 | resolved "https://registry.yarnpkg.com/express/-/express-4.16.3.tgz#6af8a502350db3246ecc4becf6b5a34d22f7ed53" 500 | integrity sha1-avilAjUNsyRuzEvs9rWjTSL37VM= 501 | dependencies: 502 | accepts "~1.3.5" 503 | array-flatten "1.1.1" 504 | body-parser "1.18.2" 505 | content-disposition "0.5.2" 506 | content-type "~1.0.4" 507 | cookie "0.3.1" 508 | cookie-signature "1.0.6" 509 | debug "2.6.9" 510 | depd "~1.1.2" 511 | encodeurl "~1.0.2" 512 | escape-html "~1.0.3" 513 | etag "~1.8.1" 514 | finalhandler "1.1.1" 515 | fresh "0.5.2" 516 | merge-descriptors "1.0.1" 517 | methods "~1.1.2" 518 | on-finished "~2.3.0" 519 | parseurl "~1.3.2" 520 | path-to-regexp "0.1.7" 521 | proxy-addr "~2.0.3" 522 | qs "6.5.1" 523 | range-parser "~1.2.0" 524 | safe-buffer "5.1.1" 525 | send "0.16.2" 526 | serve-static "1.13.2" 527 | setprototypeof "1.1.0" 528 | statuses "~1.4.0" 529 | type-is "~1.6.16" 530 | utils-merge "1.0.1" 531 | vary "~1.1.2" 532 | 533 | external-editor@^3.0.0: 534 | version "3.0.3" 535 | resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" 536 | integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA== 537 | dependencies: 538 | chardet "^0.7.0" 539 | iconv-lite "^0.4.24" 540 | tmp "^0.0.33" 541 | 542 | fast-deep-equal@^2.0.1: 543 | version "2.0.1" 544 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" 545 | integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= 546 | 547 | fast-json-stable-stringify@^2.0.0: 548 | version "2.0.0" 549 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" 550 | integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= 551 | 552 | fast-levenshtein@~2.0.4: 553 | version "2.0.6" 554 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" 555 | integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= 556 | 557 | figures@^2.0.0: 558 | version "2.0.0" 559 | resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" 560 | integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= 561 | dependencies: 562 | escape-string-regexp "^1.0.5" 563 | 564 | file-entry-cache@^2.0.0: 565 | version "2.0.0" 566 | resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" 567 | integrity sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E= 568 | dependencies: 569 | flat-cache "^1.2.1" 570 | object-assign "^4.0.1" 571 | 572 | finalhandler@1.1.1: 573 | version "1.1.1" 574 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.1.tgz#eebf4ed840079c83f4249038c9d703008301b105" 575 | integrity sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg== 576 | dependencies: 577 | debug "2.6.9" 578 | encodeurl "~1.0.2" 579 | escape-html "~1.0.3" 580 | on-finished "~2.3.0" 581 | parseurl "~1.3.2" 582 | statuses "~1.4.0" 583 | unpipe "~1.0.0" 584 | 585 | flat-cache@^1.2.1: 586 | version "1.2.2" 587 | resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" 588 | integrity sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y= 589 | dependencies: 590 | circular-json "^0.3.1" 591 | del "^2.0.2" 592 | graceful-fs "^4.1.2" 593 | write "^0.2.1" 594 | 595 | forwarded@~0.1.2: 596 | version "0.1.2" 597 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" 598 | integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ= 599 | 600 | fresh@0.5.2: 601 | version "0.5.2" 602 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" 603 | integrity sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac= 604 | 605 | fs.realpath@^1.0.0: 606 | version "1.0.0" 607 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 608 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 609 | 610 | functional-red-black-tree@^1.0.1: 611 | version "1.0.1" 612 | resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" 613 | integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= 614 | 615 | glob@^7.0.3, glob@^7.0.5: 616 | version "7.1.2" 617 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" 618 | integrity sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ== 619 | dependencies: 620 | fs.realpath "^1.0.0" 621 | inflight "^1.0.4" 622 | inherits "2" 623 | minimatch "^3.0.4" 624 | once "^1.3.0" 625 | path-is-absolute "^1.0.0" 626 | 627 | glob@^7.1.2: 628 | version "7.1.3" 629 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" 630 | integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== 631 | dependencies: 632 | fs.realpath "^1.0.0" 633 | inflight "^1.0.4" 634 | inherits "2" 635 | minimatch "^3.0.4" 636 | once "^1.3.0" 637 | path-is-absolute "^1.0.0" 638 | 639 | globals@^11.7.0: 640 | version "11.8.0" 641 | resolved "https://registry.yarnpkg.com/globals/-/globals-11.8.0.tgz#c1ef45ee9bed6badf0663c5cb90e8d1adec1321d" 642 | integrity sha512-io6LkyPVuzCHBSQV9fmOwxZkUk6nIaGmxheLDgmuFv89j0fm2aqDbIXKAGfzCMHqz3HLF2Zf8WSG6VqMh2qFmA== 643 | 644 | globby@^5.0.0: 645 | version "5.0.0" 646 | resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" 647 | integrity sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0= 648 | dependencies: 649 | array-union "^1.0.1" 650 | arrify "^1.0.0" 651 | glob "^7.0.3" 652 | object-assign "^4.0.1" 653 | pify "^2.0.0" 654 | pinkie-promise "^2.0.0" 655 | 656 | graceful-fs@^4.1.2: 657 | version "4.1.11" 658 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 659 | integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= 660 | 661 | has-flag@^3.0.0: 662 | version "3.0.0" 663 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 664 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 665 | 666 | http-errors@1.6.2: 667 | version "1.6.2" 668 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" 669 | integrity sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY= 670 | dependencies: 671 | depd "1.1.1" 672 | inherits "2.0.3" 673 | setprototypeof "1.0.3" 674 | statuses ">= 1.3.1 < 2" 675 | 676 | http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: 677 | version "1.6.3" 678 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" 679 | integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= 680 | dependencies: 681 | depd "~1.1.2" 682 | inherits "2.0.3" 683 | setprototypeof "1.1.0" 684 | statuses ">= 1.4.0 < 2" 685 | 686 | http-proxy-agent@^2.1.0: 687 | version "2.1.0" 688 | resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" 689 | integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== 690 | dependencies: 691 | agent-base "4" 692 | debug "3.1.0" 693 | 694 | https-proxy-agent@^2.2.0: 695 | version "2.2.1" 696 | resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0" 697 | integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ== 698 | dependencies: 699 | agent-base "^4.1.0" 700 | debug "^3.1.0" 701 | 702 | iconv-lite@0.4.19: 703 | version "0.4.19" 704 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" 705 | integrity sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ== 706 | 707 | iconv-lite@0.4.23: 708 | version "0.4.23" 709 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" 710 | integrity sha512-neyTUVFtahjf0mB3dZT77u+8O0QB89jFdnBkd5P1JgYPbPaia3gXXOVL2fq8VyU2gMMD7SaN7QukTB/pmXYvDA== 711 | dependencies: 712 | safer-buffer ">= 2.1.2 < 3" 713 | 714 | iconv-lite@^0.4.24, iconv-lite@~0.4.13: 715 | version "0.4.24" 716 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 717 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 718 | dependencies: 719 | safer-buffer ">= 2.1.2 < 3" 720 | 721 | ignore@^4.0.6: 722 | version "4.0.6" 723 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" 724 | integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== 725 | 726 | imurmurhash@^0.1.4: 727 | version "0.1.4" 728 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 729 | integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= 730 | 731 | inflight@^1.0.4: 732 | version "1.0.6" 733 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 734 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 735 | dependencies: 736 | once "^1.3.0" 737 | wrappy "1" 738 | 739 | inherits@2, inherits@2.0.3: 740 | version "2.0.3" 741 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 742 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 743 | 744 | inquirer@^6.1.0: 745 | version "6.2.0" 746 | resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" 747 | integrity sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg== 748 | dependencies: 749 | ansi-escapes "^3.0.0" 750 | chalk "^2.0.0" 751 | cli-cursor "^2.1.0" 752 | cli-width "^2.0.0" 753 | external-editor "^3.0.0" 754 | figures "^2.0.0" 755 | lodash "^4.17.10" 756 | mute-stream "0.0.7" 757 | run-async "^2.2.0" 758 | rxjs "^6.1.0" 759 | string-width "^2.1.0" 760 | strip-ansi "^4.0.0" 761 | through "^2.3.6" 762 | 763 | ipaddr.js@1.8.0: 764 | version "1.8.0" 765 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" 766 | integrity sha1-6qM9bd16zo9/b+DJygRA5wZzix4= 767 | 768 | is-fullwidth-code-point@^2.0.0: 769 | version "2.0.0" 770 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 771 | integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= 772 | 773 | is-path-cwd@^1.0.0: 774 | version "1.0.0" 775 | resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" 776 | integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0= 777 | 778 | is-path-in-cwd@^1.0.0: 779 | version "1.0.0" 780 | resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" 781 | integrity sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw= 782 | dependencies: 783 | is-path-inside "^1.0.0" 784 | 785 | is-path-inside@^1.0.0: 786 | version "1.0.0" 787 | resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" 788 | integrity sha1-/AbloWg/vaE95mev9xe7wQpI838= 789 | dependencies: 790 | path-is-inside "^1.0.1" 791 | 792 | is-promise@^2.1.0: 793 | version "2.1.0" 794 | resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" 795 | integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= 796 | 797 | is-resolvable@^1.1.0: 798 | version "1.1.0" 799 | resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" 800 | integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== 801 | 802 | is-stream@^1.0.1: 803 | version "1.1.0" 804 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 805 | integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ= 806 | 807 | isexe@^2.0.0: 808 | version "2.0.0" 809 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 810 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 811 | 812 | js-tokens@^4.0.0: 813 | version "4.0.0" 814 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 815 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 816 | 817 | js-yaml@^3.12.0: 818 | version "3.12.0" 819 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" 820 | integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== 821 | dependencies: 822 | argparse "^1.0.7" 823 | esprima "^4.0.0" 824 | 825 | json-schema-traverse@^0.4.1: 826 | version "0.4.1" 827 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 828 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 829 | 830 | json-stable-stringify-without-jsonify@^1.0.1: 831 | version "1.0.1" 832 | resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" 833 | integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= 834 | 835 | levn@^0.3.0, levn@~0.3.0: 836 | version "0.3.0" 837 | resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" 838 | integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= 839 | dependencies: 840 | prelude-ls "~1.1.2" 841 | type-check "~0.3.2" 842 | 843 | lodash@^4.17.10, lodash@^4.17.4, lodash@^4.17.5: 844 | version "4.17.11" 845 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" 846 | integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== 847 | 848 | macos-release@^1.0.0: 849 | version "1.1.0" 850 | resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-1.1.0.tgz#831945e29365b470aa8724b0ab36c8f8959d10fb" 851 | integrity sha512-mmLbumEYMi5nXReB9js3WGsB8UE6cDBWyIO62Z4DNx6GbRhDxHNjA1MlzSpJ2S2KM1wyiPRA0d19uHWYYvMHjA== 852 | 853 | media-typer@0.3.0: 854 | version "0.3.0" 855 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 856 | integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= 857 | 858 | merge-descriptors@1.0.1: 859 | version "1.0.1" 860 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 861 | integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E= 862 | 863 | methods@~1.1.2: 864 | version "1.1.2" 865 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 866 | integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= 867 | 868 | mime-db@~1.36.0: 869 | version "1.36.0" 870 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.36.0.tgz#5020478db3c7fe93aad7bbcc4dcf869c43363397" 871 | integrity sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw== 872 | 873 | mime-types@~2.1.18: 874 | version "2.1.20" 875 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.20.tgz#930cb719d571e903738520f8470911548ca2cc19" 876 | integrity sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A== 877 | dependencies: 878 | mime-db "~1.36.0" 879 | 880 | mime@1.4.1: 881 | version "1.4.1" 882 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" 883 | integrity sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ== 884 | 885 | mimic-fn@^1.0.0: 886 | version "1.2.0" 887 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" 888 | integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== 889 | 890 | minimatch@^3.0.4: 891 | version "3.0.4" 892 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 893 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 894 | dependencies: 895 | brace-expansion "^1.1.7" 896 | 897 | minimist@0.0.8: 898 | version "0.0.8" 899 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 900 | integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= 901 | 902 | mkdirp@^0.5.1: 903 | version "0.5.1" 904 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 905 | integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= 906 | dependencies: 907 | minimist "0.0.8" 908 | 909 | ms@2.0.0: 910 | version "2.0.0" 911 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 912 | integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= 913 | 914 | ms@^2.1.1: 915 | version "2.1.1" 916 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 917 | integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== 918 | 919 | mute-stream@0.0.7: 920 | version "0.0.7" 921 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" 922 | integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= 923 | 924 | natural-compare@^1.4.0: 925 | version "1.4.0" 926 | resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" 927 | integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= 928 | 929 | negotiator@0.6.1: 930 | version "0.6.1" 931 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 932 | integrity sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk= 933 | 934 | nice-try@^1.0.4: 935 | version "1.0.5" 936 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" 937 | integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== 938 | 939 | node-fetch@1.7.3: 940 | version "1.7.3" 941 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" 942 | integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ== 943 | dependencies: 944 | encoding "^0.1.11" 945 | is-stream "^1.0.1" 946 | 947 | node-fetch@^2.1.1: 948 | version "2.2.0" 949 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.2.0.tgz#4ee79bde909262f9775f731e3656d0db55ced5b5" 950 | integrity sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA== 951 | 952 | object-assign@^4.0.1: 953 | version "4.1.1" 954 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 955 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 956 | 957 | on-finished@~2.3.0: 958 | version "2.3.0" 959 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 960 | integrity sha1-IPEzZIGwg811M3mSoWlxqi2QaUc= 961 | dependencies: 962 | ee-first "1.1.1" 963 | 964 | once@^1.3.0: 965 | version "1.4.0" 966 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 967 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 968 | dependencies: 969 | wrappy "1" 970 | 971 | onetime@^2.0.0: 972 | version "2.0.1" 973 | resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" 974 | integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= 975 | dependencies: 976 | mimic-fn "^1.0.0" 977 | 978 | optionator@^0.8.2: 979 | version "0.8.2" 980 | resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" 981 | integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= 982 | dependencies: 983 | deep-is "~0.1.3" 984 | fast-levenshtein "~2.0.4" 985 | levn "~0.3.0" 986 | prelude-ls "~1.1.2" 987 | type-check "~0.3.2" 988 | wordwrap "~1.0.0" 989 | 990 | os-name@^2.0.1: 991 | version "2.0.1" 992 | resolved "https://registry.yarnpkg.com/os-name/-/os-name-2.0.1.tgz#b9a386361c17ae3a21736ef0599405c9a8c5dc5e" 993 | integrity sha1-uaOGNhwXrjohc27wWZQFyajF3F4= 994 | dependencies: 995 | macos-release "^1.0.0" 996 | win-release "^1.0.0" 997 | 998 | os-tmpdir@~1.0.2: 999 | version "1.0.2" 1000 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 1001 | integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= 1002 | 1003 | parseurl@~1.3.2: 1004 | version "1.3.2" 1005 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" 1006 | integrity sha1-/CidTtiZMRlGDBViUyYs3I3mW/M= 1007 | 1008 | path-is-absolute@^1.0.0: 1009 | version "1.0.1" 1010 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 1011 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 1012 | 1013 | path-is-inside@^1.0.1, path-is-inside@^1.0.2: 1014 | version "1.0.2" 1015 | resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" 1016 | integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= 1017 | 1018 | path-key@^2.0.1: 1019 | version "2.0.1" 1020 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" 1021 | integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= 1022 | 1023 | path-to-regexp@0.1.7: 1024 | version "0.1.7" 1025 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 1026 | integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= 1027 | 1028 | pify@^2.0.0: 1029 | version "2.3.0" 1030 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" 1031 | integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= 1032 | 1033 | pinkie-promise@^2.0.0: 1034 | version "2.0.1" 1035 | resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" 1036 | integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= 1037 | dependencies: 1038 | pinkie "^2.0.0" 1039 | 1040 | pinkie@^2.0.0: 1041 | version "2.0.4" 1042 | resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" 1043 | integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= 1044 | 1045 | pluralize@^7.0.0: 1046 | version "7.0.0" 1047 | resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" 1048 | integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== 1049 | 1050 | prelude-ls@~1.1.2: 1051 | version "1.1.2" 1052 | resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" 1053 | integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= 1054 | 1055 | progress@^2.0.0: 1056 | version "2.0.0" 1057 | resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" 1058 | integrity sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8= 1059 | 1060 | proxy-addr@~2.0.3: 1061 | version "2.0.4" 1062 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.4.tgz#ecfc733bf22ff8c6f407fa275327b9ab67e48b93" 1063 | integrity sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA== 1064 | dependencies: 1065 | forwarded "~0.1.2" 1066 | ipaddr.js "1.8.0" 1067 | 1068 | punycode@^2.1.0: 1069 | version "2.1.1" 1070 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 1071 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 1072 | 1073 | qs@6.5.1: 1074 | version "6.5.1" 1075 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" 1076 | integrity sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A== 1077 | 1078 | qs@6.5.2: 1079 | version "6.5.2" 1080 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36" 1081 | integrity sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA== 1082 | 1083 | range-parser@~1.2.0: 1084 | version "1.2.0" 1085 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 1086 | integrity sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4= 1087 | 1088 | raw-body@2.3.2: 1089 | version "2.3.2" 1090 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" 1091 | integrity sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k= 1092 | dependencies: 1093 | bytes "3.0.0" 1094 | http-errors "1.6.2" 1095 | iconv-lite "0.4.19" 1096 | unpipe "1.0.0" 1097 | 1098 | raw-body@2.3.3: 1099 | version "2.3.3" 1100 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3" 1101 | integrity sha512-9esiElv1BrZoI3rCDuOuKCBRbuApGGaDPQfjSflGxdy4oyzqghxu6klEkkVIvBje+FF0BX9coEv8KqW6X/7njw== 1102 | dependencies: 1103 | bytes "3.0.0" 1104 | http-errors "1.6.3" 1105 | iconv-lite "0.4.23" 1106 | unpipe "1.0.0" 1107 | 1108 | regexpp@^2.0.0: 1109 | version "2.0.1" 1110 | resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" 1111 | integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== 1112 | 1113 | require-uncached@^1.0.3: 1114 | version "1.0.3" 1115 | resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" 1116 | integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM= 1117 | dependencies: 1118 | caller-path "^0.1.0" 1119 | resolve-from "^1.0.0" 1120 | 1121 | resolve-from@^1.0.0: 1122 | version "1.0.1" 1123 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" 1124 | integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY= 1125 | 1126 | restore-cursor@^2.0.0: 1127 | version "2.0.0" 1128 | resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" 1129 | integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= 1130 | dependencies: 1131 | onetime "^2.0.0" 1132 | signal-exit "^3.0.2" 1133 | 1134 | rimraf@^2.2.8: 1135 | version "2.6.1" 1136 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" 1137 | integrity sha1-wjOOxkPfeht/5cVPqG9XQopV8z0= 1138 | dependencies: 1139 | glob "^7.0.5" 1140 | 1141 | run-async@^2.2.0: 1142 | version "2.3.0" 1143 | resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" 1144 | integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= 1145 | dependencies: 1146 | is-promise "^2.1.0" 1147 | 1148 | rxjs@^6.1.0: 1149 | version "6.3.3" 1150 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" 1151 | integrity sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw== 1152 | dependencies: 1153 | tslib "^1.9.0" 1154 | 1155 | safe-buffer@5.1.1: 1156 | version "5.1.1" 1157 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 1158 | integrity sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg== 1159 | 1160 | "safer-buffer@>= 2.1.2 < 3": 1161 | version "2.1.2" 1162 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 1163 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 1164 | 1165 | semver@^5.0.1, semver@^5.5.0, semver@^5.5.1: 1166 | version "5.5.1" 1167 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" 1168 | integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== 1169 | 1170 | send@0.16.2: 1171 | version "0.16.2" 1172 | resolved "https://registry.yarnpkg.com/send/-/send-0.16.2.tgz#6ecca1e0f8c156d141597559848df64730a6bbc1" 1173 | integrity sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw== 1174 | dependencies: 1175 | debug "2.6.9" 1176 | depd "~1.1.2" 1177 | destroy "~1.0.4" 1178 | encodeurl "~1.0.2" 1179 | escape-html "~1.0.3" 1180 | etag "~1.8.1" 1181 | fresh "0.5.2" 1182 | http-errors "~1.6.2" 1183 | mime "1.4.1" 1184 | ms "2.0.0" 1185 | on-finished "~2.3.0" 1186 | range-parser "~1.2.0" 1187 | statuses "~1.4.0" 1188 | 1189 | serve-static@1.13.2: 1190 | version "1.13.2" 1191 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.2.tgz#095e8472fd5b46237db50ce486a43f4b86c6cec1" 1192 | integrity sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw== 1193 | dependencies: 1194 | encodeurl "~1.0.2" 1195 | escape-html "~1.0.3" 1196 | parseurl "~1.3.2" 1197 | send "0.16.2" 1198 | 1199 | setprototypeof@1.0.3: 1200 | version "1.0.3" 1201 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 1202 | integrity sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ= 1203 | 1204 | setprototypeof@1.1.0: 1205 | version "1.1.0" 1206 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" 1207 | integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== 1208 | 1209 | shebang-command@^1.2.0: 1210 | version "1.2.0" 1211 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" 1212 | integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= 1213 | dependencies: 1214 | shebang-regex "^1.0.0" 1215 | 1216 | shebang-regex@^1.0.0: 1217 | version "1.0.0" 1218 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 1219 | integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= 1220 | 1221 | signal-exit@^3.0.2: 1222 | version "3.0.2" 1223 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 1224 | integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= 1225 | 1226 | slice-ansi@1.0.0: 1227 | version "1.0.0" 1228 | resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" 1229 | integrity sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg== 1230 | dependencies: 1231 | is-fullwidth-code-point "^2.0.0" 1232 | 1233 | sprintf-js@~1.0.2: 1234 | version "1.0.3" 1235 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 1236 | integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= 1237 | 1238 | "statuses@>= 1.3.1 < 2": 1239 | version "1.3.1" 1240 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 1241 | integrity sha1-+vUbnrdKrvOzrPStX2Gr8ky3uT4= 1242 | 1243 | "statuses@>= 1.4.0 < 2": 1244 | version "1.5.0" 1245 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" 1246 | integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= 1247 | 1248 | statuses@~1.4.0: 1249 | version "1.4.0" 1250 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" 1251 | integrity sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew== 1252 | 1253 | string-width@^2.1.0, string-width@^2.1.1: 1254 | version "2.1.1" 1255 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 1256 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 1257 | dependencies: 1258 | is-fullwidth-code-point "^2.0.0" 1259 | strip-ansi "^4.0.0" 1260 | 1261 | strip-ansi@^4.0.0: 1262 | version "4.0.0" 1263 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 1264 | integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= 1265 | dependencies: 1266 | ansi-regex "^3.0.0" 1267 | 1268 | strip-json-comments@^2.0.1: 1269 | version "2.0.1" 1270 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 1271 | integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= 1272 | 1273 | supports-color@^5.3.0: 1274 | version "5.5.0" 1275 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 1276 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 1277 | dependencies: 1278 | has-flag "^3.0.0" 1279 | 1280 | table@^4.0.3: 1281 | version "4.0.3" 1282 | resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" 1283 | integrity sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg== 1284 | dependencies: 1285 | ajv "^6.0.1" 1286 | ajv-keywords "^3.0.0" 1287 | chalk "^2.1.0" 1288 | lodash "^4.17.4" 1289 | slice-ansi "1.0.0" 1290 | string-width "^2.1.1" 1291 | 1292 | text-table@^0.2.0: 1293 | version "0.2.0" 1294 | resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" 1295 | integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= 1296 | 1297 | through@^2.3.6: 1298 | version "2.3.8" 1299 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 1300 | integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= 1301 | 1302 | tmp@^0.0.33: 1303 | version "0.0.33" 1304 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" 1305 | integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== 1306 | dependencies: 1307 | os-tmpdir "~1.0.2" 1308 | 1309 | tslib@^1.9.0: 1310 | version "1.9.3" 1311 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" 1312 | integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== 1313 | 1314 | type-check@~0.3.2: 1315 | version "0.3.2" 1316 | resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" 1317 | integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= 1318 | dependencies: 1319 | prelude-ls "~1.1.2" 1320 | 1321 | type-is@~1.6.15, type-is@~1.6.16: 1322 | version "1.6.16" 1323 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" 1324 | integrity sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q== 1325 | dependencies: 1326 | media-typer "0.3.0" 1327 | mime-types "~2.1.18" 1328 | 1329 | universal-user-agent@^2.0.0: 1330 | version "2.0.1" 1331 | resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-2.0.1.tgz#18e591ca52b1cb804f6b9cbc4c336cf8191f80e1" 1332 | integrity sha512-vz+heWVydO0iyYAa65VHD7WZkYzhl7BeNVy4i54p4TF8OMiLSXdbuQe4hm+fmWAsL+rVibaQHXfhvkw3c1Ws2w== 1333 | dependencies: 1334 | os-name "^2.0.1" 1335 | 1336 | unpipe@1.0.0, unpipe@~1.0.0: 1337 | version "1.0.0" 1338 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 1339 | integrity sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw= 1340 | 1341 | uri-js@^4.2.2: 1342 | version "4.2.2" 1343 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" 1344 | integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== 1345 | dependencies: 1346 | punycode "^2.1.0" 1347 | 1348 | url-template@^2.0.8: 1349 | version "2.0.8" 1350 | resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21" 1351 | integrity sha1-/FZaPMy/93MMd19WQflVV5FDnyE= 1352 | 1353 | utils-merge@1.0.1: 1354 | version "1.0.1" 1355 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" 1356 | integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= 1357 | 1358 | vary@~1.1.2: 1359 | version "1.1.2" 1360 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" 1361 | integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= 1362 | 1363 | which@^1.2.9: 1364 | version "1.3.1" 1365 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" 1366 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== 1367 | dependencies: 1368 | isexe "^2.0.0" 1369 | 1370 | win-release@^1.0.0: 1371 | version "1.1.1" 1372 | resolved "https://registry.yarnpkg.com/win-release/-/win-release-1.1.1.tgz#5fa55e02be7ca934edfc12665632e849b72e5209" 1373 | integrity sha1-X6VeAr58qTTt/BJmVjLoSbcuUgk= 1374 | dependencies: 1375 | semver "^5.0.1" 1376 | 1377 | wordwrap@~1.0.0: 1378 | version "1.0.0" 1379 | resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" 1380 | integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= 1381 | 1382 | wrappy@1: 1383 | version "1.0.2" 1384 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 1385 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 1386 | 1387 | write@^0.2.1: 1388 | version "0.2.1" 1389 | resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" 1390 | integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c= 1391 | dependencies: 1392 | mkdirp "^0.5.1" 1393 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lighthousebot", 3 | "version": "0.2.0", 4 | "author": "Eric Bidelman ", 5 | "license": "Apache-2.0", 6 | "description": "Lighthouse Bot (CI) Runner", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/ebidel/lighthousebot.git" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/ebidel/lighthousebot/issues" 13 | }, 14 | "homepage": "https://github.com/ebidel/lighthousebot#readme", 15 | "main": "runlighthouse.js", 16 | "bin": { 17 | "lighthousebot": "runlighthouse.js" 18 | }, 19 | "scripts": { 20 | "deploy": "./deploy.sh" 21 | }, 22 | "dependencies": { 23 | "minimist": "^1.2.0", 24 | "node-fetch": "^2.2.0" 25 | }, 26 | "devDependencies": { 27 | "eslint": "^5.6.1", 28 | "eslint-config-google": "^0.10.0" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /runlighthouse.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /** 4 | * Copyright 2017 Google Inc. All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 'use strict'; 19 | 20 | const fetch = require('node-fetch'); // polyfill 21 | const minimist = require('minimist'); 22 | 23 | const CI_HOST = process.env.LIGHTHOUSE_CI_HOST || 'https://lighthouse-ci.appspot.com'; 24 | const API_KEY = process.env.LIGHTHOUSE_API_KEY || process.env.API_KEY; 25 | const RUNNERS = {chrome: 'chrome', wpt: 'wpt'}; 26 | 27 | if (process.env.API_KEY) { 28 | console.log(`Warning: The environment variable API_KEY is deprecated. 29 | Please use LIGHTHOUSE_API_KEY instead.`); 30 | } 31 | 32 | function printUsageAndExit() { 33 | const usage = `Usage: 34 | runlighthouse.js [--perf,pwa,seo,a11y,bp=] [--no-comment] [--runner=${Object.keys(RUNNERS)}] 35 | 36 | Options: 37 | Minimum score values can be pased per category as a way to fail the PR if 38 | the thresholds are not met. If you don't provide thresholds, the PR will 39 | be mergeable no matter what the scores. 40 | 41 | --pwa Minimum PWA score for the PR to be considered "passing". [Number] 42 | --perf Minimum performance score for the PR to be considered "passing". [Number] 43 | --seo Minimum seo score for the PR to be considered "passing". [Number] 44 | --a11y Minimum accessibility score for the PR to be considered "passing". [Number] 45 | --bp Minimum best practices score for the PR to be considered "passing". [Number] 46 | 47 | --no-comment Doesn't post a comment to the PR issue summarizing the Lighthouse results. [Boolean] 48 | 49 | --runner Selects Lighthouse running on Chrome or WebPageTest. [--runner=${Object.keys(RUNNERS)}] 50 | 51 | --help Prints help. 52 | 53 | Examples: 54 | 55 | Runs Lighthouse and posts a summary of the results. 56 | runlighthouse.js https://example.com 57 | 58 | Fails the PR if the performance score drops below 93. Posts the summary comment. 59 | runlighthouse.js --perf=93 https://example.com 60 | 61 | Fails the PR if perf score drops below 93 or the PWA score drops below 100. Posts the summary comment. 62 | runlighthouse.js --perf=93 --pwa=100 https://example.com 63 | 64 | Runs Lighthouse on WebPageTest. Fails the PR if the perf score drops below 93. 65 | runlighthouse.js --perf=93 --runner=wpt --no-comment https://example.com`; 66 | 67 | console.log(usage); 68 | process.exit(1); 69 | } 70 | 71 | /** 72 | * Collects command lines flags and creates settings to run LH CI. 73 | * @return {!Object} Settings object. 74 | */ 75 | function getConfig() { 76 | const args = process.argv.slice(2); 77 | const argv = minimist(args, { 78 | boolean: ['comment', 'help'], 79 | default: {comment: true}, 80 | alias: {help: 'h'} 81 | }); 82 | const config = {}; 83 | 84 | if (argv.help) { 85 | printUsageAndExit(); 86 | } 87 | 88 | config.testUrl = argv._[0]; 89 | if (!config.testUrl) { 90 | console.log('Please provide a url to test.'); 91 | printUsageAndExit(); 92 | } 93 | 94 | config.addComment = argv.comment; 95 | config.thresholds = {}; 96 | if ('perf' in argv) { 97 | config.thresholds.performance = Number(argv.perf); 98 | } 99 | if ('pwa' in argv) { 100 | config.thresholds.pwa = Number(argv.pwa); 101 | } 102 | if ('seo' in argv) { 103 | config.thresholds.seo = Number(argv.seo); 104 | } 105 | if ('a11y' in argv) { 106 | config.thresholds.accessibility = Number(argv.a11y); 107 | } 108 | if ('bp' in argv) { 109 | config.thresholds['best-practices'] = Number(argv.bp); 110 | } 111 | 112 | if (!config.addComment && !Object.keys(config.thresholds).length) { 113 | console.log(`Please provide a threshold score for at least one category 114 | (pwa,perf,seo,a11y) when using --no-comment.]\n`); 115 | printUsageAndExit(); 116 | } 117 | 118 | config.runner = argv.runner || RUNNERS.chrome; 119 | const possibleRunners = Object.keys(RUNNERS); 120 | if (!possibleRunners.includes(config.runner)) { 121 | console.log( 122 | `Unknown runner "${config.runner}". Options: ${possibleRunners}`); 123 | printUsageAndExit(); 124 | } 125 | console.log(`Using runner: ${config.runner}`); 126 | 127 | config.pr = { 128 | number: parseInt(process.env.TRAVIS_PULL_REQUEST, 10), 129 | sha: process.env.TRAVIS_PULL_REQUEST_SHA 130 | }; 131 | 132 | const repoSlug = process.env.TRAVIS_PULL_REQUEST_SLUG; 133 | if (!repoSlug) { 134 | throw new Error('This script can only be run on Travis PR requests.'); 135 | } 136 | 137 | config.repo = { 138 | owner: repoSlug.split('/')[0], 139 | name: repoSlug.split('/')[1] 140 | }; 141 | 142 | return config; 143 | } 144 | 145 | /** 146 | * @param {!Object} config Settings to run the Lighthouse CI. 147 | */ 148 | function run(config) { 149 | let endpoint; 150 | let body = JSON.stringify(config); 151 | 152 | switch (config.runner) { 153 | case RUNNERS.wpt: 154 | endpoint = `${CI_HOST}/run_on_wpt`; 155 | break; 156 | case RUNNERS.chrome: // same as default 157 | default: 158 | endpoint = `${CI_HOST}/run_on_chrome`; 159 | body = JSON.stringify(Object.assign({output: 'json'}, config)); 160 | } 161 | 162 | fetch(endpoint, {method: 'POST', body, headers: { 163 | 'Content-Type': 'application/json', 164 | 'X-API-KEY': API_KEY 165 | }}) 166 | .then(resp => resp.json()) 167 | .then(json => { 168 | if (config.runner === RUNNERS.wpt) { 169 | console.log( 170 | `Started Lighthouse run on WebPageTest: ${json.data.target_url}`); 171 | return; 172 | } 173 | console.log('New Lighthouse scores:'); 174 | console.log(JSON.stringify(json)); 175 | }) 176 | .catch(err => { 177 | console.log('Lighthouse CI failed', err); 178 | process.exit(1); 179 | }); 180 | } 181 | 182 | // Run LH if this is a PR. 183 | const config = getConfig(); 184 | if (process.env.TRAVIS_EVENT_TYPE === 'pull_request') { 185 | run(config); 186 | } else { 187 | console.log('Lighthouse is not run for non-PR commits.'); 188 | } 189 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@babel/code-frame@^7.0.0": 6 | version "7.0.0" 7 | resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0.tgz#06e2ab19bdb535385559aabb5ba59729482800f8" 8 | integrity sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA== 9 | dependencies: 10 | "@babel/highlight" "^7.0.0" 11 | 12 | "@babel/highlight@^7.0.0": 13 | version "7.0.0" 14 | resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0.tgz#f710c38c8d458e6dd9a201afb637fcb781ce99e4" 15 | integrity sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw== 16 | dependencies: 17 | chalk "^2.0.0" 18 | esutils "^2.0.2" 19 | js-tokens "^4.0.0" 20 | 21 | acorn-jsx@^4.1.1: 22 | version "4.1.1" 23 | resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e" 24 | integrity sha512-JY+iV6r+cO21KtntVvFkD+iqjtdpRUpGqKWgfkCdZq1R+kbreEl8EcdcJR4SmiIgsIQT33s6QzheQ9a275Q8xw== 25 | dependencies: 26 | acorn "^5.0.3" 27 | 28 | acorn@^5.0.3, acorn@^5.6.0: 29 | version "5.7.2" 30 | resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.2.tgz#91fa871883485d06708800318404e72bfb26dcc5" 31 | integrity sha512-cJrKCNcr2kv8dlDnbw+JPUGjHZzo4myaxOLmpOX8a+rgX94YeTcTMv/LFJUSByRpc+i4GgVnnhLxvMu/2Y+rqw== 32 | 33 | ajv-keywords@^3.0.0: 34 | version "3.2.0" 35 | resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a" 36 | integrity sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo= 37 | 38 | ajv@^6.0.1: 39 | version "6.5.3" 40 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.3.tgz#71a569d189ecf4f4f321224fecb166f071dd90f9" 41 | integrity sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg== 42 | dependencies: 43 | fast-deep-equal "^2.0.1" 44 | fast-json-stable-stringify "^2.0.0" 45 | json-schema-traverse "^0.4.1" 46 | uri-js "^4.2.2" 47 | 48 | ajv@^6.5.3: 49 | version "6.5.4" 50 | resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.5.4.tgz#247d5274110db653706b550fcc2b797ca28cfc59" 51 | integrity sha512-4Wyjt8+t6YszqaXnLDfMmG/8AlO5Zbcsy3ATHncCzjW/NoPzAId8AK6749Ybjmdt+kUY1gP60fCu46oDxPv/mg== 52 | dependencies: 53 | fast-deep-equal "^2.0.1" 54 | fast-json-stable-stringify "^2.0.0" 55 | json-schema-traverse "^0.4.1" 56 | uri-js "^4.2.2" 57 | 58 | ansi-escapes@^3.0.0: 59 | version "3.1.0" 60 | resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.1.0.tgz#f73207bb81207d75fd6c83f125af26eea378ca30" 61 | integrity sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw== 62 | 63 | ansi-regex@^3.0.0: 64 | version "3.0.0" 65 | resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" 66 | integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= 67 | 68 | ansi-styles@^3.2.1: 69 | version "3.2.1" 70 | resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" 71 | integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== 72 | dependencies: 73 | color-convert "^1.9.0" 74 | 75 | argparse@^1.0.7: 76 | version "1.0.9" 77 | resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" 78 | integrity sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY= 79 | dependencies: 80 | sprintf-js "~1.0.2" 81 | 82 | array-union@^1.0.1: 83 | version "1.0.2" 84 | resolved "https://registry.yarnpkg.com/array-union/-/array-union-1.0.2.tgz#9a34410e4f4e3da23dea375be5be70f24778ec39" 85 | integrity sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk= 86 | dependencies: 87 | array-uniq "^1.0.1" 88 | 89 | array-uniq@^1.0.1: 90 | version "1.0.3" 91 | resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6" 92 | integrity sha1-r2rId6Jcx/dOBYiUdThY39sk/bY= 93 | 94 | arrify@^1.0.0: 95 | version "1.0.1" 96 | resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" 97 | integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0= 98 | 99 | balanced-match@^0.4.1: 100 | version "0.4.2" 101 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-0.4.2.tgz#cb3f3e3c732dc0f01ee70b403f302e61d7709838" 102 | integrity sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg= 103 | 104 | balanced-match@^1.0.0: 105 | version "1.0.0" 106 | resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" 107 | integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= 108 | 109 | brace-expansion@^1.0.0: 110 | version "1.1.6" 111 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.6.tgz#7197d7eaa9b87e648390ea61fc66c84427420df9" 112 | integrity sha1-cZfX6qm4fmSDkOph/GbIRCdCDfk= 113 | dependencies: 114 | balanced-match "^0.4.1" 115 | concat-map "0.0.1" 116 | 117 | brace-expansion@^1.1.7: 118 | version "1.1.11" 119 | resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" 120 | integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== 121 | dependencies: 122 | balanced-match "^1.0.0" 123 | concat-map "0.0.1" 124 | 125 | caller-path@^0.1.0: 126 | version "0.1.0" 127 | resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f" 128 | integrity sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8= 129 | dependencies: 130 | callsites "^0.2.0" 131 | 132 | callsites@^0.2.0: 133 | version "0.2.0" 134 | resolved "https://registry.yarnpkg.com/callsites/-/callsites-0.2.0.tgz#afab96262910a7f33c19a5775825c69f34e350ca" 135 | integrity sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo= 136 | 137 | chalk@^2.0.0, chalk@^2.1.0: 138 | version "2.4.1" 139 | resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e" 140 | integrity sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ== 141 | dependencies: 142 | ansi-styles "^3.2.1" 143 | escape-string-regexp "^1.0.5" 144 | supports-color "^5.3.0" 145 | 146 | chardet@^0.7.0: 147 | version "0.7.0" 148 | resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" 149 | integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== 150 | 151 | circular-json@^0.3.1: 152 | version "0.3.1" 153 | resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.3.1.tgz#be8b36aefccde8b3ca7aa2d6afc07a37242c0d2d" 154 | integrity sha1-vos2rvzN6LPKeqLWr8B6NyQsDS0= 155 | 156 | cli-cursor@^2.1.0: 157 | version "2.1.0" 158 | resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" 159 | integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= 160 | dependencies: 161 | restore-cursor "^2.0.0" 162 | 163 | cli-width@^2.0.0: 164 | version "2.1.0" 165 | resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" 166 | integrity sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao= 167 | 168 | color-convert@^1.9.0: 169 | version "1.9.3" 170 | resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" 171 | integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== 172 | dependencies: 173 | color-name "1.1.3" 174 | 175 | color-name@1.1.3: 176 | version "1.1.3" 177 | resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" 178 | integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= 179 | 180 | concat-map@0.0.1: 181 | version "0.0.1" 182 | resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" 183 | integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= 184 | 185 | cross-spawn@^6.0.5: 186 | version "6.0.5" 187 | resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" 188 | integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== 189 | dependencies: 190 | nice-try "^1.0.4" 191 | path-key "^2.0.1" 192 | semver "^5.5.0" 193 | shebang-command "^1.2.0" 194 | which "^1.2.9" 195 | 196 | debug@^4.0.1: 197 | version "4.0.1" 198 | resolved "https://registry.yarnpkg.com/debug/-/debug-4.0.1.tgz#f9bb36d439b8d1f0dd52d8fb6b46e4ebb8c1cd5b" 199 | integrity sha512-K23FHJ/Mt404FSlp6gSZCevIbTMLX0j3fmHhUEhQ3Wq0FMODW3+cUSoLdy1Gx4polAf4t/lphhmHH35BB8cLYw== 200 | dependencies: 201 | ms "^2.1.1" 202 | 203 | deep-is@~0.1.3: 204 | version "0.1.3" 205 | resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" 206 | integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= 207 | 208 | del@^2.0.2: 209 | version "2.2.2" 210 | resolved "https://registry.yarnpkg.com/del/-/del-2.2.2.tgz#c12c981d067846c84bcaf862cff930d907ffd1a8" 211 | integrity sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag= 212 | dependencies: 213 | globby "^5.0.0" 214 | is-path-cwd "^1.0.0" 215 | is-path-in-cwd "^1.0.0" 216 | object-assign "^4.0.1" 217 | pify "^2.0.0" 218 | pinkie-promise "^2.0.0" 219 | rimraf "^2.2.8" 220 | 221 | doctrine@^2.1.0: 222 | version "2.1.0" 223 | resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" 224 | integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== 225 | dependencies: 226 | esutils "^2.0.2" 227 | 228 | escape-string-regexp@^1.0.5: 229 | version "1.0.5" 230 | resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" 231 | integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= 232 | 233 | eslint-config-google@^0.10.0: 234 | version "0.10.0" 235 | resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.10.0.tgz#09adb5b1514aa99618ad0515e0cce59b8ca82d90" 236 | integrity sha512-PGlMufI13kljog4HlDkwtyqJ7ZZFOcl0ppEvhDoE1lq+8+nMe0lQs0WIZrXpQJhwxhii3SZuCHW2g/weS6Xpyw== 237 | 238 | eslint-scope@^4.0.0: 239 | version "4.0.0" 240 | resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-4.0.0.tgz#50bf3071e9338bcdc43331794a0cb533f0136172" 241 | integrity sha512-1G6UTDi7Jc1ELFwnR58HV4fK9OQK4S6N985f166xqXxpjU6plxFISJa2Ba9KCQuFa8RCnj/lSFJbHo7UFDBnUA== 242 | dependencies: 243 | esrecurse "^4.1.0" 244 | estraverse "^4.1.1" 245 | 246 | eslint-utils@^1.3.1: 247 | version "1.3.1" 248 | resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512" 249 | integrity sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q== 250 | 251 | eslint-visitor-keys@^1.0.0: 252 | version "1.0.0" 253 | resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d" 254 | integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ== 255 | 256 | eslint@^5.6.1: 257 | version "5.6.1" 258 | resolved "https://registry.yarnpkg.com/eslint/-/eslint-5.6.1.tgz#348134e32ccc09abb2df1bf282b3f6eed8c7b480" 259 | integrity sha512-hgrDtGWz368b7Wqf+v1Z69O3ZebNR0+GA7PtDdbmuz4rInFVUV9uw7whjZEiWyLzCjVb5Rs5WRN1TAS6eo7AYA== 260 | dependencies: 261 | "@babel/code-frame" "^7.0.0" 262 | ajv "^6.5.3" 263 | chalk "^2.1.0" 264 | cross-spawn "^6.0.5" 265 | debug "^4.0.1" 266 | doctrine "^2.1.0" 267 | eslint-scope "^4.0.0" 268 | eslint-utils "^1.3.1" 269 | eslint-visitor-keys "^1.0.0" 270 | espree "^4.0.0" 271 | esquery "^1.0.1" 272 | esutils "^2.0.2" 273 | file-entry-cache "^2.0.0" 274 | functional-red-black-tree "^1.0.1" 275 | glob "^7.1.2" 276 | globals "^11.7.0" 277 | ignore "^4.0.6" 278 | imurmurhash "^0.1.4" 279 | inquirer "^6.1.0" 280 | is-resolvable "^1.1.0" 281 | js-yaml "^3.12.0" 282 | json-stable-stringify-without-jsonify "^1.0.1" 283 | levn "^0.3.0" 284 | lodash "^4.17.5" 285 | minimatch "^3.0.4" 286 | mkdirp "^0.5.1" 287 | natural-compare "^1.4.0" 288 | optionator "^0.8.2" 289 | path-is-inside "^1.0.2" 290 | pluralize "^7.0.0" 291 | progress "^2.0.0" 292 | regexpp "^2.0.0" 293 | require-uncached "^1.0.3" 294 | semver "^5.5.1" 295 | strip-ansi "^4.0.0" 296 | strip-json-comments "^2.0.1" 297 | table "^4.0.3" 298 | text-table "^0.2.0" 299 | 300 | espree@^4.0.0: 301 | version "4.0.0" 302 | resolved "https://registry.yarnpkg.com/espree/-/espree-4.0.0.tgz#253998f20a0f82db5d866385799d912a83a36634" 303 | integrity sha512-kapdTCt1bjmspxStVKX6huolXVV5ZfyZguY1lcfhVVZstce3bqxH9mcLzNn3/mlgW6wQ732+0fuG9v7h0ZQoKg== 304 | dependencies: 305 | acorn "^5.6.0" 306 | acorn-jsx "^4.1.1" 307 | 308 | esprima@^4.0.0: 309 | version "4.0.1" 310 | resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" 311 | integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== 312 | 313 | esquery@^1.0.1: 314 | version "1.0.1" 315 | resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" 316 | integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== 317 | dependencies: 318 | estraverse "^4.0.0" 319 | 320 | esrecurse@^4.1.0: 321 | version "4.1.0" 322 | resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.1.0.tgz#4713b6536adf7f2ac4f327d559e7756bff648220" 323 | integrity sha1-RxO2U2rffyrE8yfVWed1a/9kgiA= 324 | dependencies: 325 | estraverse "~4.1.0" 326 | object-assign "^4.0.1" 327 | 328 | estraverse@^4.0.0, estraverse@^4.1.1: 329 | version "4.2.0" 330 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13" 331 | integrity sha1-De4/7TH81GlhjOc0IJn8GvoL2xM= 332 | 333 | estraverse@~4.1.0: 334 | version "4.1.1" 335 | resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.1.1.tgz#f6caca728933a850ef90661d0e17982ba47111a2" 336 | integrity sha1-9srKcokzqFDvkGYdDheYK6RxEaI= 337 | 338 | esutils@^2.0.2: 339 | version "2.0.2" 340 | resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" 341 | integrity sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs= 342 | 343 | external-editor@^3.0.0: 344 | version "3.0.3" 345 | resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.0.3.tgz#5866db29a97826dbe4bf3afd24070ead9ea43a27" 346 | integrity sha512-bn71H9+qWoOQKyZDo25mOMVpSmXROAsTJVVVYzrrtol3d4y+AsKjf4Iwl2Q+IuT0kFSQ1qo166UuIwqYq7mGnA== 347 | dependencies: 348 | chardet "^0.7.0" 349 | iconv-lite "^0.4.24" 350 | tmp "^0.0.33" 351 | 352 | fast-deep-equal@^2.0.1: 353 | version "2.0.1" 354 | resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" 355 | integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= 356 | 357 | fast-json-stable-stringify@^2.0.0: 358 | version "2.0.0" 359 | resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" 360 | integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= 361 | 362 | fast-levenshtein@~2.0.4: 363 | version "2.0.6" 364 | resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" 365 | integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= 366 | 367 | figures@^2.0.0: 368 | version "2.0.0" 369 | resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" 370 | integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= 371 | dependencies: 372 | escape-string-regexp "^1.0.5" 373 | 374 | file-entry-cache@^2.0.0: 375 | version "2.0.0" 376 | resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" 377 | integrity sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E= 378 | dependencies: 379 | flat-cache "^1.2.1" 380 | object-assign "^4.0.1" 381 | 382 | flat-cache@^1.2.1: 383 | version "1.2.2" 384 | resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-1.2.2.tgz#fa86714e72c21db88601761ecf2f555d1abc6b96" 385 | integrity sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y= 386 | dependencies: 387 | circular-json "^0.3.1" 388 | del "^2.0.2" 389 | graceful-fs "^4.1.2" 390 | write "^0.2.1" 391 | 392 | fs.realpath@^1.0.0: 393 | version "1.0.0" 394 | resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" 395 | integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= 396 | 397 | functional-red-black-tree@^1.0.1: 398 | version "1.0.1" 399 | resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" 400 | integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= 401 | 402 | glob@^7.0.3, glob@^7.0.5: 403 | version "7.1.1" 404 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8" 405 | integrity sha1-gFIR3wT6rxxjo2ADBs31reULLsg= 406 | dependencies: 407 | fs.realpath "^1.0.0" 408 | inflight "^1.0.4" 409 | inherits "2" 410 | minimatch "^3.0.2" 411 | once "^1.3.0" 412 | path-is-absolute "^1.0.0" 413 | 414 | glob@^7.1.2: 415 | version "7.1.3" 416 | resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.3.tgz#3960832d3f1574108342dafd3a67b332c0969df1" 417 | integrity sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ== 418 | dependencies: 419 | fs.realpath "^1.0.0" 420 | inflight "^1.0.4" 421 | inherits "2" 422 | minimatch "^3.0.4" 423 | once "^1.3.0" 424 | path-is-absolute "^1.0.0" 425 | 426 | globals@^11.7.0: 427 | version "11.7.0" 428 | resolved "https://registry.yarnpkg.com/globals/-/globals-11.7.0.tgz#a583faa43055b1aca771914bf68258e2fc125673" 429 | integrity sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg== 430 | 431 | globby@^5.0.0: 432 | version "5.0.0" 433 | resolved "https://registry.yarnpkg.com/globby/-/globby-5.0.0.tgz#ebd84667ca0dbb330b99bcfc68eac2bc54370e0d" 434 | integrity sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0= 435 | dependencies: 436 | array-union "^1.0.1" 437 | arrify "^1.0.0" 438 | glob "^7.0.3" 439 | object-assign "^4.0.1" 440 | pify "^2.0.0" 441 | pinkie-promise "^2.0.0" 442 | 443 | graceful-fs@^4.1.2: 444 | version "4.1.11" 445 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 446 | integrity sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg= 447 | 448 | has-flag@^3.0.0: 449 | version "3.0.0" 450 | resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" 451 | integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= 452 | 453 | iconv-lite@^0.4.24: 454 | version "0.4.24" 455 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" 456 | integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== 457 | dependencies: 458 | safer-buffer ">= 2.1.2 < 3" 459 | 460 | ignore@^4.0.6: 461 | version "4.0.6" 462 | resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" 463 | integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== 464 | 465 | imurmurhash@^0.1.4: 466 | version "0.1.4" 467 | resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" 468 | integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= 469 | 470 | inflight@^1.0.4: 471 | version "1.0.6" 472 | resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" 473 | integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= 474 | dependencies: 475 | once "^1.3.0" 476 | wrappy "1" 477 | 478 | inherits@2: 479 | version "2.0.3" 480 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 481 | integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= 482 | 483 | inquirer@^6.1.0: 484 | version "6.2.0" 485 | resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.2.0.tgz#51adcd776f661369dc1e894859c2560a224abdd8" 486 | integrity sha512-QIEQG4YyQ2UYZGDC4srMZ7BjHOmNk1lR2JQj5UknBapklm6WHA+VVH7N+sUdX3A7NeCfGF8o4X1S3Ao7nAcIeg== 487 | dependencies: 488 | ansi-escapes "^3.0.0" 489 | chalk "^2.0.0" 490 | cli-cursor "^2.1.0" 491 | cli-width "^2.0.0" 492 | external-editor "^3.0.0" 493 | figures "^2.0.0" 494 | lodash "^4.17.10" 495 | mute-stream "0.0.7" 496 | run-async "^2.2.0" 497 | rxjs "^6.1.0" 498 | string-width "^2.1.0" 499 | strip-ansi "^4.0.0" 500 | through "^2.3.6" 501 | 502 | is-fullwidth-code-point@^2.0.0: 503 | version "2.0.0" 504 | resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" 505 | integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= 506 | 507 | is-path-cwd@^1.0.0: 508 | version "1.0.0" 509 | resolved "https://registry.yarnpkg.com/is-path-cwd/-/is-path-cwd-1.0.0.tgz#d225ec23132e89edd38fda767472e62e65f1106d" 510 | integrity sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0= 511 | 512 | is-path-in-cwd@^1.0.0: 513 | version "1.0.0" 514 | resolved "https://registry.yarnpkg.com/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz#6477582b8214d602346094567003be8a9eac04dc" 515 | integrity sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw= 516 | dependencies: 517 | is-path-inside "^1.0.0" 518 | 519 | is-path-inside@^1.0.0: 520 | version "1.0.0" 521 | resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-1.0.0.tgz#fc06e5a1683fbda13de667aff717bbc10a48f37f" 522 | integrity sha1-/AbloWg/vaE95mev9xe7wQpI838= 523 | dependencies: 524 | path-is-inside "^1.0.1" 525 | 526 | is-promise@^2.1.0: 527 | version "2.1.0" 528 | resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" 529 | integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= 530 | 531 | is-resolvable@^1.1.0: 532 | version "1.1.0" 533 | resolved "https://registry.yarnpkg.com/is-resolvable/-/is-resolvable-1.1.0.tgz#fb18f87ce1feb925169c9a407c19318a3206ed88" 534 | integrity sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg== 535 | 536 | isexe@^2.0.0: 537 | version "2.0.0" 538 | resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" 539 | integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= 540 | 541 | js-tokens@^4.0.0: 542 | version "4.0.0" 543 | resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" 544 | integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 545 | 546 | js-yaml@^3.12.0: 547 | version "3.12.0" 548 | resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.12.0.tgz#eaed656ec8344f10f527c6bfa1b6e2244de167d1" 549 | integrity sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A== 550 | dependencies: 551 | argparse "^1.0.7" 552 | esprima "^4.0.0" 553 | 554 | json-schema-traverse@^0.4.1: 555 | version "0.4.1" 556 | resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" 557 | integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== 558 | 559 | json-stable-stringify-without-jsonify@^1.0.1: 560 | version "1.0.1" 561 | resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" 562 | integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= 563 | 564 | levn@^0.3.0, levn@~0.3.0: 565 | version "0.3.0" 566 | resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" 567 | integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= 568 | dependencies: 569 | prelude-ls "~1.1.2" 570 | type-check "~0.3.2" 571 | 572 | lodash@^4.17.10: 573 | version "4.17.11" 574 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" 575 | integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== 576 | 577 | lodash@^4.17.4, lodash@^4.17.5: 578 | version "4.17.10" 579 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" 580 | integrity sha512-UejweD1pDoXu+AD825lWwp4ZGtSwgnpZxb3JDViD7StjQz+Nb/6l093lx4OQ0foGWNRoc19mWy7BzL+UAK2iVg== 581 | 582 | mimic-fn@^1.0.0: 583 | version "1.2.0" 584 | resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" 585 | integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== 586 | 587 | minimatch@^3.0.2: 588 | version "3.0.3" 589 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" 590 | integrity sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q= 591 | dependencies: 592 | brace-expansion "^1.0.0" 593 | 594 | minimatch@^3.0.4: 595 | version "3.0.4" 596 | resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" 597 | integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== 598 | dependencies: 599 | brace-expansion "^1.1.7" 600 | 601 | minimist@0.0.8: 602 | version "0.0.8" 603 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 604 | integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= 605 | 606 | minimist@^1.2.0: 607 | version "1.2.0" 608 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" 609 | integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ= 610 | 611 | mkdirp@^0.5.1: 612 | version "0.5.1" 613 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 614 | integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= 615 | dependencies: 616 | minimist "0.0.8" 617 | 618 | ms@^2.1.1: 619 | version "2.1.1" 620 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a" 621 | integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg== 622 | 623 | mute-stream@0.0.7: 624 | version "0.0.7" 625 | resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" 626 | integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= 627 | 628 | natural-compare@^1.4.0: 629 | version "1.4.0" 630 | resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" 631 | integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= 632 | 633 | nice-try@^1.0.4: 634 | version "1.0.5" 635 | resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" 636 | integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== 637 | 638 | node-fetch@^2.2.0: 639 | version "2.2.0" 640 | resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.2.0.tgz#4ee79bde909262f9775f731e3656d0db55ced5b5" 641 | integrity sha512-OayFWziIxiHY8bCUyLX6sTpDH8Jsbp4FfYd1j1f7vZyfgkcOnAyM4oQR16f8a0s7Gl/viMGRey8eScYk4V4EZA== 642 | 643 | object-assign@^4.0.1: 644 | version "4.1.1" 645 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 646 | integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 647 | 648 | once@^1.3.0: 649 | version "1.4.0" 650 | resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" 651 | integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= 652 | dependencies: 653 | wrappy "1" 654 | 655 | onetime@^2.0.0: 656 | version "2.0.1" 657 | resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" 658 | integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= 659 | dependencies: 660 | mimic-fn "^1.0.0" 661 | 662 | optionator@^0.8.2: 663 | version "0.8.2" 664 | resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" 665 | integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= 666 | dependencies: 667 | deep-is "~0.1.3" 668 | fast-levenshtein "~2.0.4" 669 | levn "~0.3.0" 670 | prelude-ls "~1.1.2" 671 | type-check "~0.3.2" 672 | wordwrap "~1.0.0" 673 | 674 | os-tmpdir@~1.0.2: 675 | version "1.0.2" 676 | resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" 677 | integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= 678 | 679 | path-is-absolute@^1.0.0: 680 | version "1.0.1" 681 | resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" 682 | integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= 683 | 684 | path-is-inside@^1.0.1, path-is-inside@^1.0.2: 685 | version "1.0.2" 686 | resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" 687 | integrity sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM= 688 | 689 | path-key@^2.0.1: 690 | version "2.0.1" 691 | resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" 692 | integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= 693 | 694 | pify@^2.0.0: 695 | version "2.3.0" 696 | resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" 697 | integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= 698 | 699 | pinkie-promise@^2.0.0: 700 | version "2.0.1" 701 | resolved "https://registry.yarnpkg.com/pinkie-promise/-/pinkie-promise-2.0.1.tgz#2135d6dfa7a358c069ac9b178776288228450ffa" 702 | integrity sha1-ITXW36ejWMBprJsXh3YogihFD/o= 703 | dependencies: 704 | pinkie "^2.0.0" 705 | 706 | pinkie@^2.0.0: 707 | version "2.0.4" 708 | resolved "https://registry.yarnpkg.com/pinkie/-/pinkie-2.0.4.tgz#72556b80cfa0d48a974e80e77248e80ed4f7f870" 709 | integrity sha1-clVrgM+g1IqXToDnckjoDtT3+HA= 710 | 711 | pluralize@^7.0.0: 712 | version "7.0.0" 713 | resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777" 714 | integrity sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow== 715 | 716 | prelude-ls@~1.1.2: 717 | version "1.1.2" 718 | resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" 719 | integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= 720 | 721 | progress@^2.0.0: 722 | version "2.0.0" 723 | resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" 724 | integrity sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8= 725 | 726 | punycode@^2.1.0: 727 | version "2.1.1" 728 | resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" 729 | integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== 730 | 731 | regexpp@^2.0.0: 732 | version "2.0.0" 733 | resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.0.tgz#b2a7534a85ca1b033bcf5ce9ff8e56d4e0755365" 734 | integrity sha512-g2FAVtR8Uh8GO1Nv5wpxW7VFVwHcCEr4wyA8/MHiRkO8uHoR5ntAA8Uq3P1vvMTX/BeQiRVSpDGLd+Wn5HNOTA== 735 | 736 | require-uncached@^1.0.3: 737 | version "1.0.3" 738 | resolved "https://registry.yarnpkg.com/require-uncached/-/require-uncached-1.0.3.tgz#4e0d56d6c9662fd31e43011c4b95aa49955421d3" 739 | integrity sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM= 740 | dependencies: 741 | caller-path "^0.1.0" 742 | resolve-from "^1.0.0" 743 | 744 | resolve-from@^1.0.0: 745 | version "1.0.1" 746 | resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" 747 | integrity sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY= 748 | 749 | restore-cursor@^2.0.0: 750 | version "2.0.0" 751 | resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" 752 | integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= 753 | dependencies: 754 | onetime "^2.0.0" 755 | signal-exit "^3.0.2" 756 | 757 | rimraf@^2.2.8: 758 | version "2.6.1" 759 | resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" 760 | integrity sha1-wjOOxkPfeht/5cVPqG9XQopV8z0= 761 | dependencies: 762 | glob "^7.0.5" 763 | 764 | run-async@^2.2.0: 765 | version "2.3.0" 766 | resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" 767 | integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= 768 | dependencies: 769 | is-promise "^2.1.0" 770 | 771 | rxjs@^6.1.0: 772 | version "6.3.3" 773 | resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.3.3.tgz#3c6a7fa420e844a81390fb1158a9ec614f4bad55" 774 | integrity sha512-JTWmoY9tWCs7zvIk/CvRjhjGaOd+OVBM987mxFo+OW66cGpdKjZcpmc74ES1sB//7Kl/PAe8+wEakuhG4pcgOw== 775 | dependencies: 776 | tslib "^1.9.0" 777 | 778 | "safer-buffer@>= 2.1.2 < 3": 779 | version "2.1.2" 780 | resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" 781 | integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== 782 | 783 | semver@^5.5.0, semver@^5.5.1: 784 | version "5.5.1" 785 | resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" 786 | integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== 787 | 788 | shebang-command@^1.2.0: 789 | version "1.2.0" 790 | resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" 791 | integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= 792 | dependencies: 793 | shebang-regex "^1.0.0" 794 | 795 | shebang-regex@^1.0.0: 796 | version "1.0.0" 797 | resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" 798 | integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= 799 | 800 | signal-exit@^3.0.2: 801 | version "3.0.2" 802 | resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" 803 | integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= 804 | 805 | slice-ansi@1.0.0: 806 | version "1.0.0" 807 | resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-1.0.0.tgz#044f1a49d8842ff307aad6b505ed178bd950134d" 808 | integrity sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg== 809 | dependencies: 810 | is-fullwidth-code-point "^2.0.0" 811 | 812 | sprintf-js@~1.0.2: 813 | version "1.0.3" 814 | resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" 815 | integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= 816 | 817 | string-width@^2.1.0, string-width@^2.1.1: 818 | version "2.1.1" 819 | resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" 820 | integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== 821 | dependencies: 822 | is-fullwidth-code-point "^2.0.0" 823 | strip-ansi "^4.0.0" 824 | 825 | strip-ansi@^4.0.0: 826 | version "4.0.0" 827 | resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" 828 | integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= 829 | dependencies: 830 | ansi-regex "^3.0.0" 831 | 832 | strip-json-comments@^2.0.1: 833 | version "2.0.1" 834 | resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" 835 | integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= 836 | 837 | supports-color@^5.3.0: 838 | version "5.5.0" 839 | resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" 840 | integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== 841 | dependencies: 842 | has-flag "^3.0.0" 843 | 844 | table@^4.0.3: 845 | version "4.0.3" 846 | resolved "https://registry.yarnpkg.com/table/-/table-4.0.3.tgz#00b5e2b602f1794b9acaf9ca908a76386a7813bc" 847 | integrity sha512-S7rnFITmBH1EnyKcvxBh1LjYeQMmnZtCXSEbHcH6S0NoKit24ZuFO/T1vDcLdYsLQkM188PVVhQmzKIuThNkKg== 848 | dependencies: 849 | ajv "^6.0.1" 850 | ajv-keywords "^3.0.0" 851 | chalk "^2.1.0" 852 | lodash "^4.17.4" 853 | slice-ansi "1.0.0" 854 | string-width "^2.1.1" 855 | 856 | text-table@^0.2.0: 857 | version "0.2.0" 858 | resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" 859 | integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= 860 | 861 | through@^2.3.6: 862 | version "2.3.8" 863 | resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" 864 | integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= 865 | 866 | tmp@^0.0.33: 867 | version "0.0.33" 868 | resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" 869 | integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== 870 | dependencies: 871 | os-tmpdir "~1.0.2" 872 | 873 | tslib@^1.9.0: 874 | version "1.9.3" 875 | resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" 876 | integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== 877 | 878 | type-check@~0.3.2: 879 | version "0.3.2" 880 | resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" 881 | integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= 882 | dependencies: 883 | prelude-ls "~1.1.2" 884 | 885 | uri-js@^4.2.2: 886 | version "4.2.2" 887 | resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" 888 | integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== 889 | dependencies: 890 | punycode "^2.1.0" 891 | 892 | which@^1.2.9: 893 | version "1.3.1" 894 | resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" 895 | integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== 896 | dependencies: 897 | isexe "^2.0.0" 898 | 899 | wordwrap@~1.0.0: 900 | version "1.0.0" 901 | resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" 902 | integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= 903 | 904 | wrappy@1: 905 | version "1.0.2" 906 | resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" 907 | integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= 908 | 909 | write@^0.2.1: 910 | version "0.2.1" 911 | resolved "https://registry.yarnpkg.com/write/-/write-0.2.1.tgz#5fc03828e264cea3fe91455476f7a3c566cb0757" 912 | integrity sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c= 913 | dependencies: 914 | mkdirp "^0.5.1" 915 | --------------------------------------------------------------------------------