├── .gitignore ├── LICENSE ├── Procfile ├── README.md ├── game-server.js ├── package-lock.json ├── package.json ├── public ├── js │ ├── index.js │ ├── modules │ │ ├── renderUpdates.js │ │ └── utils.js │ └── player.js └── views │ ├── game-code-wrong.html │ ├── game-host.html │ ├── game-not-host.html │ ├── game-started.html │ ├── index.html │ └── style.css └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | *node_modules 2 | .env 3 | .vscode 4 | .eslintrc.json 5 | .prettierrc 6 | node_modules 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A scalable networking framework to build realtime multiplayer games with simultaneously running game rooms 2 | 3 | This starter kit allows you to add multiplayer functionality (that follows the [Client/Server strategy](https://www.gabrielgambetta.com/client-server-game-architecture.html)) to your game. It provides a communication framework so that your players can communicate with a central server, in realtime, for the entire duration of the gameplay. 4 | 5 | It also allows you to implement a 'game rooms' feature using Node JS worker threads, allowing you to spin up multiple instances of the game, each with a separate group of players. 6 | 7 | ![Game demo gif](https://user-images.githubusercontent.com/5900152/91305901-ac064900-e7a3-11ea-8fb9-b94ca7310777.gif) 8 | 9 | ## Games built using this framework 10 | 11 | 1. Multiplayer space invaders - [GitHub project](https://github.com/Srushtika/realtime-multiplayer-space-invaders) | [Tutorial](https://dev.to/ablydev/building-a-realtime-multiplayer-browser-game-in-less-than-a-day-part-1-4-14pm) | [Demo](https://space-invaders-multiplayer.herokuapp.com/) 12 | 13 | 2. Multiplayer Flappy birds - [GitHub Project](https://github.com/Srushtika/multiplayer-flappy-bird) | [Video tutorial](https://www.youtube.com/watch?v=ReGHyTh1ydU) 14 | 15 | (...make a PR to add yours!) 16 | 17 | ## The tech stack 18 | 19 | ### On the server side 20 | 21 | - [Node JS](https://nodejs.org/en/) 22 | - [Express](https://expressjs.com/) 23 | 24 | ### On the client side 25 | 26 | - [Vanilla JS](https://developer.mozilla.org/en-US/docs/Web/JavaScript) 27 | 28 | ### Libraries used 29 | 30 | - [Ably Realtime](https://www.ably.io) 31 | - [Bootstrap](https://getbootstrap.com/) 32 | - [DotEnv](https://www.npmjs.com/package/dotenv) 33 | 34 | ## How to run this kit locally 35 | 36 | 1. Clone this repo 37 | 38 | ```sh 39 | git clone https://github.com/Srushtika/multiplayer-scalable-game-networking-starter-kit.git 40 | ``` 41 | 42 | 2. Change directory into the project folder 43 | 44 | ```sh 45 | cd multiplayer-scalable-game-networking-starter-kit.git 46 | ``` 47 | 48 | 3. Install the dependencies 49 | 50 | ```sh 51 | npm install 52 | ``` 53 | 54 | 4. Create a free account with [Ably Realtime](https://www.ably.io/) to get your Ably API KEY. Add a new file in called `.env` and add the following. (Remember to replace the placeholder with your own API Key. You can get your Ably API key from your Ably dashboard): 55 | 56 | ``` 57 | ABLY_API_KEY= 58 | PORT=5000 59 | ``` 60 | 61 | 5. Run the server 62 | 63 | ```sh 64 | node server.js 65 | ``` 66 | 67 | 6. To see the realtime communication in action, open the app in two separate browser windows side-by-side: [https://localhost:5000](https://localhost:5000) 68 | 69 | 7. Create a game room in one window and join the room as a 'non-host' player by using the unique code in the other window. 70 | 71 | 8. Start the game when you are ready and use the arrow keys to publish dummy player input. You will see the change happening at the same time in both the windows. 72 | 73 | Voila! Your multiplayer game networking framework is up and running, now replace the game logic with yours and make it your own. 74 | 75 | Feel free to share your multiplayer game with me on [Twitter](https://twitter.com/Srushtika), I'll be happy to give it a shoutout! 76 | 77 | --- 78 | 79 | ## What's in which file? 80 | 81 | ### On the server 82 | 83 | #### 1. `server.js` 84 | 85 | This file has the main server thread. It performs three functions: 86 | 87 | - Serve the HTML and JS for front-end clients using Express. 88 | - Authenticate front-end clients with the Ably Realtime service using [Token Auth strategy](https://www.ably.io/documentation/core-features/authentication#token-authentication). 89 | - Create and manage Node JS worker threads when a host player requests to create a new game room. 90 | 91 | #### 2. `game-server.js` 92 | 93 | This file represents a Node JS worker thread and a new instance of this file will run for every game room created. When the game is finished, the worker thread is killed. When a new player joins or leaves the worker thread communicates with the parent thread (main thread aka server.js) and lets it know the number of players (among other things). 94 | 95 | ### On the client 96 | 97 | #### 1. `index.html` and `index.js` 98 | 99 | These files represent the home page where a player can choose to host a new game or join a game using the room's unique code. 100 | 101 | Homepage 102 | 103 | #### 2. `game-host.html` and `game-not-host.html` 104 | 105 | As the names suggest, these are the HTML files for the host player and non-host player respectively. Being the host of the game allows for additional controls like starting or stopping the game, hence the different views. 106 | 107 | a. Staging area for host player 108 | 109 | Host staging 110 | 111 | b. Game area for host player 112 | 113 | Host game 114 | 115 | c. Staging area for non-host player 116 | 117 | Non host staging 118 | 119 | d. Game area for non-host player 120 | 121 | Non host game 122 | 123 | #### 3. `game-started.html` and `game-code-wrong.html` 124 | 125 | Again as the names suggest, these pages are shown when a non-host player tries to join a game using a unique code, either when the game has already started or that game code doesn't exist, respectively. 126 | 127 | Game started 128 | 129 | Game room doesn't exist 130 | 131 | #### 4. `player.js` 132 | 133 | This file contains the main front-end logic for both host and non-host type players. It uses ES6 modules to import two other files: 134 | 135 | ##### 4.a. `modules/renderUpdates.js` 136 | 137 | This file contains all the methods that render the game UI on the webpage as per the latest realtime updates. 138 | 139 | ##### 4.b. `modules/utils.js` 140 | 141 | This file contains all the utility methods used in the game. 142 | 143 | #### 5. `style.css` 144 | 145 | This file has all the styles that are applied on top of Bootstrap styles. 146 | 147 | --- 148 | 149 | ## Core concepts 150 | 151 | ### The communication architecture 152 | 153 | Before we look at the architecture and design of the app, we need to understand a few concepts based on which this app is built 154 | 155 | #### Client-Server game strategy with an authoritative server 156 | 157 | The [Client-Server game building strategy](https://www.gabrielgambetta.com/client-server-game-architecture.html) allows for high scalability when compared to the [Peer-to-Peer strategy](https://web.archive.org/web/20190407004521/https://gafferongames.com/post/what_every_programmer_needs_to_know_about_game_networking/). In this design, the game server can be considered as the single source of truth and is responsible to maintain the latest game state at all times. All the players send their state to the game server, which in turn collates them together and sends it back at the same time to all the players. 158 | 159 | The client-side script will use this state received from the game server to render the game objects on players' screens accordingly, ensuring all the players are fully in-sync. 160 | 161 | ![Client/Server game strategy](https://user-images.githubusercontent.com/5900152/90788135-61955000-e2fd-11ea-9448-0e02ab45030a.png) 162 | 163 | #### The WebSockets protocol 164 | 165 | The [WebSockets protocol](https://www.ably.io/concepts/websockets), unlike HTTP, is a stateful communications protocol that works over TCP. The communication initially starts off as an HTTP handshake, but if both the communicating parties agree to continue over WebSockets then the connection is elevated; giving rise to a full-duplex, persistent connection. This means the connection remains open for the duration that the application is in use. This gives the server a way to initiate any communication and send data to pre-subscribed clients, so they don’t have to keep sending requests inquiring about the availability of new data. Which is exactly what we need in our game! 166 | 167 | This project uses [Ably Realtime](https://www.ably.io) to implement WebSocket based realtime messaging between the game server and the players. Ably, by default, deals with scalability, protocol interoperability, reliable message ordering, guaranteed message delivery historical message retention and authentication, so we don't have to. This communication follows the [Pub/Sub messaging pattern](https://www.ably.io/concepts/pub-sub). 168 | 169 | ##### Publish/Subscribe messaging pattern 170 | 171 | [Pub/Sub](https://www.ably.io/concepts/pub-sub) messaging allows various front-end or back-end clients to publish some data and/or subscribe to some data. For any active subscriptions, these clients will receive asynchronous event callbacks when a new message is published. 172 | 173 | ##### Channels 174 | 175 | In any realtime app, there's a lot of moving data involved. [Channels](https://www.ably.io/documentation/core-features/channels) help us group this data logically and let us implement subscriptions per channel. This allowing us to implement the custom callback logic for different scenarios. In the diagram above, each color would represent a channel. 176 | 177 | ##### Presence 178 | 179 | [Presence](https://www.ably.io/documentation/core-features/presence) is an Ably feature using which you can track the connection status of various clients on a channel. In essence, you can see who has just come online and who has left using each client's unique `clientId` 180 | 181 | #### Sequence of events with Node JS worker threads (For an example game) 182 | 183 | ![Sequence of events](https://user-images.githubusercontent.com/5900152/90802008-8cd46b00-e30e-11ea-8c37-5c99a73c8edd.png) 184 | 185 | --- 186 | 187 | ## Documentation 188 | 189 | ### Creating Node JS worker threads 190 | 191 | This kit uses Node JS worker threads to create new game rooms so various groups of people can simultaneously play the game. 192 | 193 | To create and use Node JS worker threads, from the main thread, you'll need to require the `worker_threads` library: 194 | 195 | ```js 196 | const { 197 | Worker, 198 | isMainThread, 199 | parentPort, 200 | workerData, 201 | threadId, 202 | MessageChannel, 203 | } = require('worker_threads'); 204 | ``` 205 | 206 | and instance a new worker and pass it two parameters: 207 | 208 | a) path to the worker file 209 | 210 | b) data as a JSON object 211 | 212 | ```js 213 | const worker = new Worker('./game-server.js', { 214 | workerData: { 215 | hostNickname: hostNickname, 216 | hostRoomCode: hostRoomCode, 217 | hostClientId: hostClientId, 218 | }, 219 | }); 220 | ``` 221 | 222 | In the worker file, you'll need to require the same library `worker_threads`. You'll have access to the `workerData` object directly. For example, in the worker thread, you can access the host nickname with `workerData.hostNickname` 223 | 224 | ##### Communication between worker and main threads 225 | 226 | The worker thread can publish data to the main thread as follows: 227 | 228 | ```js 229 | parentPort.postMessage({ 230 | roomCode: roomCode, 231 | totalPlayers: totalPlayers, 232 | isGameOn: isGameOn, 233 | isGameOver: isGameOver, 234 | }); 235 | ``` 236 | 237 | In this kit, the worker thread communicates with the main thread on three occasions: 238 | 239 | 1. When a new player joins the game room. 240 | 2. When an existing player leaves the game room. 241 | 3. When the game is over and the worker thread is going to be killed. 242 | 243 | This information is used by the main server thread to maintain a list of active worker threads, along with the number of players in each. 244 | 245 | #### Connecting to Ably 246 | 247 | In order to use this kit, you will need an Ably API key. If you are not already signed up, you can [sign up now for a free Ably account](https://www.ably.io/signup). Once you have an Ably account: 248 | 249 | - Log into your app dashboard 250 | - Under **Your apps**, click on **Manage app** for any app you wish to use for this tutorial, or create a new one with the **Create New App** button 251 | - Click on the **API Keys** tab 252 | - Copy the secret **API Key** value from your Root key. 253 | 254 | The server-side scripts connect to Ably using [Basic Authentication](https://www.ably.io/documentation/core-features/authentication#basic-authentication), i.e. by using the API Key directly as shown below: 255 | 256 | ```js 257 | const realtime = Ably.Realtime({ 258 | key: ABLY_API_KEY, 259 | echoMessages: false, 260 | }); 261 | ``` 262 | 263 | Note: Setting the `echoMessages` false prevents the server from receiving its own messages. 264 | 265 | The main server thread uses Express to listen to HTTP requests. It has an `/auth` endpoint that is used by the client-side scripts to authenticate with Ably using tokens. This is a recommended strategy as placing your secret API Key in a front-end script exposes it to potential misuse. The client-side scripts connect to Ably using [Token Authentication](https://www.ably.io/documentation/core-features/authentication#token-authentication) as shown below: 266 | 267 | ```js 268 | const realtime = new Ably.Realtime({ 269 | authUrl: '/auth', 270 | }); 271 | ``` 272 | 273 | #### Ably channel names used by this project 274 | 275 | 1. `main-game-thread` - Used by the main server thread to listen for host player entries and leaves via presence, to be able to create new Node JS worker threads for new game rooms. 276 | 277 | 2. `:primary` - Main game state channel for a particular game room. It'll be used by players to enter or leave presence on the game room and by the worker thread to stream game state updates. 278 | 279 | 3. `:player-ch-` - Unique channel for every player, which is used to publish their state (or input) to the worker thread for that unique game room. The worker thread is subscribed to one such channel per player. 280 | 281 | You can also add any other channels that you may need in your game. 282 | 283 | Note: Due to the fact that the above channel names exist in a unique channel namespace identified by the unique room code (separated from channel names with a :), you can guarantee that one game room's data never creeps into the other. 284 | 285 | --- 286 | 287 | ## Data structures 288 | 289 | The game's worker thread (server) stores all the players in an associative array as a key value pair with the following structure: 290 | 291 | ```js 292 | globalPlayersState[ 293 | "" : { 294 | id: "", 295 | nickname: player.data.nickname, 296 | isAlive: true, 297 | isConnected: true, 298 | score: 0, 299 | left: '', 300 | top: '', 301 | color: '', 302 | }, 303 | "" : { 304 | id: "", 305 | nickname: player.data.nickname, 306 | isAlive: false, 307 | isConnected: true, 308 | score: 0, 309 | left: '', 310 | top: '', 311 | color: '', 312 | }, 313 | ] 314 | ``` 315 | 316 | The game's worker thread (server) also keeps a track of all the channels meant for each player's input, so it can attach and detach and from them as needed. This is also stored using an associative array with the following structure: 317 | 318 | ```js 319 | playerChannels[ 320 | '': {''}, 321 | '': {''} 322 | ] 323 | ``` 324 | 325 | The global game state of all the players is published by the game's worker thread (server) at a high frequency to all the players. The payload of this game state has the following structure: 326 | 327 | ```js 328 | gameRoomChannel.publish('game-state', { 329 | globalPlayersState: globalPlayersState, 330 | totalPlayers: totalPlayers, 331 | isGameOn: isGameOn, 332 | isGameOver: isGameOver, 333 | }); 334 | ``` 335 | 336 | The client-side script also stores the game state of each player locally and updates it as per the latest data from the server. This has exactly the same structure as in the server: 337 | 338 | ```js 339 | localGameState[ 340 | "" : { 341 | id: "", 342 | nickname: player.data.nickname, 343 | isAlive: true, 344 | isConnected: true, 345 | score: 0, 346 | left: '', 347 | top: '', 348 | color: '', 349 | }, 350 | "" : { 351 | id: "", 352 | nickname: player.data.nickname, 353 | isAlive: false, 354 | isConnected: true, 355 | score: 0, 356 | left: '', 357 | top: '', 358 | color: '', 359 | }, 360 | ] 361 | ``` 362 | 363 | In the example game, HTML divs represent player bodies, so that is stored in another associative array, the attributes of which are updated as per the latest data in the `localGameState`: 364 | 365 | ```js 366 | playerDivs[ 367 | "" : {''}, 368 | "" : {''} 369 | ] 370 | ``` 371 | 372 | --- 373 | 374 | ## Load tests and limits 375 | 376 | - All of Ably's messaging limits, broken down by package can be found in a [support article](https://support.ably.com/support/solutions/articles/3000053845-do-you-have-any-connection-message-rate-or-other-limits-on-accounts-). 377 | 378 | - We are currently performing load and performance tests on this framework and will update this guide with that info when it's available. If this is important to you, please [leave a message to me directly on Twitter](https://www.twitter.com/Srushtika) 379 | -------------------------------------------------------------------------------- /game-server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-jsdoc */ 2 | /** 3 | * This is the game server for 4 | * a single game room 5 | */ 6 | const { parentPort, workerData } = require('worker_threads'); 7 | // eslint-disable-next-line no-unused-vars 8 | const envConfig = require('dotenv').config(); 9 | const Ably = require('ably'); 10 | 11 | const ABLY_API_KEY = process.env.ABLY_API_KEY; 12 | const GAME_STATE_PUBLISH_FREQ_MS = 100; 13 | const PLAYER_GAME_AREA_WIDTH = 790; 14 | const PLAYER_GAME_AREA_HEIGHT = 390; 15 | const globalPlayersState = {}; 16 | const playerChannels = {}; 17 | let isGameOn = false; 18 | let isGameOver = false; 19 | let totalPlayers = 0; 20 | const gameRoomChName = workerData.hostRoomCode + ':primary'; 21 | const roomCode = workerData.hostRoomCode; 22 | const hostClientId = workerData.hostClientId; 23 | let gameRoomChannel; 24 | let gameTickerOn = false; 25 | 26 | // instantiate Ably 27 | // eslint-disable-next-line new-cap 28 | const realtime = Ably.Realtime({ 29 | key: ABLY_API_KEY, 30 | echoMessages: false 31 | }); 32 | 33 | // wait until connection with Ably is established 34 | realtime.connection.once('connected', () => { 35 | // create the channel object 36 | gameRoomChannel = realtime.channels.get(gameRoomChName); 37 | 38 | // subscribe to new players entering the game via 'presence' 39 | gameRoomChannel.presence.subscribe('enter', (player) => { 40 | const newPlayerId = player.clientId; 41 | totalPlayers++; 42 | /** 43 | * let the main thread know the 44 | * updated number of players 45 | */ 46 | parentPort.postMessage({ 47 | roomCode: roomCode, 48 | totalPlayers: totalPlayers, 49 | isGameOn: isGameOn, 50 | isGameOver: isGameOver 51 | }); 52 | 53 | // start publishing game updates as soon as the 54 | // first player joins 55 | if (totalPlayers === 1) { 56 | gameTickerOn = true; 57 | startGameDataTicker(); 58 | } 59 | 60 | // create an initial object for this 61 | // newly joined player 62 | newPlayerState = { 63 | id: newPlayerId, 64 | nickname: player.data.nickname, 65 | isAlive: true, 66 | isConnected: true, 67 | score: 0, 68 | left: Math.round(Math.random() * PLAYER_GAME_AREA_WIDTH + 1), 69 | top: Math.round(Math.random() * PLAYER_GAME_AREA_HEIGHT + 1), 70 | color: randomColorGenerator() 71 | }; 72 | // add the new player's state object to the 73 | // global players state associative array 74 | globalPlayersState[newPlayerId] = newPlayerState; 75 | 76 | // get the unique channel for this player 77 | playerChannels[newPlayerId] = realtime.channels.get( 78 | roomCode + ':player-ch-' + player.clientId 79 | ); 80 | // subscribe to this player's state on their unique channel 81 | subscribeToPlayerState(playerChannels[newPlayerId], newPlayerId); 82 | }); 83 | 84 | // subscribe to players leaving the game via presence 85 | gameRoomChannel.presence.subscribe('leave', (player) => { 86 | const leavingPlayerId = player.clientId; 87 | totalPlayers--; 88 | globalPlayersState[leavingPlayerId].isConnected = false; 89 | /** 90 | * optionally let the main thread know the 91 | * number of players 92 | */ 93 | parentPort.postMessage({ 94 | roomCode: roomCode, 95 | totalPlayers: totalPlayers 96 | }); 97 | 98 | /** 99 | * delete this players entry from the 100 | * associative array after waiting a bit 101 | * to make sure the players have received at least one 102 | * server tick update declaring this player disconnected 103 | */ 104 | setTimeout(() => { 105 | delete globalPlayersState[leavingPlayerId]; 106 | }, GAME_STATE_PUBLISH_FREQ_MS * 2); 107 | 108 | /** 109 | * if it's the last player to leave, kill the woker thread 110 | * aka the game server for this game room 111 | */ 112 | if (totalPlayers <= 0) { 113 | killWorkerThread(); 114 | } 115 | }); 116 | 117 | /** 118 | * let the host player know that the game room 119 | * is ready, so they can enter via presence 120 | * and let other players do so too 121 | * */ 122 | gameRoomChannel.publish('thread-ready', { 123 | start: true 124 | }); 125 | }); 126 | 127 | /** 128 | * method to start the server game ticker 129 | * to continuously stream the global state 130 | * to all the players connected 131 | * */ 132 | function startGameDataTicker() { 133 | const tickInterval = setInterval(() => { 134 | if (!gameTickerOn) { 135 | clearInterval(tickInterval); 136 | } else { 137 | // publish the latest game state 138 | gameRoomChannel.publish('game-state', { 139 | globalPlayersState: globalPlayersState, 140 | totalPlayers: totalPlayers, 141 | isGameOn: isGameOn, 142 | isGameOver: isGameOver 143 | }); 144 | } 145 | }, GAME_STATE_PUBLISH_FREQ_MS); 146 | } 147 | 148 | // subscribe to player channel to get their latest state 149 | function subscribeToPlayerState(playerChannel, playerId) { 150 | // subscribe to the player state event where 151 | // the players will send their state updates 152 | playerChannel.subscribe('player-state', (msg) => { 153 | const { left, top } = msg.data; 154 | const state = globalPlayersState[playerId]; 155 | globalPlayersState[playerId] = { ...state, left, top }; 156 | }); 157 | 158 | // subscribe to the player death event 159 | playerChannel.subscribe('player-dead', (msg) => { 160 | globalPlayersState[msg.data.deadPlayerId].isAlive = false; 161 | }); 162 | // subscribe to events that only the game host can publish 163 | if (playerId === hostClientId) { 164 | playerChannel.subscribe('start-game', (msg) => { 165 | if (!isGameOn) { 166 | isGameOn = true; 167 | parentPort.postMessage({ 168 | roomCode: roomCode, 169 | totalPlayers: totalPlayers, 170 | isGameOn: isGameOn, 171 | isGameOver: isGameOver 172 | }); 173 | } 174 | }); 175 | playerChannel.subscribe('end-game', (msg) => { 176 | if (isGameOn) { 177 | isGameOn = false; 178 | isGameOver = true; 179 | } 180 | }); 181 | } 182 | } 183 | 184 | // kill the worker thread 185 | function killWorkerThread() { 186 | // detach from all the channels to immediately 187 | // bring the active channel count down 188 | for (const item in playerChannels) { 189 | if ({}.hasOwnProperty.call(playerChannels, item)) { 190 | playerChannels[item].detach(); 191 | } 192 | } 193 | gameRoomChannel.detach(); 194 | // publish a message to the main thread befoe this thread exits 195 | parentPort.postMessage({ 196 | killWorker: true, 197 | roomCode: roomCode, 198 | totalPlayers: totalPlayers 199 | }); 200 | process.exit(0); 201 | } 202 | 203 | // method to randomly generate a color for the player 204 | function randomColorGenerator() { 205 | return '#' + Math.random().toString(16).slice(-6); 206 | } 207 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multiplayer-web-game-ably-starter-kit", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@ably/msgpack-js": { 8 | "version": "0.3.3", 9 | "resolved": "https://registry.npmjs.org/@ably/msgpack-js/-/msgpack-js-0.3.3.tgz", 10 | "integrity": "sha512-H7oWg97VyA1JhWUP7YN7zwp9W1ozCqMSsqCcXNz4XLmZNdJKT2ntF/6DPgbviFgUpShjQlbPC/iamisTjwLHdQ==", 11 | "requires": { 12 | "bops": "~0.0.6" 13 | } 14 | }, 15 | "@babel/code-frame": { 16 | "version": "7.10.4", 17 | "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", 18 | "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", 19 | "dev": true, 20 | "requires": { 21 | "@babel/highlight": "^7.10.4" 22 | } 23 | }, 24 | "@babel/generator": { 25 | "version": "7.11.5", 26 | "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.11.5.tgz", 27 | "integrity": "sha512-9UqHWJ4IwRTy4l0o8gq2ef8ws8UPzvtMkVKjTLAiRmza9p9V6Z+OfuNd9fB1j5Q67F+dVJtPC2sZXI8NM9br4g==", 28 | "dev": true, 29 | "requires": { 30 | "@babel/types": "^7.11.5", 31 | "jsesc": "^2.5.1", 32 | "source-map": "^0.6.1" 33 | } 34 | }, 35 | "@babel/helper-function-name": { 36 | "version": "7.10.4", 37 | "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.10.4.tgz", 38 | "integrity": "sha512-YdaSyz1n8gY44EmN7x44zBn9zQ1Ry2Y+3GTA+3vH6Mizke1Vw0aWDM66FOYEPw8//qKkmqOckrGgTYa+6sceqQ==", 39 | "dev": true, 40 | "requires": { 41 | "@babel/helper-get-function-arity": "^7.10.4", 42 | "@babel/template": "^7.10.4", 43 | "@babel/types": "^7.10.4" 44 | } 45 | }, 46 | "@babel/helper-get-function-arity": { 47 | "version": "7.10.4", 48 | "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz", 49 | "integrity": "sha512-EkN3YDB+SRDgiIUnNgcmiD361ti+AVbL3f3Henf6dqqUyr5dMsorno0lJWJuLhDhkI5sYEpgj6y9kB8AOU1I2A==", 50 | "dev": true, 51 | "requires": { 52 | "@babel/types": "^7.10.4" 53 | } 54 | }, 55 | "@babel/helper-split-export-declaration": { 56 | "version": "7.11.0", 57 | "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.11.0.tgz", 58 | "integrity": "sha512-74Vejvp6mHkGE+m+k5vHY93FX2cAtrw1zXrZXRlG4l410Nm9PxfEiVTn1PjDPV5SnmieiueY4AFg2xqhNFuuZg==", 59 | "dev": true, 60 | "requires": { 61 | "@babel/types": "^7.11.0" 62 | } 63 | }, 64 | "@babel/helper-validator-identifier": { 65 | "version": "7.10.4", 66 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz", 67 | "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", 68 | "dev": true 69 | }, 70 | "@babel/highlight": { 71 | "version": "7.10.4", 72 | "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.10.4.tgz", 73 | "integrity": "sha512-i6rgnR/YgPEQzZZnbTHHuZdlE8qyoBNalD6F+q4vAFlcMEcqmkoG+mPqJYJCo63qPf74+Y1UZsl3l6f7/RIkmA==", 74 | "dev": true, 75 | "requires": { 76 | "@babel/helper-validator-identifier": "^7.10.4", 77 | "chalk": "^2.0.0", 78 | "js-tokens": "^4.0.0" 79 | }, 80 | "dependencies": { 81 | "chalk": { 82 | "version": "2.4.2", 83 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 84 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 85 | "dev": true, 86 | "requires": { 87 | "ansi-styles": "^3.2.1", 88 | "escape-string-regexp": "^1.0.5", 89 | "supports-color": "^5.3.0" 90 | } 91 | } 92 | } 93 | }, 94 | "@babel/parser": { 95 | "version": "7.11.5", 96 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.11.5.tgz", 97 | "integrity": "sha512-X9rD8qqm695vgmeaQ4fvz/o3+Wk4ZzQvSHkDBgpYKxpD4qTAUm88ZKtHkVqIOsYFFbIQ6wQYhC6q7pjqVK0E0Q==", 98 | "dev": true 99 | }, 100 | "@babel/template": { 101 | "version": "7.10.4", 102 | "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.10.4.tgz", 103 | "integrity": "sha512-ZCjD27cGJFUB6nmCB1Enki3r+L5kJveX9pq1SvAUKoICy6CZ9yD8xO086YXdYhvNjBdnekm4ZnaP5yC8Cs/1tA==", 104 | "dev": true, 105 | "requires": { 106 | "@babel/code-frame": "^7.10.4", 107 | "@babel/parser": "^7.10.4", 108 | "@babel/types": "^7.10.4" 109 | } 110 | }, 111 | "@babel/traverse": { 112 | "version": "7.11.5", 113 | "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.11.5.tgz", 114 | "integrity": "sha512-EjiPXt+r7LiCZXEfRpSJd+jUMnBd4/9OUv7Nx3+0u9+eimMwJmG0Q98lw4/289JCoxSE8OolDMNZaaF/JZ69WQ==", 115 | "dev": true, 116 | "requires": { 117 | "@babel/code-frame": "^7.10.4", 118 | "@babel/generator": "^7.11.5", 119 | "@babel/helper-function-name": "^7.10.4", 120 | "@babel/helper-split-export-declaration": "^7.11.0", 121 | "@babel/parser": "^7.11.5", 122 | "@babel/types": "^7.11.5", 123 | "debug": "^4.1.0", 124 | "globals": "^11.1.0", 125 | "lodash": "^4.17.19" 126 | }, 127 | "dependencies": { 128 | "debug": { 129 | "version": "4.1.1", 130 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 131 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 132 | "dev": true, 133 | "requires": { 134 | "ms": "^2.1.1" 135 | } 136 | }, 137 | "globals": { 138 | "version": "11.12.0", 139 | "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", 140 | "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", 141 | "dev": true 142 | }, 143 | "ms": { 144 | "version": "2.1.2", 145 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 146 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 147 | "dev": true 148 | } 149 | } 150 | }, 151 | "@babel/types": { 152 | "version": "7.11.5", 153 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.11.5.tgz", 154 | "integrity": "sha512-bvM7Qz6eKnJVFIn+1LPtjlBFPVN5jNDc1XmN15vWe7Q3DPBufWWsLiIvUu7xW87uTG6QoggpIDnUgLQvPheU+Q==", 155 | "dev": true, 156 | "requires": { 157 | "@babel/helper-validator-identifier": "^7.10.4", 158 | "lodash": "^4.17.19", 159 | "to-fast-properties": "^2.0.0" 160 | } 161 | }, 162 | "@eslint/eslintrc": { 163 | "version": "0.1.0", 164 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.1.0.tgz", 165 | "integrity": "sha512-bfL5365QSCmH6cPeFT7Ywclj8C7LiF7sO6mUGzZhtAMV7iID1Euq6740u/SRi4C80NOnVz/CEfK8/HO+nCAPJg==", 166 | "dev": true, 167 | "requires": { 168 | "ajv": "^6.12.4", 169 | "debug": "^4.1.1", 170 | "import-fresh": "^3.2.1", 171 | "strip-json-comments": "^3.1.1" 172 | }, 173 | "dependencies": { 174 | "ajv": { 175 | "version": "6.12.4", 176 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.4.tgz", 177 | "integrity": "sha512-eienB2c9qVQs2KWexhkrdMLVDoIQCz5KSeLxwg9Lzk4DOfBtIK9PQwwufcsn1jjGuf9WZmqPMbGxOzfcuphJCQ==", 178 | "dev": true, 179 | "requires": { 180 | "fast-deep-equal": "^3.1.1", 181 | "fast-json-stable-stringify": "^2.0.0", 182 | "json-schema-traverse": "^0.4.1", 183 | "uri-js": "^4.2.2" 184 | } 185 | }, 186 | "debug": { 187 | "version": "4.1.1", 188 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 189 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 190 | "dev": true, 191 | "requires": { 192 | "ms": "^2.1.1" 193 | } 194 | }, 195 | "ms": { 196 | "version": "2.1.2", 197 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 198 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 199 | "dev": true 200 | } 201 | } 202 | }, 203 | "@types/color-name": { 204 | "version": "1.1.1", 205 | "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", 206 | "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", 207 | "dev": true 208 | }, 209 | "@types/json5": { 210 | "version": "0.0.29", 211 | "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", 212 | "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", 213 | "dev": true 214 | }, 215 | "ably": { 216 | "version": "1.2.1", 217 | "resolved": "https://registry.npmjs.org/ably/-/ably-1.2.1.tgz", 218 | "integrity": "sha512-2rTKOgOHoMAgnfLUJWDfNvR+F5gnWmH5QLe8fUo9aWn4DRTduIbjofmp3kGt9bjGKpp1MS9dLfOA3cRF/HgmRg==", 219 | "requires": { 220 | "@ably/msgpack-js": "^0.3.3", 221 | "request": "^2.87.0", 222 | "ws": "^5.1" 223 | } 224 | }, 225 | "accepts": { 226 | "version": "1.3.7", 227 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", 228 | "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", 229 | "requires": { 230 | "mime-types": "~2.1.24", 231 | "negotiator": "0.6.2" 232 | } 233 | }, 234 | "acorn": { 235 | "version": "7.4.0", 236 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", 237 | "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", 238 | "dev": true 239 | }, 240 | "acorn-jsx": { 241 | "version": "5.2.0", 242 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.2.0.tgz", 243 | "integrity": "sha512-HiUX/+K2YpkpJ+SzBffkM/AQ2YE03S0U1kjTLVpoJdhZMOWy8qvXVN9JdLqv2QsaQ6MPYQIuNmwD8zOiYUofLQ==", 244 | "dev": true 245 | }, 246 | "ajv": { 247 | "version": "6.12.3", 248 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.3.tgz", 249 | "integrity": "sha512-4K0cK3L1hsqk9xIb2z9vs/XU+PGJZ9PNpJRDS9YLzmNdX6jmVPfamLvTJr0aDAusnHyCHO6MjzlkAsgtqp9teA==", 250 | "requires": { 251 | "fast-deep-equal": "^3.1.1", 252 | "fast-json-stable-stringify": "^2.0.0", 253 | "json-schema-traverse": "^0.4.1", 254 | "uri-js": "^4.2.2" 255 | } 256 | }, 257 | "ansi-colors": { 258 | "version": "4.1.1", 259 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 260 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 261 | "dev": true 262 | }, 263 | "ansi-escapes": { 264 | "version": "4.3.1", 265 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", 266 | "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", 267 | "dev": true, 268 | "requires": { 269 | "type-fest": "^0.11.0" 270 | }, 271 | "dependencies": { 272 | "type-fest": { 273 | "version": "0.11.0", 274 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", 275 | "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", 276 | "dev": true 277 | } 278 | } 279 | }, 280 | "ansi-regex": { 281 | "version": "5.0.0", 282 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", 283 | "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", 284 | "dev": true 285 | }, 286 | "ansi-styles": { 287 | "version": "3.2.1", 288 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", 289 | "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", 290 | "dev": true, 291 | "requires": { 292 | "color-convert": "^1.9.0" 293 | } 294 | }, 295 | "argparse": { 296 | "version": "1.0.10", 297 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", 298 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", 299 | "dev": true, 300 | "requires": { 301 | "sprintf-js": "~1.0.2" 302 | } 303 | }, 304 | "array-flatten": { 305 | "version": "1.1.1", 306 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", 307 | "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=" 308 | }, 309 | "array-includes": { 310 | "version": "3.1.1", 311 | "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.1.tgz", 312 | "integrity": "sha512-c2VXaCHl7zPsvpkFsw4nxvFie4fh1ur9bpcgsVkIjqn0H/Xwdg+7fv3n2r/isyS8EBj5b06M9kHyZuIr4El6WQ==", 313 | "dev": true, 314 | "requires": { 315 | "define-properties": "^1.1.3", 316 | "es-abstract": "^1.17.0", 317 | "is-string": "^1.0.5" 318 | } 319 | }, 320 | "array.prototype.flat": { 321 | "version": "1.2.3", 322 | "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz", 323 | "integrity": "sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ==", 324 | "dev": true, 325 | "requires": { 326 | "define-properties": "^1.1.3", 327 | "es-abstract": "^1.17.0-next.1" 328 | } 329 | }, 330 | "asn1": { 331 | "version": "0.2.4", 332 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", 333 | "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", 334 | "requires": { 335 | "safer-buffer": "~2.1.0" 336 | } 337 | }, 338 | "assert-plus": { 339 | "version": "1.0.0", 340 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 341 | "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" 342 | }, 343 | "astral-regex": { 344 | "version": "1.0.0", 345 | "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", 346 | "integrity": "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==", 347 | "dev": true 348 | }, 349 | "async-limiter": { 350 | "version": "1.0.1", 351 | "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", 352 | "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" 353 | }, 354 | "asynckit": { 355 | "version": "0.4.0", 356 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 357 | "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" 358 | }, 359 | "aws-sign2": { 360 | "version": "0.7.0", 361 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 362 | "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=" 363 | }, 364 | "aws4": { 365 | "version": "1.10.0", 366 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz", 367 | "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==" 368 | }, 369 | "babel-eslint": { 370 | "version": "10.1.0", 371 | "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", 372 | "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", 373 | "dev": true, 374 | "requires": { 375 | "@babel/code-frame": "^7.0.0", 376 | "@babel/parser": "^7.7.0", 377 | "@babel/traverse": "^7.7.0", 378 | "@babel/types": "^7.7.0", 379 | "eslint-visitor-keys": "^1.0.0", 380 | "resolve": "^1.12.0" 381 | } 382 | }, 383 | "balanced-match": { 384 | "version": "1.0.0", 385 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 386 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 387 | "dev": true 388 | }, 389 | "base64-js": { 390 | "version": "0.0.2", 391 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.2.tgz", 392 | "integrity": "sha1-Ak8Pcq+iW3X5wO5zzU9V7Bvtl4Q=" 393 | }, 394 | "bcrypt-pbkdf": { 395 | "version": "1.0.2", 396 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 397 | "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", 398 | "requires": { 399 | "tweetnacl": "^0.14.3" 400 | } 401 | }, 402 | "body-parser": { 403 | "version": "1.19.0", 404 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", 405 | "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", 406 | "requires": { 407 | "bytes": "3.1.0", 408 | "content-type": "~1.0.4", 409 | "debug": "2.6.9", 410 | "depd": "~1.1.2", 411 | "http-errors": "1.7.2", 412 | "iconv-lite": "0.4.24", 413 | "on-finished": "~2.3.0", 414 | "qs": "6.7.0", 415 | "raw-body": "2.4.0", 416 | "type-is": "~1.6.17" 417 | }, 418 | "dependencies": { 419 | "qs": { 420 | "version": "6.7.0", 421 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 422 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 423 | } 424 | } 425 | }, 426 | "bops": { 427 | "version": "0.0.7", 428 | "resolved": "https://registry.npmjs.org/bops/-/bops-0.0.7.tgz", 429 | "integrity": "sha1-tKClqDmkBkVK8P4FqLkaenZqVOI=", 430 | "requires": { 431 | "base64-js": "0.0.2", 432 | "to-utf8": "0.0.1" 433 | } 434 | }, 435 | "brace-expansion": { 436 | "version": "1.1.11", 437 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 438 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 439 | "dev": true, 440 | "requires": { 441 | "balanced-match": "^1.0.0", 442 | "concat-map": "0.0.1" 443 | } 444 | }, 445 | "bytes": { 446 | "version": "3.1.0", 447 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", 448 | "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" 449 | }, 450 | "callsites": { 451 | "version": "3.1.0", 452 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 453 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 454 | "dev": true 455 | }, 456 | "caseless": { 457 | "version": "0.12.0", 458 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 459 | "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" 460 | }, 461 | "chalk": { 462 | "version": "4.1.0", 463 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", 464 | "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", 465 | "dev": true, 466 | "requires": { 467 | "ansi-styles": "^4.1.0", 468 | "supports-color": "^7.1.0" 469 | }, 470 | "dependencies": { 471 | "ansi-styles": { 472 | "version": "4.2.1", 473 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", 474 | "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", 475 | "dev": true, 476 | "requires": { 477 | "@types/color-name": "^1.1.1", 478 | "color-convert": "^2.0.1" 479 | } 480 | }, 481 | "color-convert": { 482 | "version": "2.0.1", 483 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 484 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 485 | "dev": true, 486 | "requires": { 487 | "color-name": "~1.1.4" 488 | } 489 | }, 490 | "color-name": { 491 | "version": "1.1.4", 492 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 493 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 494 | "dev": true 495 | }, 496 | "has-flag": { 497 | "version": "4.0.0", 498 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 499 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 500 | "dev": true 501 | }, 502 | "supports-color": { 503 | "version": "7.2.0", 504 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 505 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 506 | "dev": true, 507 | "requires": { 508 | "has-flag": "^4.0.0" 509 | } 510 | } 511 | } 512 | }, 513 | "chardet": { 514 | "version": "0.7.0", 515 | "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", 516 | "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", 517 | "dev": true 518 | }, 519 | "cli-cursor": { 520 | "version": "3.1.0", 521 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", 522 | "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", 523 | "dev": true, 524 | "requires": { 525 | "restore-cursor": "^3.1.0" 526 | } 527 | }, 528 | "cli-width": { 529 | "version": "3.0.0", 530 | "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", 531 | "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", 532 | "dev": true 533 | }, 534 | "color-convert": { 535 | "version": "1.9.3", 536 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", 537 | "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", 538 | "dev": true, 539 | "requires": { 540 | "color-name": "1.1.3" 541 | } 542 | }, 543 | "color-name": { 544 | "version": "1.1.3", 545 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", 546 | "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", 547 | "dev": true 548 | }, 549 | "combined-stream": { 550 | "version": "1.0.8", 551 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 552 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 553 | "requires": { 554 | "delayed-stream": "~1.0.0" 555 | } 556 | }, 557 | "concat-map": { 558 | "version": "0.0.1", 559 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 560 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 561 | "dev": true 562 | }, 563 | "contains-path": { 564 | "version": "0.1.0", 565 | "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", 566 | "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", 567 | "dev": true 568 | }, 569 | "content-disposition": { 570 | "version": "0.5.3", 571 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", 572 | "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", 573 | "requires": { 574 | "safe-buffer": "5.1.2" 575 | }, 576 | "dependencies": { 577 | "safe-buffer": { 578 | "version": "5.1.2", 579 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 580 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 581 | } 582 | } 583 | }, 584 | "content-type": { 585 | "version": "1.0.4", 586 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", 587 | "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" 588 | }, 589 | "cookie": { 590 | "version": "0.4.0", 591 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", 592 | "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==" 593 | }, 594 | "cookie-signature": { 595 | "version": "1.0.6", 596 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", 597 | "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" 598 | }, 599 | "core-util-is": { 600 | "version": "1.0.2", 601 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 602 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" 603 | }, 604 | "cross-spawn": { 605 | "version": "7.0.3", 606 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", 607 | "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", 608 | "dev": true, 609 | "requires": { 610 | "path-key": "^3.1.0", 611 | "shebang-command": "^2.0.0", 612 | "which": "^2.0.1" 613 | } 614 | }, 615 | "dashdash": { 616 | "version": "1.14.1", 617 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 618 | "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", 619 | "requires": { 620 | "assert-plus": "^1.0.0" 621 | } 622 | }, 623 | "debug": { 624 | "version": "2.6.9", 625 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 626 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 627 | "requires": { 628 | "ms": "2.0.0" 629 | } 630 | }, 631 | "deep-is": { 632 | "version": "0.1.3", 633 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 634 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", 635 | "dev": true 636 | }, 637 | "define-properties": { 638 | "version": "1.1.3", 639 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", 640 | "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", 641 | "dev": true, 642 | "requires": { 643 | "object-keys": "^1.0.12" 644 | } 645 | }, 646 | "delayed-stream": { 647 | "version": "1.0.0", 648 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 649 | "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" 650 | }, 651 | "depd": { 652 | "version": "1.1.2", 653 | "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", 654 | "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" 655 | }, 656 | "destroy": { 657 | "version": "1.0.4", 658 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", 659 | "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" 660 | }, 661 | "doctrine": { 662 | "version": "3.0.0", 663 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", 664 | "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", 665 | "dev": true, 666 | "requires": { 667 | "esutils": "^2.0.2" 668 | } 669 | }, 670 | "dotenv": { 671 | "version": "8.2.0", 672 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.2.0.tgz", 673 | "integrity": "sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw==" 674 | }, 675 | "ecc-jsbn": { 676 | "version": "0.1.2", 677 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 678 | "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", 679 | "requires": { 680 | "jsbn": "~0.1.0", 681 | "safer-buffer": "^2.1.0" 682 | } 683 | }, 684 | "ee-first": { 685 | "version": "1.1.1", 686 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 687 | "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" 688 | }, 689 | "emoji-regex": { 690 | "version": "7.0.3", 691 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", 692 | "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", 693 | "dev": true 694 | }, 695 | "encodeurl": { 696 | "version": "1.0.2", 697 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", 698 | "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" 699 | }, 700 | "enquirer": { 701 | "version": "2.3.6", 702 | "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", 703 | "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", 704 | "dev": true, 705 | "requires": { 706 | "ansi-colors": "^4.1.1" 707 | } 708 | }, 709 | "error-ex": { 710 | "version": "1.3.2", 711 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", 712 | "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", 713 | "dev": true, 714 | "requires": { 715 | "is-arrayish": "^0.2.1" 716 | } 717 | }, 718 | "es-abstract": { 719 | "version": "1.17.6", 720 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", 721 | "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", 722 | "dev": true, 723 | "requires": { 724 | "es-to-primitive": "^1.2.1", 725 | "function-bind": "^1.1.1", 726 | "has": "^1.0.3", 727 | "has-symbols": "^1.0.1", 728 | "is-callable": "^1.2.0", 729 | "is-regex": "^1.1.0", 730 | "object-inspect": "^1.7.0", 731 | "object-keys": "^1.1.1", 732 | "object.assign": "^4.1.0", 733 | "string.prototype.trimend": "^1.0.1", 734 | "string.prototype.trimstart": "^1.0.1" 735 | } 736 | }, 737 | "es-to-primitive": { 738 | "version": "1.2.1", 739 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", 740 | "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", 741 | "dev": true, 742 | "requires": { 743 | "is-callable": "^1.1.4", 744 | "is-date-object": "^1.0.1", 745 | "is-symbol": "^1.0.2" 746 | } 747 | }, 748 | "escape-html": { 749 | "version": "1.0.3", 750 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 751 | "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" 752 | }, 753 | "escape-string-regexp": { 754 | "version": "1.0.5", 755 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 756 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 757 | "dev": true 758 | }, 759 | "eslint": { 760 | "version": "7.8.0", 761 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.8.0.tgz", 762 | "integrity": "sha512-qgtVyLZqKd2ZXWnLQA4NtVbOyH56zivOAdBFWE54RFkSZjokzNrcP4Z0eVWsZ+84ByXv+jL9k/wE1ENYe8xRFw==", 763 | "dev": true, 764 | "requires": { 765 | "@babel/code-frame": "^7.0.0", 766 | "@eslint/eslintrc": "^0.1.0", 767 | "ajv": "^6.10.0", 768 | "chalk": "^4.0.0", 769 | "cross-spawn": "^7.0.2", 770 | "debug": "^4.0.1", 771 | "doctrine": "^3.0.0", 772 | "enquirer": "^2.3.5", 773 | "eslint-scope": "^5.1.0", 774 | "eslint-utils": "^2.1.0", 775 | "eslint-visitor-keys": "^1.3.0", 776 | "espree": "^7.3.0", 777 | "esquery": "^1.2.0", 778 | "esutils": "^2.0.2", 779 | "file-entry-cache": "^5.0.1", 780 | "functional-red-black-tree": "^1.0.1", 781 | "glob-parent": "^5.0.0", 782 | "globals": "^12.1.0", 783 | "ignore": "^4.0.6", 784 | "import-fresh": "^3.0.0", 785 | "imurmurhash": "^0.1.4", 786 | "is-glob": "^4.0.0", 787 | "js-yaml": "^3.13.1", 788 | "json-stable-stringify-without-jsonify": "^1.0.1", 789 | "levn": "^0.4.1", 790 | "lodash": "^4.17.19", 791 | "minimatch": "^3.0.4", 792 | "natural-compare": "^1.4.0", 793 | "optionator": "^0.9.1", 794 | "progress": "^2.0.0", 795 | "regexpp": "^3.1.0", 796 | "semver": "^7.2.1", 797 | "strip-ansi": "^6.0.0", 798 | "strip-json-comments": "^3.1.0", 799 | "table": "^5.2.3", 800 | "text-table": "^0.2.0", 801 | "v8-compile-cache": "^2.0.3" 802 | }, 803 | "dependencies": { 804 | "debug": { 805 | "version": "4.1.1", 806 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 807 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 808 | "dev": true, 809 | "requires": { 810 | "ms": "^2.1.1" 811 | } 812 | }, 813 | "ms": { 814 | "version": "2.1.2", 815 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 816 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 817 | "dev": true 818 | } 819 | } 820 | }, 821 | "eslint-config-esnext": { 822 | "version": "4.1.0", 823 | "resolved": "https://registry.npmjs.org/eslint-config-esnext/-/eslint-config-esnext-4.1.0.tgz", 824 | "integrity": "sha512-GhfVEXdqYKEIIj7j+Fw2SQdL9qyZMekgXfq6PyXM66cQw0B435ddjz3P3kxOBVihMRJ0xGYjosaveQz5Y6z0uA==", 825 | "dev": true, 826 | "requires": { 827 | "babel-eslint": "^10.0.1", 828 | "eslint": "^6.8.0", 829 | "eslint-plugin-babel": "^5.2.1", 830 | "eslint-plugin-import": "^2.14.0" 831 | }, 832 | "dependencies": { 833 | "ansi-regex": { 834 | "version": "4.1.0", 835 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 836 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 837 | "dev": true 838 | }, 839 | "chalk": { 840 | "version": "2.4.2", 841 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 842 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 843 | "dev": true, 844 | "requires": { 845 | "ansi-styles": "^3.2.1", 846 | "escape-string-regexp": "^1.0.5", 847 | "supports-color": "^5.3.0" 848 | } 849 | }, 850 | "cross-spawn": { 851 | "version": "6.0.5", 852 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 853 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 854 | "dev": true, 855 | "requires": { 856 | "nice-try": "^1.0.4", 857 | "path-key": "^2.0.1", 858 | "semver": "^5.5.0", 859 | "shebang-command": "^1.2.0", 860 | "which": "^1.2.9" 861 | }, 862 | "dependencies": { 863 | "semver": { 864 | "version": "5.7.1", 865 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 866 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 867 | "dev": true 868 | } 869 | } 870 | }, 871 | "debug": { 872 | "version": "4.1.1", 873 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 874 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 875 | "dev": true, 876 | "requires": { 877 | "ms": "^2.1.1" 878 | } 879 | }, 880 | "eslint": { 881 | "version": "6.8.0", 882 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", 883 | "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", 884 | "dev": true, 885 | "requires": { 886 | "@babel/code-frame": "^7.0.0", 887 | "ajv": "^6.10.0", 888 | "chalk": "^2.1.0", 889 | "cross-spawn": "^6.0.5", 890 | "debug": "^4.0.1", 891 | "doctrine": "^3.0.0", 892 | "eslint-scope": "^5.0.0", 893 | "eslint-utils": "^1.4.3", 894 | "eslint-visitor-keys": "^1.1.0", 895 | "espree": "^6.1.2", 896 | "esquery": "^1.0.1", 897 | "esutils": "^2.0.2", 898 | "file-entry-cache": "^5.0.1", 899 | "functional-red-black-tree": "^1.0.1", 900 | "glob-parent": "^5.0.0", 901 | "globals": "^12.1.0", 902 | "ignore": "^4.0.6", 903 | "import-fresh": "^3.0.0", 904 | "imurmurhash": "^0.1.4", 905 | "inquirer": "^7.0.0", 906 | "is-glob": "^4.0.0", 907 | "js-yaml": "^3.13.1", 908 | "json-stable-stringify-without-jsonify": "^1.0.1", 909 | "levn": "^0.3.0", 910 | "lodash": "^4.17.14", 911 | "minimatch": "^3.0.4", 912 | "mkdirp": "^0.5.1", 913 | "natural-compare": "^1.4.0", 914 | "optionator": "^0.8.3", 915 | "progress": "^2.0.0", 916 | "regexpp": "^2.0.1", 917 | "semver": "^6.1.2", 918 | "strip-ansi": "^5.2.0", 919 | "strip-json-comments": "^3.0.1", 920 | "table": "^5.2.3", 921 | "text-table": "^0.2.0", 922 | "v8-compile-cache": "^2.0.3" 923 | } 924 | }, 925 | "eslint-utils": { 926 | "version": "1.4.3", 927 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", 928 | "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", 929 | "dev": true, 930 | "requires": { 931 | "eslint-visitor-keys": "^1.1.0" 932 | } 933 | }, 934 | "espree": { 935 | "version": "6.2.1", 936 | "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", 937 | "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", 938 | "dev": true, 939 | "requires": { 940 | "acorn": "^7.1.1", 941 | "acorn-jsx": "^5.2.0", 942 | "eslint-visitor-keys": "^1.1.0" 943 | } 944 | }, 945 | "levn": { 946 | "version": "0.3.0", 947 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 948 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", 949 | "dev": true, 950 | "requires": { 951 | "prelude-ls": "~1.1.2", 952 | "type-check": "~0.3.2" 953 | } 954 | }, 955 | "ms": { 956 | "version": "2.1.2", 957 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 958 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 959 | "dev": true 960 | }, 961 | "optionator": { 962 | "version": "0.8.3", 963 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", 964 | "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", 965 | "dev": true, 966 | "requires": { 967 | "deep-is": "~0.1.3", 968 | "fast-levenshtein": "~2.0.6", 969 | "levn": "~0.3.0", 970 | "prelude-ls": "~1.1.2", 971 | "type-check": "~0.3.2", 972 | "word-wrap": "~1.2.3" 973 | } 974 | }, 975 | "path-key": { 976 | "version": "2.0.1", 977 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 978 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", 979 | "dev": true 980 | }, 981 | "prelude-ls": { 982 | "version": "1.1.2", 983 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 984 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", 985 | "dev": true 986 | }, 987 | "regexpp": { 988 | "version": "2.0.1", 989 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", 990 | "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", 991 | "dev": true 992 | }, 993 | "semver": { 994 | "version": "6.3.0", 995 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 996 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", 997 | "dev": true 998 | }, 999 | "shebang-command": { 1000 | "version": "1.2.0", 1001 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 1002 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 1003 | "dev": true, 1004 | "requires": { 1005 | "shebang-regex": "^1.0.0" 1006 | } 1007 | }, 1008 | "shebang-regex": { 1009 | "version": "1.0.0", 1010 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 1011 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 1012 | "dev": true 1013 | }, 1014 | "strip-ansi": { 1015 | "version": "5.2.0", 1016 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 1017 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 1018 | "dev": true, 1019 | "requires": { 1020 | "ansi-regex": "^4.1.0" 1021 | } 1022 | }, 1023 | "type-check": { 1024 | "version": "0.3.2", 1025 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 1026 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 1027 | "dev": true, 1028 | "requires": { 1029 | "prelude-ls": "~1.1.2" 1030 | } 1031 | }, 1032 | "which": { 1033 | "version": "1.3.1", 1034 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 1035 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 1036 | "dev": true, 1037 | "requires": { 1038 | "isexe": "^2.0.0" 1039 | } 1040 | } 1041 | } 1042 | }, 1043 | "eslint-config-google": { 1044 | "version": "0.14.0", 1045 | "resolved": "https://registry.npmjs.org/eslint-config-google/-/eslint-config-google-0.14.0.tgz", 1046 | "integrity": "sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==", 1047 | "dev": true 1048 | }, 1049 | "eslint-config-node": { 1050 | "version": "4.1.0", 1051 | "resolved": "https://registry.npmjs.org/eslint-config-node/-/eslint-config-node-4.1.0.tgz", 1052 | "integrity": "sha512-Wz17xV5O2WFG8fGdMYEBdbiL6TL7YNJSJvSX9V4sXQownewfYmoqlly7wxqLkOUv/57pq6LnnotMiQQrrPjCqQ==", 1053 | "dev": true, 1054 | "requires": { 1055 | "eslint": "^6.8.0", 1056 | "eslint-config-esnext": "^4.1.0" 1057 | }, 1058 | "dependencies": { 1059 | "ansi-regex": { 1060 | "version": "4.1.0", 1061 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 1062 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 1063 | "dev": true 1064 | }, 1065 | "chalk": { 1066 | "version": "2.4.2", 1067 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", 1068 | "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", 1069 | "dev": true, 1070 | "requires": { 1071 | "ansi-styles": "^3.2.1", 1072 | "escape-string-regexp": "^1.0.5", 1073 | "supports-color": "^5.3.0" 1074 | } 1075 | }, 1076 | "cross-spawn": { 1077 | "version": "6.0.5", 1078 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", 1079 | "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", 1080 | "dev": true, 1081 | "requires": { 1082 | "nice-try": "^1.0.4", 1083 | "path-key": "^2.0.1", 1084 | "semver": "^5.5.0", 1085 | "shebang-command": "^1.2.0", 1086 | "which": "^1.2.9" 1087 | }, 1088 | "dependencies": { 1089 | "semver": { 1090 | "version": "5.7.1", 1091 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 1092 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 1093 | "dev": true 1094 | } 1095 | } 1096 | }, 1097 | "debug": { 1098 | "version": "4.1.1", 1099 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", 1100 | "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", 1101 | "dev": true, 1102 | "requires": { 1103 | "ms": "^2.1.1" 1104 | } 1105 | }, 1106 | "eslint": { 1107 | "version": "6.8.0", 1108 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-6.8.0.tgz", 1109 | "integrity": "sha512-K+Iayyo2LtyYhDSYwz5D5QdWw0hCacNzyq1Y821Xna2xSJj7cijoLLYmLxTQgcgZ9mC61nryMy9S7GRbYpI5Ig==", 1110 | "dev": true, 1111 | "requires": { 1112 | "@babel/code-frame": "^7.0.0", 1113 | "ajv": "^6.10.0", 1114 | "chalk": "^2.1.0", 1115 | "cross-spawn": "^6.0.5", 1116 | "debug": "^4.0.1", 1117 | "doctrine": "^3.0.0", 1118 | "eslint-scope": "^5.0.0", 1119 | "eslint-utils": "^1.4.3", 1120 | "eslint-visitor-keys": "^1.1.0", 1121 | "espree": "^6.1.2", 1122 | "esquery": "^1.0.1", 1123 | "esutils": "^2.0.2", 1124 | "file-entry-cache": "^5.0.1", 1125 | "functional-red-black-tree": "^1.0.1", 1126 | "glob-parent": "^5.0.0", 1127 | "globals": "^12.1.0", 1128 | "ignore": "^4.0.6", 1129 | "import-fresh": "^3.0.0", 1130 | "imurmurhash": "^0.1.4", 1131 | "inquirer": "^7.0.0", 1132 | "is-glob": "^4.0.0", 1133 | "js-yaml": "^3.13.1", 1134 | "json-stable-stringify-without-jsonify": "^1.0.1", 1135 | "levn": "^0.3.0", 1136 | "lodash": "^4.17.14", 1137 | "minimatch": "^3.0.4", 1138 | "mkdirp": "^0.5.1", 1139 | "natural-compare": "^1.4.0", 1140 | "optionator": "^0.8.3", 1141 | "progress": "^2.0.0", 1142 | "regexpp": "^2.0.1", 1143 | "semver": "^6.1.2", 1144 | "strip-ansi": "^5.2.0", 1145 | "strip-json-comments": "^3.0.1", 1146 | "table": "^5.2.3", 1147 | "text-table": "^0.2.0", 1148 | "v8-compile-cache": "^2.0.3" 1149 | } 1150 | }, 1151 | "eslint-utils": { 1152 | "version": "1.4.3", 1153 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.3.tgz", 1154 | "integrity": "sha512-fbBN5W2xdY45KulGXmLHZ3c3FHfVYmKg0IrAKGOkT/464PQsx2UeIzfz1RmEci+KLm1bBaAzZAh8+/E+XAeZ8Q==", 1155 | "dev": true, 1156 | "requires": { 1157 | "eslint-visitor-keys": "^1.1.0" 1158 | } 1159 | }, 1160 | "espree": { 1161 | "version": "6.2.1", 1162 | "resolved": "https://registry.npmjs.org/espree/-/espree-6.2.1.tgz", 1163 | "integrity": "sha512-ysCxRQY3WaXJz9tdbWOwuWr5Y/XrPTGX9Kiz3yoUXwW0VZ4w30HTkQLaGx/+ttFjF8i+ACbArnB4ce68a9m5hw==", 1164 | "dev": true, 1165 | "requires": { 1166 | "acorn": "^7.1.1", 1167 | "acorn-jsx": "^5.2.0", 1168 | "eslint-visitor-keys": "^1.1.0" 1169 | } 1170 | }, 1171 | "levn": { 1172 | "version": "0.3.0", 1173 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 1174 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", 1175 | "dev": true, 1176 | "requires": { 1177 | "prelude-ls": "~1.1.2", 1178 | "type-check": "~0.3.2" 1179 | } 1180 | }, 1181 | "ms": { 1182 | "version": "2.1.2", 1183 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 1184 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 1185 | "dev": true 1186 | }, 1187 | "optionator": { 1188 | "version": "0.8.3", 1189 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", 1190 | "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", 1191 | "dev": true, 1192 | "requires": { 1193 | "deep-is": "~0.1.3", 1194 | "fast-levenshtein": "~2.0.6", 1195 | "levn": "~0.3.0", 1196 | "prelude-ls": "~1.1.2", 1197 | "type-check": "~0.3.2", 1198 | "word-wrap": "~1.2.3" 1199 | } 1200 | }, 1201 | "path-key": { 1202 | "version": "2.0.1", 1203 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", 1204 | "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", 1205 | "dev": true 1206 | }, 1207 | "prelude-ls": { 1208 | "version": "1.1.2", 1209 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 1210 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", 1211 | "dev": true 1212 | }, 1213 | "regexpp": { 1214 | "version": "2.0.1", 1215 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", 1216 | "integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", 1217 | "dev": true 1218 | }, 1219 | "semver": { 1220 | "version": "6.3.0", 1221 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 1222 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", 1223 | "dev": true 1224 | }, 1225 | "shebang-command": { 1226 | "version": "1.2.0", 1227 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", 1228 | "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", 1229 | "dev": true, 1230 | "requires": { 1231 | "shebang-regex": "^1.0.0" 1232 | } 1233 | }, 1234 | "shebang-regex": { 1235 | "version": "1.0.0", 1236 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", 1237 | "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", 1238 | "dev": true 1239 | }, 1240 | "strip-ansi": { 1241 | "version": "5.2.0", 1242 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 1243 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 1244 | "dev": true, 1245 | "requires": { 1246 | "ansi-regex": "^4.1.0" 1247 | } 1248 | }, 1249 | "type-check": { 1250 | "version": "0.3.2", 1251 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 1252 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 1253 | "dev": true, 1254 | "requires": { 1255 | "prelude-ls": "~1.1.2" 1256 | } 1257 | }, 1258 | "which": { 1259 | "version": "1.3.1", 1260 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 1261 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 1262 | "dev": true, 1263 | "requires": { 1264 | "isexe": "^2.0.0" 1265 | } 1266 | } 1267 | } 1268 | }, 1269 | "eslint-config-prettier": { 1270 | "version": "6.11.0", 1271 | "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", 1272 | "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", 1273 | "dev": true, 1274 | "requires": { 1275 | "get-stdin": "^6.0.0" 1276 | } 1277 | }, 1278 | "eslint-import-resolver-node": { 1279 | "version": "0.3.4", 1280 | "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz", 1281 | "integrity": "sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA==", 1282 | "dev": true, 1283 | "requires": { 1284 | "debug": "^2.6.9", 1285 | "resolve": "^1.13.1" 1286 | } 1287 | }, 1288 | "eslint-module-utils": { 1289 | "version": "2.6.0", 1290 | "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz", 1291 | "integrity": "sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA==", 1292 | "dev": true, 1293 | "requires": { 1294 | "debug": "^2.6.9", 1295 | "pkg-dir": "^2.0.0" 1296 | } 1297 | }, 1298 | "eslint-plugin-babel": { 1299 | "version": "5.3.1", 1300 | "resolved": "https://registry.npmjs.org/eslint-plugin-babel/-/eslint-plugin-babel-5.3.1.tgz", 1301 | "integrity": "sha512-VsQEr6NH3dj664+EyxJwO4FCYm/00JhYb3Sk3ft8o+fpKuIfQ9TaW6uVUfvwMXHcf/lsnRIoyFPsLMyiWCSL/g==", 1302 | "dev": true, 1303 | "requires": { 1304 | "eslint-rule-composer": "^0.3.0" 1305 | } 1306 | }, 1307 | "eslint-plugin-es": { 1308 | "version": "3.0.1", 1309 | "resolved": "https://registry.npmjs.org/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz", 1310 | "integrity": "sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ==", 1311 | "dev": true, 1312 | "requires": { 1313 | "eslint-utils": "^2.0.0", 1314 | "regexpp": "^3.0.0" 1315 | } 1316 | }, 1317 | "eslint-plugin-import": { 1318 | "version": "2.22.0", 1319 | "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz", 1320 | "integrity": "sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg==", 1321 | "dev": true, 1322 | "requires": { 1323 | "array-includes": "^3.1.1", 1324 | "array.prototype.flat": "^1.2.3", 1325 | "contains-path": "^0.1.0", 1326 | "debug": "^2.6.9", 1327 | "doctrine": "1.5.0", 1328 | "eslint-import-resolver-node": "^0.3.3", 1329 | "eslint-module-utils": "^2.6.0", 1330 | "has": "^1.0.3", 1331 | "minimatch": "^3.0.4", 1332 | "object.values": "^1.1.1", 1333 | "read-pkg-up": "^2.0.0", 1334 | "resolve": "^1.17.0", 1335 | "tsconfig-paths": "^3.9.0" 1336 | }, 1337 | "dependencies": { 1338 | "doctrine": { 1339 | "version": "1.5.0", 1340 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", 1341 | "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", 1342 | "dev": true, 1343 | "requires": { 1344 | "esutils": "^2.0.2", 1345 | "isarray": "^1.0.0" 1346 | } 1347 | } 1348 | } 1349 | }, 1350 | "eslint-plugin-node": { 1351 | "version": "11.1.0", 1352 | "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz", 1353 | "integrity": "sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g==", 1354 | "dev": true, 1355 | "requires": { 1356 | "eslint-plugin-es": "^3.0.0", 1357 | "eslint-utils": "^2.0.0", 1358 | "ignore": "^5.1.1", 1359 | "minimatch": "^3.0.4", 1360 | "resolve": "^1.10.1", 1361 | "semver": "^6.1.0" 1362 | }, 1363 | "dependencies": { 1364 | "ignore": { 1365 | "version": "5.1.8", 1366 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", 1367 | "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", 1368 | "dev": true 1369 | }, 1370 | "semver": { 1371 | "version": "6.3.0", 1372 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", 1373 | "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", 1374 | "dev": true 1375 | } 1376 | } 1377 | }, 1378 | "eslint-plugin-prettier": { 1379 | "version": "3.1.4", 1380 | "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", 1381 | "integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==", 1382 | "dev": true, 1383 | "requires": { 1384 | "prettier-linter-helpers": "^1.0.0" 1385 | } 1386 | }, 1387 | "eslint-rule-composer": { 1388 | "version": "0.3.0", 1389 | "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", 1390 | "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", 1391 | "dev": true 1392 | }, 1393 | "eslint-scope": { 1394 | "version": "5.1.0", 1395 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.0.tgz", 1396 | "integrity": "sha512-iiGRvtxWqgtx5m8EyQUJihBloE4EnYeGE/bz1wSPwJE6tZuJUtHlhqDM4Xj2ukE8Dyy1+HCZ4hE0fzIVMzb58w==", 1397 | "dev": true, 1398 | "requires": { 1399 | "esrecurse": "^4.1.0", 1400 | "estraverse": "^4.1.1" 1401 | } 1402 | }, 1403 | "eslint-utils": { 1404 | "version": "2.1.0", 1405 | "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", 1406 | "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", 1407 | "dev": true, 1408 | "requires": { 1409 | "eslint-visitor-keys": "^1.1.0" 1410 | } 1411 | }, 1412 | "eslint-visitor-keys": { 1413 | "version": "1.3.0", 1414 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", 1415 | "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", 1416 | "dev": true 1417 | }, 1418 | "espree": { 1419 | "version": "7.3.0", 1420 | "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.0.tgz", 1421 | "integrity": "sha512-dksIWsvKCixn1yrEXO8UosNSxaDoSYpq9reEjZSbHLpT5hpaCAKTLBwq0RHtLrIr+c0ByiYzWT8KTMRzoRCNlw==", 1422 | "dev": true, 1423 | "requires": { 1424 | "acorn": "^7.4.0", 1425 | "acorn-jsx": "^5.2.0", 1426 | "eslint-visitor-keys": "^1.3.0" 1427 | } 1428 | }, 1429 | "esprima": { 1430 | "version": "4.0.1", 1431 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 1432 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 1433 | "dev": true 1434 | }, 1435 | "esquery": { 1436 | "version": "1.3.1", 1437 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.3.1.tgz", 1438 | "integrity": "sha512-olpvt9QG0vniUBZspVRN6lwB7hOZoTRtT+jzR+tS4ffYx2mzbw+z0XCOk44aaLYKApNX5nMm+E+P6o25ip/DHQ==", 1439 | "dev": true, 1440 | "requires": { 1441 | "estraverse": "^5.1.0" 1442 | }, 1443 | "dependencies": { 1444 | "estraverse": { 1445 | "version": "5.2.0", 1446 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", 1447 | "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", 1448 | "dev": true 1449 | } 1450 | } 1451 | }, 1452 | "esrecurse": { 1453 | "version": "4.3.0", 1454 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 1455 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 1456 | "dev": true, 1457 | "requires": { 1458 | "estraverse": "^5.2.0" 1459 | }, 1460 | "dependencies": { 1461 | "estraverse": { 1462 | "version": "5.2.0", 1463 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", 1464 | "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", 1465 | "dev": true 1466 | } 1467 | } 1468 | }, 1469 | "estraverse": { 1470 | "version": "4.3.0", 1471 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", 1472 | "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", 1473 | "dev": true 1474 | }, 1475 | "esutils": { 1476 | "version": "2.0.3", 1477 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 1478 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 1479 | "dev": true 1480 | }, 1481 | "etag": { 1482 | "version": "1.8.1", 1483 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 1484 | "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" 1485 | }, 1486 | "express": { 1487 | "version": "4.17.1", 1488 | "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", 1489 | "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", 1490 | "requires": { 1491 | "accepts": "~1.3.7", 1492 | "array-flatten": "1.1.1", 1493 | "body-parser": "1.19.0", 1494 | "content-disposition": "0.5.3", 1495 | "content-type": "~1.0.4", 1496 | "cookie": "0.4.0", 1497 | "cookie-signature": "1.0.6", 1498 | "debug": "2.6.9", 1499 | "depd": "~1.1.2", 1500 | "encodeurl": "~1.0.2", 1501 | "escape-html": "~1.0.3", 1502 | "etag": "~1.8.1", 1503 | "finalhandler": "~1.1.2", 1504 | "fresh": "0.5.2", 1505 | "merge-descriptors": "1.0.1", 1506 | "methods": "~1.1.2", 1507 | "on-finished": "~2.3.0", 1508 | "parseurl": "~1.3.3", 1509 | "path-to-regexp": "0.1.7", 1510 | "proxy-addr": "~2.0.5", 1511 | "qs": "6.7.0", 1512 | "range-parser": "~1.2.1", 1513 | "safe-buffer": "5.1.2", 1514 | "send": "0.17.1", 1515 | "serve-static": "1.14.1", 1516 | "setprototypeof": "1.1.1", 1517 | "statuses": "~1.5.0", 1518 | "type-is": "~1.6.18", 1519 | "utils-merge": "1.0.1", 1520 | "vary": "~1.1.2" 1521 | }, 1522 | "dependencies": { 1523 | "qs": { 1524 | "version": "6.7.0", 1525 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", 1526 | "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" 1527 | }, 1528 | "safe-buffer": { 1529 | "version": "5.1.2", 1530 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 1531 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 1532 | } 1533 | } 1534 | }, 1535 | "extend": { 1536 | "version": "3.0.2", 1537 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 1538 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 1539 | }, 1540 | "external-editor": { 1541 | "version": "3.1.0", 1542 | "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", 1543 | "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", 1544 | "dev": true, 1545 | "requires": { 1546 | "chardet": "^0.7.0", 1547 | "iconv-lite": "^0.4.24", 1548 | "tmp": "^0.0.33" 1549 | } 1550 | }, 1551 | "extsprintf": { 1552 | "version": "1.3.0", 1553 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 1554 | "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" 1555 | }, 1556 | "fast-deep-equal": { 1557 | "version": "3.1.3", 1558 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 1559 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 1560 | }, 1561 | "fast-diff": { 1562 | "version": "1.2.0", 1563 | "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", 1564 | "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", 1565 | "dev": true 1566 | }, 1567 | "fast-json-stable-stringify": { 1568 | "version": "2.1.0", 1569 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 1570 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 1571 | }, 1572 | "fast-levenshtein": { 1573 | "version": "2.0.6", 1574 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 1575 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", 1576 | "dev": true 1577 | }, 1578 | "figures": { 1579 | "version": "3.2.0", 1580 | "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", 1581 | "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", 1582 | "dev": true, 1583 | "requires": { 1584 | "escape-string-regexp": "^1.0.5" 1585 | } 1586 | }, 1587 | "file-entry-cache": { 1588 | "version": "5.0.1", 1589 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-5.0.1.tgz", 1590 | "integrity": "sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==", 1591 | "dev": true, 1592 | "requires": { 1593 | "flat-cache": "^2.0.1" 1594 | } 1595 | }, 1596 | "finalhandler": { 1597 | "version": "1.1.2", 1598 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", 1599 | "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", 1600 | "requires": { 1601 | "debug": "2.6.9", 1602 | "encodeurl": "~1.0.2", 1603 | "escape-html": "~1.0.3", 1604 | "on-finished": "~2.3.0", 1605 | "parseurl": "~1.3.3", 1606 | "statuses": "~1.5.0", 1607 | "unpipe": "~1.0.0" 1608 | } 1609 | }, 1610 | "find-up": { 1611 | "version": "2.1.0", 1612 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", 1613 | "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", 1614 | "dev": true, 1615 | "requires": { 1616 | "locate-path": "^2.0.0" 1617 | } 1618 | }, 1619 | "flat-cache": { 1620 | "version": "2.0.1", 1621 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-2.0.1.tgz", 1622 | "integrity": "sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==", 1623 | "dev": true, 1624 | "requires": { 1625 | "flatted": "^2.0.0", 1626 | "rimraf": "2.6.3", 1627 | "write": "1.0.3" 1628 | } 1629 | }, 1630 | "flatted": { 1631 | "version": "2.0.2", 1632 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", 1633 | "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", 1634 | "dev": true 1635 | }, 1636 | "forever-agent": { 1637 | "version": "0.6.1", 1638 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 1639 | "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" 1640 | }, 1641 | "form-data": { 1642 | "version": "2.3.3", 1643 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 1644 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 1645 | "requires": { 1646 | "asynckit": "^0.4.0", 1647 | "combined-stream": "^1.0.6", 1648 | "mime-types": "^2.1.12" 1649 | } 1650 | }, 1651 | "forwarded": { 1652 | "version": "0.1.2", 1653 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", 1654 | "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=" 1655 | }, 1656 | "fresh": { 1657 | "version": "0.5.2", 1658 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 1659 | "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" 1660 | }, 1661 | "fs.realpath": { 1662 | "version": "1.0.0", 1663 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 1664 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 1665 | "dev": true 1666 | }, 1667 | "function-bind": { 1668 | "version": "1.1.1", 1669 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", 1670 | "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", 1671 | "dev": true 1672 | }, 1673 | "functional-red-black-tree": { 1674 | "version": "1.0.1", 1675 | "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", 1676 | "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", 1677 | "dev": true 1678 | }, 1679 | "get-stdin": { 1680 | "version": "6.0.0", 1681 | "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", 1682 | "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", 1683 | "dev": true 1684 | }, 1685 | "getpass": { 1686 | "version": "0.1.7", 1687 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 1688 | "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", 1689 | "requires": { 1690 | "assert-plus": "^1.0.0" 1691 | } 1692 | }, 1693 | "glob": { 1694 | "version": "7.1.6", 1695 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", 1696 | "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", 1697 | "dev": true, 1698 | "requires": { 1699 | "fs.realpath": "^1.0.0", 1700 | "inflight": "^1.0.4", 1701 | "inherits": "2", 1702 | "minimatch": "^3.0.4", 1703 | "once": "^1.3.0", 1704 | "path-is-absolute": "^1.0.0" 1705 | } 1706 | }, 1707 | "glob-parent": { 1708 | "version": "5.1.1", 1709 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz", 1710 | "integrity": "sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ==", 1711 | "dev": true, 1712 | "requires": { 1713 | "is-glob": "^4.0.1" 1714 | } 1715 | }, 1716 | "globals": { 1717 | "version": "12.4.0", 1718 | "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", 1719 | "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", 1720 | "dev": true, 1721 | "requires": { 1722 | "type-fest": "^0.8.1" 1723 | } 1724 | }, 1725 | "graceful-fs": { 1726 | "version": "4.2.4", 1727 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", 1728 | "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", 1729 | "dev": true 1730 | }, 1731 | "har-schema": { 1732 | "version": "2.0.0", 1733 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 1734 | "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=" 1735 | }, 1736 | "har-validator": { 1737 | "version": "5.1.5", 1738 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", 1739 | "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", 1740 | "requires": { 1741 | "ajv": "^6.12.3", 1742 | "har-schema": "^2.0.0" 1743 | } 1744 | }, 1745 | "has": { 1746 | "version": "1.0.3", 1747 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", 1748 | "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", 1749 | "dev": true, 1750 | "requires": { 1751 | "function-bind": "^1.1.1" 1752 | } 1753 | }, 1754 | "has-flag": { 1755 | "version": "3.0.0", 1756 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", 1757 | "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", 1758 | "dev": true 1759 | }, 1760 | "has-symbols": { 1761 | "version": "1.0.1", 1762 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", 1763 | "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", 1764 | "dev": true 1765 | }, 1766 | "hosted-git-info": { 1767 | "version": "2.8.8", 1768 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", 1769 | "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", 1770 | "dev": true 1771 | }, 1772 | "http-errors": { 1773 | "version": "1.7.2", 1774 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", 1775 | "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", 1776 | "requires": { 1777 | "depd": "~1.1.2", 1778 | "inherits": "2.0.3", 1779 | "setprototypeof": "1.1.1", 1780 | "statuses": ">= 1.5.0 < 2", 1781 | "toidentifier": "1.0.0" 1782 | } 1783 | }, 1784 | "http-signature": { 1785 | "version": "1.2.0", 1786 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 1787 | "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", 1788 | "requires": { 1789 | "assert-plus": "^1.0.0", 1790 | "jsprim": "^1.2.2", 1791 | "sshpk": "^1.7.0" 1792 | } 1793 | }, 1794 | "iconv-lite": { 1795 | "version": "0.4.24", 1796 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", 1797 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", 1798 | "requires": { 1799 | "safer-buffer": ">= 2.1.2 < 3" 1800 | } 1801 | }, 1802 | "ignore": { 1803 | "version": "4.0.6", 1804 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", 1805 | "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", 1806 | "dev": true 1807 | }, 1808 | "import-fresh": { 1809 | "version": "3.2.1", 1810 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.2.1.tgz", 1811 | "integrity": "sha512-6e1q1cnWP2RXD9/keSkxHScg508CdXqXWgWBaETNhyuBFz+kUZlKboh+ISK+bU++DmbHimVBrOz/zzPe0sZ3sQ==", 1812 | "dev": true, 1813 | "requires": { 1814 | "parent-module": "^1.0.0", 1815 | "resolve-from": "^4.0.0" 1816 | } 1817 | }, 1818 | "imurmurhash": { 1819 | "version": "0.1.4", 1820 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 1821 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 1822 | "dev": true 1823 | }, 1824 | "inflight": { 1825 | "version": "1.0.6", 1826 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 1827 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 1828 | "dev": true, 1829 | "requires": { 1830 | "once": "^1.3.0", 1831 | "wrappy": "1" 1832 | } 1833 | }, 1834 | "inherits": { 1835 | "version": "2.0.3", 1836 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 1837 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" 1838 | }, 1839 | "inquirer": { 1840 | "version": "7.3.3", 1841 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz", 1842 | "integrity": "sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==", 1843 | "dev": true, 1844 | "requires": { 1845 | "ansi-escapes": "^4.2.1", 1846 | "chalk": "^4.1.0", 1847 | "cli-cursor": "^3.1.0", 1848 | "cli-width": "^3.0.0", 1849 | "external-editor": "^3.0.3", 1850 | "figures": "^3.0.0", 1851 | "lodash": "^4.17.19", 1852 | "mute-stream": "0.0.8", 1853 | "run-async": "^2.4.0", 1854 | "rxjs": "^6.6.0", 1855 | "string-width": "^4.1.0", 1856 | "strip-ansi": "^6.0.0", 1857 | "through": "^2.3.6" 1858 | }, 1859 | "dependencies": { 1860 | "emoji-regex": { 1861 | "version": "8.0.0", 1862 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1863 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1864 | "dev": true 1865 | }, 1866 | "is-fullwidth-code-point": { 1867 | "version": "3.0.0", 1868 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 1869 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 1870 | "dev": true 1871 | }, 1872 | "string-width": { 1873 | "version": "4.2.0", 1874 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", 1875 | "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", 1876 | "dev": true, 1877 | "requires": { 1878 | "emoji-regex": "^8.0.0", 1879 | "is-fullwidth-code-point": "^3.0.0", 1880 | "strip-ansi": "^6.0.0" 1881 | } 1882 | } 1883 | } 1884 | }, 1885 | "ipaddr.js": { 1886 | "version": "1.9.1", 1887 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 1888 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==" 1889 | }, 1890 | "is-arrayish": { 1891 | "version": "0.2.1", 1892 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 1893 | "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", 1894 | "dev": true 1895 | }, 1896 | "is-callable": { 1897 | "version": "1.2.0", 1898 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", 1899 | "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", 1900 | "dev": true 1901 | }, 1902 | "is-date-object": { 1903 | "version": "1.0.2", 1904 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", 1905 | "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", 1906 | "dev": true 1907 | }, 1908 | "is-extglob": { 1909 | "version": "2.1.1", 1910 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 1911 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 1912 | "dev": true 1913 | }, 1914 | "is-fullwidth-code-point": { 1915 | "version": "2.0.0", 1916 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 1917 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 1918 | "dev": true 1919 | }, 1920 | "is-glob": { 1921 | "version": "4.0.1", 1922 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", 1923 | "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", 1924 | "dev": true, 1925 | "requires": { 1926 | "is-extglob": "^2.1.1" 1927 | } 1928 | }, 1929 | "is-regex": { 1930 | "version": "1.1.1", 1931 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", 1932 | "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", 1933 | "dev": true, 1934 | "requires": { 1935 | "has-symbols": "^1.0.1" 1936 | } 1937 | }, 1938 | "is-string": { 1939 | "version": "1.0.5", 1940 | "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.5.tgz", 1941 | "integrity": "sha512-buY6VNRjhQMiF1qWDouloZlQbRhDPCebwxSjxMjxgemYT46YMd2NR0/H+fBhEfWX4A/w9TBJ+ol+okqJKFE6vQ==", 1942 | "dev": true 1943 | }, 1944 | "is-symbol": { 1945 | "version": "1.0.3", 1946 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", 1947 | "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", 1948 | "dev": true, 1949 | "requires": { 1950 | "has-symbols": "^1.0.1" 1951 | } 1952 | }, 1953 | "is-typedarray": { 1954 | "version": "1.0.0", 1955 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 1956 | "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=" 1957 | }, 1958 | "isarray": { 1959 | "version": "1.0.0", 1960 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 1961 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 1962 | "dev": true 1963 | }, 1964 | "isexe": { 1965 | "version": "2.0.0", 1966 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 1967 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 1968 | "dev": true 1969 | }, 1970 | "isstream": { 1971 | "version": "0.1.2", 1972 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 1973 | "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" 1974 | }, 1975 | "js-tokens": { 1976 | "version": "4.0.0", 1977 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 1978 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 1979 | "dev": true 1980 | }, 1981 | "js-yaml": { 1982 | "version": "3.14.0", 1983 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.0.tgz", 1984 | "integrity": "sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A==", 1985 | "dev": true, 1986 | "requires": { 1987 | "argparse": "^1.0.7", 1988 | "esprima": "^4.0.0" 1989 | } 1990 | }, 1991 | "jsbn": { 1992 | "version": "0.1.1", 1993 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 1994 | "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=" 1995 | }, 1996 | "jsesc": { 1997 | "version": "2.5.2", 1998 | "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", 1999 | "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", 2000 | "dev": true 2001 | }, 2002 | "json-schema": { 2003 | "version": "0.2.3", 2004 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", 2005 | "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" 2006 | }, 2007 | "json-schema-traverse": { 2008 | "version": "0.4.1", 2009 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 2010 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 2011 | }, 2012 | "json-stable-stringify-without-jsonify": { 2013 | "version": "1.0.1", 2014 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 2015 | "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", 2016 | "dev": true 2017 | }, 2018 | "json-stringify-safe": { 2019 | "version": "5.0.1", 2020 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 2021 | "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=" 2022 | }, 2023 | "json5": { 2024 | "version": "1.0.1", 2025 | "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", 2026 | "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", 2027 | "dev": true, 2028 | "requires": { 2029 | "minimist": "^1.2.0" 2030 | } 2031 | }, 2032 | "jsprim": { 2033 | "version": "1.4.1", 2034 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", 2035 | "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", 2036 | "requires": { 2037 | "assert-plus": "1.0.0", 2038 | "extsprintf": "1.3.0", 2039 | "json-schema": "0.2.3", 2040 | "verror": "1.10.0" 2041 | } 2042 | }, 2043 | "levn": { 2044 | "version": "0.4.1", 2045 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 2046 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 2047 | "dev": true, 2048 | "requires": { 2049 | "prelude-ls": "^1.2.1", 2050 | "type-check": "~0.4.0" 2051 | } 2052 | }, 2053 | "load-json-file": { 2054 | "version": "2.0.0", 2055 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz", 2056 | "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=", 2057 | "dev": true, 2058 | "requires": { 2059 | "graceful-fs": "^4.1.2", 2060 | "parse-json": "^2.2.0", 2061 | "pify": "^2.0.0", 2062 | "strip-bom": "^3.0.0" 2063 | } 2064 | }, 2065 | "locate-path": { 2066 | "version": "2.0.0", 2067 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", 2068 | "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", 2069 | "dev": true, 2070 | "requires": { 2071 | "p-locate": "^2.0.0", 2072 | "path-exists": "^3.0.0" 2073 | } 2074 | }, 2075 | "lodash": { 2076 | "version": "4.17.20", 2077 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", 2078 | "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", 2079 | "dev": true 2080 | }, 2081 | "media-typer": { 2082 | "version": "0.3.0", 2083 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", 2084 | "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" 2085 | }, 2086 | "merge-descriptors": { 2087 | "version": "1.0.1", 2088 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", 2089 | "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=" 2090 | }, 2091 | "methods": { 2092 | "version": "1.1.2", 2093 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 2094 | "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" 2095 | }, 2096 | "mime": { 2097 | "version": "1.6.0", 2098 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", 2099 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" 2100 | }, 2101 | "mime-db": { 2102 | "version": "1.44.0", 2103 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", 2104 | "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==" 2105 | }, 2106 | "mime-types": { 2107 | "version": "2.1.27", 2108 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", 2109 | "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", 2110 | "requires": { 2111 | "mime-db": "1.44.0" 2112 | } 2113 | }, 2114 | "mimic-fn": { 2115 | "version": "2.1.0", 2116 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 2117 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", 2118 | "dev": true 2119 | }, 2120 | "minimatch": { 2121 | "version": "3.0.4", 2122 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 2123 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 2124 | "dev": true, 2125 | "requires": { 2126 | "brace-expansion": "^1.1.7" 2127 | } 2128 | }, 2129 | "minimist": { 2130 | "version": "1.2.5", 2131 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", 2132 | "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", 2133 | "dev": true 2134 | }, 2135 | "mkdirp": { 2136 | "version": "0.5.5", 2137 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", 2138 | "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", 2139 | "dev": true, 2140 | "requires": { 2141 | "minimist": "^1.2.5" 2142 | } 2143 | }, 2144 | "ms": { 2145 | "version": "2.0.0", 2146 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 2147 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" 2148 | }, 2149 | "mute-stream": { 2150 | "version": "0.0.8", 2151 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", 2152 | "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", 2153 | "dev": true 2154 | }, 2155 | "natural-compare": { 2156 | "version": "1.4.0", 2157 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 2158 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", 2159 | "dev": true 2160 | }, 2161 | "negotiator": { 2162 | "version": "0.6.2", 2163 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", 2164 | "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" 2165 | }, 2166 | "nice-try": { 2167 | "version": "1.0.5", 2168 | "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", 2169 | "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", 2170 | "dev": true 2171 | }, 2172 | "normalize-package-data": { 2173 | "version": "2.5.0", 2174 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", 2175 | "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", 2176 | "dev": true, 2177 | "requires": { 2178 | "hosted-git-info": "^2.1.4", 2179 | "resolve": "^1.10.0", 2180 | "semver": "2 || 3 || 4 || 5", 2181 | "validate-npm-package-license": "^3.0.1" 2182 | }, 2183 | "dependencies": { 2184 | "semver": { 2185 | "version": "5.7.1", 2186 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", 2187 | "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", 2188 | "dev": true 2189 | } 2190 | } 2191 | }, 2192 | "oauth-sign": { 2193 | "version": "0.9.0", 2194 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 2195 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" 2196 | }, 2197 | "object-inspect": { 2198 | "version": "1.8.0", 2199 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", 2200 | "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", 2201 | "dev": true 2202 | }, 2203 | "object-keys": { 2204 | "version": "1.1.1", 2205 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", 2206 | "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", 2207 | "dev": true 2208 | }, 2209 | "object.assign": { 2210 | "version": "4.1.0", 2211 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", 2212 | "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", 2213 | "dev": true, 2214 | "requires": { 2215 | "define-properties": "^1.1.2", 2216 | "function-bind": "^1.1.1", 2217 | "has-symbols": "^1.0.0", 2218 | "object-keys": "^1.0.11" 2219 | } 2220 | }, 2221 | "object.values": { 2222 | "version": "1.1.1", 2223 | "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.1.tgz", 2224 | "integrity": "sha512-WTa54g2K8iu0kmS/us18jEmdv1a4Wi//BZ/DTVYEcH0XhLM5NYdpDHja3gt57VrZLcNAO2WGA+KpWsDBaHt6eA==", 2225 | "dev": true, 2226 | "requires": { 2227 | "define-properties": "^1.1.3", 2228 | "es-abstract": "^1.17.0-next.1", 2229 | "function-bind": "^1.1.1", 2230 | "has": "^1.0.3" 2231 | } 2232 | }, 2233 | "on-finished": { 2234 | "version": "2.3.0", 2235 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", 2236 | "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", 2237 | "requires": { 2238 | "ee-first": "1.1.1" 2239 | } 2240 | }, 2241 | "once": { 2242 | "version": "1.4.0", 2243 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 2244 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 2245 | "dev": true, 2246 | "requires": { 2247 | "wrappy": "1" 2248 | } 2249 | }, 2250 | "onetime": { 2251 | "version": "5.1.2", 2252 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", 2253 | "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", 2254 | "dev": true, 2255 | "requires": { 2256 | "mimic-fn": "^2.1.0" 2257 | } 2258 | }, 2259 | "optionator": { 2260 | "version": "0.9.1", 2261 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", 2262 | "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", 2263 | "dev": true, 2264 | "requires": { 2265 | "deep-is": "^0.1.3", 2266 | "fast-levenshtein": "^2.0.6", 2267 | "levn": "^0.4.1", 2268 | "prelude-ls": "^1.2.1", 2269 | "type-check": "^0.4.0", 2270 | "word-wrap": "^1.2.3" 2271 | } 2272 | }, 2273 | "os-tmpdir": { 2274 | "version": "1.0.2", 2275 | "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", 2276 | "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", 2277 | "dev": true 2278 | }, 2279 | "p-limit": { 2280 | "version": "1.3.0", 2281 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", 2282 | "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", 2283 | "dev": true, 2284 | "requires": { 2285 | "p-try": "^1.0.0" 2286 | } 2287 | }, 2288 | "p-locate": { 2289 | "version": "2.0.0", 2290 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", 2291 | "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", 2292 | "dev": true, 2293 | "requires": { 2294 | "p-limit": "^1.1.0" 2295 | } 2296 | }, 2297 | "p-try": { 2298 | "version": "1.0.0", 2299 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", 2300 | "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", 2301 | "dev": true 2302 | }, 2303 | "parent-module": { 2304 | "version": "1.0.1", 2305 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 2306 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 2307 | "dev": true, 2308 | "requires": { 2309 | "callsites": "^3.0.0" 2310 | } 2311 | }, 2312 | "parse-json": { 2313 | "version": "2.2.0", 2314 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", 2315 | "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", 2316 | "dev": true, 2317 | "requires": { 2318 | "error-ex": "^1.2.0" 2319 | } 2320 | }, 2321 | "parseurl": { 2322 | "version": "1.3.3", 2323 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 2324 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" 2325 | }, 2326 | "path-exists": { 2327 | "version": "3.0.0", 2328 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", 2329 | "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", 2330 | "dev": true 2331 | }, 2332 | "path-is-absolute": { 2333 | "version": "1.0.1", 2334 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 2335 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 2336 | "dev": true 2337 | }, 2338 | "path-key": { 2339 | "version": "3.1.1", 2340 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 2341 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 2342 | "dev": true 2343 | }, 2344 | "path-parse": { 2345 | "version": "1.0.6", 2346 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", 2347 | "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", 2348 | "dev": true 2349 | }, 2350 | "path-to-regexp": { 2351 | "version": "0.1.7", 2352 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", 2353 | "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" 2354 | }, 2355 | "path-type": { 2356 | "version": "2.0.0", 2357 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz", 2358 | "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=", 2359 | "dev": true, 2360 | "requires": { 2361 | "pify": "^2.0.0" 2362 | } 2363 | }, 2364 | "performance-now": { 2365 | "version": "2.1.0", 2366 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 2367 | "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" 2368 | }, 2369 | "pify": { 2370 | "version": "2.3.0", 2371 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 2372 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", 2373 | "dev": true 2374 | }, 2375 | "pkg-dir": { 2376 | "version": "2.0.0", 2377 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", 2378 | "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", 2379 | "dev": true, 2380 | "requires": { 2381 | "find-up": "^2.1.0" 2382 | } 2383 | }, 2384 | "prelude-ls": { 2385 | "version": "1.2.1", 2386 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 2387 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 2388 | "dev": true 2389 | }, 2390 | "prettier": { 2391 | "version": "2.1.1", 2392 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.1.1.tgz", 2393 | "integrity": "sha512-9bY+5ZWCfqj3ghYBLxApy2zf6m+NJo5GzmLTpr9FsApsfjriNnS2dahWReHMi7qNPhhHl9SYHJs2cHZLgexNIw==", 2394 | "dev": true 2395 | }, 2396 | "prettier-linter-helpers": { 2397 | "version": "1.0.0", 2398 | "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", 2399 | "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", 2400 | "dev": true, 2401 | "requires": { 2402 | "fast-diff": "^1.1.2" 2403 | } 2404 | }, 2405 | "progress": { 2406 | "version": "2.0.3", 2407 | "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", 2408 | "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", 2409 | "dev": true 2410 | }, 2411 | "proxy-addr": { 2412 | "version": "2.0.6", 2413 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", 2414 | "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", 2415 | "requires": { 2416 | "forwarded": "~0.1.2", 2417 | "ipaddr.js": "1.9.1" 2418 | } 2419 | }, 2420 | "psl": { 2421 | "version": "1.8.0", 2422 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", 2423 | "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" 2424 | }, 2425 | "punycode": { 2426 | "version": "2.1.1", 2427 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", 2428 | "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" 2429 | }, 2430 | "qs": { 2431 | "version": "6.5.2", 2432 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", 2433 | "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==" 2434 | }, 2435 | "range-parser": { 2436 | "version": "1.2.1", 2437 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 2438 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" 2439 | }, 2440 | "raw-body": { 2441 | "version": "2.4.0", 2442 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", 2443 | "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", 2444 | "requires": { 2445 | "bytes": "3.1.0", 2446 | "http-errors": "1.7.2", 2447 | "iconv-lite": "0.4.24", 2448 | "unpipe": "1.0.0" 2449 | } 2450 | }, 2451 | "read-pkg": { 2452 | "version": "2.0.0", 2453 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz", 2454 | "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=", 2455 | "dev": true, 2456 | "requires": { 2457 | "load-json-file": "^2.0.0", 2458 | "normalize-package-data": "^2.3.2", 2459 | "path-type": "^2.0.0" 2460 | } 2461 | }, 2462 | "read-pkg-up": { 2463 | "version": "2.0.0", 2464 | "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz", 2465 | "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=", 2466 | "dev": true, 2467 | "requires": { 2468 | "find-up": "^2.0.0", 2469 | "read-pkg": "^2.0.0" 2470 | } 2471 | }, 2472 | "regexpp": { 2473 | "version": "3.1.0", 2474 | "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", 2475 | "integrity": "sha512-ZOIzd8yVsQQA7j8GCSlPGXwg5PfmA1mrq0JP4nGhh54LaKN3xdai/vHUDu74pKwV8OxseMS65u2NImosQcSD0Q==", 2476 | "dev": true 2477 | }, 2478 | "request": { 2479 | "version": "2.88.2", 2480 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 2481 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 2482 | "requires": { 2483 | "aws-sign2": "~0.7.0", 2484 | "aws4": "^1.8.0", 2485 | "caseless": "~0.12.0", 2486 | "combined-stream": "~1.0.6", 2487 | "extend": "~3.0.2", 2488 | "forever-agent": "~0.6.1", 2489 | "form-data": "~2.3.2", 2490 | "har-validator": "~5.1.3", 2491 | "http-signature": "~1.2.0", 2492 | "is-typedarray": "~1.0.0", 2493 | "isstream": "~0.1.2", 2494 | "json-stringify-safe": "~5.0.1", 2495 | "mime-types": "~2.1.19", 2496 | "oauth-sign": "~0.9.0", 2497 | "performance-now": "^2.1.0", 2498 | "qs": "~6.5.2", 2499 | "safe-buffer": "^5.1.2", 2500 | "tough-cookie": "~2.5.0", 2501 | "tunnel-agent": "^0.6.0", 2502 | "uuid": "^3.3.2" 2503 | } 2504 | }, 2505 | "resolve": { 2506 | "version": "1.17.0", 2507 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", 2508 | "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", 2509 | "dev": true, 2510 | "requires": { 2511 | "path-parse": "^1.0.6" 2512 | } 2513 | }, 2514 | "resolve-from": { 2515 | "version": "4.0.0", 2516 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 2517 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 2518 | "dev": true 2519 | }, 2520 | "restore-cursor": { 2521 | "version": "3.1.0", 2522 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", 2523 | "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", 2524 | "dev": true, 2525 | "requires": { 2526 | "onetime": "^5.1.0", 2527 | "signal-exit": "^3.0.2" 2528 | } 2529 | }, 2530 | "rimraf": { 2531 | "version": "2.6.3", 2532 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", 2533 | "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", 2534 | "dev": true, 2535 | "requires": { 2536 | "glob": "^7.1.3" 2537 | } 2538 | }, 2539 | "run-async": { 2540 | "version": "2.4.1", 2541 | "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", 2542 | "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", 2543 | "dev": true 2544 | }, 2545 | "rxjs": { 2546 | "version": "6.6.2", 2547 | "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", 2548 | "integrity": "sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg==", 2549 | "dev": true, 2550 | "requires": { 2551 | "tslib": "^1.9.0" 2552 | } 2553 | }, 2554 | "safe-buffer": { 2555 | "version": "5.2.1", 2556 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 2557 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" 2558 | }, 2559 | "safer-buffer": { 2560 | "version": "2.1.2", 2561 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 2562 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 2563 | }, 2564 | "semver": { 2565 | "version": "7.3.2", 2566 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", 2567 | "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", 2568 | "dev": true 2569 | }, 2570 | "send": { 2571 | "version": "0.17.1", 2572 | "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", 2573 | "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", 2574 | "requires": { 2575 | "debug": "2.6.9", 2576 | "depd": "~1.1.2", 2577 | "destroy": "~1.0.4", 2578 | "encodeurl": "~1.0.2", 2579 | "escape-html": "~1.0.3", 2580 | "etag": "~1.8.1", 2581 | "fresh": "0.5.2", 2582 | "http-errors": "~1.7.2", 2583 | "mime": "1.6.0", 2584 | "ms": "2.1.1", 2585 | "on-finished": "~2.3.0", 2586 | "range-parser": "~1.2.1", 2587 | "statuses": "~1.5.0" 2588 | }, 2589 | "dependencies": { 2590 | "ms": { 2591 | "version": "2.1.1", 2592 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", 2593 | "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" 2594 | } 2595 | } 2596 | }, 2597 | "serve-static": { 2598 | "version": "1.14.1", 2599 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", 2600 | "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", 2601 | "requires": { 2602 | "encodeurl": "~1.0.2", 2603 | "escape-html": "~1.0.3", 2604 | "parseurl": "~1.3.3", 2605 | "send": "0.17.1" 2606 | } 2607 | }, 2608 | "setprototypeof": { 2609 | "version": "1.1.1", 2610 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", 2611 | "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" 2612 | }, 2613 | "shebang-command": { 2614 | "version": "2.0.0", 2615 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 2616 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 2617 | "dev": true, 2618 | "requires": { 2619 | "shebang-regex": "^3.0.0" 2620 | } 2621 | }, 2622 | "shebang-regex": { 2623 | "version": "3.0.0", 2624 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 2625 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 2626 | "dev": true 2627 | }, 2628 | "signal-exit": { 2629 | "version": "3.0.3", 2630 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", 2631 | "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", 2632 | "dev": true 2633 | }, 2634 | "slice-ansi": { 2635 | "version": "2.1.0", 2636 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", 2637 | "integrity": "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ==", 2638 | "dev": true, 2639 | "requires": { 2640 | "ansi-styles": "^3.2.0", 2641 | "astral-regex": "^1.0.0", 2642 | "is-fullwidth-code-point": "^2.0.0" 2643 | } 2644 | }, 2645 | "source-map": { 2646 | "version": "0.6.1", 2647 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 2648 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 2649 | "dev": true 2650 | }, 2651 | "spdx-correct": { 2652 | "version": "3.1.1", 2653 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", 2654 | "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", 2655 | "dev": true, 2656 | "requires": { 2657 | "spdx-expression-parse": "^3.0.0", 2658 | "spdx-license-ids": "^3.0.0" 2659 | } 2660 | }, 2661 | "spdx-exceptions": { 2662 | "version": "2.3.0", 2663 | "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", 2664 | "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", 2665 | "dev": true 2666 | }, 2667 | "spdx-expression-parse": { 2668 | "version": "3.0.1", 2669 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", 2670 | "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", 2671 | "dev": true, 2672 | "requires": { 2673 | "spdx-exceptions": "^2.1.0", 2674 | "spdx-license-ids": "^3.0.0" 2675 | } 2676 | }, 2677 | "spdx-license-ids": { 2678 | "version": "3.0.5", 2679 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", 2680 | "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", 2681 | "dev": true 2682 | }, 2683 | "sprintf-js": { 2684 | "version": "1.0.3", 2685 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 2686 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 2687 | "dev": true 2688 | }, 2689 | "sshpk": { 2690 | "version": "1.16.1", 2691 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", 2692 | "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", 2693 | "requires": { 2694 | "asn1": "~0.2.3", 2695 | "assert-plus": "^1.0.0", 2696 | "bcrypt-pbkdf": "^1.0.0", 2697 | "dashdash": "^1.12.0", 2698 | "ecc-jsbn": "~0.1.1", 2699 | "getpass": "^0.1.1", 2700 | "jsbn": "~0.1.0", 2701 | "safer-buffer": "^2.0.2", 2702 | "tweetnacl": "~0.14.0" 2703 | } 2704 | }, 2705 | "statuses": { 2706 | "version": "1.5.0", 2707 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", 2708 | "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" 2709 | }, 2710 | "string-width": { 2711 | "version": "3.1.0", 2712 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", 2713 | "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", 2714 | "dev": true, 2715 | "requires": { 2716 | "emoji-regex": "^7.0.1", 2717 | "is-fullwidth-code-point": "^2.0.0", 2718 | "strip-ansi": "^5.1.0" 2719 | }, 2720 | "dependencies": { 2721 | "ansi-regex": { 2722 | "version": "4.1.0", 2723 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", 2724 | "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", 2725 | "dev": true 2726 | }, 2727 | "strip-ansi": { 2728 | "version": "5.2.0", 2729 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", 2730 | "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", 2731 | "dev": true, 2732 | "requires": { 2733 | "ansi-regex": "^4.1.0" 2734 | } 2735 | } 2736 | } 2737 | }, 2738 | "string.prototype.trimend": { 2739 | "version": "1.0.1", 2740 | "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", 2741 | "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", 2742 | "dev": true, 2743 | "requires": { 2744 | "define-properties": "^1.1.3", 2745 | "es-abstract": "^1.17.5" 2746 | } 2747 | }, 2748 | "string.prototype.trimstart": { 2749 | "version": "1.0.1", 2750 | "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", 2751 | "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", 2752 | "dev": true, 2753 | "requires": { 2754 | "define-properties": "^1.1.3", 2755 | "es-abstract": "^1.17.5" 2756 | } 2757 | }, 2758 | "strip-ansi": { 2759 | "version": "6.0.0", 2760 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", 2761 | "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", 2762 | "dev": true, 2763 | "requires": { 2764 | "ansi-regex": "^5.0.0" 2765 | } 2766 | }, 2767 | "strip-bom": { 2768 | "version": "3.0.0", 2769 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 2770 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 2771 | "dev": true 2772 | }, 2773 | "strip-json-comments": { 2774 | "version": "3.1.1", 2775 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 2776 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 2777 | "dev": true 2778 | }, 2779 | "supports-color": { 2780 | "version": "5.5.0", 2781 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", 2782 | "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", 2783 | "dev": true, 2784 | "requires": { 2785 | "has-flag": "^3.0.0" 2786 | } 2787 | }, 2788 | "table": { 2789 | "version": "5.4.6", 2790 | "resolved": "https://registry.npmjs.org/table/-/table-5.4.6.tgz", 2791 | "integrity": "sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug==", 2792 | "dev": true, 2793 | "requires": { 2794 | "ajv": "^6.10.2", 2795 | "lodash": "^4.17.14", 2796 | "slice-ansi": "^2.1.0", 2797 | "string-width": "^3.0.0" 2798 | } 2799 | }, 2800 | "text-table": { 2801 | "version": "0.2.0", 2802 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 2803 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 2804 | "dev": true 2805 | }, 2806 | "through": { 2807 | "version": "2.3.8", 2808 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 2809 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 2810 | "dev": true 2811 | }, 2812 | "tmp": { 2813 | "version": "0.0.33", 2814 | "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", 2815 | "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", 2816 | "dev": true, 2817 | "requires": { 2818 | "os-tmpdir": "~1.0.2" 2819 | } 2820 | }, 2821 | "to-fast-properties": { 2822 | "version": "2.0.0", 2823 | "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", 2824 | "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=", 2825 | "dev": true 2826 | }, 2827 | "to-utf8": { 2828 | "version": "0.0.1", 2829 | "resolved": "https://registry.npmjs.org/to-utf8/-/to-utf8-0.0.1.tgz", 2830 | "integrity": "sha1-0Xrqcv8vujm55DYBvns/9y4ImFI=" 2831 | }, 2832 | "toidentifier": { 2833 | "version": "1.0.0", 2834 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", 2835 | "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" 2836 | }, 2837 | "tough-cookie": { 2838 | "version": "2.5.0", 2839 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 2840 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 2841 | "requires": { 2842 | "psl": "^1.1.28", 2843 | "punycode": "^2.1.1" 2844 | } 2845 | }, 2846 | "tsconfig-paths": { 2847 | "version": "3.9.0", 2848 | "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", 2849 | "integrity": "sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw==", 2850 | "dev": true, 2851 | "requires": { 2852 | "@types/json5": "^0.0.29", 2853 | "json5": "^1.0.1", 2854 | "minimist": "^1.2.0", 2855 | "strip-bom": "^3.0.0" 2856 | } 2857 | }, 2858 | "tslib": { 2859 | "version": "1.13.0", 2860 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.13.0.tgz", 2861 | "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", 2862 | "dev": true 2863 | }, 2864 | "tunnel-agent": { 2865 | "version": "0.6.0", 2866 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 2867 | "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", 2868 | "requires": { 2869 | "safe-buffer": "^5.0.1" 2870 | } 2871 | }, 2872 | "tweetnacl": { 2873 | "version": "0.14.5", 2874 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 2875 | "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" 2876 | }, 2877 | "type-check": { 2878 | "version": "0.4.0", 2879 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 2880 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 2881 | "dev": true, 2882 | "requires": { 2883 | "prelude-ls": "^1.2.1" 2884 | } 2885 | }, 2886 | "type-fest": { 2887 | "version": "0.8.1", 2888 | "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", 2889 | "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", 2890 | "dev": true 2891 | }, 2892 | "type-is": { 2893 | "version": "1.6.18", 2894 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", 2895 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", 2896 | "requires": { 2897 | "media-typer": "0.3.0", 2898 | "mime-types": "~2.1.24" 2899 | } 2900 | }, 2901 | "unpipe": { 2902 | "version": "1.0.0", 2903 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 2904 | "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" 2905 | }, 2906 | "uri-js": { 2907 | "version": "4.2.2", 2908 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", 2909 | "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", 2910 | "requires": { 2911 | "punycode": "^2.1.0" 2912 | } 2913 | }, 2914 | "utils-merge": { 2915 | "version": "1.0.1", 2916 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 2917 | "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" 2918 | }, 2919 | "uuid": { 2920 | "version": "3.4.0", 2921 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 2922 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 2923 | }, 2924 | "v8-compile-cache": { 2925 | "version": "2.1.1", 2926 | "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz", 2927 | "integrity": "sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ==", 2928 | "dev": true 2929 | }, 2930 | "validate-npm-package-license": { 2931 | "version": "3.0.4", 2932 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", 2933 | "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", 2934 | "dev": true, 2935 | "requires": { 2936 | "spdx-correct": "^3.0.0", 2937 | "spdx-expression-parse": "^3.0.0" 2938 | } 2939 | }, 2940 | "vary": { 2941 | "version": "1.1.2", 2942 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 2943 | "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" 2944 | }, 2945 | "verror": { 2946 | "version": "1.10.0", 2947 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 2948 | "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", 2949 | "requires": { 2950 | "assert-plus": "^1.0.0", 2951 | "core-util-is": "1.0.2", 2952 | "extsprintf": "^1.2.0" 2953 | } 2954 | }, 2955 | "which": { 2956 | "version": "2.0.2", 2957 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 2958 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 2959 | "dev": true, 2960 | "requires": { 2961 | "isexe": "^2.0.0" 2962 | } 2963 | }, 2964 | "word-wrap": { 2965 | "version": "1.2.3", 2966 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", 2967 | "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", 2968 | "dev": true 2969 | }, 2970 | "wrappy": { 2971 | "version": "1.0.2", 2972 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 2973 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 2974 | "dev": true 2975 | }, 2976 | "write": { 2977 | "version": "1.0.3", 2978 | "resolved": "https://registry.npmjs.org/write/-/write-1.0.3.tgz", 2979 | "integrity": "sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==", 2980 | "dev": true, 2981 | "requires": { 2982 | "mkdirp": "^0.5.1" 2983 | } 2984 | }, 2985 | "ws": { 2986 | "version": "5.2.2", 2987 | "resolved": "https://registry.npmjs.org/ws/-/ws-5.2.2.tgz", 2988 | "integrity": "sha512-jaHFD6PFv6UgoIVda6qZllptQsMlDEJkTQcybzzXDYM1XO9Y8em691FGMPmM46WGyLU4z9KMgQN+qrux/nhlHA==", 2989 | "requires": { 2990 | "async-limiter": "~1.0.0" 2991 | } 2992 | } 2993 | } 2994 | } 2995 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multiplayer-web-game-ably-starter-kit", 3 | "version": "1.0.0", 4 | "description": "This repository contains a starter template for building any realtime multiplayer game on the web with Ably using the client/server architecture. This project uses vanilla JS on the client-side and NodeJS on the server-side", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/Srushtika/multiplayer-web-game-ably-starter-kit.git" 13 | }, 14 | "author": "Srushtika Neelakantam", 15 | "license": "ISC", 16 | "bugs": { 17 | "url": "https://github.com/Srushtika/multiplayer-web-game-ably-starter-kit/issues" 18 | }, 19 | "homepage": "https://github.com/Srushtika/multiplayer-web-game-ably-starter-kit#readme", 20 | "dependencies": { 21 | "ably": "^1.2.1", 22 | "dotenv": "^8.2.0", 23 | "express": "^4.17.1" 24 | }, 25 | "devDependencies": { 26 | "eslint": "^7.8.0", 27 | "eslint-config-google": "^0.14.0", 28 | "eslint-config-node": "^4.1.0", 29 | "eslint-config-prettier": "^6.11.0", 30 | "eslint-plugin-node": "^11.1.0", 31 | "eslint-plugin-prettier": "^3.1.4", 32 | "prettier": "^2.1.1" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /public/js/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-jsdoc */ 2 | 'use strict'; 3 | 4 | /** 5 | * method to create or join a room on button click 6 | */ 7 | // eslint-disable-next-line no-unused-vars 8 | function createOrJoin(action) { 9 | localStorage.clear(); 10 | let roomCode; 11 | const isHost = action === 'create'; 12 | const nickname = document.getElementById(action + '-nickname').value; 13 | if (isHost) { 14 | roomCode = getRandomRoomId(); 15 | } else { 16 | roomCode = document.getElementById('join-room-code').value; 17 | } 18 | localStorage.setItem('isHost', isHost); 19 | localStorage.setItem('nickname', nickname); 20 | localStorage.setItem('roomCode', roomCode); 21 | window.location.replace('/game?roomCode=' + roomCode + '&isHost=' + isHost); 22 | } 23 | 24 | // method to generate a random room Id 25 | function getRandomRoomId() { 26 | return 'room-' + Math.random().toString(36).substr(2, 8); 27 | } 28 | -------------------------------------------------------------------------------- /public/js/modules/renderUpdates.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-jsdoc */ 2 | 'use strict'; 3 | const playerDivs = {}; 4 | const gameAreaParentDiv = document.getElementById('game-area'); 5 | 6 | // method to update the game display 7 | function updateGameDisplay( 8 | localGameState, 9 | myGameRoomCode, 10 | myClientId, 11 | myNickname, 12 | amIHost 13 | ) { 14 | // loop through all the players in the local game state 15 | // associative array and create or move their block as per their latest positon 16 | for (const item in localGameState) { 17 | if (!playerDivs[item] && localGameState[item].isAlive) { 18 | playerDivs[item] = {}; 19 | playerDivs[item].el = document.createElement('div'); 20 | playerDivs[item].el.classList.add('player-block'); 21 | gameAreaParentDiv.appendChild(playerDivs[item].el); 22 | playerDivs[item].el.style.left = localGameState[item].left + 'px'; 23 | playerDivs[item].el.style.top = localGameState[item].top + 'px'; 24 | playerDivs[item].el.style.backgroundColor = localGameState[item].color; 25 | if (item === myClientId) { 26 | document.getElementById( 27 | 'player-color-indicator' 28 | ).style.backgroundColor = localGameState[item].color; 29 | document.getElementById('player-room-info').innerHTML = 30 | '  in “' + 31 | myGameRoomCode + 32 | '” with clientId “' + 33 | myClientId + 34 | '” and nickname “' + 35 | myNickname + 36 | '”'; 37 | } 38 | } else if (playerDivs[item]) { 39 | playerDivs[item].el.style.left = localGameState[item].left + 'px'; 40 | playerDivs[item].el.style.top = localGameState[item].top + 'px'; 41 | } 42 | } 43 | } 44 | 45 | // method to add presence updates to a list 46 | function updatePresenceList(playerNickname, update, amIHost, totalPlayers) { 47 | const listItem = `
  • ${playerNickname} ${update}
  • `; 48 | if (amIHost) { 49 | const listEl = document.getElementById('host-players-list'); 50 | listEl.innerHTML += listItem; 51 | listEl.scrollTop = listEl.scrollHeight; 52 | document.getElementById('player-count').innerHTML = 53 | totalPlayers + ' player(s) online including you'; 54 | } else { 55 | const listEl = document.getElementById('not-host-players-list'); 56 | listEl.innerHTML += listItem; 57 | listEl.scrollTop = listEl.scrollHeight; 58 | } 59 | } 60 | 61 | // method to add game updates to a list 62 | function updateGameNewsList(playerNickname, update) { 63 | const listItem = `
  • ${playerNickname} ${update}
  • `; 64 | const listEl = document.getElementById('game-updates-list'); 65 | listEl.innerHTML += listItem; 66 | listEl.scrollTop = listEl.scrollHeight; 67 | } 68 | 69 | // method to add a blink effect adn remove the player from the game area 70 | // and the associative array 71 | function blinkAndRemovePlayer(playerId) { 72 | let blinkCount = 0; 73 | const blinkInterval = setInterval(function () { 74 | if (blinkCount >= 5) { 75 | gameAreaParentDiv.removeChild(playerDivs[playerId].el); 76 | delete playerDivs[playerId]; 77 | clearInterval(blinkInterval); 78 | } else { 79 | playerDivs[playerId].el.style.display = 80 | playerDivs[playerId].el.style.display === 'none' ? '' : 'none'; 81 | blinkCount++; 82 | } 83 | }, 250); 84 | } 85 | 86 | function showGameArea(amIHost) { 87 | if (amIHost) { 88 | document.getElementById('host-waiting').style.display = 'none'; 89 | document.getElementById('host-gameplay').style.display = 'block'; 90 | } else { 91 | document.getElementById('not-host-waiting').style.display = 'none'; 92 | document.getElementById('not-host-gameplay').style.display = 'block'; 93 | } 94 | } 95 | 96 | function showEndGameAlert(amIHost) { 97 | amIHost === true 98 | ? (document.getElementById('alert-host').style.display = 'block') 99 | : (document.getElementById('alert-not-host').style.display = 'block'); 100 | } 101 | 102 | function showRoomCodeToShare(roomCode) { 103 | const roomNotReadyDiv = document.getElementById('room-not-ready'); 104 | roomNotReadyDiv.style.display = 'none'; 105 | const roomReadyDiv = document.getElementById('room-ready'); 106 | roomReadyDiv.style.display = 'block'; 107 | document.getElementById('random-room-code').innerHTML = roomCode; 108 | } 109 | 110 | export { 111 | updateGameDisplay, 112 | updatePresenceList, 113 | updateGameNewsList, 114 | blinkAndRemovePlayer, 115 | showGameArea, 116 | showEndGameAlert, 117 | showRoomCodeToShare 118 | }; 119 | -------------------------------------------------------------------------------- /public/js/modules/utils.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-jsdoc */ 2 | 'use strict'; 3 | 4 | const PLAYER_GAME_AREA_WIDTH = 800; 5 | const PLAYER_GAME_AREA_HEIGHT = 400; 6 | 7 | function calculateAndMovePlayer(e, myLeft, myTop) { 8 | let playerMove = false; 9 | switch (e.keyCode) { 10 | case 37: // leftArrowKey 11 | if (myLeft >= 20) { 12 | myLeft -= 20; 13 | playerMove = true; 14 | } 15 | break; 16 | case 39: // rightArrowKey 17 | if (myLeft <= PLAYER_GAME_AREA_WIDTH - 20) { 18 | myLeft += 20; 19 | playerMove = true; 20 | } 21 | break; 22 | case 38: // upArrowKey 23 | if (myTop >= 20) { 24 | myTop -= 20; 25 | playerMove = true; 26 | } 27 | break; 28 | case 40: // downArrowKey 29 | if (myTop <= PLAYER_GAME_AREA_HEIGHT - 20) { 30 | myTop += 20; 31 | playerMove = true; 32 | } 33 | break; 34 | } 35 | 36 | return [playerMove, myLeft, myTop]; 37 | } 38 | 39 | export { calculateAndMovePlayer }; 40 | -------------------------------------------------------------------------------- /public/js/player.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-jsdoc */ 2 | 'use strict'; 3 | import * as renderModule from './modules/renderUpdates.js'; 4 | import * as utilsModule from './modules/utils.js'; 5 | 6 | const playerVars = { 7 | myClientId: '', 8 | localGameState: {}, 9 | isGameOn: false, 10 | totalPlayers: 1, 11 | endGameClicked: false, 12 | myNickname: localStorage.getItem('nickname'), 13 | myGameRoomCode: localStorage.getItem('roomCode'), 14 | amIHost: localStorage.getItem('isHost') === 'true' 15 | }; 16 | 17 | const channelNames = { 18 | myPublishChName: '', 19 | globalChName: '', 20 | myGameRoomChName: playerVars.myGameRoomCode + ':primary' 21 | }; 22 | 23 | const channelInstances = { 24 | myPublishChannel: '', 25 | myGameRoomChannel: '', 26 | globalChannel: '' 27 | }; 28 | 29 | window.copyCode = copyCode; 30 | window.startGame = startGame; 31 | window.endGame = endGame; 32 | window.fakeDeath = fakeDeath; 33 | 34 | document.addEventListener('keydown', move); 35 | 36 | // instantiate the Ably library 37 | // authenticate via Token Auth strategy 38 | const realtime = new Ably.Realtime({ 39 | authUrl: '/auth' 40 | }); 41 | 42 | // wait until connection with Ably is established 43 | realtime.connection.once('connected', () => { 44 | // save the current players clientId 45 | playerVars.myClientId = realtime.auth.clientId; 46 | // call a method to attach to channels 47 | attachChannels(); 48 | 49 | // request a new worker thread or enter existing 50 | if (playerVars.amIHost) { 51 | waitForGameRoom(); 52 | enterMainThread(); 53 | } else { 54 | enterGameRoom(); 55 | } 56 | receiveGlobalGameState(); 57 | }); 58 | 59 | // method to attach to channels 60 | function attachChannels() { 61 | // channel to publish player input 62 | channelNames.myPublishChName = 63 | playerVars.myGameRoomCode + ':player-ch-' + playerVars.myClientId; 64 | channelInstances.myPublishChannel = realtime.channels.get( 65 | channelNames.myPublishChName 66 | ); 67 | // channel to receive global game state updates 68 | channelInstances.myGameRoomChannel = realtime.channels.get( 69 | channelNames.myGameRoomChName 70 | ); 71 | } 72 | 73 | // method to wait for the worker thread to be ready 74 | function waitForGameRoom() { 75 | channelInstances.myGameRoomChannel.subscribe('thread-ready', (msg) => { 76 | channelInstances.globalChannel.detach(); 77 | enterGameRoom(); 78 | renderModule.showRoomCodeToShare(playerVars.myGameRoomCode); 79 | }); 80 | } 81 | 82 | // method to enter presence on the main server thread (for hosts only) 83 | function enterMainThread() { 84 | channelNames.globalChName = 'main-game-thread'; 85 | channelInstances.globalChannel = realtime.channels.get( 86 | channelNames.globalChName 87 | ); 88 | channelInstances.globalChannel.presence.enter({ 89 | nickname: playerVars.myNickname, 90 | roomCode: playerVars.myGameRoomCode, 91 | isHost: playerVars.amIHost 92 | }); 93 | } 94 | 95 | // method to enter presence on the game server worker thread (for all players) 96 | function enterGameRoom() { 97 | channelInstances.myGameRoomChannel.presence.enter({ 98 | nickname: playerVars.myNickname, 99 | isHost: playerVars.amIHost 100 | }); 101 | } 102 | 103 | // method to subscribe to global game state updates from game server 104 | function receiveGlobalGameState() { 105 | channelInstances.myGameRoomChannel.subscribe('game-state', (msg) => { 106 | if (msg.data.isGameOn && !playerVars.isGameOn) { 107 | renderModule.showGameArea(playerVars.amIHost); 108 | playerVars.isGameOn = true; 109 | } 110 | if (!msg.data.isGameOver) { 111 | updateLocalState(msg.data); 112 | } else { 113 | endGameAndCleanup(msg.data.isGameOn); 114 | } 115 | }); 116 | } 117 | 118 | // method to update local variables based on the update received from the server 119 | function updateLocalState(msgData) { 120 | playerVars.totalPlayers = msgData.totalPlayers; 121 | for (const item in msgData.globalPlayersState) { 122 | if ( 123 | playerVars.localGameState[item] && 124 | msgData.globalPlayersState[item].isConnected 125 | ) { 126 | handlePlayerStateUpdate(msgData.globalPlayersState[item], item); 127 | } else if ( 128 | playerVars.localGameState[item] && 129 | !msgData.globalPlayersState[item].isConnected 130 | ) { 131 | handleExistingPlayerLeft( 132 | msgData.data.globalPlayersState[item].nickname, 133 | item 134 | ); 135 | } else if ( 136 | !playerVars.localGameState[item] && 137 | msgData.globalPlayersState[item].isConnected 138 | ) { 139 | handleNewPlayerJoined(msgData.globalPlayersState[item], item); 140 | } 141 | } 142 | 143 | // call a method to update the game display as per latest state 144 | renderModule.updateGameDisplay( 145 | playerVars.localGameState, 146 | playerVars.myGameRoomCode, 147 | playerVars.myClientId, 148 | playerVars.myNickname, 149 | playerVars.amIHost 150 | ); 151 | } 152 | 153 | // method to end the game and detach from channels 154 | function endGameAndCleanup(isGlobalGameOn) { 155 | if (!isGlobalGameOn && playerVars.isGameOn) { 156 | renderModule.showEndGameAlert(playerVars.amIHost); 157 | channelInstances.myGameRoomChannel.detach(); 158 | channelInstances.myPublishChannel.detach(); 159 | realtime.connection.close(); 160 | // redirect to the homepage after a bit 161 | setTimeout(() => { 162 | if (!playerVars.endGameClicked) { 163 | window.location.replace('/?restart'); 164 | } 165 | playerVars.endGameClicked = true; 166 | }, 3000); 167 | } 168 | } 169 | 170 | // method to update the UI as per player state 171 | function handlePlayerStateUpdate(globalState, playerId) { 172 | const { isAlive, nickname } = globalState; 173 | if (isAlive) { 174 | playerVars.localGameState[playerId] = { 175 | ...globalState 176 | }; 177 | } else if (!isAlive && playerVars.localGameState[playerId].isAlive) { 178 | playerVars.localGameState[playerId].isAlive = false; 179 | renderModule.updateGameNewsList(nickname, 'died'); 180 | renderModule.blinkAndRemovePlayer(playerId); 181 | } 182 | } 183 | 184 | // method to handle a player leaving the game 185 | function handleExistingPlayerLeft(nickname, playerId) { 186 | renderModule.blinkAndRemovePlayer(playerId); 187 | if (!playerVars.isGameOn) { 188 | renderModule.updatePresenceList( 189 | nickname, 190 | 'left', 191 | playerVars.amIHost, 192 | playerVars.totalPlayers 193 | ); 194 | } else { 195 | renderModule.updateGameNewsList(nickname, 'left'); 196 | } 197 | delete playerVars.localGameState[playerId]; 198 | } 199 | 200 | // method to handle a new player joining the game 201 | function handleNewPlayerJoined(newPlayerState, playerId) { 202 | // create a new entry for this player and copy the latest state from the server 203 | playerVars.localGameState[playerId] = { ...newPlayerState }; 204 | // update the presence list 205 | renderModule.updatePresenceList( 206 | newPlayerState.nickname, 207 | 'joined', 208 | playerVars.amIHost, 209 | playerVars.totalPlayers 210 | ); 211 | } 212 | 213 | // method to handle keydown events 214 | function move(e) { 215 | const playerPosition = utilsModule.calculateAndMovePlayer( 216 | e, 217 | playerVars.localGameState[playerVars.myClientId].left, 218 | playerVars.localGameState[playerVars.myClientId].top 219 | ); 220 | if (playerPosition[0]) { 221 | channelInstances.myPublishChannel.publish('player-state', { 222 | left: playerPosition[1], 223 | top: playerPosition[2] 224 | }); 225 | } 226 | } 227 | 228 | // method to start the game 229 | // only game hosts have this button 230 | function startGame() { 231 | channelInstances.myPublishChannel.publish('start-game', { 232 | startGame: true 233 | }); 234 | } 235 | 236 | // method to end the game 237 | // only game hosts have this button 238 | function endGame() { 239 | channelInstances.myPublishChannel.publish('end-game', { 240 | endGame: true 241 | }); 242 | } 243 | 244 | // method to fake death 245 | // all players have this button 246 | function fakeDeath() { 247 | channelInstances.myPublishChannel.publish('player-dead', { 248 | deadPlayerId: playerVars.myClientId 249 | }); 250 | } 251 | 252 | // method to copy the room code to clipboard on button click 253 | function copyCode() { 254 | navigator.clipboard.writeText(playerVars.myGameRoomCode); 255 | } 256 | -------------------------------------------------------------------------------- /public/views/game-code-wrong.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 16 | 17 | Wrong game room 18 | 19 | 20 |

    This game room doesn't exist 🌚

    21 | 22 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /public/views/game-host.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 18 | 24 | 25 | 26 | My multiplayer game 27 | 28 | 32 | 33 | 34 | 35 | 38 |
    39 |
    40 |
    41 |
    42 |
    You are the host
    43 |
    44 |

    Creating your unique game room...

    45 |
    46 |
    47 |

    48 | Being the host gives you the control to start this game. 49 |

    50 |

    Share this room code to let others join

    51 |
    52 |

    53 | 56 |
    57 |

    58 | As the other players join, they will appear on the right. You 59 | can wait until all expected players are in, then start the game. 60 |

    61 | 64 |
    65 | 68 |
    69 |
    70 |
    71 |
    72 |
    Players' join / leave updates
    73 |
      77 |
      78 |
      79 |
      80 |
      81 |
      82 |
      83 |
      84 |

      You are

      85 |
      86 |
      87 |
      88 |

       

      89 |
      90 |
      91 | 92 |

      Use arrow keys to move your block around the game area

      93 |
      94 |
      95 | 98 | 101 |
      102 |
      103 |
      Game updates
      104 |
        108 |
        109 |
        110 | 111 | 112 | 113 | 118 | 123 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /public/views/game-not-host.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 13 | 19 | 20 | 21 | My multiplayer game 22 | 23 | 27 | 28 | 29 | 30 | 33 |
        34 |
        35 |
        36 |
        37 | You're in! Waiting for the host to start the game... 38 |
        39 | 42 |
          46 |
          47 |
          48 |
          49 |
          50 |
          51 |
          52 |

          You are

          53 |
          54 |
          55 |
          56 |

           

          57 |
          58 |
          59 | 60 |

          Use arrow keys to move your block around the game area

          61 |
          62 |
          63 | 66 |
          67 |
          68 |
          Game updates
          69 |
            73 |
            74 |
            75 | 76 | 77 | 78 | 83 | 88 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /public/views/game-started.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 16 | 17 | Game started 18 | 19 | 20 |

            Snoozers losers, this game has already started, sorry! ⏱

            21 | 22 | 23 | 28 | 29 | -------------------------------------------------------------------------------- /public/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | My multiplayer game 25 | 26 | 27 |
            28 |
            29 |

            30 | Multiplayer web game starter kit 31 |

            32 |

            powered by Ably Realtime

            33 |
            34 |
            35 |
            36 |
            37 | 63 |
            64 |
            70 |
            71 |
            72 | 78 |
            79 | 86 |
            87 |
            88 |
            94 |
            95 |
            96 | 102 |
            103 |
            104 | 110 |
            111 | 118 |
            119 |
            120 |
            121 |
            122 |
            123 |
            124 |
            125 | 126 | 127 | 128 | 133 | 138 | 143 | 144 | 145 | 146 | -------------------------------------------------------------------------------- /public/views/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background-color: #f5f5f5; 3 | text-align: center; 4 | } 5 | 6 | h1 { 7 | margin: 80px 0px 20px 0px; 8 | } 9 | 10 | h4 { 11 | margin-bottom: 50px; 12 | } 13 | 14 | input { 15 | text-align: center; 16 | } 17 | 18 | .content { 19 | text-align: center; 20 | } 21 | 22 | .card-flex { 23 | justify-content: center; 24 | } 25 | 26 | .container { 27 | width: 50%; 28 | text-align: center; 29 | } 30 | 31 | .room-input { 32 | display: inline-block; 33 | width: 50%; 34 | padding: 20px; 35 | text-align: center; 36 | } 37 | 38 | .random-code { 39 | padding: 2px 10px 10px 10px; 40 | display: flex; 41 | justify-content: center; 42 | } 43 | 44 | .copy { 45 | color: #0069d9; 46 | } 47 | 48 | .copy:hover { 49 | color: black; 50 | } 51 | 52 | .copy:active { 53 | color: gray; 54 | } 55 | 56 | .host-game { 57 | display: flex; 58 | width: 100%; 59 | } 60 | 61 | .room-ready { 62 | display: none; 63 | } 64 | 65 | .host-info { 66 | width: 50%; 67 | margin: 180px 90px; 68 | } 69 | 70 | .player-info { 71 | width: 50%; 72 | margin: 150px 90px; 73 | } 74 | 75 | .players-list { 76 | height: 440px; 77 | max-height: 440px; 78 | overflow-y: auto; 79 | } 80 | 81 | .not-host-game { 82 | width: 100%; 83 | } 84 | 85 | .waiting { 86 | width: 50%; 87 | margin: 150px auto; 88 | max-height: 440px; 89 | } 90 | 91 | .host-gameplay { 92 | display: none; 93 | } 94 | 95 | .not-host-gameplay { 96 | display: none; 97 | } 98 | 99 | .game-area { 100 | display: inline-block; 101 | width: 800px; 102 | height: 400px; 103 | padding: 20px; 104 | margin: 40px; 105 | text-align: center; 106 | position: relative; 107 | } 108 | 109 | .player-color-block { 110 | width: 10px; 111 | height: 10px; 112 | } 113 | 114 | .player-block { 115 | width: 10px; 116 | height: 10px; 117 | position: absolute; 118 | } 119 | 120 | .player-color { 121 | display: flex; 122 | justify-content: center; 123 | align-items: baseline; 124 | margin: 40px auto 5px auto; 125 | } 126 | 127 | .player-text { 128 | padding-right: 10px; 129 | } 130 | 131 | #alert-host { 132 | display: none; 133 | } 134 | 135 | #alert-not-host { 136 | display: none; 137 | } 138 | 139 | .game-updates-list { 140 | height: 150px; 141 | max-height: 440px; 142 | overflow-y: auto; 143 | } 144 | 145 | .game-updates-card { 146 | width: 50%; 147 | margin: 60px auto; 148 | max-height: 180px; 149 | } 150 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable require-jsdoc */ 2 | /** 3 | * This is the main server file, responsible for three operations: 4 | * 1. Server the HTML/JS files to the players 5 | * 2. Serve as an auth server to authenticate front-end clients 6 | * with Ably and assign a unique clientId to each player 7 | * 3. Create a new worker thread for each game room using NodeJS worker threads 8 | */ 9 | const { Worker, isMainThread, threadId } = require('worker_threads'); 10 | // eslint-disable-next-line no-unused-vars 11 | const envConfig = require('dotenv').config(); 12 | const express = require('express'); 13 | const Ably = require('ably'); 14 | 15 | const app = express(); 16 | const ABLY_API_KEY = process.env.ABLY_API_KEY; 17 | const globalGameChName = 'main-game-thread'; 18 | 19 | let globalChannel; 20 | const activeGameRooms = {}; 21 | let totalPlayersThroughout = 0; 22 | 23 | // instantiate the Ably library 24 | // eslint-disable-next-line new-cap 25 | const realtime = Ably.Realtime({ 26 | key: ABLY_API_KEY, 27 | // echo messages client option is true by default 28 | // making it false will prevent the server from 29 | // receiving the messages published by it 30 | echoMessages: false 31 | }); 32 | 33 | app.use(express.static('public')); 34 | 35 | /** 36 | * the auth endpoint will allow front end clients to 37 | * authenticate with Ably using Token auth 38 | */ 39 | app.get('/auth', (request, response) => { 40 | // assign each front-end client a unique clientId 41 | const tokenParams = { clientId: uniqueId() }; 42 | realtime.auth.createTokenRequest(tokenParams, function (err, tokenRequest) { 43 | if (err) { 44 | response 45 | .status(500) 46 | .send('Error requesting token: ' + JSON.stringify(err)); 47 | } else { 48 | // return the token request to the front-end client 49 | response.setHeader('Content-Type', 'application/json'); 50 | response.send(JSON.stringify(tokenRequest)); 51 | } 52 | }); 53 | }); 54 | 55 | // create a uniqueId to assign to clients on auth 56 | const uniqueId = function () { 57 | return 'id-' + Math.random().toString(36).substr(2, 16); 58 | }; 59 | 60 | // show the index.html file on the home page 61 | app.get('/', (request, response) => { 62 | response.sendFile(__dirname + '/public/views/index.html'); 63 | }); 64 | 65 | // show the game html page to the client 66 | app.get('/game', (request, response) => { 67 | const requestedRoomCode = request.query.roomCode; 68 | const isReqHost = request.query.isHost === 'true'; 69 | /** 70 | * check if the requested game room exists 71 | * and if the client is a host or not, 72 | * and serve the HTML files accordingly 73 | * */ 74 | if (!isReqHost && activeGameRooms[requestedRoomCode]) { 75 | if (!activeGameRooms[requestedRoomCode].isGameOn) { 76 | response.sendFile(__dirname + '/public/views/game-not-host.html'); 77 | } else { 78 | response.sendFile(__dirname + '/public/views/game-started.html'); 79 | } 80 | } else if (isReqHost) { 81 | response.sendFile(__dirname + '/public/views/game-host.html'); 82 | } else { 83 | response.sendFile(__dirname + '/public/views/game-code-wrong.html'); 84 | } 85 | }); 86 | 87 | // add a listener for the express server 88 | const listener = app.listen(process.env.PORT, () => { 89 | console.log('Your app is listening on port ' + listener.address().port); 90 | }); 91 | 92 | // wait until connection with Ably is established 93 | realtime.connection.once('connected', () => { 94 | // create the channel object 95 | globalChannel = realtime.channels.get(globalGameChName); 96 | // subscribe to new host players entering the game 97 | globalChannel.presence.subscribe('enter', (player) => { 98 | // generate a new worker thread for each game room 99 | generateNewGameThread( 100 | player.data.isHost, 101 | player.data.nickname, 102 | player.data.roomCode, 103 | player.clientId 104 | ); 105 | }); 106 | }); 107 | 108 | // method to create and update a new worker thread 109 | function generateNewGameThread( 110 | isHost, 111 | hostNickname, 112 | hostRoomCode, 113 | hostClientId 114 | ) { 115 | if (isHost && isMainThread) { 116 | const worker = new Worker('./game-server.js', { 117 | workerData: { 118 | hostNickname: hostNickname, 119 | hostRoomCode: hostRoomCode, 120 | hostClientId: hostClientId 121 | } 122 | }); 123 | console.log(`CREATING NEW THREAD WITH ID ${threadId}`); 124 | worker.on('error', (error) => { 125 | console.log(`WORKER EXITED DUE TO AN ERROR ${error}`); 126 | }); 127 | // wait for an update from the worker thread to confirm it's created 128 | worker.on('message', (msg) => { 129 | if (msg.roomCode && !msg.killWorker) { 130 | // save all live threads in an associative array 131 | activeGameRooms[msg.roomCode] = { 132 | roomCode: msg.roomCode, 133 | totalPlayers: msg.totalPlayers, 134 | isGameOn: msg.isGameOn 135 | }; 136 | totalPlayersThroughout += totalPlayersThroughout; 137 | } else if (msg.roomCode && msg.killWorker) { 138 | // delete the thread entry from the associative array 139 | // if the killWorker method was invoked in the thread 140 | totalPlayersThroughout -= msg.totalPlayers; 141 | delete activeGameRooms[msg.roomCode]; 142 | } 143 | }); 144 | // check if any error occurs when a worker thread quits 145 | worker.on('exit', (code) => { 146 | console.log(`WORKER EXITED WITH THREAD ID ${threadId}`); 147 | if (code !== 0) { 148 | console.log(`WORKER EXITED DUE TO AN ERROR WITH CODE ${code}`); 149 | } 150 | }); 151 | } 152 | } 153 | --------------------------------------------------------------------------------