From 6475fdd5db00166983b55af5fb26b73feb228bd2 Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Sun, 10 May 2020 14:16:12 +0200 Subject: [PATCH 01/16] feat(front): cleaned hanling of whiteboard info * Created an InfoService to centralized the logic * Added an info icon --- src/css/main.css | 20 ++++++++- src/index.html | 7 ++- src/js/const.js | 1 + src/js/icons.js | 4 +- src/js/main.js | 13 ++++-- src/js/services/InfoService.js | 79 ++++++++++++++++++++++++++++++++++ 6 files changed, 117 insertions(+), 7 deletions(-) create mode 100644 src/js/services/InfoService.js diff --git a/src/css/main.css b/src/css/main.css index a2d36f1..2b03121 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -1,3 +1,7 @@ +:root { + --selected-icon-bg-color: #dfdfdf; +} + body { position: relative; margin: 0px; @@ -78,7 +82,7 @@ button { } .whiteboard-tool.active:not(:disabled) { - background: #dfdfdf; + background: var(--selected-icon-bg-color); } #whiteboardThicknessSlider { @@ -114,3 +118,17 @@ button { min-width: 50px; cursor: pointer; } + +#displayWhiteboardInfoBtn.active { + background: var(--selected-icon-bg-color); +} + +#whiteboardInfoContainer { + position: absolute; + bottom: 10px; + right: 10px; +} + +.displayNone { + display: none; +} diff --git a/src/index.html b/src/index.html index f34965d..335fdf4 100644 --- a/src/index.html +++ b/src/index.html @@ -224,6 +224,10 @@ + +
@@ -247,7 +251,8 @@
-
+
+

Whiteboard information:

# msg. sent to server: 0

# msg. received from server: 0

diff --git a/src/js/const.js b/src/js/const.js index 9645fb8..b8db9ec 100644 --- a/src/js/const.js +++ b/src/js/const.js @@ -1,2 +1,3 @@ export const POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA = 1; // 1px export const POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA = 10; // 1ms +export const REFRESH_INFO_FREQUENCY = 5; // 5 times per second diff --git a/src/js/icons.js b/src/js/icons.js index c6bbf98..6c39955 100644 --- a/src/js/icons.js +++ b/src/js/icons.js @@ -18,6 +18,7 @@ import { faExpandArrowsAlt, faLock, faLockOpen, + faInfoCircle, } from "@fortawesome/free-solid-svg-icons"; import { faSquare, @@ -50,7 +51,8 @@ library.add( faFileAlt, faPlusSquare, faLock, - faLockOpen + faLockOpen, + faInfoCircle ); dom.i2svg(); diff --git a/src/js/main.js b/src/js/main.js index 221ecfd..c16dace 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -7,6 +7,7 @@ import { dom } from "@fortawesome/fontawesome-svg-core"; import pdfjsLib from "pdfjs-dist/webpack"; import shortcutFunctions from "./shortcutFunctions"; import ReadOnlyService from "./services/ReadOnlyService"; +import InfoService from "./services/InfoService"; function main() { var whiteboardId = getQueryVariable("whiteboardid"); @@ -48,10 +49,9 @@ function main() { signaling_socket.on("connect", function () { console.log("Websocket connected!"); - let messageReceivedCount = 0; signaling_socket.on("drawToWhiteboard", function (content) { whiteboard.handleEventsAndData(content, true); - $("#messageReceivedCount")[0].innerText = String(messageReceivedCount++); + InfoService.incrementNbMessagesReceived(); }); signaling_socket.on("refreshUserBadges", function () { @@ -84,7 +84,6 @@ function main() { $("#uploadWebDavBtn").show(); } - let messageSentCount = 0; whiteboard.loadWhiteboard("#whiteboardContainer", { //Load the whiteboard whiteboardId: whiteboardId, @@ -97,7 +96,7 @@ function main() { // } content["at"] = accessToken; signaling_socket.emit("drawToWhiteboard", content); - $("#messageSentCount")[0].innerText = String(messageSentCount++); + InfoService.incrementNbMessagesSent(); }, }); @@ -382,6 +381,10 @@ function main() { showBasicAlert("Copied Whiteboard-URL to clipboard.", { hideAfter: 2 }); }); + $("#displayWhiteboardInfoBtn").click(() => { + InfoService.toggleDisplayInfo(); + }); + var btnsMini = false; $("#minMaxBtn").click(function () { if (!btnsMini) { @@ -597,8 +600,10 @@ function main() { if (process.env.NODE_ENV === "production") { ReadOnlyService.activateReadOnlyMode(); + InfoService.hideInfo(); } else { ReadOnlyService.deactivateReadOnlyMode(); + InfoService.displayInfo(); } }); diff --git a/src/js/services/InfoService.js b/src/js/services/InfoService.js new file mode 100644 index 0000000..ed67fe6 --- /dev/null +++ b/src/js/services/InfoService.js @@ -0,0 +1,79 @@ +import { REFRESH_INFO_FREQUENCY } from "../const"; + +const REFRESH_INTERVAL = 1000 / REFRESH_INFO_FREQUENCY; + +/** + * Class the handle the information about the whiteboard + */ +class InfoService { + /** + * @type {boolean} + * @private + */ + _infoAreDisplayed = false; + + /** + * @type {number} + * @private + */ + _nbMessagesSent = 0; + + /** + * @type {number} + * @private + */ + _nbMessagesReceived = 0; + + /** + * Holds the interval Id + * @type {number} + * @private + */ + _refreshInfoIntervalId = undefined; + + incrementNbMessagesReceived() { + this._nbMessagesReceived++; + } + + incrementNbMessagesSent() { + this._nbMessagesSent++; + } + + refreshDisplayedInfo() { + $("#messageReceivedCount")[0].innerText = String(this._nbMessagesReceived); + $("#messageSentCount")[0].innerText = String(this._nbMessagesSent); + } + + displayInfo() { + $("#whiteboardInfoContainer").toggleClass("displayNone", false); + $("#displayWhiteboardInfoBtn").toggleClass("active", true); + this._infoAreDisplayed = true; + + this.refreshDisplayedInfo(); + this._refreshInfoIntervalId = setInterval(() => { + // refresh only on a specific interval to reduce + // refreshing cost + this.refreshDisplayedInfo(); + }, REFRESH_INTERVAL); + } + + hideInfo() { + $("#whiteboardInfoContainer").toggleClass("displayNone", true); + $("#displayWhiteboardInfoBtn").toggleClass("active", false); + this._infoAreDisplayed = false; + if (this._refreshInfoIntervalId) { + clearInterval(this._refreshInfoIntervalId); + this._refreshInfoIntervalId = undefined; + } + } + + toggleDisplayInfo() { + if (this._infoAreDisplayed) { + this.hideInfo(); + } else { + this.displayInfo(); + } + } +} + +export default new InfoService(); From 685caffd437e3b739c00307c0a68cb3bb12ac71f Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Sun, 10 May 2020 15:34:19 +0200 Subject: [PATCH 02/16] feat: 'new websocket' to share whiteboard info * share whiteboard info only on change and at specific frequency * front update to track nb user connected --- scripts/WhiteboardServerSideInfo.js | 45 +++++++++++++++++++++++++++++ scripts/config.js | 5 ++++ scripts/server-backend.js | 42 ++++++++++++++++++++++++--- src/index.html | 1 + src/js/main.js | 4 +++ src/js/services/InfoService.js | 16 ++++++++++ 6 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 scripts/WhiteboardServerSideInfo.js create mode 100644 scripts/config.js diff --git a/scripts/WhiteboardServerSideInfo.js b/scripts/WhiteboardServerSideInfo.js new file mode 100644 index 0000000..7b075be --- /dev/null +++ b/scripts/WhiteboardServerSideInfo.js @@ -0,0 +1,45 @@ +class WhiteboardServerSideInfo { + constructor() { + /** + * @type {number} + * @private + */ + this._nbConnectedUsers = 0; + + /** + * Variable to tell if these info have been sent or not + * + * @private + * @type {boolean} + */ + this._hasNonSentUpdates = false; + } + + incrementNbConnectedUsers() { + this._nbConnectedUsers++; + this._hasNonSentUpdates = true; + } + + decrementNbConnectedUsers() { + this._nbConnectedUsers--; + this._hasNonSentUpdates = true; + } + + hasConnectedUser() { + return this._nbConnectedUsers > 0; + } + + asObject() { + return { nbConnectedUsers: this._nbConnectedUsers }; + } + + infoWasSent() { + this._hasNonSentUpdates = false; + } + + shouldSendInfo() { + return this._hasNonSentUpdates; + } +} + +module.exports = WhiteboardServerSideInfo; diff --git a/scripts/config.js b/scripts/config.js new file mode 100644 index 0000000..5f97f52 --- /dev/null +++ b/scripts/config.js @@ -0,0 +1,5 @@ +const config = { + whiteboardInfoBroadcastFreq: 1, // once per second +}; + +module.exports = config; diff --git a/scripts/server-backend.js b/scripts/server-backend.js index 7d84260..35a41d3 100644 --- a/scripts/server-backend.js +++ b/scripts/server-backend.js @@ -1,6 +1,9 @@ -const { getArgs } = require("./utils"); const path = require("path"); +const { getArgs } = require("./utils"); +const config = require("./config"); +const WhiteboardServerSideInfo = require("./WhiteboardServerSideInfo"); + function startBackendServer(port) { var accessToken = ""; //Can be set here or as start parameter (node server.js --accesstoken=MYTOKEN) var disableSmallestScreen = false; //Can be set to true if you dont want to show (node server.js --disablesmallestscreen=true) @@ -205,9 +208,26 @@ function startBackendServer(port) { } var smallestScreenResolutions = {}; + + /** + * @type {Map} + */ + const infoByWhiteboard = new Map(); + + setInterval(() => { + infoByWhiteboard.forEach((info, whiteboardId) => { + if (info.shouldSendInfo()) { + io.sockets + .in(whiteboardId) + .compress(false) + .emit("whiteboardInfoUpdate", info.asObject()); + info.infoWasSent(); + } + }); + }, (1 / config.whiteboardInfoBroadcastFreq) * 1000); + io.on("connection", function (socket) { var whiteboardId = null; - socket.on("disconnect", function () { if ( smallestScreenResolutions && @@ -217,8 +237,15 @@ function startBackendServer(port) { ) { delete smallestScreenResolutions[whiteboardId][socket.id]; } - socket.compress(false).broadcast.emit("refreshUserBadges", null); //Removes old user Badges - sendSmallestScreenResolution(); + + const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId); + whiteboardServerSideInfo.decrementNbConnectedUsers(); + if (whiteboardServerSideInfo.hasConnectedUser()) { + socket.compress(false).broadcast.emit("refreshUserBadges", null); //Removes old user Badges + sendSmallestScreenResolution(); + } else { + infoByWhiteboard.delete(whiteboardId); + } }); socket.on("drawToWhiteboard", function (content) { @@ -236,6 +263,13 @@ function startBackendServer(port) { if (accessToken === "" || accessToken == content["at"]) { whiteboardId = content["wid"]; socket.join(whiteboardId); //Joins room name=wid + if (!infoByWhiteboard.has(whiteboardId)) { + infoByWhiteboard.set(whiteboardId, new WhiteboardServerSideInfo()); + } + + const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId); + whiteboardServerSideInfo.incrementNbConnectedUsers(); + smallestScreenResolutions[whiteboardId] = smallestScreenResolutions[whiteboardId] ? smallestScreenResolutions[whiteboardId] : {}; diff --git a/src/index.html b/src/index.html index 335fdf4..5284ee4 100644 --- a/src/index.html +++ b/src/index.html @@ -253,6 +253,7 @@

Whiteboard information:

+

# connected users: 0

# msg. sent to server: 0

# msg. received from server: 0

diff --git a/src/js/main.js b/src/js/main.js index c16dace..dbdaa04 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -49,6 +49,10 @@ function main() { signaling_socket.on("connect", function () { console.log("Websocket connected!"); + signaling_socket.on("whiteboardInfoUpdate", (info) => { + InfoService.updateInfoFromServer(info); + }); + signaling_socket.on("drawToWhiteboard", function (content) { whiteboard.handleEventsAndData(content, true); InfoService.incrementNbMessagesReceived(); diff --git a/src/js/services/InfoService.js b/src/js/services/InfoService.js index ed67fe6..49af8c0 100644 --- a/src/js/services/InfoService.js +++ b/src/js/services/InfoService.js @@ -12,6 +12,14 @@ class InfoService { */ _infoAreDisplayed = false; + /** + * Holds the number of user connected to the server + * + * @type {number} + * @private + */ + _nbConnectedUsers = 0; + /** * @type {number} * @private @@ -31,6 +39,13 @@ class InfoService { */ _refreshInfoIntervalId = undefined; + /** + * @param {number} nbConnectedUsers + */ + updateInfoFromServer({ nbConnectedUsers }) { + this._nbConnectedUsers = nbConnectedUsers; + } + incrementNbMessagesReceived() { this._nbMessagesReceived++; } @@ -42,6 +57,7 @@ class InfoService { refreshDisplayedInfo() { $("#messageReceivedCount")[0].innerText = String(this._nbMessagesReceived); $("#messageSentCount")[0].innerText = String(this._nbMessagesSent); + $("#connectedUsersCount")[0].innerText = String(this._nbConnectedUsers); } displayInfo() { From 4476ce3284b8c9370c00c6d8f2d5ae092c0650ca Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Sun, 10 May 2020 16:43:11 +0200 Subject: [PATCH 03/16] refacto: handling of smallest screen size as a whiteboard info * Also started a bit of config handling cleaning --- scripts/WhiteboardServerSideInfo.js | 55 +++++++++++++++- scripts/config.js | 40 ++++++++++++ scripts/server-backend.js | 98 +++++------------------------ src/index.html | 1 + src/js/main.js | 5 +- src/js/services/InfoService.js | 22 ++++++- src/js/whiteboard.js | 37 ++++++----- 7 files changed, 153 insertions(+), 105 deletions(-) diff --git a/scripts/WhiteboardServerSideInfo.js b/scripts/WhiteboardServerSideInfo.js index 7b075be..c846a87 100644 --- a/scripts/WhiteboardServerSideInfo.js +++ b/scripts/WhiteboardServerSideInfo.js @@ -1,4 +1,8 @@ +const config = require("./config"); + class WhiteboardServerSideInfo { + static defaultScreenResolution = { w: 1000, h: 1000 }; + constructor() { /** * @type {number} @@ -6,6 +10,12 @@ class WhiteboardServerSideInfo { */ this._nbConnectedUsers = 0; + /** + * @type {Map} + * @private + */ + this._screenResolutionByClients = new Map(); + /** * Variable to tell if these info have been sent or not * @@ -29,8 +39,37 @@ class WhiteboardServerSideInfo { return this._nbConnectedUsers > 0; } - asObject() { - return { nbConnectedUsers: this._nbConnectedUsers }; + /** + * Store information about the client's screen resolution + * + * @param {number} clientId + * @param {number} w client's width + * @param {number} h client's hight + */ + setScreenResolutionForClient(clientId, { w, h }) { + this._screenResolutionByClients.set(clientId, { w, h }); + this._hasNonSentUpdates = true; + } + + /** + * Delete the stored information about the client's screen resoltion + * @param clientId + */ + deleteScreenResolutionOfClient(clientId) { + this._screenResolutionByClients.delete(clientId); + this._hasNonSentUpdates = true; + } + + /** + * Get the smallest client's screen size on a whiteboard + * @return {{w: number, h: number}} + */ + getSmallestScreenResolution() { + const { _screenResolutionByClients: resolutions } = this; + return { + w: Math.min(...Array.from(resolutions.values()).map((res) => res.w)), + h: Math.min(...Array.from(resolutions.values()).map((res) => res.h)), + }; } infoWasSent() { @@ -40,6 +79,18 @@ class WhiteboardServerSideInfo { shouldSendInfo() { return this._hasNonSentUpdates; } + + asObject() { + const out = { + nbConnectedUsers: this._nbConnectedUsers, + }; + + if (!config.disableSmallestScreen) { + out.smallestScreenResolution = this.getSmallestScreenResolution(); + } + + return out; + } } module.exports = WhiteboardServerSideInfo; diff --git a/scripts/config.js b/scripts/config.js index 5f97f52..62a1f11 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -1,5 +1,45 @@ +const { getArgs } = require("./utils"); + const config = { + accessToken: "", + disableSmallestScreen: false, + webdav: false, + whiteboardInfoBroadcastFreq: 1, // once per second }; +/** + * Update the config based on the CLI args + * @param {object} startArgs + */ +function updateConfigFromStartArgs(startArgs) { + if (startArgs["accesstoken"]) { + config.accessToken = startArgs["accesstoken"]; + } + if (startArgs["disablesmallestscreen"]) { + config.disableSmallestScreen = true; + } + if (startArgs["webdav"]) { + config.webdav = true; + } +} + +/** + * Update the config based on the env variables + */ +function updateConfigFromEnv() { + if (process.env.accesstoken) { + config.accessToken = process.env.accesstoken; + } + if (process.env.disablesmallestscreen) { + config.disablesmallestscreen = true; + } + if (process.env.webdav) { + config.webdav = true; + } +} + +updateConfigFromEnv(); +updateConfigFromStartArgs(getArgs()); + module.exports = config; diff --git a/scripts/server-backend.js b/scripts/server-backend.js index 35a41d3..01ac837 100644 --- a/scripts/server-backend.js +++ b/scripts/server-backend.js @@ -1,13 +1,10 @@ const path = require("path"); -const { getArgs } = require("./utils"); const config = require("./config"); const WhiteboardServerSideInfo = require("./WhiteboardServerSideInfo"); function startBackendServer(port) { - var accessToken = ""; //Can be set here or as start parameter (node server.js --accesstoken=MYTOKEN) - var disableSmallestScreen = false; //Can be set to true if you dont want to show (node server.js --disablesmallestscreen=true) - var webdav = false; //Can be set to true if you want to allow webdav save (node server.js --webdav=true) + console.info("Starting backend server with config", config); var fs = require("fs-extra"); var express = require("express"); @@ -29,36 +26,8 @@ function startBackendServer(port) { server.listen(port); var io = require("socket.io")(server, { path: "/ws-api" }); console.log("Webserver & socketserver running on port:" + port); - if (process.env.accesstoken) { - accessToken = process.env.accesstoken; - } - if (process.env.disablesmallestscreen) { - disablesmallestscreen = true; - } - if (process.env.webdav) { - webdav = true; - } - var startArgs = getArgs(); - if (startArgs["accesstoken"]) { - accessToken = startArgs["accesstoken"]; - } - if (startArgs["disablesmallestscreen"]) { - disableSmallestScreen = true; - } - if (startArgs["webdav"]) { - webdav = true; - } - - if (accessToken !== "") { - console.log("AccessToken set to: " + accessToken); - } - if (disableSmallestScreen) { - console.log("Disabled showing smallest screen resolution!"); - } - if (webdav) { - console.log("Webdav save is enabled!"); - } + const { accessToken, webdav } = config; app.get("/api/loadwhiteboard", function (req, res) { var wid = req["query"]["wid"]; @@ -207,8 +176,6 @@ function startBackendServer(port) { } } - var smallestScreenResolutions = {}; - /** * @type {Map} */ @@ -229,20 +196,15 @@ function startBackendServer(port) { io.on("connection", function (socket) { var whiteboardId = null; socket.on("disconnect", function () { - if ( - smallestScreenResolutions && - smallestScreenResolutions[whiteboardId] && - socket && - socket.id - ) { - delete smallestScreenResolutions[whiteboardId][socket.id]; + const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId); + + if (socket && socket.id) { + whiteboardServerSideInfo.deleteScreenResolutionOfClient(socket.id); } - const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId); whiteboardServerSideInfo.decrementNbConnectedUsers(); if (whiteboardServerSideInfo.hasConnectedUser()) { socket.compress(false).broadcast.emit("refreshUserBadges", null); //Removes old user Badges - sendSmallestScreenResolution(); } else { infoByWhiteboard.delete(whiteboardId); } @@ -269,14 +231,10 @@ function startBackendServer(port) { const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId); whiteboardServerSideInfo.incrementNbConnectedUsers(); - - smallestScreenResolutions[whiteboardId] = smallestScreenResolutions[whiteboardId] - ? smallestScreenResolutions[whiteboardId] - : {}; - smallestScreenResolutions[whiteboardId][socket.id] = content[ - "windowWidthHeight" - ] || { w: 10000, h: 10000 }; - sendSmallestScreenResolution(); + whiteboardServerSideInfo.setScreenResolutionForClient( + socket.id, + content["windowWidthHeight"] || WhiteboardServerSideInfo.defaultScreenResolution + ); } else { socket.emit("wrongAccessToken", true); } @@ -284,38 +242,14 @@ function startBackendServer(port) { socket.on("updateScreenResolution", function (content) { content = escapeAllContentStrings(content); - if ( - smallestScreenResolutions[whiteboardId] && - (accessToken === "" || accessToken == content["at"]) - ) { - smallestScreenResolutions[whiteboardId][socket.id] = content[ - "windowWidthHeight" - ] || { w: 10000, h: 10000 }; - sendSmallestScreenResolution(); + if (accessToken === "" || accessToken == content["at"]) { + const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId); + whiteboardServerSideInfo.setScreenResolutionForClient( + socket.id, + content["windowWidthHeight"] || WhiteboardServerSideInfo.defaultScreenResolution + ); } }); - - function sendSmallestScreenResolution() { - if (disableSmallestScreen) { - return; - } - var smallestWidth = 10000; - var smallestHeight = 10000; - for (var i in smallestScreenResolutions[whiteboardId]) { - smallestWidth = - smallestWidth > smallestScreenResolutions[whiteboardId][i]["w"] - ? smallestScreenResolutions[whiteboardId][i]["w"] - : smallestWidth; - smallestHeight = - smallestHeight > smallestScreenResolutions[whiteboardId][i]["h"] - ? smallestScreenResolutions[whiteboardId][i]["h"] - : smallestHeight; - } - io.to(whiteboardId).emit("updateSmallestScreenResolution", { - w: smallestWidth, - h: smallestHeight, - }); - } }); //Prevent cross site scripting (xss) diff --git a/src/index.html b/src/index.html index 5284ee4..d5b3bd3 100644 --- a/src/index.html +++ b/src/index.html @@ -254,6 +254,7 @@

Whiteboard information:

# connected users: 0

+

Smallest screen resolution: Unknown.

# msg. sent to server: 0

# msg. received from server: 0

diff --git a/src/js/main.js b/src/js/main.js index dbdaa04..084c0fe 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -51,6 +51,7 @@ function main() { signaling_socket.on("whiteboardInfoUpdate", (info) => { InfoService.updateInfoFromServer(info); + whiteboard.updateSmallestScreenResolution(); }); signaling_socket.on("drawToWhiteboard", function (content) { @@ -69,10 +70,6 @@ function main() { } }); - signaling_socket.on("updateSmallestScreenResolution", function (widthHeight) { - whiteboard.updateSmallestScreenResolution(widthHeight["w"], widthHeight["h"]); - }); - signaling_socket.emit("joinWhiteboard", { wid: whiteboardId, at: accessToken, diff --git a/src/js/services/InfoService.js b/src/js/services/InfoService.js index 49af8c0..fbbe3d0 100644 --- a/src/js/services/InfoService.js +++ b/src/js/services/InfoService.js @@ -20,6 +20,13 @@ class InfoService { */ _nbConnectedUsers = 0; + /** + * + * @type {{w: number, h: number}} + * @private + */ + _smallestScreenResolution = undefined; + /** * @type {number} * @private @@ -41,9 +48,20 @@ class InfoService { /** * @param {number} nbConnectedUsers + * @param {{w: number, h: number}} smallestScreenResolution */ - updateInfoFromServer({ nbConnectedUsers }) { + updateInfoFromServer({ nbConnectedUsers, smallestScreenResolution = undefined }) { this._nbConnectedUsers = nbConnectedUsers; + if (smallestScreenResolution) { + this._smallestScreenResolution = smallestScreenResolution; + } + } + + /** + * @returns {(undefined|{w: number, h: number})} + */ + get smallestScreenResolution() { + return this._smallestScreenResolution; } incrementNbMessagesReceived() { @@ -58,6 +76,8 @@ class InfoService { $("#messageReceivedCount")[0].innerText = String(this._nbMessagesReceived); $("#messageSentCount")[0].innerText = String(this._nbMessagesSent); $("#connectedUsersCount")[0].innerText = String(this._nbConnectedUsers); + const { _smallestScreenResolution: ssr } = this; + $("#smallestScreenResolution")[0].innerText = ssr ? `(${ssr.w}, ${ssr.h})` : "Unknown"; } displayInfo() { diff --git a/src/js/whiteboard.js b/src/js/whiteboard.js index e9a9673..a2303b0 100644 --- a/src/js/whiteboard.js +++ b/src/js/whiteboard.js @@ -6,6 +6,7 @@ import { POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA, } from "./const"; import ReadOnlyService from "./services/ReadOnlyService"; +import InfoService from "./services/InfoService"; const RAD_TO_DEG = 180.0 / Math.PI; const DEG_TO_RAD = Math.PI / 180.0; @@ -363,7 +364,7 @@ const whiteboard = { ```
- +
``` @@ -1061,21 +1062,25 @@ const whiteboard = { _this.setTextboxFontColor(_this.latestActiveTextBoxId, color); } }, - updateSmallestScreenResolution(width, height) { - this.backgroundGrid.empty(); - if (width < $(window).width() || height < $(window).height()) { - this.backgroundGrid.append( - '
' - ); - this.backgroundGrid.append( - '
smallest screen participating
' - ); + updateSmallestScreenResolution() { + const { smallestScreenResolution } = InfoService; + if (smallestScreenResolution) { + const { w: width, h: height } = smallestScreenResolution; + this.backgroundGrid.empty(); + if (width < $(window).width() || height < $(window).height()) { + this.backgroundGrid.append( + '
' + ); + this.backgroundGrid.append( + '
smallest screen participating
' + ); + } } }, handleEventsAndData: function (content, isNewData, doneCallback) { From 0b1d1943ec935ae83a71a569bec582f16ab3c342 Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Sun, 10 May 2020 22:19:32 +0200 Subject: [PATCH 04/16] feat(backend): new config handling based on file * Config should be provided as Yaml file now * Other way to provide configuration are now deprectaed * The config format is checked agaist a schema with the json-schema standard * Tests are are added to the project (in the backend for config parsing) --- config.default.yml | 17 + package-lock.json | 4033 ++++++++++++++++++++++++++- package.json | 5 +- scripts/WhiteboardServerSideInfo.js | 2 +- scripts/config-schema.json | 46 + scripts/config.js | 87 +- scripts/server-backend.js | 6 +- scripts/utils.js | 73 + scripts/utils.test.js | 48 + 9 files changed, 4280 insertions(+), 37 deletions(-) create mode 100644 config.default.yml create mode 100644 scripts/config-schema.json create mode 100644 scripts/utils.test.js diff --git a/config.default.yml b/config.default.yml new file mode 100644 index 0000000..72ae98f --- /dev/null +++ b/config.default.yml @@ -0,0 +1,17 @@ +backend: + # TODO + accessToken: "" + # TODO + webdav: false + performance: + # Whiteboard information broadcasting frequency (in /s) + # => diminishing this will result in more latency + whiteboardInfoBroadcastFreq: 1 + +frontend: + # When an editable whiteboard is loading in a client, + # should it be started in read-only mode. + setReadOnlyOnWhiteboardLoad: false + # Show smallest screen indicator + showSmallestScreenIndicator: true + # performance: diff --git a/package-lock.json b/package-lock.json index 26d4ec3..31b7aee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -481,6 +481,24 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz", + "integrity": "sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, "@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", @@ -499,6 +517,15 @@ "@babel/helper-plugin-utils": "^7.8.0" } }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.8.3.tgz", + "integrity": "sha512-Zpg2Sgc++37kuFl6ppq2Q7Awc6E6AIW671x5PY8E/f7MCIyPPGK/EoeZXvvY3P42exZ3Q4/t3YOzP/HiN79jDg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.3" + } + }, "@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", @@ -1015,6 +1042,22 @@ "to-fast-properties": "^2.0.0" } }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "@cnakazawa/watch": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", + "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "dev": true, + "requires": { + "exec-sh": "^0.3.2", + "minimist": "^1.2.0" + } + }, "@fortawesome/fontawesome-common-types": { "version": "0.2.28", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.2.28.tgz", @@ -1057,6 +1100,714 @@ "@fortawesome/fontawesome-common-types": "^0.2.28" } }, + "@istanbuljs/load-nyc-config": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.0.0.tgz", + "integrity": "sha512-ZR0rq/f/E4f4XcgnDvtMWXCUJpi8eO0rssVhmztsZqLIEFA9UUP9zmpE0VxlM+kv/E1ul2I876Fwil2ayptDVg==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.2.tgz", + "integrity": "sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw==", + "dev": true + }, + "@jest/console": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.0.1.tgz", + "integrity": "sha512-9t1KUe/93coV1rBSxMmBAOIK3/HVpwxArCA1CxskKyRiv6o8J70V8C/V3OJminVCTa2M0hQI9AWRd5wxu2dAHw==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1", + "chalk": "^4.0.0", + "jest-message-util": "^26.0.1", + "jest-util": "^26.0.1", + "slash": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/core": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.0.1.tgz", + "integrity": "sha512-Xq3eqYnxsG9SjDC+WLeIgf7/8KU6rddBxH+SCt18gEpOhAGYC/Mq+YbtlNcIdwjnnT+wDseXSbU0e5X84Y4jTQ==", + "dev": true, + "requires": { + "@jest/console": "^26.0.1", + "@jest/reporters": "^26.0.1", + "@jest/test-result": "^26.0.1", + "@jest/transform": "^26.0.1", + "@jest/types": "^26.0.1", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^26.0.1", + "jest-config": "^26.0.1", + "jest-haste-map": "^26.0.1", + "jest-message-util": "^26.0.1", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.0.1", + "jest-resolve-dependencies": "^26.0.1", + "jest-runner": "^26.0.1", + "jest-runtime": "^26.0.1", + "jest-snapshot": "^26.0.1", + "jest-util": "^26.0.1", + "jest-validate": "^26.0.1", + "jest-watcher": "^26.0.1", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@jest/environment": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.0.1.tgz", + "integrity": "sha512-xBDxPe8/nx251u0VJ2dFAFz2H23Y98qdIaNwnMK6dFQr05jc+Ne/2np73lOAx+5mSBO/yuQldRrQOf6hP1h92g==", + "dev": true, + "requires": { + "@jest/fake-timers": "^26.0.1", + "@jest/types": "^26.0.1", + "jest-mock": "^26.0.1" + } + }, + "@jest/fake-timers": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.0.1.tgz", + "integrity": "sha512-Oj/kCBnTKhm7CR+OJSjZty6N1bRDr9pgiYQr4wY221azLz5PHi08x/U+9+QpceAYOWheauLP8MhtSVFrqXQfhg==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1", + "@sinonjs/fake-timers": "^6.0.1", + "jest-message-util": "^26.0.1", + "jest-mock": "^26.0.1", + "jest-util": "^26.0.1" + } + }, + "@jest/globals": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.0.1.tgz", + "integrity": "sha512-iuucxOYB7BRCvT+TYBzUqUNuxFX1hqaR6G6IcGgEqkJ5x4htNKo1r7jk1ji9Zj8ZMiMw0oB5NaA7k5Tx6MVssA==", + "dev": true, + "requires": { + "@jest/environment": "^26.0.1", + "@jest/types": "^26.0.1", + "expect": "^26.0.1" + } + }, + "@jest/reporters": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.0.1.tgz", + "integrity": "sha512-NWWy9KwRtE1iyG/m7huiFVF9YsYv/e+mbflKRV84WDoJfBqUrNRyDbL/vFxQcYLl8IRqI4P3MgPn386x76Gf2g==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^26.0.1", + "@jest/test-result": "^26.0.1", + "@jest/transform": "^26.0.1", + "@jest/types": "^26.0.1", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^26.0.1", + "jest-resolve": "^26.0.1", + "jest-util": "^26.0.1", + "jest-worker": "^26.0.0", + "node-notifier": "^7.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^4.1.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@jest/source-map": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.0.0.tgz", + "integrity": "sha512-S2Z+Aj/7KOSU2TfW0dyzBze7xr95bkm5YXNUqqCek+HE0VbNNSNzrRwfIi5lf7wvzDTSS0/ib8XQ1krFNyYgbQ==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + } + } + }, + "@jest/test-result": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.0.1.tgz", + "integrity": "sha512-oKwHvOI73ICSYRPe8WwyYPTtiuOAkLSbY8/MfWF3qDEd/sa8EDyZzin3BaXTqufir/O/Gzea4E8Zl14XU4Mlyg==", + "dev": true, + "requires": { + "@jest/console": "^26.0.1", + "@jest/types": "^26.0.1", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.0.1.tgz", + "integrity": "sha512-ssga8XlwfP8YjbDcmVhwNlrmblddMfgUeAkWIXts1V22equp2GMIHxm7cyeD5Q/B0ZgKPK/tngt45sH99yLLGg==", + "dev": true, + "requires": { + "@jest/test-result": "^26.0.1", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.0.1", + "jest-runner": "^26.0.1", + "jest-runtime": "^26.0.1" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + } + } + }, + "@jest/transform": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.0.1.tgz", + "integrity": "sha512-pPRkVkAQ91drKGbzCfDOoHN838+FSbYaEAvBXvKuWeeRRUD8FjwXkqfUNUZL6Ke48aA/1cqq/Ni7kVMCoqagWA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^26.0.1", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.0.1", + "jest-regex-util": "^26.0.0", + "jest-util": "^26.0.1", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@jest/types": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.0.1.tgz", + "integrity": "sha512-IbtjvqI9+eS1qFnOIEL7ggWmT+iK/U+Vde9cGWtYb/b6XgKb3X44ZAe/z9YZzoAAZ/E92m0DqrilF934IGNnQA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^1.1.1", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "@sinonjs/commons": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.7.2.tgz", + "integrity": "sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, "@sphinxxxx/color-conversion": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.2.2.tgz", @@ -1069,6 +1820,47 @@ "integrity": "sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==", "dev": true }, + "@types/babel__core": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.7.tgz", + "integrity": "sha512-RL62NqSFPCDK2FM1pSDH0scHpJvsXtZNiYlMB73DgPBaG1E38ZYVL+ei5EkWRbr+KC4YNiAUNBnRj+bgwpgjMw==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.1", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.1.tgz", + "integrity": "sha512-bBKm+2VPJcMRVwNhxKu8W+5/zT7pwNEqeokFOmbvVSqGzFneNxYcEBro9Ac7/N9tlsaPYnZLK8J1LWKkMsLAew==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.0.2.tgz", + "integrity": "sha512-/K6zCpeW7Imzgab2bLkLEbz0+1JlFSrUMdw7KoIIu+IUdu51GWaBZpd3y1VXGVXzynvGa4DaIaxNZHiON3GXUg==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.0.11.tgz", + "integrity": "sha512-ddHK5icION5U6q11+tV2f9Mo6CZVuT8GJKld2q9LqHSZbvLbH34Kcu2yFGckZut453+eQU6btIA3RihmnRgI+Q==", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", @@ -1092,12 +1884,46 @@ "@types/node": "*" } }, + "@types/graceful-fs": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.3.tgz", + "integrity": "sha512-AiHRaEB50LQg0pZmm659vNBb9f4SJ0qrAnteuzhSeAUcJKxoYgEnprg/83kppCnc2zvtCKbdZry1a5pVY3lOTQ==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/html-minifier-terser": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.0.0.tgz", "integrity": "sha512-q95SP4FdkmF0CwO0F2q0H6ZgudsApaY/yCtAQNRn1gduef5fGpyEphzy0YCq/N0UFvDSnLg5V8jFK/YGXlDiCw==", "dev": true }, + "@types/istanbul-lib-coverage": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz", + "integrity": "sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz", + "integrity": "sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*", + "@types/istanbul-lib-report": "*" + } + }, "@types/minimatch": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", @@ -1110,18 +1936,36 @@ "integrity": "sha512-WE4IOAC6r/yBZss1oQGM5zs2D7RuKR6Q+w+X2SouPofnWn+LbCqClRyhO3ZE7Ix8nmFgo/oVuuE01cJT2XB13A==", "dev": true }, + "@types/normalize-package-data": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", + "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", + "dev": true + }, "@types/parse-json": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", "dev": true }, + "@types/prettier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.0.0.tgz", + "integrity": "sha512-/rM+sWiuOZ5dvuVzV37sUuklsbg+JPOP8d+nNFlo2ZtfpzPiPvh1/gc8liWOLBqe+sR+ZM7guPaIcTt6UZTo7Q==", + "dev": true + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", "dev": true }, + "@types/stack-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", + "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "dev": true + }, "@types/tapable": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.5.tgz", @@ -1162,6 +2006,21 @@ "source-map": "^0.6.1" } }, + "@types/yargs": { + "version": "15.0.4", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz", + "integrity": "sha512-9T1auFmbPZoxHz0enUFlUuKRy3it01R+hlggyVUMtnCTQRunsQYifnSGb8hET4Xo8yiC0o0r1paW3ud5+rbURg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", + "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", + "dev": true + }, "@webassemblyjs/ast": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", @@ -1388,11 +2247,11 @@ "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, "ajv": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", - "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", + "version": "6.12.2", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.2.tgz", + "integrity": "sha512-k+V+hzjm5q/Mr8ef/1Y9goCmlsK4I6Sm74teeyGvFk1XrOsbsKLjEdrvny42CZ+a8sXbk8KWpY/bDwS+FLL2UQ==", "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -1415,6 +2274,23 @@ "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", "dev": true }, + "ansi-escapes": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.1.tgz", + "integrity": "sha512-JWF7ocqNrp8u9oqpgV+wH5ftbt+cfvv+PTjOvKLT3AdYly/LmORARfEVT1iyjwN+4MqE5UmVKoAdIBqeoCHgLA==", + "dev": true, + "requires": { + "type-fest": "^0.11.0" + }, + "dependencies": { + "type-fest": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.11.0.tgz", + "integrity": "sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==", + "dev": true + } + } + }, "ansi-html": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", @@ -1463,6 +2339,14 @@ "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", "dev": true }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "requires": { + "sprintf-js": "~1.0.2" + } + }, "arr-diff": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", @@ -1685,6 +2569,86 @@ "integrity": "sha512-m2CvfDW4+1qfDdsrtf4dwOslQC3yhbgyBFptncp4wvtdrDHqueW7slsYv4gArie056phvQFhT2nRcGS4bnm6mA==", "dev": true }, + "babel-jest": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.0.1.tgz", + "integrity": "sha512-Z4GGmSNQ8pX3WS1O+6v3fo41YItJJZsVxG5gIQ+HuB/iuAQBJxMTHTwz292vuYws1LnHfwSRgoqI+nxdy/pcvw==", + "dev": true, + "requires": { + "@jest/transform": "^26.0.1", + "@jest/types": "^26.0.1", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^26.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "babel-loader": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", @@ -1707,6 +2671,30 @@ "object.assign": "^4.1.0" } }, + "babel-plugin-istanbul": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", + "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^4.0.0", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.0.0.tgz", + "integrity": "sha512-+AuoehOrjt9irZL7DOt2+4ZaTM6dlu1s5TTS46JBa0/qem4dy7VNW3tMb96qeEqcIh20LD73TVNtmVEeymTG7w==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__traverse": "^7.0.6" + } + }, "babel-plugin-minify-builtins": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/babel-plugin-minify-builtins/-/babel-plugin-minify-builtins-0.5.0.tgz", @@ -1873,6 +2861,34 @@ "integrity": "sha1-viQcqBQEAwZ4t0hxcyK4nQyP4oA=", "dev": true }, + "babel-preset-current-node-syntax": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.2.tgz", + "integrity": "sha512-u/8cS+dEiK1SFILbOC8/rUI3ml9lboKuuMvZ/4aQnQmhecQAgPw5ew066C1ObnEAUmlx7dv/s2z52psWEtLNiw==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.0.0.tgz", + "integrity": "sha512-9ce+DatAa31DpR4Uir8g4Ahxs5K4W4L8refzt+qHWQANb6LhGcAEfIFgLUwk67oya2cCUd6t4eUMtO/z64ocNw==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^26.0.0", + "babel-preset-current-node-syntax": "^0.1.2" + } + }, "babel-preset-minify": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/babel-preset-minify/-/babel-preset-minify-0.5.1.tgz", @@ -2235,6 +3251,15 @@ "pkg-up": "^2.0.0" } }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, "buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -2356,6 +3381,15 @@ "integrity": "sha512-igMQ4dlqnf4tWv0xjaaE02op9AJ2oQzXKjWf4EuAHFN694Uo9/EfPVIPJcmn2WkU9RqozCxx5e2KPcVClHDbDw==", "dev": true }, + "capture-exit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", + "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "dev": true, + "requires": { + "rsvp": "^4.8.4" + } + }, "caseless": { "version": "0.12.0", "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", @@ -2372,6 +3406,12 @@ "supports-color": "^5.3.0" } }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, "chokidar": { "version": "2.1.8", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", @@ -2476,12 +3516,24 @@ "wrap-ansi": "^5.1.0" } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", "dev": true }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -3000,6 +4052,12 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "decimal.js": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.0.tgz", + "integrity": "sha512-vDPw+rDgn3bZe1+F/pyEwb1oMG2XTlRVgAa6B4KccTEpYgF8w6eQllVbQcfIJnZyvzFtFpxnpGtx8dd7DJp/Rw==", + "dev": true + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -3025,6 +4083,12 @@ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, "default-gateway": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", @@ -3131,12 +4195,24 @@ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", "dev": true }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, "detect-node": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", "dev": true }, + "diff-sequences": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.0.0.tgz", + "integrity": "sha512-JC/eHYEC3aSS0vZGjuoc4vHA0yAQTzhQQldXMeMF+JlxLGJlCO38Gma82NV9gk1jGFz8mDzUMeaKXvjRRdJ2dg==", + "dev": true + }, "diffie-hellman": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", @@ -3560,6 +4636,12 @@ "safe-buffer": "^5.1.1" } }, + "exec-sh": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.4.tgz", + "integrity": "sha512-sEFIkc61v75sWeOe72qyrqg2Qg0OuLESziUDk/O/z2qgS15y2gWVFrI6f2Qn/qw/0/NCfCEsmNA4zOjkwEZT1A==", + "dev": true + }, "execa": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", @@ -3575,6 +4657,12 @@ "strip-eof": "^1.0.0" } }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -3619,6 +4707,47 @@ "homedir-polyfill": "^1.0.1" } }, + "expect": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-26.0.1.tgz", + "integrity": "sha512-QcCy4nygHeqmbw564YxNbHTJlXh47dVID2BUP52cZFpLU9zHViMFK6h07cC1wf7GYCTIigTdAXhVua8Yl1FkKg==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1", + "ansi-styles": "^4.0.0", + "jest-get-type": "^26.0.0", + "jest-matcher-utils": "^26.0.1", + "jest-message-util": "^26.0.1", + "jest-regex-util": "^26.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, "express": { "version": "4.16.2", "resolved": "https://registry.npmjs.org/express/-/express-4.16.2.tgz", @@ -3753,9 +4882,9 @@ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=" }, "fast-deep-equal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", - "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==" }, "fast-json-stable-stringify": { "version": "2.0.0", @@ -3776,6 +4905,15 @@ "websocket-driver": ">=0.5.1" } }, + "fb-watchman": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", + "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, "figgy-pudding": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", @@ -4678,6 +5816,13 @@ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz", "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==" }, + "growly": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", + "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", + "dev": true, + "optional": true + }, "handle-thing": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", @@ -4810,6 +5955,12 @@ "parse-passwd": "^1.0.0" } }, + "hosted-git-info": { + "version": "2.8.8", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", + "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "dev": true + }, "hot-patcher": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/hot-patcher/-/hot-patcher-0.5.0.tgz", @@ -4841,6 +5992,12 @@ "integrity": "sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==", "dev": true }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, "html-minifier-terser": { "version": "5.0.5", "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.0.5.tgz", @@ -4980,6 +6137,12 @@ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", "dev": true }, + "human-signals": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", + "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "dev": true + }, "husky": { "version": "4.2.5", "resolved": "https://registry.npmjs.org/husky/-/husky-4.2.5.tgz", @@ -5333,6 +6496,15 @@ "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", "dev": true }, + "is-ci": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", + "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -5378,6 +6550,13 @@ } } }, + "is-docker": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", + "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", + "dev": true, + "optional": true + }, "is-extendable": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", @@ -5396,6 +6575,12 @@ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", "dev": true }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -5458,6 +6643,12 @@ "isobject": "^3.0.1" } }, + "is-potential-custom-element-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", + "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", + "dev": true + }, "is-regex": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", @@ -5521,6 +6712,2304 @@ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=" }, + "istanbul-lib-coverage": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", + "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", + "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", + "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "istanbul-reports": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", + "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-26.0.1.tgz", + "integrity": "sha512-29Q54kn5Bm7ZGKIuH2JRmnKl85YRigp0o0asTc6Sb6l2ch1DCXIeZTLLFy9ultJvhkTqbswF5DEx4+RlkmCxWg==", + "dev": true, + "requires": { + "@jest/core": "^26.0.1", + "import-local": "^3.0.2", + "jest-cli": "^26.0.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "import-local": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", + "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "jest-cli": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.0.1.tgz", + "integrity": "sha512-pFLfSOBcbG9iOZWaMK4Een+tTxi/Wcm34geqZEqrst9cZDkTQ1LZ2CnBrTlHWuYAiTMFr0EQeK52ScyFU8wK+w==", + "dev": true, + "requires": { + "@jest/core": "^26.0.1", + "@jest/test-result": "^26.0.1", + "@jest/types": "^26.0.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "is-ci": "^2.0.0", + "jest-config": "^26.0.1", + "jest-util": "^26.0.1", + "jest-validate": "^26.0.1", + "prompts": "^2.0.1", + "yargs": "^15.3.1" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "jest-changed-files": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.0.1.tgz", + "integrity": "sha512-q8LP9Sint17HaE2LjxQXL+oYWW/WeeXMPE2+Op9X3mY8IEGFVc14xRxFjUuXUbcPAlDLhtWdIEt59GdQbn76Hw==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1", + "execa": "^4.0.0", + "throat": "^5.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.2.tgz", + "integrity": "sha512-PD6G8QG3S4FK/XCGFbEQrDqO2AnMMsy0meR7lerlIOHAAbkuavGU/pOqprrlvfTNjvowivTeBsjebAL0NSoMxw==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "execa": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.0.1.tgz", + "integrity": "sha512-SCjM/zlBdOK8Q5TIjOn6iEHZaPHFsMoTxXQ2nvUvtPnuohz3H2dIozSg+etNR98dGoYUp2ENSKLL/XaMmbxVgw==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "get-stream": "^5.0.0", + "human-signals": "^1.1.1", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.0", + "onetime": "^5.1.0", + "signal-exit": "^3.0.2", + "strip-final-newline": "^2.0.0" + } + }, + "get-stream": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", + "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + }, + "is-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", + "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "jest-config": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.0.1.tgz", + "integrity": "sha512-9mWKx2L1LFgOXlDsC4YSeavnblN6A4CPfXFiobq+YYLaBMymA/SczN7xYTSmLaEYHZOcB98UdoN4m5uNt6tztg==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^26.0.1", + "@jest/types": "^26.0.1", + "babel-jest": "^26.0.1", + "chalk": "^4.0.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-environment-jsdom": "^26.0.1", + "jest-environment-node": "^26.0.1", + "jest-get-type": "^26.0.0", + "jest-jasmine2": "^26.0.1", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.0.1", + "jest-util": "^26.0.1", + "jest-validate": "^26.0.1", + "micromatch": "^4.0.2", + "pretty-format": "^26.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-diff": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.0.1.tgz", + "integrity": "sha512-odTcHyl5X+U+QsczJmOjWw5tPvww+y9Yim5xzqxVl/R1j4z71+fHW4g8qu1ugMmKdFdxw+AtQgs5mupPnzcIBQ==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^26.0.0", + "jest-get-type": "^26.0.0", + "pretty-format": "^26.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-docblock": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", + "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.0.1.tgz", + "integrity": "sha512-OTgJlwXCAR8NIWaXFL5DBbeS4QIYPuNASkzSwMCJO+ywo9BEa6TqkaSWsfR7VdbMLdgYJqSfQcIyjJCNwl5n4Q==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1", + "chalk": "^4.0.0", + "jest-get-type": "^26.0.0", + "jest-util": "^26.0.1", + "pretty-format": "^26.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-environment-jsdom": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.0.1.tgz", + "integrity": "sha512-u88NJa3aptz2Xix2pFhihRBAatwZHWwSiRLBDBQE1cdJvDjPvv7ZGA0NQBxWwDDn7D0g1uHqxM8aGgfA9Bx49g==", + "dev": true, + "requires": { + "@jest/environment": "^26.0.1", + "@jest/fake-timers": "^26.0.1", + "@jest/types": "^26.0.1", + "jest-mock": "^26.0.1", + "jest-util": "^26.0.1", + "jsdom": "^16.2.2" + }, + "dependencies": { + "abab": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", + "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "dev": true + }, + "acorn": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.2.0.tgz", + "integrity": "sha512-apwXVmYVpQ34m/i71vrApRrRKCWQnZZF1+npOD0WV5xZFfwWOmKGQ2RWlfdy9vWITsenisM8M0Qeq8agcFHNiQ==", + "dev": true + }, + "acorn-globals": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "acorn-walk": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.1.1.tgz", + "integrity": "sha512-wdlPY2tm/9XBr7QkKlq0WQVgiuGTX6YWPyRyBviSoScBuLfTVQhvwg6wJ369GJ/1nPfTLMfnrFIfjqVg6d+jQQ==", + "dev": true + }, + "browser-process-hrtime": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", + "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "dev": true + }, + "cssom": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", + "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "dev": true + } + } + }, + "data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "domexception": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "dev": true, + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true + } + } + }, + "escodegen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.1.tgz", + "integrity": "sha512-Bmt7NcRySdIfNPfU2ZoXDrrXsG9ZjvDxcAlMfDUgRBjLOWTuIACXPBFJH7Z+cLb40JeQco5toikyc9t9P8E9SQ==", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "jsdom": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.2.2.tgz", + "integrity": "sha512-pDFQbcYtKBHxRaP55zGXCJWgFHkDAYbKcsXEK/3Icu9nKYZkutUXfLBwbD+09XDutkYSHcgfQLZ0qvpAAm9mvg==", + "dev": true, + "requires": { + "abab": "^2.0.3", + "acorn": "^7.1.1", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.2.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.0", + "domexception": "^2.0.1", + "escodegen": "^1.14.1", + "html-encoding-sniffer": "^2.0.1", + "is-potential-custom-element-name": "^1.0.0", + "nwsapi": "^2.2.0", + "parse5": "5.1.1", + "request": "^2.88.2", + "request-promise-native": "^1.0.8", + "saxes": "^5.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^3.0.1", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.0.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0", + "ws": "^7.2.3", + "xml-name-validator": "^3.0.0" + } + }, + "mime-db": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", + "dev": true + }, + "mime-types": { + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", + "dev": true, + "requires": { + "mime-db": "1.44.0" + } + }, + "nwsapi": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", + "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", + "dev": true + }, + "parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true + }, + "qs": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", + "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", + "dev": true + }, + "request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "dev": true, + "requires": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "request-promise-core": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.3.tgz", + "integrity": "sha512-QIs2+ArIGQVp5ZYbWD5ZLCY29D5CfWizP8eWnm8FoGD1TX61veauETVQbrV60662V0oFBkrDOuaBI8XgtuyYAQ==", + "dev": true, + "requires": { + "lodash": "^4.17.15" + } + }, + "request-promise-native": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.8.tgz", + "integrity": "sha512-dapwLGqkHtwL5AEbfenuzjTYg35Jd6KPytsC2/TLkVMz8rm+tNt72MGUWT1RP/aYawMpN6HqbNGBQaRcBtjQMQ==", + "dev": true, + "requires": { + "request-promise-core": "1.1.3", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" + }, + "dependencies": { + "tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "requires": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + } + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + }, + "saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true + }, + "tough-cookie": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", + "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "dev": true, + "requires": { + "ip-regex": "^2.1.0", + "psl": "^1.1.28", + "punycode": "^2.1.1" + } + }, + "tr46": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", + "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "w3c-hr-time": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", + "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "dev": true, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "dev": true, + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.1.0.tgz", + "integrity": "sha512-vEIkwNi9Hqt4TV9RdnaBPNt+E2Sgmo3gePebCRgZ1R7g6d23+53zCTnuB0amKI4AXq6VM8jj2DUAa0S1vjJxkw==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true + } + } + }, + "ws": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz", + "integrity": "sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w==", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true + } + } + }, + "jest-environment-node": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.0.1.tgz", + "integrity": "sha512-4FRBWcSn5yVo0KtNav7+5NH5Z/tEgDLp7VRQVS5tCouWORxj+nI+1tOLutM07Zb2Qi7ja+HEDoOUkjBSWZg/IQ==", + "dev": true, + "requires": { + "@jest/environment": "^26.0.1", + "@jest/fake-timers": "^26.0.1", + "@jest/types": "^26.0.1", + "jest-mock": "^26.0.1", + "jest-util": "^26.0.1" + } + }, + "jest-get-type": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.0.0.tgz", + "integrity": "sha512-zRc1OAPnnws1EVfykXOj19zo2EMw5Hi6HLbFCSjpuJiXtOWAYIjNsHVSbpQ8bDX7L5BGYGI8m+HmKdjHYFF0kg==", + "dev": true + }, + "jest-haste-map": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.0.1.tgz", + "integrity": "sha512-J9kBl/EdjmDsvyv7CiyKY5+DsTvVOScenprz/fGqfLg/pm1gdjbwwQ98nW0t+OIt+f+5nAVaElvn/6wP5KO7KA==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1", + "@types/graceful-fs": "^4.1.2", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-serializer": "^26.0.0", + "jest-util": "^26.0.1", + "jest-worker": "^26.0.0", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7", + "which": "^2.0.2" + }, + "dependencies": { + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", + "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", + "dev": true, + "optional": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "jest-jasmine2": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.0.1.tgz", + "integrity": "sha512-ILaRyiWxiXOJ+RWTKupzQWwnPaeXPIoLS5uW41h18varJzd9/7I0QJGqg69fhTT1ev9JpSSo9QtalriUN0oqOg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^26.0.1", + "@jest/source-map": "^26.0.0", + "@jest/test-result": "^26.0.1", + "@jest/types": "^26.0.1", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^26.0.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^26.0.1", + "jest-matcher-utils": "^26.0.1", + "jest-message-util": "^26.0.1", + "jest-runtime": "^26.0.1", + "jest-snapshot": "^26.0.1", + "jest-util": "^26.0.1", + "pretty-format": "^26.0.1", + "throat": "^5.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-leak-detector": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.0.1.tgz", + "integrity": "sha512-93FR8tJhaYIWrWsbmVN1pQ9ZNlbgRpfvrnw5LmgLRX0ckOJ8ut/I35CL7awi2ecq6Ca4lL59bEK9hr7nqoHWPA==", + "dev": true, + "requires": { + "jest-get-type": "^26.0.0", + "pretty-format": "^26.0.1" + } + }, + "jest-matcher-utils": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.0.1.tgz", + "integrity": "sha512-PUMlsLth0Azen8Q2WFTwnSkGh2JZ8FYuwijC8NR47vXKpsrKmA1wWvgcj1CquuVfcYiDEdj985u5Wmg7COEARw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^26.0.1", + "jest-get-type": "^26.0.0", + "pretty-format": "^26.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-message-util": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.0.1.tgz", + "integrity": "sha512-CbK8uQREZ8umUfo8+zgIfEt+W7HAHjQCoRaNs4WxKGhAYBGwEyvxuK81FXa7VeB9pwDEXeeKOB2qcsNVCAvB7Q==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.0.1", + "@types/stack-utils": "^1.0.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-mock": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.0.1.tgz", + "integrity": "sha512-MpYTBqycuPYSY6xKJognV7Ja46/TeRbAZept987Zp+tuJvMN0YBWyyhG9mXyYQaU3SBI0TUlSaO5L3p49agw7Q==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1" + } + }, + "jest-pnp-resolver": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.1.tgz", + "integrity": "sha512-pgFw2tm54fzgYvc/OHrnysABEObZCUNFnhjoRjaVOCN8NYc032/gVjPaHD4Aq6ApkSieWtfKAFQtmDKAmhupnQ==", + "dev": true + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-resolve": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.0.1.tgz", + "integrity": "sha512-6jWxk0IKZkPIVTvq6s72RH735P8f9eCJW3IM5CX/SJFeKq1p2cZx0U49wf/SdMlhaB/anann5J2nCJj6HrbezQ==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-pnp-resolver": "^1.2.1", + "jest-util": "^26.0.1", + "read-pkg-up": "^7.0.1", + "resolve": "^1.17.0", + "slash": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "resolve": { + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", + "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-resolve-dependencies": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.0.1.tgz", + "integrity": "sha512-9d5/RS/ft0vB/qy7jct/qAhzJsr6fRQJyGAFigK3XD4hf9kIbEH5gks4t4Z7kyMRhowU6HWm/o8ILqhaHdSqLw==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1", + "jest-regex-util": "^26.0.0", + "jest-snapshot": "^26.0.1" + } + }, + "jest-runner": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.0.1.tgz", + "integrity": "sha512-CApm0g81b49Znm4cZekYQK67zY7kkB4umOlI2Dx5CwKAzdgw75EN+ozBHRvxBzwo1ZLYZ07TFxkaPm+1t4d8jA==", + "dev": true, + "requires": { + "@jest/console": "^26.0.1", + "@jest/environment": "^26.0.1", + "@jest/test-result": "^26.0.1", + "@jest/types": "^26.0.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-config": "^26.0.1", + "jest-docblock": "^26.0.0", + "jest-haste-map": "^26.0.1", + "jest-jasmine2": "^26.0.1", + "jest-leak-detector": "^26.0.1", + "jest-message-util": "^26.0.1", + "jest-resolve": "^26.0.1", + "jest-runtime": "^26.0.1", + "jest-util": "^26.0.1", + "jest-worker": "^26.0.0", + "source-map-support": "^0.5.6", + "throat": "^5.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-runtime": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.0.1.tgz", + "integrity": "sha512-Ci2QhYFmANg5qaXWf78T2Pfo6GtmIBn2rRaLnklRyEucmPccmCKvS9JPljcmtVamsdMmkyNkVFb9pBTD6si9Lw==", + "dev": true, + "requires": { + "@jest/console": "^26.0.1", + "@jest/environment": "^26.0.1", + "@jest/fake-timers": "^26.0.1", + "@jest/globals": "^26.0.1", + "@jest/source-map": "^26.0.0", + "@jest/test-result": "^26.0.1", + "@jest/transform": "^26.0.1", + "@jest/types": "^26.0.1", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.4", + "jest-config": "^26.0.1", + "jest-haste-map": "^26.0.1", + "jest-message-util": "^26.0.1", + "jest-mock": "^26.0.1", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.0.1", + "jest-snapshot": "^26.0.1", + "jest-util": "^26.0.1", + "jest-validate": "^26.0.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^15.3.1" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "string-width": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", + "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "yargs": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.3.1.tgz", + "integrity": "sha512-92O1HWEjw27sBfgmXiixJWT5hRBp2eobqXicLtPBIDBhYB+1HpwZlXmbW2luivBJHBzki+7VyCLRtAkScbTBQA==", + "dev": true, + "requires": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.1" + } + }, + "yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "dev": true, + "requires": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + } + } + } + }, + "jest-serializer": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.0.0.tgz", + "integrity": "sha512-sQGXLdEGWFAE4wIJ2ZaIDb+ikETlUirEOBsLXdoBbeLhTHkZUJwgk3+M8eyFizhM6le43PDCCKPA1hzkSDo4cQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.4" + }, + "dependencies": { + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + } + } + }, + "jest-snapshot": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.0.1.tgz", + "integrity": "sha512-jxd+cF7+LL+a80qh6TAnTLUZHyQoWwEHSUFJjkw35u3Gx+BZUNuXhYvDqHXr62UQPnWo2P6fvQlLjsU93UKyxA==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0", + "@jest/types": "^26.0.1", + "@types/prettier": "^2.0.0", + "chalk": "^4.0.0", + "expect": "^26.0.1", + "graceful-fs": "^4.2.4", + "jest-diff": "^26.0.1", + "jest-get-type": "^26.0.0", + "jest-matcher-utils": "^26.0.1", + "jest-message-util": "^26.0.1", + "jest-resolve": "^26.0.1", + "make-dir": "^3.0.0", + "natural-compare": "^1.4.0", + "pretty-format": "^26.0.1", + "semver": "^7.3.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-util": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.0.1.tgz", + "integrity": "sha512-byQ3n7ad1BO/WyFkYvlWQHTsomB6GIewBh8tlGtusiylAlaxQ1UpS0XYH0ngOyhZuHVLN79Qvl6/pMiDMSSG1g==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "make-dir": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "graceful-fs": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", + "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-validate": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.0.1.tgz", + "integrity": "sha512-u0xRc+rbmov/VqXnX3DlkxD74rHI/CfS5xaV2VpeaVySjbb1JioNVOyly5b56q2l9ZKe7bVG5qWmjfctkQb0bA==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1", + "camelcase": "^6.0.0", + "chalk": "^4.0.0", + "jest-get-type": "^26.0.0", + "leven": "^3.1.0", + "pretty-format": "^26.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "camelcase": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.0.0.tgz", + "integrity": "sha512-8KMDF1Vz2gzOq54ONPJS65IvTUaB1cHJ2DMM7MbPmLZljDH1qpzzLsWdiN9pHh6qvkRVDTi/07+eNGch/oLU4w==", + "dev": true + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-watcher": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.0.1.tgz", + "integrity": "sha512-pdZPydsS8475f89kGswaNsN3rhP6lnC3/QDCppP7bg1L9JQz7oU9Mb/5xPETk1RHDCWeqmVC47M4K5RR7ejxFw==", + "dev": true, + "requires": { + "@jest/test-result": "^26.0.1", + "@jest/types": "^26.0.1", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^26.0.1", + "string-length": "^4.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", + "integrity": "sha512-N9oWFcegS0sFr9oh1oz2d7Npos6vNoWW9HvtCg5N1KRFpUhaAhvTv5Y58g880fZaEYSNm3qDz8SU1UrGvp+n7A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-worker": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.0.0.tgz", + "integrity": "sha512-pPaYa2+JnwmiZjK9x7p9BoZht+47ecFCDFA/CJxspHzeDvQcfVBLWzCiWyo+EGrSiQMWZtCFo9iSvMZnAAo8vw==", + "dev": true, + "requires": { + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "jquery": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.0.tgz", @@ -5539,6 +9028,22 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "dev": true }, + "js-yaml": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", + "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "dependencies": { + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" + } + } + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -5666,6 +9171,12 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", @@ -5794,6 +9305,15 @@ "semver": "^5.6.0" } }, + "makeerror": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", + "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "dev": true, + "requires": { + "tmpl": "1.0.x" + } + }, "map-age-cleaner": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz", @@ -6100,6 +9620,12 @@ "to-regex": "^3.0.1" } }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true + }, "negotiator": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz", @@ -6138,6 +9664,12 @@ "integrity": "sha512-7ASaDa3pD+lJ3WvXFsxekJQelBKRpne+GOVbLbtHYdd7pFspyeuJHnWfLplGf3SwKGbfs/aYl5V/JCIaHVUKKQ==", "dev": true }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", + "dev": true + }, "node-libs-browser": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", @@ -6177,12 +9709,81 @@ } } }, + "node-modules-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", + "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", + "dev": true + }, + "node-notifier": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-7.0.0.tgz", + "integrity": "sha512-y8ThJESxsHcak81PGpzWwQKxzk+5YtP3IxR8AYdpXQ1IB6FmcVzFdZXrkPin49F/DKUCfeeiziB8ptY9npzGuA==", + "dev": true, + "optional": true, + "requires": { + "growly": "^1.3.0", + "is-wsl": "^2.1.1", + "semver": "^7.2.1", + "shellwords": "^0.1.1", + "uuid": "^7.0.3", + "which": "^2.0.2" + }, + "dependencies": { + "is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "optional": true, + "requires": { + "is-docker": "^2.0.0" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==", + "dev": true, + "optional": true + }, + "uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "dev": true, + "optional": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "optional": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, "node-releases": { "version": "1.1.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.53.tgz", "integrity": "sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ==", "dev": true }, + "normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "requires": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6425,6 +10026,12 @@ "integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=", "dev": true }, + "p-each-series": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", + "integrity": "sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==", + "dev": true + }, "p-finally": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", @@ -6682,6 +10289,12 @@ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, + "picomatch": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", + "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==", + "dev": true + }, "pify": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", @@ -6703,6 +10316,15 @@ "pinkie": "^2.0.0" } }, + "pirates": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", + "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, "pkg-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", @@ -6915,6 +10537,51 @@ "utila": "~0.4" } }, + "pretty-format": { + "version": "26.0.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.0.1.tgz", + "integrity": "sha512-SWxz6MbupT3ZSlL0Po4WF/KujhQaVehijR2blyRDCzk9e45EaYMVhMBn49fnRuHxtkSpXTes1GxNpVmH86Bxfw==", + "dev": true, + "requires": { + "@jest/types": "^26.0.1", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^16.12.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", + "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "dev": true, + "requires": { + "@types/color-name": "^1.1.1", + "color-convert": "^2.0.1" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + } + } + }, "pretty-quick": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-2.0.1.tgz", @@ -7098,6 +10765,16 @@ "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", "dev": true }, + "prompts": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", + "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.4" + } + }, "proxy-addr": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.2.tgz", @@ -7235,6 +10912,94 @@ "unpipe": "1.0.0" } }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true + }, + "read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "requires": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "dependencies": { + "type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", + "dev": true + } + } + }, + "read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "requires": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + } + } + }, "readable-stream": { "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", @@ -7596,6 +11361,12 @@ "inherits": "^2.0.1" } }, + "rsvp": { + "version": "4.8.5", + "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", + "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", + "dev": true + }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", @@ -7624,6 +11395,23 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "sane": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", + "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", + "dev": true, + "requires": { + "@cnakazawa/watch": "^1.0.3", + "anymatch": "^2.0.0", + "capture-exit": "^2.0.0", + "exec-sh": "^0.3.2", + "execa": "^1.0.0", + "fb-watchman": "^2.0.0", + "micromatch": "^3.1.4", + "minimist": "^1.1.1", + "walker": "~1.0.5" + } + }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -7817,12 +11605,25 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shellwords": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", + "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "dev": true, + "optional": true + }, "signal-exit": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", "dev": true }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "slash": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", @@ -8080,6 +11881,38 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, + "spdx-correct": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz", + "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz", + "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz", + "integrity": "sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q==", + "dev": true + }, "spdy": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", @@ -8161,6 +11994,11 @@ "extend-shallow": "^3.0.0" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" + }, "sshpk": { "version": "1.16.1", "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", @@ -8186,6 +12024,23 @@ "figgy-pudding": "^3.5.1" } }, + "stack-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.2.tgz", + "integrity": "sha512-0H7QK2ECz3fyZMzQ8rH0j2ykpfbnd20BFtfg/SqVC2+sCTtcw0aDTGB7dk+de4U4uUeuz6nOtJcrkFFLG1B0Rg==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -8256,6 +12111,33 @@ "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", "dev": true }, + "string-length": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.1.tgz", + "integrity": "sha512-PKyXUd0LK0ePjSOnWn34V2uD6acUWev9uy0Ft05k0E8xRW+SKcA0F7eMr7h5xlzfn+4O3N+55rduYyet3Jk+jw==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.0" + } + } + } + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -8327,6 +12209,12 @@ "ansi-regex": "^4.1.0" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, "strip-eof": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", @@ -8371,6 +12259,33 @@ "has-flag": "^3.0.0" } }, + "supports-hyperlinks": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.1.0.tgz", + "integrity": "sha512-zoE5/e+dnEijk6ASB6/qrK+oYdm2do1hjoLWrqUC/8WEIW1gbxFcKuBof7sW8ArN6e+AYvsE8HBGiVRWL/F5CA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "dependencies": { + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", + "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "symbol-tree": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.2.tgz", @@ -8382,6 +12297,16 @@ "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", "dev": true }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, "terser": { "version": "4.6.11", "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.11.tgz", @@ -8431,6 +12356,23 @@ } } }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "dev": true + }, "through2": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", @@ -8456,6 +12398,12 @@ "setimmediate": "^1.0.4" } }, + "tmpl": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", + "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", + "dev": true + }, "to-array": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", @@ -8571,6 +12519,18 @@ "prelude-ls": "~1.1.2" } }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true + }, + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + }, "type-is": { "version": "1.6.15", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.15.tgz", @@ -8586,6 +12546,15 @@ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "dev": true }, + "typedarray-to-buffer": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", + "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", @@ -8822,6 +12791,35 @@ "integrity": "sha512-CNmdbwQMBjwr9Gsmohvm0pbL954tJrNzf6gWL3K+QMQf00PF7ERGrEiLgjuU3mKreLC2MeGhUsNV9ybTbLgd3w==", "dev": true }, + "v8-to-istanbul": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz", + "integrity": "sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", + "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", + "dev": true + } + } + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, "vanilla-picker": { "version": "2.10.1", "resolved": "https://registry.npmjs.org/vanilla-picker/-/vanilla-picker-2.10.1.tgz", @@ -8870,6 +12868,15 @@ "xml-name-validator": "^3.0.0" } }, + "walker": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", + "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "dev": true, + "requires": { + "makeerror": "1.0.x" + } + }, "watchpack": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", @@ -9832,6 +13839,18 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", "dev": true }, + "write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, "ws": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", diff --git a/package.json b/package.json index 3a99b54..3a5d003 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "webpack --config config/webpack.build.js", "start:dev": "node scripts/server.js --mode=development", "start:prod": "npm run build && node scripts/server.js --mode=production", - "test": "echo \"No tests needed!\" && exit 1", + "test": "jest", "pretty-quick": "pretty-quick", "format": "prettier --write .", "style": "prettier --check ." @@ -28,10 +28,12 @@ } }, "dependencies": { + "ajv": "6.12.2", "dompurify": "^2.0.7", "express": "4.*", "formidable": "1.*", "fs-extra": "7.*", + "js-yaml": "3.13.1", "jsdom": "^14.0.0", "pdfjs-dist": "^2.3.200", "socket.io": "2.*", @@ -54,6 +56,7 @@ "css-loader": "^3.5.2", "html-webpack-plugin": "^4.2.0", "husky": "^4.2.5", + "jest": "26.0.1", "jquery": "^3.2.1", "jquery-ui": "^1.12.1", "keymage": "^1.1.3", diff --git a/scripts/WhiteboardServerSideInfo.js b/scripts/WhiteboardServerSideInfo.js index c846a87..8b148a3 100644 --- a/scripts/WhiteboardServerSideInfo.js +++ b/scripts/WhiteboardServerSideInfo.js @@ -85,7 +85,7 @@ class WhiteboardServerSideInfo { nbConnectedUsers: this._nbConnectedUsers, }; - if (!config.disableSmallestScreen) { + if (config.frontend.showSmallestScreenIndicator) { out.smallestScreenResolution = this.getSmallestScreenResolution(); } diff --git a/scripts/config-schema.json b/scripts/config-schema.json new file mode 100644 index 0000000..0370f35 --- /dev/null +++ b/scripts/config-schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Whiteboard config", + "type": "object", + "properties": { + "backend": { + "type": "object", + "required": ["accessToken", "performance", "webdav"], + "additionalProperties": false, + "properties": { + "accessToken": { + "type": "string" + }, + "webdav": { + "type": "boolean" + }, + "performance": { + "additionalProperties": false, + "type": "object", + "required": ["whiteboardInfoBroadcastFreq"], + "properties": { + "whiteboardInfoBroadcastFreq": { + "type": "number", + "minimum": 0 + } + } + } + } + }, + "frontend": { + "type": "object", + "additionalProperties": false, + "required": ["setReadOnlyOnWhiteboardLoad", "showSmallestScreenIndicator"], + "properties": { + "setReadOnlyOnWhiteboardLoad": { + "type": "boolean" + }, + "showSmallestScreenIndicator": { + "type": "boolean" + } + } + } + }, + "required": ["backend", "frontend"], + "additionalProperties": false +} diff --git a/scripts/config.js b/scripts/config.js index 62a1f11..2e94c1e 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -1,45 +1,84 @@ -const { getArgs } = require("./utils"); +const util = require("util"); -const config = { - accessToken: "", - disableSmallestScreen: false, - webdav: false, +const { + getArgs, + getDefaultConfig, + getConfig, + deepMergeConfigs, + isConfigValid, +} = require("./utils"); - whiteboardInfoBroadcastFreq: 1, // once per second -}; +const defaultConfig = getDefaultConfig(); + +const cliArgs = getArgs(); +let userConfig = {}; + +if (cliArgs["config"]) { + userConfig = getConfig(cliArgs["config"]); +} + +const config = deepMergeConfigs(defaultConfig, userConfig); /** * Update the config based on the CLI args * @param {object} startArgs */ function updateConfigFromStartArgs(startArgs) { - if (startArgs["accesstoken"]) { - config.accessToken = startArgs["accesstoken"]; - } - if (startArgs["disablesmallestscreen"]) { - config.disableSmallestScreen = true; - } - if (startArgs["webdav"]) { - config.webdav = true; + function deprecateCliArg(key, callback) { + const val = startArgs[key]; + if (val) { + console.warn( + "\x1b[33m\x1b[1m", + `Setting config values (${key}) from the CLI is deprecated. ` + + "This ability will be removed in the next major version. " + + "You should use the config file. " + ); + callback(val); + } } + + deprecateCliArg("accesstoken", (val) => (config.backend.accessToken = val)); + deprecateCliArg( + "disablesmallestscreen", + () => (config.backend.showSmallestScreenIndicator = false) + ); + deprecateCliArg("webdav", () => (config.backend.webdav = true)); } /** * Update the config based on the env variables */ function updateConfigFromEnv() { - if (process.env.accesstoken) { - config.accessToken = process.env.accesstoken; - } - if (process.env.disablesmallestscreen) { - config.disablesmallestscreen = true; - } - if (process.env.webdav) { - config.webdav = true; + function deprecateEnv(key, callback) { + const val = process.env[key]; + if (val) { + console.warn( + "\x1b[33m\x1b[1m", + `Setting config values (${key}) from the environment is deprecated. ` + + "This ability will be removed in the next major version. " + + "You should use the config file. " + ); + callback(val); + } } + + deprecateEnv("accesstoken", (val) => (config.backend.accessToken = val)); + deprecateEnv( + "disablesmallestscreen", + () => (config.backend.showSmallestScreenIndicator = false) + ); + deprecateEnv("webdav", () => (config.backend.webdav = true)); } +// compatibility layer +// FIXME: remove this in next major updateConfigFromEnv(); -updateConfigFromStartArgs(getArgs()); +// FIXME: remove this in next major +updateConfigFromStartArgs(cliArgs); + +if (!isConfigValid(config, true)) { + throw new Error("Config is not valid. Check logs for details"); +} +console.info(util.inspect(config, { showHidden: false, depth: null, colors: true })); module.exports = config; diff --git a/scripts/server-backend.js b/scripts/server-backend.js index 01ac837..d673bc6 100644 --- a/scripts/server-backend.js +++ b/scripts/server-backend.js @@ -4,8 +4,6 @@ const config = require("./config"); const WhiteboardServerSideInfo = require("./WhiteboardServerSideInfo"); function startBackendServer(port) { - console.info("Starting backend server with config", config); - var fs = require("fs-extra"); var express = require("express"); var formidable = require("formidable"); //form upload processing @@ -27,7 +25,7 @@ function startBackendServer(port) { var io = require("socket.io")(server, { path: "/ws-api" }); console.log("Webserver & socketserver running on port:" + port); - const { accessToken, webdav } = config; + const { accessToken, webdav } = config.backend; app.get("/api/loadwhiteboard", function (req, res) { var wid = req["query"]["wid"]; @@ -191,7 +189,7 @@ function startBackendServer(port) { info.infoWasSent(); } }); - }, (1 / config.whiteboardInfoBroadcastFreq) * 1000); + }, (1 / config.backend.performance.whiteboardInfoBroadcastFreq) * 1000); io.on("connection", function (socket) { var whiteboardId = null; diff --git a/scripts/utils.js b/scripts/utils.js index d18a01b..1c12423 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,3 +1,12 @@ +const path = require("path"); +const fs = require("fs"); +const yaml = require("js-yaml"); + +const Ajv = require("ajv"); +const ajv = new Ajv({ allErrors: true }); + +const configSchema = require("./config-schema.json"); + function getArgs() { const args = {}; process.argv.slice(2, process.argv.length).forEach((arg) => { @@ -17,4 +26,68 @@ function getArgs() { return args; } +/** + * TODO + * + * @param path + * @return {any} + */ +function getConfig(path) { + return yaml.safeLoad(fs.readFileSync(path, "utf8")); +} + +/** + * TODO + * @param config + * @param warn + * @return {*} + */ +function isConfigValid(config, warn = true) { + const validate = ajv.compile(configSchema); + const isValid = validate(config); + + if (!isValid && warn) console.warn(validate.errors); + + return isValid; +} + +/** + * TODO + * @return {*} + */ +function getDefaultConfig() { + const defaultConfigPath = path.join(__dirname, "..", "config.default.yml"); + return getConfig(defaultConfigPath); +} + +/** + * TODO + * Deep merges objects, not arrays. + * + * @param baseConfig + * @param overrideConfig + * @return {{}} + */ +function deepMergeConfigs(baseConfig, overrideConfig) { + const out = {}; + + Object.entries(baseConfig).forEach(([key, val]) => { + out[key] = val; + if (overrideConfig.hasOwnProperty(key)) { + const overrideVal = overrideConfig[key]; + if (typeof val === "object" && !Array.isArray(val) && val !== null) { + out[key] = deepMergeConfigs(val, overrideVal); + } else { + out[key] = overrideVal; + } + } + }); + + return out; +} + module.exports.getArgs = getArgs; +module.exports.getConfig = getConfig; +module.exports.getDefaultConfig = getDefaultConfig; +module.exports.deepMergeConfigs = deepMergeConfigs; +module.exports.isConfigValid = isConfigValid; diff --git a/scripts/utils.test.js b/scripts/utils.test.js new file mode 100644 index 0000000..308073c --- /dev/null +++ b/scripts/utils.test.js @@ -0,0 +1,48 @@ +const { getDefaultConfig, deepMergeConfigs, isConfigValid } = require("./utils"); + +test("Load default config", () => { + const defaultConfig = getDefaultConfig(); + expect(typeof defaultConfig).toBe("object"); +}); + +test("Full config override", () => { + const defaultConfig = getDefaultConfig(); + expect(deepMergeConfigs(defaultConfig, defaultConfig)).toEqual(defaultConfig); +}); + +test("Simple partial config override", () => { + expect(deepMergeConfigs({ test: true }, { test: false }).test).toBe(false); + expect(deepMergeConfigs({ test: false }, { test: true }).test).toBe(true); +}); + +test("Simple deep config override", () => { + expect(deepMergeConfigs({ stage1: { stage2: true } }, { stage1: { stage2: false } })).toEqual({ + stage1: { stage2: false }, + }); +}); + +test("Complex object config override", () => { + expect( + deepMergeConfigs({ stage1: { stage2: true, stage2b: true } }, { stage1: { stage2: false } }) + ).toEqual({ + stage1: { stage2: false, stage2b: true }, + }); +}); + +test("Override default config", () => { + const defaultConfig = getDefaultConfig(); + const overrideConfig1 = { frontend: { setReadOnlyOnWhiteboardLoad: true } }; + + expect( + deepMergeConfigs(defaultConfig, overrideConfig1).frontend.setReadOnlyOnWhiteboardLoad + ).toBe(true); +}); + +test("Dumb config is not valid", () => { + expect(isConfigValid({}, false)).toBe(false); +}); + +test("Default config is valid", () => { + const defaultConfig = getDefaultConfig(); + expect(isConfigValid(defaultConfig)).toBe(true); +}); From 52f52b62e4b2c6f6a7be1be9b72fbbd4d0ee15dc Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Sun, 10 May 2020 22:20:04 +0200 Subject: [PATCH 05/16] feat(ci): run test --- .../workflows/{linting-code.yml => linting-testing-code.yml} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename .github/workflows/{linting-code.yml => linting-testing-code.yml} (87%) diff --git a/.github/workflows/linting-code.yml b/.github/workflows/linting-testing-code.yml similarity index 87% rename from .github/workflows/linting-code.yml rename to .github/workflows/linting-testing-code.yml index f900057..a2191fd 100644 --- a/.github/workflows/linting-code.yml +++ b/.github/workflows/linting-testing-code.yml @@ -1,6 +1,6 @@ # This workflow will do a clean install of node dependencies and check the code style -name: Linting code CI +name: Linting and testing code CI on: push: @@ -19,3 +19,4 @@ jobs: node-version: 12.x - run: npm ci - run: npm run style + - run: npm run test From 9fda0a2c4bea0f24a4d79e30e9d07851569f41fe Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Sun, 10 May 2020 23:10:54 +0200 Subject: [PATCH 06/16] fix(back): prevent crash --- scripts/server-backend.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/server-backend.js b/scripts/server-backend.js index d673bc6..dec238a 100644 --- a/scripts/server-backend.js +++ b/scripts/server-backend.js @@ -194,6 +194,7 @@ function startBackendServer(port) { io.on("connection", function (socket) { var whiteboardId = null; socket.on("disconnect", function () { + if (infoByWhiteboard.has(whiteboardId)) { const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId); if (socket && socket.id) { @@ -201,11 +202,13 @@ function startBackendServer(port) { } whiteboardServerSideInfo.decrementNbConnectedUsers(); + if (whiteboardServerSideInfo.hasConnectedUser()) { socket.compress(false).broadcast.emit("refreshUserBadges", null); //Removes old user Badges } else { infoByWhiteboard.delete(whiteboardId); } + } }); socket.on("drawToWhiteboard", function (content) { From b0337d9f5bfbb1a68d3353f63e04d8b4eef1e44f Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Sun, 10 May 2020 23:13:55 +0200 Subject: [PATCH 07/16] feat: dynamic frontend configuration --- config.default.yml | 8 ++- scripts/config-schema.json | 30 ++++++++++- scripts/server-backend.js | 22 ++++---- scripts/utils.test.js | 8 +-- src/js/const.js | 3 -- src/js/main.js | 87 ++++++++++++++------------------ src/js/services/ConfigService.js | 51 +++++++++++++++++++ src/js/services/InfoService.js | 6 +-- src/js/utils.js | 28 ++++++++++ src/js/whiteboard.js | 20 ++++---- 10 files changed, 180 insertions(+), 83 deletions(-) delete mode 100644 src/js/const.js create mode 100644 src/js/services/ConfigService.js diff --git a/config.default.yml b/config.default.yml index 72ae98f..bbfd85e 100644 --- a/config.default.yml +++ b/config.default.yml @@ -11,7 +11,11 @@ backend: frontend: # When an editable whiteboard is loading in a client, # should it be started in read-only mode. - setReadOnlyOnWhiteboardLoad: false + readOnlyOnWhiteboardLoad: false # Show smallest screen indicator showSmallestScreenIndicator: true - # performance: + performance: + pointerEventsThreshold: + minDistDelta: 1 + minTimeDelta: 33 + refreshInfoFreq: 5 diff --git a/scripts/config-schema.json b/scripts/config-schema.json index 0370f35..a389e1e 100644 --- a/scripts/config-schema.json +++ b/scripts/config-schema.json @@ -30,13 +30,39 @@ "frontend": { "type": "object", "additionalProperties": false, - "required": ["setReadOnlyOnWhiteboardLoad", "showSmallestScreenIndicator"], + "required": ["readOnlyOnWhiteboardLoad", "showSmallestScreenIndicator", "performance"], "properties": { - "setReadOnlyOnWhiteboardLoad": { + "readOnlyOnWhiteboardLoad": { "type": "boolean" }, "showSmallestScreenIndicator": { "type": "boolean" + }, + "performance": { + "type": "object", + "additionalProperties": false, + "required": ["pointerEventsThreshold", "refreshInfoFreq"], + "properties": { + "pointerEventsThreshold": { + "type": "object", + "additionalProperties": false, + "required": ["minDistDelta", "minTimeDelta"], + "properties": { + "minDistDelta": { + "type": "number", + "minimum": 0 + }, + "minTimeDelta": { + "type": "number", + "minimum": 0 + } + } + }, + "refreshInfoFreq": { + "type": "number", + "minimum": 0 + } + } } } } diff --git a/scripts/server-backend.js b/scripts/server-backend.js index dec238a..d24a961 100644 --- a/scripts/server-backend.js +++ b/scripts/server-backend.js @@ -195,19 +195,19 @@ function startBackendServer(port) { var whiteboardId = null; socket.on("disconnect", function () { if (infoByWhiteboard.has(whiteboardId)) { - const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId); + const whiteboardServerSideInfo = infoByWhiteboard.get(whiteboardId); - if (socket && socket.id) { - whiteboardServerSideInfo.deleteScreenResolutionOfClient(socket.id); - } + if (socket && socket.id) { + whiteboardServerSideInfo.deleteScreenResolutionOfClient(socket.id); + } - whiteboardServerSideInfo.decrementNbConnectedUsers(); + whiteboardServerSideInfo.decrementNbConnectedUsers(); - if (whiteboardServerSideInfo.hasConnectedUser()) { - socket.compress(false).broadcast.emit("refreshUserBadges", null); //Removes old user Badges - } else { - infoByWhiteboard.delete(whiteboardId); - } + if (whiteboardServerSideInfo.hasConnectedUser()) { + socket.compress(false).broadcast.emit("refreshUserBadges", null); //Removes old user Badges + } else { + infoByWhiteboard.delete(whiteboardId); + } } }); @@ -224,6 +224,8 @@ function startBackendServer(port) { socket.on("joinWhiteboard", function (content) { content = escapeAllContentStrings(content); if (accessToken === "" || accessToken == content["at"]) { + socket.emit("whiteboardConfig", { common: config.frontend }); + whiteboardId = content["wid"]; socket.join(whiteboardId); //Joins room name=wid if (!infoByWhiteboard.has(whiteboardId)) { diff --git a/scripts/utils.test.js b/scripts/utils.test.js index 308073c..d35256c 100644 --- a/scripts/utils.test.js +++ b/scripts/utils.test.js @@ -31,11 +31,11 @@ test("Complex object config override", () => { test("Override default config", () => { const defaultConfig = getDefaultConfig(); - const overrideConfig1 = { frontend: { setReadOnlyOnWhiteboardLoad: true } }; + const overrideConfig1 = { frontend: { readOnlyOnWhiteboardLoad: true } }; - expect( - deepMergeConfigs(defaultConfig, overrideConfig1).frontend.setReadOnlyOnWhiteboardLoad - ).toBe(true); + expect(deepMergeConfigs(defaultConfig, overrideConfig1).frontend.readOnlyOnWhiteboardLoad).toBe( + true + ); }); test("Dumb config is not valid", () => { diff --git a/src/js/const.js b/src/js/const.js deleted file mode 100644 index b8db9ec..0000000 --- a/src/js/const.js +++ /dev/null @@ -1,3 +0,0 @@ -export const POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA = 1; // 1px -export const POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA = 10; // 1ms -export const REFRESH_INFO_FREQUENCY = 5; // 5 times per second diff --git a/src/js/main.js b/src/js/main.js index 084c0fe..04dbbb0 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -8,47 +8,48 @@ import pdfjsLib from "pdfjs-dist/webpack"; import shortcutFunctions from "./shortcutFunctions"; import ReadOnlyService from "./services/ReadOnlyService"; import InfoService from "./services/InfoService"; +import { getQueryVariable, getSubDir } from "./utils"; +import ConfigService from "./services/ConfigService"; + +let whiteboardId = getQueryVariable("whiteboardid"); +const randomid = getQueryVariable("randomid"); +if (randomid && !whiteboardId) { + //set random whiteboard on empty whiteboardid + whiteboardId = Array(2) + .fill(null) + .map(() => Math.random().toString(36).substr(2)) + .join(""); + const urlParams = new URLSearchParams(window.location.search); + urlParams.set("whiteboardid", whiteboardId); + window.location.search = urlParams; +} + +whiteboardId = whiteboardId || "myNewWhiteboard"; +whiteboardId = unescape(encodeURIComponent(whiteboardId)).replace(/[^a-zA-Z0-9 ]/g, ""); +const myUsername = getQueryVariable("username") || "unknown" + (Math.random() + "").substring(2, 6); +const accessToken = getQueryVariable("accesstoken") || ""; + +// Custom Html Title +const title = getQueryVariable("title"); +if (!title === false) { + document.title = decodeURIComponent(title); +} + +const subdir = getSubDir(); +let signaling_socket; function main() { - var whiteboardId = getQueryVariable("whiteboardid"); - var randomid = getQueryVariable("randomid"); - if (randomid && !whiteboardId) { - //set random whiteboard on empty whiteboardid - whiteboardId = Array(2) - .fill(null) - .map(() => Math.random().toString(36).substr(2)) - .join(""); - const urlParams = new URLSearchParams(window.location.search); - urlParams.set("whiteboardid", whiteboardId); - window.location.search = urlParams; - } - - whiteboardId = whiteboardId || "myNewWhiteboard"; - whiteboardId = unescape(encodeURIComponent(whiteboardId)).replace(/[^a-zA-Z0-9 ]/g, ""); - var myUsername = getQueryVariable("username"); - var accessToken = getQueryVariable("accesstoken"); - myUsername = myUsername || "unknown" + (Math.random() + "").substring(2, 6); - accessToken = accessToken || ""; - var accessDenied = false; - - // Custom Html Title - var title = getQueryVariable("title"); - if (!title === false) { - document.title = decodeURIComponent(title); - } - - var url = document.URL.substr(0, document.URL.lastIndexOf("/")); - var signaling_socket = null; - var urlSplit = url.split("/"); - var subdir = ""; - for (var i = 3; i < urlSplit.length; i++) { - subdir = subdir + "/" + urlSplit[i]; - } signaling_socket = io("", { path: subdir + "/ws-api" }); // Connect even if we are in a subdir behind a reverse proxy signaling_socket.on("connect", function () { console.log("Websocket connected!"); + signaling_socket.on("whiteboardConfig", (serverResponse) => { + ConfigService.initFromServer(serverResponse); + // Inti whiteboard only when we have the config from the server + initWhiteboard(); + }); + signaling_socket.on("whiteboardInfoUpdate", (info) => { InfoService.updateInfoFromServer(info); whiteboard.updateSmallestScreenResolution(); @@ -63,6 +64,7 @@ function main() { whiteboard.refreshUserBadges(); }); + let accessDenied = false; signaling_socket.on("wrongAccessToken", function () { if (!accessDenied) { accessDenied = true; @@ -76,7 +78,9 @@ function main() { windowWidthHeight: { w: $(window).width(), h: $(window).height() }, }); }); +} +function initWhiteboard() { $(document).ready(function () { // by default set in readOnly mode ReadOnlyService.activateReadOnlyMode(); @@ -599,7 +603,7 @@ function main() { // fix bug cursor not showing up whiteboard.refreshCursorAppearance(); - if (process.env.NODE_ENV === "production") { + if (process.env.NODE_ENV === "production" && ConfigService.readOnlyOnWhiteboardLoad) { ReadOnlyService.activateReadOnlyMode(); InfoService.hideInfo(); } else { @@ -795,19 +799,6 @@ function main() { }, 1000 * options.hideAfter); } } - - // get 'GET' parameter by variable name - function getQueryVariable(variable) { - var query = window.location.search.substring(1); - var vars = query.split("&"); - for (var i = 0; i < vars.length; i++) { - var pair = vars[i].split("="); - if (pair[0] == variable) { - return pair[1]; - } - } - return false; - } } export default main; diff --git a/src/js/services/ConfigService.js b/src/js/services/ConfigService.js new file mode 100644 index 0000000..6f8ef64 --- /dev/null +++ b/src/js/services/ConfigService.js @@ -0,0 +1,51 @@ +class ConfigService { + /** + * @readonly + * @type {boolean} + */ + readOnlyOnWhiteboardLoad = false; + + /** + * @readonly + * @type {boolean} + */ + showSmallestScreenIndicator = true; + + /** + * @readonly + * @type {number} + */ + pointerEventsThresholdMinDistDelta = 0; + + /** + * @readonly + * @type {number} + */ + pointerEventsThresholdMinTimeDelta = 0; + + /** + * @readonly + * @type {number} + */ + refreshInfoInterval = 1000; + + /** + * Init the service from the config sent by the server + * + * @param {object} serverResponse + */ + initFromServer(serverResponse) { + const { common } = serverResponse; + const { readOnlyOnWhiteboardLoad, showSmallestScreenIndicator, performance } = common; + + this.readOnlyOnWhiteboardLoad = readOnlyOnWhiteboardLoad; + this.showSmallestScreenIndicator = showSmallestScreenIndicator; + this.pointerEventsThresholdMinDistDelta = performance.pointerEventsThreshold.minDistDelta; + this.pointerEventsThresholdMinTimeDelta = performance.pointerEventsThreshold.minTimeDelta; + this.refreshInfoInterval = 1000 / performance.refreshInfoFreq; + + console.log("Whiteboard config from server:", serverResponse, "parsed:", this); + } +} + +export default new ConfigService(); diff --git a/src/js/services/InfoService.js b/src/js/services/InfoService.js index fbbe3d0..8e5d705 100644 --- a/src/js/services/InfoService.js +++ b/src/js/services/InfoService.js @@ -1,6 +1,4 @@ -import { REFRESH_INFO_FREQUENCY } from "../const"; - -const REFRESH_INTERVAL = 1000 / REFRESH_INFO_FREQUENCY; +import ConfigService from "./ConfigService"; /** * Class the handle the information about the whiteboard @@ -90,7 +88,7 @@ class InfoService { // refresh only on a specific interval to reduce // refreshing cost this.refreshDisplayedInfo(); - }, REFRESH_INTERVAL); + }, ConfigService.refreshInfoInterval); } hideInfo() { diff --git a/src/js/utils.js b/src/js/utils.js index d5329d3..1e02d15 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -14,3 +14,31 @@ export function computeDist(p1, p2) { export function getCurrentTimeMs() { return new Date().getTime(); } + +/** + * get 'GET' parameter by variable name + * @param variable + * @return {boolean|*} + */ +export function getQueryVariable(variable) { + const query = window.location.search.substring(1); + const vars = query.split("&"); + for (let i = 0; i < vars.length; i++) { + const pair = vars[i].split("="); + if (pair[0] === variable) { + return pair[1]; + } + } + return false; +} + +export function getSubDir() { + const url = document.URL.substr(0, document.URL.lastIndexOf("/")); + const urlSplit = url.split("/"); + let subdir = ""; + for (let i = 3; i < urlSplit.length; i++) { + subdir = subdir + "/" + urlSplit[i]; + } + + return subdir; +} diff --git a/src/js/whiteboard.js b/src/js/whiteboard.js index a2303b0..e27add6 100644 --- a/src/js/whiteboard.js +++ b/src/js/whiteboard.js @@ -1,12 +1,9 @@ import { dom } from "@fortawesome/fontawesome-svg-core"; import { getCurrentTimeMs } from "./utils"; import Point from "./classes/Point"; -import { - POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA, - POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA, -} from "./const"; import ReadOnlyService from "./services/ReadOnlyService"; import InfoService from "./services/InfoService"; +import ConfigService from "./services/ConfigService"; const RAD_TO_DEG = 180.0 / Math.PI; const DEG_TO_RAD = Math.PI / 180.0; @@ -215,11 +212,11 @@ const whiteboard = { const pointerSentTime = getCurrentTimeMs(); if ( pointerSentTime - _this.lastPointerSentTime > - POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA + ConfigService.pointerEventsThresholdMinTimeDelta ) { if ( _this.lastPointerPosition.distTo(currentPos) > - POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA + ConfigService.pointerEventsThresholdMinDistDelta ) { _this.lastPointerSentTime = pointerSentTime; _this.lastPointerPosition = currentPos; @@ -545,11 +542,14 @@ const whiteboard = { }); const pointerSentTime = getCurrentTimeMs(); - if (pointerSentTime - _this.lastPointerSentTime > POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA) { + if ( + pointerSentTime - _this.lastPointerSentTime > + ConfigService.pointerEventsThresholdMinTimeDelta + ) { const newPointerPosition = currentPos; if ( _this.lastPointerPosition.distTo(newPointerPosition) > - POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA + ConfigService.pointerEventsThresholdMinDistDelta ) { _this.lastPointerSentTime = pointerSentTime; _this.lastPointerPosition = newPointerPosition; @@ -882,12 +882,12 @@ const whiteboard = { // At least 100 ms between messages to reduce server load if ( pointerSentTime - _this.lastPointerSentTime > - POINTER_EVENT_THRESHOLD_MIN_TIME_DELTA + ConfigService.pointerEventsThresholdMinTimeDelta ) { // Minimal distance between messages to reduce server load if ( _this.lastPointerPosition.distTo(newPointerPosition) > - POINTER_EVENT_THRESHOLD_MIN_DIST_DELTA + ConfigService.pointerEventsThresholdMinDistDelta ) { _this.lastPointerSentTime = pointerSentTime; _this.lastPointerPosition = newPointerPosition; From 409681b21714454d3d29bbfa9c53fef7c5f6c7b1 Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Mon, 11 May 2020 11:06:42 +0200 Subject: [PATCH 08/16] feat(front): creating throttling service * ease throttling of events accross the frontend --- src/js/services/ThrottlingService.js | 41 +++++++++++++ src/js/whiteboard.js | 91 +++++++++------------------- 2 files changed, 70 insertions(+), 62 deletions(-) create mode 100644 src/js/services/ThrottlingService.js diff --git a/src/js/services/ThrottlingService.js b/src/js/services/ThrottlingService.js new file mode 100644 index 0000000..227e78d --- /dev/null +++ b/src/js/services/ThrottlingService.js @@ -0,0 +1,41 @@ +import Point from "../classes/Point"; +import { getCurrentTimeMs } from "../utils"; +import ConfigService from "./ConfigService"; + +class ThrottlingService { + /** + * @type {number} + * @private + */ + _lastSuccessTime = 0; + + /** + * @type {Point} + * @private + */ + _lastPointPosition = new Point(0, 0); + + /** + * Helper to throttle events based on the configuration. + * Only if checks are ok, the onSuccess callback will be called. + * + * @param {Point} newPosition New point position to base the throttling on + * @param {function()} onSuccess Callback called when the throttling is successful + */ + throttle(newPosition, onSuccess) { + const newTime = getCurrentTimeMs(); + const { _lastPointPosition, _lastSuccessTime } = this; + if (newTime - _lastSuccessTime > ConfigService.pointerEventsThresholdMinTimeDelta) { + if ( + _lastPointPosition.distTo(newPosition) > + ConfigService.pointerEventsThresholdMinDistDelta + ) { + onSuccess(); + this._lastPointPosition = newPosition; + this._lastSuccessTime = newTime; + } + } + } +} + +export default new ThrottlingService(); diff --git a/src/js/whiteboard.js b/src/js/whiteboard.js index e27add6..353add6 100644 --- a/src/js/whiteboard.js +++ b/src/js/whiteboard.js @@ -1,9 +1,8 @@ import { dom } from "@fortawesome/fontawesome-svg-core"; -import { getCurrentTimeMs } from "./utils"; import Point from "./classes/Point"; import ReadOnlyService from "./services/ReadOnlyService"; import InfoService from "./services/InfoService"; -import ConfigService from "./services/ConfigService"; +import ThrottlingService from "./services/ThrottlingService"; const RAD_TO_DEG = 180.0 / Math.PI; const DEG_TO_RAD = Math.PI / 180.0; @@ -209,25 +208,15 @@ const whiteboard = { const currentPos = Point.fromEvent(e); - const pointerSentTime = getCurrentTimeMs(); - if ( - pointerSentTime - _this.lastPointerSentTime > - ConfigService.pointerEventsThresholdMinTimeDelta - ) { - if ( - _this.lastPointerPosition.distTo(currentPos) > - ConfigService.pointerEventsThresholdMinDistDelta - ) { - _this.lastPointerSentTime = pointerSentTime; - _this.lastPointerPosition = currentPos; - _this.sendFunction({ - t: "cursor", - event: "move", - d: [currentPos.x, currentPos.y], - username: _this.settings.username, - }); - } - } + ThrottlingService.throttle(currentPos, () => { + _this.lastPointerPosition = currentPos; + _this.sendFunction({ + t: "cursor", + event: "move", + d: [currentPos.x, currentPos.y], + username: _this.settings.username, + }); + }); }); _this.mouseOverlay.on("mousemove touchmove", function (e) { @@ -541,26 +530,15 @@ const whiteboard = { _this.prevPos = currentPos; }); - const pointerSentTime = getCurrentTimeMs(); - if ( - pointerSentTime - _this.lastPointerSentTime > - ConfigService.pointerEventsThresholdMinTimeDelta - ) { - const newPointerPosition = currentPos; - if ( - _this.lastPointerPosition.distTo(newPointerPosition) > - ConfigService.pointerEventsThresholdMinDistDelta - ) { - _this.lastPointerSentTime = pointerSentTime; - _this.lastPointerPosition = newPointerPosition; - _this.sendFunction({ - t: "cursor", - event: "move", - d: [newPointerPosition.x, newPointerPosition.y], - username: _this.settings.username, - }); - } - } + ThrottlingService.throttle(currentPos, () => { + _this.lastPointerPosition = currentPos; + _this.sendFunction({ + t: "cursor", + event: "move", + d: [currentPos.x, currentPos.y], + username: _this.settings.username, + }); + }); }, triggerMouseOver: function () { var _this = this; @@ -877,28 +855,17 @@ const whiteboard = { currX += textBox.width() - 4; } - const pointerSentTime = getCurrentTimeMs(); const newPointerPosition = new Point(currX, currY); - // At least 100 ms between messages to reduce server load - if ( - pointerSentTime - _this.lastPointerSentTime > - ConfigService.pointerEventsThresholdMinTimeDelta - ) { - // Minimal distance between messages to reduce server load - if ( - _this.lastPointerPosition.distTo(newPointerPosition) > - ConfigService.pointerEventsThresholdMinDistDelta - ) { - _this.lastPointerSentTime = pointerSentTime; - _this.lastPointerPosition = newPointerPosition; - _this.sendFunction({ - t: "cursor", - event: "move", - d: [newPointerPosition.x, newPointerPosition.y], - username: _this.settings.username, - }); - } - } + + ThrottlingService.throttle(newPointerPosition, () => { + _this.lastPointerPosition = newPointerPosition; + _this.sendFunction({ + t: "cursor", + event: "move", + d: [newPointerPosition.x, newPointerPosition.y], + username: _this.settings.username, + }); + }); }); this.textContainer.append(textBox); textBox.draggable({ From ca47c41c69271bda5c6379c265344555de6d199d Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Mon, 11 May 2020 14:12:20 +0200 Subject: [PATCH 09/16] feat: throttling configuration --- config.default.yml | 9 +++-- scripts/config-schema.json | 34 ++++++++++------- scripts/utils.js | 23 +++++++++-- src/js/services/ConfigService.js | 42 ++++++++++++++------- src/js/services/ConfigService.utils.js | 21 +++++++++++ src/js/services/ConfigService.utils.test.js | 29 ++++++++++++++ src/js/services/InfoService.js | 12 ++++-- src/js/services/ThrottlingService.js | 4 +- 8 files changed, 134 insertions(+), 40 deletions(-) create mode 100644 src/js/services/ConfigService.utils.js create mode 100644 src/js/services/ConfigService.utils.test.js diff --git a/config.default.yml b/config.default.yml index bbfd85e..9adf047 100644 --- a/config.default.yml +++ b/config.default.yml @@ -4,7 +4,7 @@ backend: # TODO webdav: false performance: - # Whiteboard information broadcasting frequency (in /s) + # Whiteboard information broadcasting frequency (in Hz i.e. /s) # => diminishing this will result in more latency whiteboardInfoBroadcastFreq: 1 @@ -15,7 +15,8 @@ frontend: # Show smallest screen indicator showSmallestScreenIndicator: true performance: - pointerEventsThreshold: - minDistDelta: 1 - minTimeDelta: 33 + pointerEventsThrottling: + - fromNbUser: 0 + minDistDelta: 1 + maxFreq: 30 refreshInfoFreq: 5 diff --git a/scripts/config-schema.json b/scripts/config-schema.json index a389e1e..eeb1a80 100644 --- a/scripts/config-schema.json +++ b/scripts/config-schema.json @@ -41,20 +41,28 @@ "performance": { "type": "object", "additionalProperties": false, - "required": ["pointerEventsThreshold", "refreshInfoFreq"], + "required": ["pointerEventsThrottling", "refreshInfoFreq"], "properties": { - "pointerEventsThreshold": { - "type": "object", - "additionalProperties": false, - "required": ["minDistDelta", "minTimeDelta"], - "properties": { - "minDistDelta": { - "type": "number", - "minimum": 0 - }, - "minTimeDelta": { - "type": "number", - "minimum": 0 + "pointerEventsThrottling": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "additionalProperties": false, + "required": ["fromNbUser", "minDistDelta", "maxFreq"], + "properties": { + "fromNbUser": { + "type": "number", + "minimum": 0 + }, + "minDistDelta": { + "type": "number", + "minimum": 0 + }, + "maxFreq": { + "type": "number", + "minimum": 0 + } } } }, diff --git a/scripts/utils.js b/scripts/utils.js index 1c12423..754a3f2 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -44,11 +44,28 @@ function getConfig(path) { */ function isConfigValid(config, warn = true) { const validate = ajv.compile(configSchema); - const isValid = validate(config); + const isValidAgainstSchema = validate(config); - if (!isValid && warn) console.warn(validate.errors); + if (!isValidAgainstSchema && warn) console.warn(validate.errors); - return isValid; + let structureIsValid = false; + try { + structureIsValid = config.frontend.performance.pointerEventsThrottling.some( + (item) => item.fromNbUser === 0 + ); + } catch (e) { + if (!e instanceof TypeError) { + throw e; + } + } + + if (!structureIsValid && warn) + console.warn( + "At least one item under frontend.performance.pointerEventsThrottling" + + "must have fromNbUser set to 0" + ); + + return isValidAgainstSchema && structureIsValid; } /** diff --git a/src/js/services/ConfigService.js b/src/js/services/ConfigService.js index 6f8ef64..74f3967 100644 --- a/src/js/services/ConfigService.js +++ b/src/js/services/ConfigService.js @@ -1,4 +1,12 @@ +import { getThrottling } from "./ConfigService.utils"; + class ConfigService { + /** + * @type {object} + * @private + */ + _configFromServer = {}; + /** * @readonly * @type {boolean} @@ -13,15 +21,9 @@ class ConfigService { /** * @readonly - * @type {number} + * @type {{minDistDelta: number, minTimeDelta: number}} */ - pointerEventsThresholdMinDistDelta = 0; - - /** - * @readonly - * @type {number} - */ - pointerEventsThresholdMinTimeDelta = 0; + pointerEventsThrottling = { minDistDelta: 0, minTimeDelta: 0 }; /** * @readonly @@ -32,19 +34,31 @@ class ConfigService { /** * Init the service from the config sent by the server * - * @param {object} serverResponse + * @param {object} configFromServer */ - initFromServer(serverResponse) { - const { common } = serverResponse; + initFromServer(configFromServer) { + this._configFromServer = configFromServer; + + const { common } = configFromServer; const { readOnlyOnWhiteboardLoad, showSmallestScreenIndicator, performance } = common; this.readOnlyOnWhiteboardLoad = readOnlyOnWhiteboardLoad; this.showSmallestScreenIndicator = showSmallestScreenIndicator; - this.pointerEventsThresholdMinDistDelta = performance.pointerEventsThreshold.minDistDelta; - this.pointerEventsThresholdMinTimeDelta = performance.pointerEventsThreshold.minTimeDelta; this.refreshInfoInterval = 1000 / performance.refreshInfoFreq; - console.log("Whiteboard config from server:", serverResponse, "parsed:", this); + console.log("Whiteboard config from server:", configFromServer, "parsed:", this); + } + + /** + * TODO + */ + refreshNbUserDependant(nbUser) { + const { _configFromServer } = this; + const { common } = _configFromServer; + const { performance } = common; + const { pointerEventsThrottling } = performance; + + this.pointerEventsThrottling = getThrottling(pointerEventsThrottling, nbUser); } } diff --git a/src/js/services/ConfigService.utils.js b/src/js/services/ConfigService.utils.js new file mode 100644 index 0000000..f2111ac --- /dev/null +++ b/src/js/services/ConfigService.utils.js @@ -0,0 +1,21 @@ +/** + * Helper to extract the correct correct throttling values based on the config and the number of user + * + * @param {Array.<{fromNbUser: number, minDistDelta: number, maxFreq: number}>} pointerEventsThrottling + * @param {number} nbUser + * @return {{minDistDelta: number, minTimeDelta: number}} + */ +export function getThrottling(pointerEventsThrottling, nbUser) { + let tmpOut = pointerEventsThrottling[0]; + let lastDistToNbUser = nbUser - tmpOut.fromNbUser; + if (lastDistToNbUser < 0) lastDistToNbUser = Number.MAX_VALUE; + for (const el of pointerEventsThrottling) { + const distToNbUser = nbUser - el.fromNbUser; + if (el.fromNbUser <= nbUser && distToNbUser <= lastDistToNbUser) { + tmpOut = el; + lastDistToNbUser = distToNbUser; + } + } + + return { minDistDelta: tmpOut.minDistDelta, minTimeDelta: 1000 * (1 / tmpOut.maxFreq) }; +} diff --git a/src/js/services/ConfigService.utils.test.js b/src/js/services/ConfigService.utils.test.js new file mode 100644 index 0000000..d60ed41 --- /dev/null +++ b/src/js/services/ConfigService.utils.test.js @@ -0,0 +1,29 @@ +import { getThrottling } from "./ConfigService.utils"; + +test("Simple throttling config", () => { + const throttling = [{ fromNbUser: 0, minDistDelta: 1, maxFreq: 1 }]; + + const target0 = { minDistDelta: 1, minTimeDelta: 1000 }; + expect(getThrottling(throttling, 0)).toEqual(target0); + + const target100 = { minDistDelta: 1, minTimeDelta: 1000 }; + expect(getThrottling(throttling, 100)).toEqual(target100); +}); + +test("Complex throttling config", () => { + // mix ordering + const throttling = [ + { fromNbUser: 100, minDistDelta: 100, maxFreq: 1 }, + { fromNbUser: 0, minDistDelta: 1, maxFreq: 1 }, + { fromNbUser: 50, minDistDelta: 50, maxFreq: 1 }, + ]; + + const target0 = { minDistDelta: 1, minTimeDelta: 1000 }; + expect(getThrottling(throttling, 0)).toEqual(target0); + + const target50 = { minDistDelta: 50, minTimeDelta: 1000 }; + expect(getThrottling(throttling, 50)).toEqual(target50); + + const target100 = { minDistDelta: 100, minTimeDelta: 1000 }; + expect(getThrottling(throttling, 100)).toEqual(target100); +}); diff --git a/src/js/services/InfoService.js b/src/js/services/InfoService.js index 8e5d705..f9a37c9 100644 --- a/src/js/services/InfoService.js +++ b/src/js/services/InfoService.js @@ -14,9 +14,9 @@ class InfoService { * Holds the number of user connected to the server * * @type {number} - * @private + * @readonly */ - _nbConnectedUsers = 0; + nbConnectedUsers = -1; /** * @@ -49,7 +49,11 @@ class InfoService { * @param {{w: number, h: number}} smallestScreenResolution */ updateInfoFromServer({ nbConnectedUsers, smallestScreenResolution = undefined }) { - this._nbConnectedUsers = nbConnectedUsers; + if (this.nbConnectedUsers !== nbConnectedUsers) { + // Refresh config service parameters on nb connected user change + ConfigService.refreshNbUserDependant(nbConnectedUsers); + } + this.nbConnectedUsers = nbConnectedUsers; if (smallestScreenResolution) { this._smallestScreenResolution = smallestScreenResolution; } @@ -73,7 +77,7 @@ class InfoService { refreshDisplayedInfo() { $("#messageReceivedCount")[0].innerText = String(this._nbMessagesReceived); $("#messageSentCount")[0].innerText = String(this._nbMessagesSent); - $("#connectedUsersCount")[0].innerText = String(this._nbConnectedUsers); + $("#connectedUsersCount")[0].innerText = String(this.nbConnectedUsers); const { _smallestScreenResolution: ssr } = this; $("#smallestScreenResolution")[0].innerText = ssr ? `(${ssr.w}, ${ssr.h})` : "Unknown"; } diff --git a/src/js/services/ThrottlingService.js b/src/js/services/ThrottlingService.js index 227e78d..2fa2408 100644 --- a/src/js/services/ThrottlingService.js +++ b/src/js/services/ThrottlingService.js @@ -25,10 +25,10 @@ class ThrottlingService { throttle(newPosition, onSuccess) { const newTime = getCurrentTimeMs(); const { _lastPointPosition, _lastSuccessTime } = this; - if (newTime - _lastSuccessTime > ConfigService.pointerEventsThresholdMinTimeDelta) { + if (newTime - _lastSuccessTime > ConfigService.pointerEventsThrottling.minTimeDelta) { if ( _lastPointPosition.distTo(newPosition) > - ConfigService.pointerEventsThresholdMinDistDelta + ConfigService.pointerEventsThrottling.minDistDelta ) { onSuccess(); this._lastPointPosition = newPosition; From efaa4b795cde2a0cd0abf308ea5f9469420ef7ee Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Mon, 11 May 2020 14:18:59 +0200 Subject: [PATCH 10/16] refacto(backend): regrouped config related handling --- scripts/WhiteboardServerSideInfo.js | 2 +- scripts/{ => config}/config-schema.json | 0 scripts/{ => config}/config.js | 10 +-- scripts/config/utils.js | 92 +++++++++++++++++++++++++ scripts/{ => config}/utils.test.js | 0 scripts/server-backend.js | 2 +- scripts/utils.js | 90 ------------------------ 7 files changed, 97 insertions(+), 99 deletions(-) rename scripts/{ => config}/config-schema.json (100%) rename scripts/{ => config}/config.js (94%) create mode 100644 scripts/config/utils.js rename scripts/{ => config}/utils.test.js (100%) diff --git a/scripts/WhiteboardServerSideInfo.js b/scripts/WhiteboardServerSideInfo.js index 8b148a3..04bf79f 100644 --- a/scripts/WhiteboardServerSideInfo.js +++ b/scripts/WhiteboardServerSideInfo.js @@ -1,4 +1,4 @@ -const config = require("./config"); +const config = require("./config/config"); class WhiteboardServerSideInfo { static defaultScreenResolution = { w: 1000, h: 1000 }; diff --git a/scripts/config-schema.json b/scripts/config/config-schema.json similarity index 100% rename from scripts/config-schema.json rename to scripts/config/config-schema.json diff --git a/scripts/config.js b/scripts/config/config.js similarity index 94% rename from scripts/config.js rename to scripts/config/config.js index 2e94c1e..c1f6266 100644 --- a/scripts/config.js +++ b/scripts/config/config.js @@ -1,12 +1,8 @@ const util = require("util"); -const { - getArgs, - getDefaultConfig, - getConfig, - deepMergeConfigs, - isConfigValid, -} = require("./utils"); +const { getDefaultConfig, getConfig, deepMergeConfigs, isConfigValid } = require("./utils"); + +const { getArgs } = require("./../utils"); const defaultConfig = getDefaultConfig(); diff --git a/scripts/config/utils.js b/scripts/config/utils.js new file mode 100644 index 0000000..e05702f --- /dev/null +++ b/scripts/config/utils.js @@ -0,0 +1,92 @@ +const path = require("path"); +const fs = require("fs"); +const yaml = require("js-yaml"); + +const Ajv = require("ajv"); +const ajv = new Ajv({ allErrors: true }); + +const configSchema = require("./config-schema.json"); + +/** + * Load a yaml config file from a given path. + * + * @param path + * @return {Object} + */ +function getConfig(path) { + return yaml.safeLoad(fs.readFileSync(path, "utf8")); +} + +/** + * Check that a config object is valid. + * + * @param {Object} config Config object + * @param {boolean} warn Should we warn in console for errors + * @return {boolean} + */ +function isConfigValid(config, warn = true) { + const validate = ajv.compile(configSchema); + const isValidAgainstSchema = validate(config); + + if (!isValidAgainstSchema && warn) console.warn(validate.errors); + + let structureIsValid = false; + try { + structureIsValid = config.frontend.performance.pointerEventsThrottling.some( + (item) => item.fromNbUser === 0 + ); + } catch (e) { + if (!e instanceof TypeError) { + throw e; + } + } + + if (!structureIsValid && warn) + console.warn( + "At least one item under frontend.performance.pointerEventsThrottling" + + "must have fromNbUser set to 0" + ); + + return isValidAgainstSchema && structureIsValid; +} + +/** + * Load the default project config + * @return {Object} + */ +function getDefaultConfig() { + const defaultConfigPath = path.join(__dirname, "..", "..", "config.default.yml"); + return getConfig(defaultConfigPath); +} + +/** + * Deep merge of project config + * + * Objects are merged, not arrays + * + * @param baseConfig + * @param overrideConfig + * @return {Object} + */ +function deepMergeConfigs(baseConfig, overrideConfig) { + const out = {}; + + Object.entries(baseConfig).forEach(([key, val]) => { + out[key] = val; + if (overrideConfig.hasOwnProperty(key)) { + const overrideVal = overrideConfig[key]; + if (typeof val === "object" && !Array.isArray(val) && val !== null) { + out[key] = deepMergeConfigs(val, overrideVal); + } else { + out[key] = overrideVal; + } + } + }); + + return out; +} + +module.exports.getConfig = getConfig; +module.exports.getDefaultConfig = getDefaultConfig; +module.exports.deepMergeConfigs = deepMergeConfigs; +module.exports.isConfigValid = isConfigValid; diff --git a/scripts/utils.test.js b/scripts/config/utils.test.js similarity index 100% rename from scripts/utils.test.js rename to scripts/config/utils.test.js diff --git a/scripts/server-backend.js b/scripts/server-backend.js index d24a961..54d14f7 100644 --- a/scripts/server-backend.js +++ b/scripts/server-backend.js @@ -1,6 +1,6 @@ const path = require("path"); -const config = require("./config"); +const config = require("./config/config"); const WhiteboardServerSideInfo = require("./WhiteboardServerSideInfo"); function startBackendServer(port) { diff --git a/scripts/utils.js b/scripts/utils.js index 754a3f2..d18a01b 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -1,12 +1,3 @@ -const path = require("path"); -const fs = require("fs"); -const yaml = require("js-yaml"); - -const Ajv = require("ajv"); -const ajv = new Ajv({ allErrors: true }); - -const configSchema = require("./config-schema.json"); - function getArgs() { const args = {}; process.argv.slice(2, process.argv.length).forEach((arg) => { @@ -26,85 +17,4 @@ function getArgs() { return args; } -/** - * TODO - * - * @param path - * @return {any} - */ -function getConfig(path) { - return yaml.safeLoad(fs.readFileSync(path, "utf8")); -} - -/** - * TODO - * @param config - * @param warn - * @return {*} - */ -function isConfigValid(config, warn = true) { - const validate = ajv.compile(configSchema); - const isValidAgainstSchema = validate(config); - - if (!isValidAgainstSchema && warn) console.warn(validate.errors); - - let structureIsValid = false; - try { - structureIsValid = config.frontend.performance.pointerEventsThrottling.some( - (item) => item.fromNbUser === 0 - ); - } catch (e) { - if (!e instanceof TypeError) { - throw e; - } - } - - if (!structureIsValid && warn) - console.warn( - "At least one item under frontend.performance.pointerEventsThrottling" + - "must have fromNbUser set to 0" - ); - - return isValidAgainstSchema && structureIsValid; -} - -/** - * TODO - * @return {*} - */ -function getDefaultConfig() { - const defaultConfigPath = path.join(__dirname, "..", "config.default.yml"); - return getConfig(defaultConfigPath); -} - -/** - * TODO - * Deep merges objects, not arrays. - * - * @param baseConfig - * @param overrideConfig - * @return {{}} - */ -function deepMergeConfigs(baseConfig, overrideConfig) { - const out = {}; - - Object.entries(baseConfig).forEach(([key, val]) => { - out[key] = val; - if (overrideConfig.hasOwnProperty(key)) { - const overrideVal = overrideConfig[key]; - if (typeof val === "object" && !Array.isArray(val) && val !== null) { - out[key] = deepMergeConfigs(val, overrideVal); - } else { - out[key] = overrideVal; - } - } - }); - - return out; -} - module.exports.getArgs = getArgs; -module.exports.getConfig = getConfig; -module.exports.getDefaultConfig = getDefaultConfig; -module.exports.deepMergeConfigs = deepMergeConfigs; -module.exports.isConfigValid = isConfigValid; From ce16a9d9999baa6e951b54980e5569b87b8e2651 Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Mon, 11 May 2020 15:12:37 +0200 Subject: [PATCH 11/16] refacto(frontend): clean ES2020 private field with getter --- src/js/classes/Point.js | 59 +++++++++--------- src/js/services/ConfigService.js | 48 +++++++++------ src/js/services/InfoService.js | 92 +++++++++++++++++----------- src/js/services/ReadOnlyService.js | 29 ++++----- src/js/services/ThrottlingService.js | 22 ++++--- 5 files changed, 142 insertions(+), 108 deletions(-) diff --git a/src/js/classes/Point.js b/src/js/classes/Point.js index 55f0717..4c8fd81 100644 --- a/src/js/classes/Point.js +++ b/src/js/classes/Point.js @@ -1,34 +1,41 @@ import { computeDist } from "../utils"; class Point { + /** + * @type {number} + */ + #x; + get x() { + return this.#x; + } + + /** + * @type {number} + */ + #y; + get y() { + return this.#y; + } + + /** + * @type {Point} + */ + static #lastKnownPos = new Point(0, 0); + static get lastKnownPos() { + return Point.#lastKnownPos; + } + /** * @param {number} x * @param {number} y */ constructor(x, y) { - /** - * @type {number} - * @private - */ - this._x = x; - - /** - * @type {number} - * @private - */ - this._y = y; - } - - get x() { - return this._x; - } - - get y() { - return this._y; + this.#x = x; + this.#y = y; } get isZeroZero() { - return this._x === 0 && this._y === 0; + return this.#x === 0 && this.#y === 0; } /** @@ -50,20 +57,14 @@ class Point { y = touch.clientY - $("#mouseOverlay").offset().top; } else { // if it's a touchend event - return Point._lastKnownPos; + return Point.#lastKnownPos; } } - Point._lastKnownPos = new Point(x - epsilon, y - epsilon); - return Point._lastKnownPos; + Point.#lastKnownPos = new Point(x - epsilon, y - epsilon); + return Point.#lastKnownPos; } - /** - * @type {Point} - * @private - */ - static _lastKnownPos = new Point(0, 0); - /** * Compute euclidean distance between points * diff --git a/src/js/services/ConfigService.js b/src/js/services/ConfigService.js index 74f3967..8eee6a0 100644 --- a/src/js/services/ConfigService.js +++ b/src/js/services/ConfigService.js @@ -3,33 +3,43 @@ import { getThrottling } from "./ConfigService.utils"; class ConfigService { /** * @type {object} - * @private */ - _configFromServer = {}; + #configFromServer = {}; + get configFromServer() { + return this.#configFromServer; + } /** - * @readonly * @type {boolean} */ - readOnlyOnWhiteboardLoad = false; + #readOnlyOnWhiteboardLoad = false; + get readOnlyOnWhiteboardLoad() { + return this.#readOnlyOnWhiteboardLoad; + } /** - * @readonly * @type {boolean} */ - showSmallestScreenIndicator = true; + #showSmallestScreenIndicator = true; + get showSmallestScreenIndicator() { + return this.#showSmallestScreenIndicator; + } /** - * @readonly * @type {{minDistDelta: number, minTimeDelta: number}} */ - pointerEventsThrottling = { minDistDelta: 0, minTimeDelta: 0 }; + #pointerEventsThrottling = { minDistDelta: 0, minTimeDelta: 0 }; + get pointerEventsThrottling() { + return this.#pointerEventsThrottling; + } /** - * @readonly * @type {number} */ - refreshInfoInterval = 1000; + #refreshInfoInterval = 1000; + get refreshInfoInterval() { + return this.#refreshInfoInterval; + } /** * Init the service from the config sent by the server @@ -37,28 +47,30 @@ class ConfigService { * @param {object} configFromServer */ initFromServer(configFromServer) { - this._configFromServer = configFromServer; + this.#configFromServer = configFromServer; const { common } = configFromServer; const { readOnlyOnWhiteboardLoad, showSmallestScreenIndicator, performance } = common; - this.readOnlyOnWhiteboardLoad = readOnlyOnWhiteboardLoad; - this.showSmallestScreenIndicator = showSmallestScreenIndicator; - this.refreshInfoInterval = 1000 / performance.refreshInfoFreq; + this.#readOnlyOnWhiteboardLoad = readOnlyOnWhiteboardLoad; + this.#showSmallestScreenIndicator = showSmallestScreenIndicator; + this.#refreshInfoInterval = 1000 / performance.refreshInfoFreq; console.log("Whiteboard config from server:", configFromServer, "parsed:", this); } /** - * TODO + * Refresh config that depends on the number of user connected to whiteboard + * + * @param {number} nbUser */ refreshNbUserDependant(nbUser) { - const { _configFromServer } = this; - const { common } = _configFromServer; + const { configFromServer } = this; + const { common } = configFromServer; const { performance } = common; const { pointerEventsThrottling } = performance; - this.pointerEventsThrottling = getThrottling(pointerEventsThrottling, nbUser); + this.#pointerEventsThrottling = getThrottling(pointerEventsThrottling, nbUser); } } diff --git a/src/js/services/InfoService.js b/src/js/services/InfoService.js index f9a37c9..56176f9 100644 --- a/src/js/services/InfoService.js +++ b/src/js/services/InfoService.js @@ -6,107 +6,127 @@ import ConfigService from "./ConfigService"; class InfoService { /** * @type {boolean} - * @private */ - _infoAreDisplayed = false; + #infoAreDisplayed = false; + get infoAreDisplayed() { + return this.#infoAreDisplayed; + } /** * Holds the number of user connected to the server * * @type {number} - * @readonly */ - nbConnectedUsers = -1; + #nbConnectedUsers = -1; + get nbConnectedUsers() { + return this.#nbConnectedUsers; + } /** - * * @type {{w: number, h: number}} - * @private */ - _smallestScreenResolution = undefined; + #smallestScreenResolution = undefined; + get smallestScreenResolution() { + return this.#smallestScreenResolution; + } /** * @type {number} - * @private */ - _nbMessagesSent = 0; + #nbMessagesSent = 0; + get nbMessagesSent() { + return this.#nbMessagesSent; + } /** * @type {number} - * @private */ - _nbMessagesReceived = 0; + #nbMessagesReceived = 0; + get nbMessagesReceived() { + return this.#nbMessagesReceived; + } /** * Holds the interval Id * @type {number} - * @private */ - _refreshInfoIntervalId = undefined; + #refreshInfoIntervalId = undefined; + get refreshInfoIntervalId() { + return this.#refreshInfoIntervalId; + } /** * @param {number} nbConnectedUsers * @param {{w: number, h: number}} smallestScreenResolution */ updateInfoFromServer({ nbConnectedUsers, smallestScreenResolution = undefined }) { - if (this.nbConnectedUsers !== nbConnectedUsers) { + if (this.#nbConnectedUsers !== nbConnectedUsers) { // Refresh config service parameters on nb connected user change ConfigService.refreshNbUserDependant(nbConnectedUsers); } - this.nbConnectedUsers = nbConnectedUsers; + this.#nbConnectedUsers = nbConnectedUsers; if (smallestScreenResolution) { - this._smallestScreenResolution = smallestScreenResolution; + this.#smallestScreenResolution = smallestScreenResolution; } } - /** - * @returns {(undefined|{w: number, h: number})} - */ - get smallestScreenResolution() { - return this._smallestScreenResolution; - } - incrementNbMessagesReceived() { - this._nbMessagesReceived++; + this.#nbMessagesReceived++; } incrementNbMessagesSent() { - this._nbMessagesSent++; + this.#nbMessagesSent++; } refreshDisplayedInfo() { - $("#messageReceivedCount")[0].innerText = String(this._nbMessagesReceived); - $("#messageSentCount")[0].innerText = String(this._nbMessagesSent); - $("#connectedUsersCount")[0].innerText = String(this.nbConnectedUsers); - const { _smallestScreenResolution: ssr } = this; + const { + nbMessagesReceived, + nbMessagesSent, + nbConnectedUsers, + smallestScreenResolution: ssr, + } = this; + $("#messageReceivedCount")[0].innerText = String(nbMessagesReceived); + $("#messageSentCount")[0].innerText = String(nbMessagesSent); + $("#connectedUsersCount")[0].innerText = String(nbConnectedUsers); $("#smallestScreenResolution")[0].innerText = ssr ? `(${ssr.w}, ${ssr.h})` : "Unknown"; } + /** + * Show the info div + */ displayInfo() { $("#whiteboardInfoContainer").toggleClass("displayNone", false); $("#displayWhiteboardInfoBtn").toggleClass("active", true); - this._infoAreDisplayed = true; + this.#infoAreDisplayed = true; this.refreshDisplayedInfo(); - this._refreshInfoIntervalId = setInterval(() => { + this.#refreshInfoIntervalId = setInterval(() => { // refresh only on a specific interval to reduce // refreshing cost this.refreshDisplayedInfo(); }, ConfigService.refreshInfoInterval); } + /** + * Hide the info div + */ hideInfo() { $("#whiteboardInfoContainer").toggleClass("displayNone", true); $("#displayWhiteboardInfoBtn").toggleClass("active", false); - this._infoAreDisplayed = false; - if (this._refreshInfoIntervalId) { - clearInterval(this._refreshInfoIntervalId); - this._refreshInfoIntervalId = undefined; + this.#infoAreDisplayed = false; + const { refreshInfoIntervalId } = this; + if (refreshInfoIntervalId) { + clearInterval(refreshInfoIntervalId); + this.#refreshInfoIntervalId = undefined; } } + /** + * Switch between hiding and showing the info div + */ toggleDisplayInfo() { - if (this._infoAreDisplayed) { + const { infoAreDisplayed } = this; + if (infoAreDisplayed) { this.hideInfo(); } else { this.displayInfo(); diff --git a/src/js/services/ReadOnlyService.js b/src/js/services/ReadOnlyService.js index dd1568f..449c4c4 100644 --- a/src/js/services/ReadOnlyService.js +++ b/src/js/services/ReadOnlyService.js @@ -4,23 +4,27 @@ class ReadOnlyService { /** * @type {boolean} - * @private */ - _readOnlyActive = true; + #readOnlyActive = true; + get readOnlyActive() { + return this.#readOnlyActive; + } /** * @type {object} - * @private */ - _previousToolHtmlElem = null; + #previousToolHtmlElem = null; + get previousToolHtmlElem() { + return this.#previousToolHtmlElem; + } /** * Activate read-only mode */ activateReadOnlyMode() { - this._readOnlyActive = true; + this.#readOnlyActive = true; - this._previousToolHtmlElem = $(".whiteboard-tool.active"); + this.#previousToolHtmlElem = $(".whiteboard-tool.active"); // switch to mouse tool to prevent the use of the // other tools @@ -36,7 +40,7 @@ class ReadOnlyService { * Deactivate read-only mode */ deactivateReadOnlyMode() { - this._readOnlyActive = false; + this.#readOnlyActive = false; $(".whiteboard-tool").prop("disabled", false); $(".whiteboard-edit-group > button").prop("disabled", false); @@ -45,15 +49,8 @@ class ReadOnlyService { $("#whiteboardLockBtn").hide(); // restore previously selected tool - if (this._previousToolHtmlElem) this._previousToolHtmlElem.click(); - } - - /** - * Get the read-only status - * @returns {boolean} - */ - get readOnlyActive() { - return this._readOnlyActive; + const { previousToolHtmlElem } = this; + if (previousToolHtmlElem) previousToolHtmlElem.click(); } } diff --git a/src/js/services/ThrottlingService.js b/src/js/services/ThrottlingService.js index 2fa2408..b73921e 100644 --- a/src/js/services/ThrottlingService.js +++ b/src/js/services/ThrottlingService.js @@ -5,15 +5,19 @@ import ConfigService from "./ConfigService"; class ThrottlingService { /** * @type {number} - * @private */ - _lastSuccessTime = 0; + #lastSuccessTime = 0; + get lastSuccessTime() { + return this.#lastSuccessTime; + } /** * @type {Point} - * @private */ - _lastPointPosition = new Point(0, 0); + #lastPointPosition = new Point(0, 0); + get lastPointPosition() { + return this.#lastPointPosition; + } /** * Helper to throttle events based on the configuration. @@ -24,15 +28,15 @@ class ThrottlingService { */ throttle(newPosition, onSuccess) { const newTime = getCurrentTimeMs(); - const { _lastPointPosition, _lastSuccessTime } = this; - if (newTime - _lastSuccessTime > ConfigService.pointerEventsThrottling.minTimeDelta) { + const { lastPointPosition, lastSuccessTime } = this; + if (newTime - lastSuccessTime > ConfigService.pointerEventsThrottling.minTimeDelta) { if ( - _lastPointPosition.distTo(newPosition) > + lastPointPosition.distTo(newPosition) > ConfigService.pointerEventsThrottling.minDistDelta ) { onSuccess(); - this._lastPointPosition = newPosition; - this._lastSuccessTime = newTime; + this.#lastPointPosition = newPosition; + this.#lastSuccessTime = newTime; } } } From dbc7e8c2f9f6b6ba6c55df3260581fc2309eb2fb Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Mon, 11 May 2020 15:28:14 +0200 Subject: [PATCH 12/16] feat(config): show / hide info on load --- config.default.yml | 9 ++++++--- scripts/config/config-schema.json | 16 +++++++++++++--- scripts/config/utils.test.js | 8 ++++---- src/js/main.js | 10 +++++++--- src/js/services/ConfigService.js | 14 +++++++++----- 5 files changed, 39 insertions(+), 18 deletions(-) diff --git a/config.default.yml b/config.default.yml index 9adf047..9155131 100644 --- a/config.default.yml +++ b/config.default.yml @@ -9,9 +9,12 @@ backend: whiteboardInfoBroadcastFreq: 1 frontend: - # When an editable whiteboard is loading in a client, - # should it be started in read-only mode. - readOnlyOnWhiteboardLoad: false + # When a whiteboard is loading in a client + onWhiteboardLoad: + # should an (editable) whiteboard be started in read-only mode by default + setReadOnly: false + # should the whiteboard info be displayed by default + displayInfo: false # Show smallest screen indicator showSmallestScreenIndicator: true performance: diff --git a/scripts/config/config-schema.json b/scripts/config/config-schema.json index eeb1a80..f138966 100644 --- a/scripts/config/config-schema.json +++ b/scripts/config/config-schema.json @@ -30,10 +30,20 @@ "frontend": { "type": "object", "additionalProperties": false, - "required": ["readOnlyOnWhiteboardLoad", "showSmallestScreenIndicator", "performance"], + "required": ["onWhiteboardLoad", "showSmallestScreenIndicator", "performance"], "properties": { - "readOnlyOnWhiteboardLoad": { - "type": "boolean" + "onWhiteboardLoad": { + "type": "object", + "additionalProperties": false, + "required": ["displayInfo", "setReadOnly"], + "properties": { + "setReadOnly": { + "type": "boolean" + }, + "displayInfo": { + "type": "boolean" + } + } }, "showSmallestScreenIndicator": { "type": "boolean" diff --git a/scripts/config/utils.test.js b/scripts/config/utils.test.js index d35256c..7dac58c 100644 --- a/scripts/config/utils.test.js +++ b/scripts/config/utils.test.js @@ -31,11 +31,11 @@ test("Complex object config override", () => { test("Override default config", () => { const defaultConfig = getDefaultConfig(); - const overrideConfig1 = { frontend: { readOnlyOnWhiteboardLoad: true } }; + const overrideConfig1 = { frontend: { onWhiteboardLoad: { setReadOnly: true } } }; - expect(deepMergeConfigs(defaultConfig, overrideConfig1).frontend.readOnlyOnWhiteboardLoad).toBe( - true - ); + expect( + deepMergeConfigs(defaultConfig, overrideConfig1).frontend.onWhiteboardLoad.setReadOnly + ).toBe(true); }); test("Dumb config is not valid", () => { diff --git a/src/js/main.js b/src/js/main.js index 04dbbb0..c76b3d7 100644 --- a/src/js/main.js +++ b/src/js/main.js @@ -603,10 +603,14 @@ function initWhiteboard() { // fix bug cursor not showing up whiteboard.refreshCursorAppearance(); - if (process.env.NODE_ENV === "production" && ConfigService.readOnlyOnWhiteboardLoad) { - ReadOnlyService.activateReadOnlyMode(); - InfoService.hideInfo(); + if (process.env.NODE_ENV === "production") { + if (ConfigService.readOnlyOnWhiteboardLoad) ReadOnlyService.activateReadOnlyMode(); + else ReadOnlyService.deactivateReadOnlyMode(); + + if (ConfigService.displayInfoOnWhiteboardLoad) InfoService.displayInfo(); + else InfoService.hideInfo(); } else { + // in dev ReadOnlyService.deactivateReadOnlyMode(); InfoService.displayInfo(); } diff --git a/src/js/services/ConfigService.js b/src/js/services/ConfigService.js index 8eee6a0..097bc21 100644 --- a/src/js/services/ConfigService.js +++ b/src/js/services/ConfigService.js @@ -10,11 +10,15 @@ class ConfigService { } /** - * @type {boolean} + * @type {{displayInfo: boolean, setReadOnly: boolean}} + * @readonly */ - #readOnlyOnWhiteboardLoad = false; + #onWhiteboardLoad = { setReadOnly: false, displayInfo: false }; get readOnlyOnWhiteboardLoad() { - return this.#readOnlyOnWhiteboardLoad; + return this.#onWhiteboardLoad.setReadOnly; + } + get displayInfoOnWhiteboardLoad() { + return this.#onWhiteboardLoad.displayInfo; } /** @@ -50,9 +54,9 @@ class ConfigService { this.#configFromServer = configFromServer; const { common } = configFromServer; - const { readOnlyOnWhiteboardLoad, showSmallestScreenIndicator, performance } = common; + const { onWhiteboardLoad, showSmallestScreenIndicator, performance } = common; - this.#readOnlyOnWhiteboardLoad = readOnlyOnWhiteboardLoad; + this.#onWhiteboardLoad = onWhiteboardLoad; this.#showSmallestScreenIndicator = showSmallestScreenIndicator; this.#refreshInfoInterval = 1000 / performance.refreshInfoFreq; From f9804e750fc25bd23f0bedb3319b2c29ae359bca Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Mon, 11 May 2020 16:00:45 +0200 Subject: [PATCH 13/16] feat(config): cleaned & doc --- config.default.yml | 44 +++++++++++++++------ scripts/config/config-schema.json | 8 ++-- scripts/config/config.js | 4 +- scripts/config/utils.js | 4 +- scripts/server-backend.js | 4 +- src/js/services/ConfigService.js | 6 +-- src/js/services/ConfigService.utils.js | 18 ++++----- src/js/services/ConfigService.utils.test.js | 8 ++-- src/js/services/InfoService.js | 2 +- 9 files changed, 59 insertions(+), 39 deletions(-) diff --git a/config.default.yml b/config.default.yml index 9155131..9b6688b 100644 --- a/config.default.yml +++ b/config.default.yml @@ -1,25 +1,45 @@ +# Backend configuration backend: - # TODO + # Access token required for interacting with the server -- string (empty string for no restrictions) accessToken: "" - # TODO - webdav: false + + # Is webdav saving enabled -- boolean + enableWebdav: false + + # Backend performance tweaks performance: - # Whiteboard information broadcasting frequency (in Hz i.e. /s) + # Whiteboard information broadcasting frequency (in Hz i.e. /s) -- number # => diminishing this will result in more latency whiteboardInfoBroadcastFreq: 1 +# Frontend configuration frontend: - # When a whiteboard is loading in a client + # When a whiteboard is loaded on a client onWhiteboardLoad: - # should an (editable) whiteboard be started in read-only mode by default + # should an (editable) whiteboard be started in read-only mode by default -- boolean setReadOnly: false - # should the whiteboard info be displayed by default + + # should the whiteboard info be displayed by default -- boolean displayInfo: false - # Show smallest screen indicator + + # Show the smallest screen indicator ? (with dotted lines) -- boolean showSmallestScreenIndicator: true + + # Frontend performance tweaks performance: - pointerEventsThrottling: - - fromNbUser: 0 - minDistDelta: 1 - maxFreq: 30 + # Refresh frequency of the debug / info div (in Hz i.e. /s) -- number refreshInfoFreq: 5 + + # Throttling of pointer events (except drawing related) -- array of object (one must have fromUserCount == 0) + # Throttling of events can be defined for different user count levels + # Throttling consist of skipping certain events (i.e. not broadcasting them to others) + pointerEventsThrottling: + - # User count from which the specific throttling is applied -- number + fromUserCount: 0 + # Min screen distance (in pixels) below which throttling is applied + minDistDelta: 1 + # Maximum frequency above which throttling is applied + maxFreq: 30 + - fromUserCount: 10 + minDistDelta: 5 + maxFreq: 10 diff --git a/scripts/config/config-schema.json b/scripts/config/config-schema.json index f138966..b482909 100644 --- a/scripts/config/config-schema.json +++ b/scripts/config/config-schema.json @@ -5,13 +5,13 @@ "properties": { "backend": { "type": "object", - "required": ["accessToken", "performance", "webdav"], + "required": ["accessToken", "performance", "enableWebdav"], "additionalProperties": false, "properties": { "accessToken": { "type": "string" }, - "webdav": { + "enableWebdav": { "type": "boolean" }, "performance": { @@ -59,9 +59,9 @@ "items": { "type": "object", "additionalProperties": false, - "required": ["fromNbUser", "minDistDelta", "maxFreq"], + "required": ["fromUserCount", "minDistDelta", "maxFreq"], "properties": { - "fromNbUser": { + "fromUserCount": { "type": "number", "minimum": 0 }, diff --git a/scripts/config/config.js b/scripts/config/config.js index c1f6266..10c9996 100644 --- a/scripts/config/config.js +++ b/scripts/config/config.js @@ -38,7 +38,7 @@ function updateConfigFromStartArgs(startArgs) { "disablesmallestscreen", () => (config.backend.showSmallestScreenIndicator = false) ); - deprecateCliArg("webdav", () => (config.backend.webdav = true)); + deprecateCliArg("webdav", () => (config.backend.enableWebdav = true)); } /** @@ -63,7 +63,7 @@ function updateConfigFromEnv() { "disablesmallestscreen", () => (config.backend.showSmallestScreenIndicator = false) ); - deprecateEnv("webdav", () => (config.backend.webdav = true)); + deprecateEnv("webdav", () => (config.backend.enableWebdav = true)); } // compatibility layer diff --git a/scripts/config/utils.js b/scripts/config/utils.js index e05702f..c44d151 100644 --- a/scripts/config/utils.js +++ b/scripts/config/utils.js @@ -33,7 +33,7 @@ function isConfigValid(config, warn = true) { let structureIsValid = false; try { structureIsValid = config.frontend.performance.pointerEventsThrottling.some( - (item) => item.fromNbUser === 0 + (item) => item.fromUserCount === 0 ); } catch (e) { if (!e instanceof TypeError) { @@ -44,7 +44,7 @@ function isConfigValid(config, warn = true) { if (!structureIsValid && warn) console.warn( "At least one item under frontend.performance.pointerEventsThrottling" + - "must have fromNbUser set to 0" + "must have fromUserCount set to 0" ); return isValidAgainstSchema && structureIsValid; diff --git a/scripts/server-backend.js b/scripts/server-backend.js index 54d14f7..66ae122 100644 --- a/scripts/server-backend.js +++ b/scripts/server-backend.js @@ -25,7 +25,7 @@ function startBackendServer(port) { var io = require("socket.io")(server, { path: "/ws-api" }); console.log("Webserver & socketserver running on port:" + port); - const { accessToken, webdav } = config.backend; + const { accessToken, enableWebdav } = config.backend; app.get("/api/loadwhiteboard", function (req, res) { var wid = req["query"]["wid"]; @@ -117,7 +117,7 @@ function startBackendServer(port) { } else { if (webdavaccess) { //Save image to webdav - if (webdav) { + if (enableWebdav) { saveImageToWebdav( "./public/uploads/" + filename, filename, diff --git a/src/js/services/ConfigService.js b/src/js/services/ConfigService.js index 097bc21..0405f65 100644 --- a/src/js/services/ConfigService.js +++ b/src/js/services/ConfigService.js @@ -66,15 +66,15 @@ class ConfigService { /** * Refresh config that depends on the number of user connected to whiteboard * - * @param {number} nbUser + * @param {number} userCount */ - refreshNbUserDependant(nbUser) { + refreshUserCountDependant(userCount) { const { configFromServer } = this; const { common } = configFromServer; const { performance } = common; const { pointerEventsThrottling } = performance; - this.#pointerEventsThrottling = getThrottling(pointerEventsThrottling, nbUser); + this.#pointerEventsThrottling = getThrottling(pointerEventsThrottling, userCount); } } diff --git a/src/js/services/ConfigService.utils.js b/src/js/services/ConfigService.utils.js index f2111ac..1733880 100644 --- a/src/js/services/ConfigService.utils.js +++ b/src/js/services/ConfigService.utils.js @@ -1,19 +1,19 @@ /** - * Helper to extract the correct correct throttling values based on the config and the number of user + * Helper to extract the correct throttling values based on the config and the number of user * - * @param {Array.<{fromNbUser: number, minDistDelta: number, maxFreq: number}>} pointerEventsThrottling - * @param {number} nbUser + * @param {Array.<{fromUserCount: number, minDistDelta: number, maxFreq: number}>} pointerEventsThrottling + * @param {number} userCount * @return {{minDistDelta: number, minTimeDelta: number}} */ -export function getThrottling(pointerEventsThrottling, nbUser) { +export function getThrottling(pointerEventsThrottling, userCount) { let tmpOut = pointerEventsThrottling[0]; - let lastDistToNbUser = nbUser - tmpOut.fromNbUser; - if (lastDistToNbUser < 0) lastDistToNbUser = Number.MAX_VALUE; + let lastDistToUserCount = userCount - tmpOut.fromUserCount; + if (lastDistToUserCount < 0) lastDistToUserCount = Number.MAX_VALUE; for (const el of pointerEventsThrottling) { - const distToNbUser = nbUser - el.fromNbUser; - if (el.fromNbUser <= nbUser && distToNbUser <= lastDistToNbUser) { + const distToUserCount = userCount - el.fromUserCount; + if (el.fromUserCount <= userCount && distToUserCount <= lastDistToUserCount) { tmpOut = el; - lastDistToNbUser = distToNbUser; + lastDistToUserCount = distToUserCount; } } diff --git a/src/js/services/ConfigService.utils.test.js b/src/js/services/ConfigService.utils.test.js index d60ed41..acb96ca 100644 --- a/src/js/services/ConfigService.utils.test.js +++ b/src/js/services/ConfigService.utils.test.js @@ -1,7 +1,7 @@ import { getThrottling } from "./ConfigService.utils"; test("Simple throttling config", () => { - const throttling = [{ fromNbUser: 0, minDistDelta: 1, maxFreq: 1 }]; + const throttling = [{ fromUserCount: 0, minDistDelta: 1, maxFreq: 1 }]; const target0 = { minDistDelta: 1, minTimeDelta: 1000 }; expect(getThrottling(throttling, 0)).toEqual(target0); @@ -13,9 +13,9 @@ test("Simple throttling config", () => { test("Complex throttling config", () => { // mix ordering const throttling = [ - { fromNbUser: 100, minDistDelta: 100, maxFreq: 1 }, - { fromNbUser: 0, minDistDelta: 1, maxFreq: 1 }, - { fromNbUser: 50, minDistDelta: 50, maxFreq: 1 }, + { fromUserCount: 100, minDistDelta: 100, maxFreq: 1 }, + { fromUserCount: 0, minDistDelta: 1, maxFreq: 1 }, + { fromUserCount: 50, minDistDelta: 50, maxFreq: 1 }, ]; const target0 = { minDistDelta: 1, minTimeDelta: 1000 }; diff --git a/src/js/services/InfoService.js b/src/js/services/InfoService.js index 56176f9..162d6eb 100644 --- a/src/js/services/InfoService.js +++ b/src/js/services/InfoService.js @@ -62,7 +62,7 @@ class InfoService { updateInfoFromServer({ nbConnectedUsers, smallestScreenResolution = undefined }) { if (this.#nbConnectedUsers !== nbConnectedUsers) { // Refresh config service parameters on nb connected user change - ConfigService.refreshNbUserDependant(nbConnectedUsers); + ConfigService.refreshUserCountDependant(nbConnectedUsers); } this.#nbConnectedUsers = nbConnectedUsers; if (smallestScreenResolution) { From 2dfb1079a7c7fc207343dc4bf41e9c8ef4707644 Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Mon, 11 May 2020 16:50:33 +0200 Subject: [PATCH 14/16] doc: updated README & created updating guide and node >= 12 --- .gitignore | 2 ++ Dockerfile | 6 +++--- README.md | 45 +++++++++++++++++++++++++++---------------- config.default.yml | 2 +- doc/updating_guide.md | 13 +++++++++++++ docker-compose.yml | 3 +-- 6 files changed, 48 insertions(+), 23 deletions(-) create mode 100644 doc/updating_guide.md diff --git a/.gitignore b/.gitignore index 3e18fdc..2970374 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +/config.run.yml + # Logs logs *.log diff --git a/Dockerfile b/Dockerfile index 49c92b2..5c0470e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:11 as base +FROM node:12 as base # Create app directory RUN mkdir -p /opt/app @@ -19,7 +19,7 @@ RUN npm run build # Final image ##################### -FROM node:11-alpine +FROM node:12-alpine ENV NODE_ENV=prod MAINTAINER cracker0dks @@ -28,7 +28,7 @@ MAINTAINER cracker0dks RUN mkdir -p /opt/app WORKDIR /opt/app -COPY ./package.json ./package-lock.json ./ +COPY ./package.json ./package-lock.json config.default.yml ./ RUN npm ci --only=prod COPY scripts ./scripts diff --git a/README.md b/README.md index 0759645..3054515 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,10 @@ This is a lightweight NodeJS collaborative Whiteboard/Sketchboard witch can easi [HERE](https://cloud13.de/testwhiteboard/) (Reset every night) +## Updating + +Information related to updating this app can be found [here](./doc/updating_guide.md). + ## Some Features - Shows remote user cursors while drawing @@ -34,7 +38,7 @@ You can run this app with and without docker ### Without Docker -1. install the latest NodeJs +1. install the latest NodeJs (version >= 12) 2. Clone the app 3. Run `npm ci` inside the folder 4. Run `npm run start:prod` @@ -108,13 +112,26 @@ Call your site with GET parameters to change the WhiteboardID or the Username - title => Change the name of the Browser Tab - randomid => if set to true, a random whiteboardId will be generated if not given aswell -## Security - AccessToken (Optional) +## Configuration -To prevent clients who might know or guess the base URL from abusing the server to upload files and stuff..., you can set an accesstoken at server start. +Many settings of this project can be set using a simple `yaml` file, to change some behaviors or tweak performances. -Server (Without docker): `node scripts/server.js --accesstoken="mySecToken"` +### Config. file -Server (With docker): `docker run -d -p 8080:8080 rofl256/whiteboard --accesstoken="mySecToken"` +To run the project with custom settings: + +1. Create a `config.run.yml` file based on the content of [`config.default.yml`](./config.default.yml), +2. Change the settings, +3. Run the project with your custom configuration (it will be merged into the default one): + +- locally: `node scripts/server.js --config=./config.run.yml` +- docker: `docker run -d -p 8080:8080 -v $(pwd)/config.run.yml:/config.run.yml:ro rofl256/whiteboard --config=/config.run.yml` + +### Highlights + +#### Security - AccessToken (Optional) + +To prevent clients who might know or guess the base URL from abusing the server to upload files and stuff..., you can set an accesstoken at server start (see [here](./config.default.yml)). Then set the same token on the client side as well: @@ -122,15 +139,11 @@ Then set the same token on the client side as well: Done! -## WebDAV (Optional) +#### WebDAV (Optional) This function allows your users to save the whiteboard directly to a webdav server (Nextcloud) as image without downloading it. -To enable it: - -Server (Without docker): `node scripts/server.js --webdav=true` - -Server (With docker): `docker run -d -p 8080:8080 rofl256/whiteboard --webdav=true` +To enable set `enableWebdav` to `true` in the [configuration](./config.default.yml). Then set the same parameter on the client side as well: @@ -142,17 +155,15 @@ Note: For the most owncloud/nextcloud setups you have to set the WebDav-Server U Done! +### And many more (performance, etc.) + +Many more settings can be tweaked. All of them are described in the [default config file](./config.default.yml). + ## Things you may want to know - Whiteboards are gone if you restart the Server, so keep that in mind (or save your whiteboard) - You should be able to customize the layout without ever touching the whiteboard.js (take a look at index.html & main.js) -## All server start parameters (also docker) - -- accesstoken => take a look at "Security - AccessToken" for a full explanation -- disablesmallestscreen => set this to "true" if you don't want show the "smallest screen" indicator (A dotted gray line) to the users -- webdav => Enable the function to save to a webdav-server (Must also be enabled on the client; Take a look at the webdav section) - ## ToDo - Make undo function more reliable on texts diff --git a/config.default.yml b/config.default.yml index 9b6688b..2ac3912 100644 --- a/config.default.yml +++ b/config.default.yml @@ -3,7 +3,7 @@ backend: # Access token required for interacting with the server -- string (empty string for no restrictions) accessToken: "" - # Is webdav saving enabled -- boolean + # Enable the function to save to a webdav-server (check README for more info) -- boolean enableWebdav: false # Backend performance tweaks diff --git a/doc/updating_guide.md b/doc/updating_guide.md new file mode 100644 index 0000000..4e3fe6c --- /dev/null +++ b/doc/updating_guide.md @@ -0,0 +1,13 @@ +# Updating guide + +## From v1.x to 2.x (or latest) + +Configuration handling has been updated: the ability to change settings from the CLI or the environment has been removed. + +**Configuration is now handled with a yml config file**, which can be overridden with the `--config` CLI argument. + +Here is the mapping from old cli argument / env variables to the new config file object: + +- accesstoken => `backend.accessToken` +- webdav => `backend.enableWebdav` +- disablesmallestscreen => `frontend.showSmallestScreenIndicator` diff --git a/docker-compose.yml b/docker-compose.yml index 878bd00..c156505 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,5 +5,4 @@ services: restart: always ports: - "8080:8080/tcp" - environment: - - ACCESSTOKEN=mysecrettoken + command: --config=./config.default.yml From 203e35b3e43a755de4fe5bc5c621bf56d41ce43e Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Mon, 11 May 2020 17:32:20 +0200 Subject: [PATCH 15/16] feat(front): show smallest screen indicator based on config --- src/js/whiteboard.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/js/whiteboard.js b/src/js/whiteboard.js index 353add6..89923f6 100644 --- a/src/js/whiteboard.js +++ b/src/js/whiteboard.js @@ -3,6 +3,7 @@ import Point from "./classes/Point"; import ReadOnlyService from "./services/ReadOnlyService"; import InfoService from "./services/InfoService"; import ThrottlingService from "./services/ThrottlingService"; +import ConfigService from "./services/ConfigService"; const RAD_TO_DEG = 180.0 / Math.PI; const DEG_TO_RAD = Math.PI / 180.0; @@ -1031,7 +1032,8 @@ const whiteboard = { }, updateSmallestScreenResolution() { const { smallestScreenResolution } = InfoService; - if (smallestScreenResolution) { + const { showSmallestScreenIndicator } = ConfigService; + if (showSmallestScreenIndicator && smallestScreenResolution) { const { w: width, h: height } = smallestScreenResolution; this.backgroundGrid.empty(); if (width < $(window).width() || height < $(window).height()) { From 00f7b10ac9297ecd7fdc185f153c245a70b3a277 Mon Sep 17 00:00:00 2001 From: Florent Chehab Date: Mon, 11 May 2020 17:54:41 +0200 Subject: [PATCH 16/16] chore: added doc on new classes --- scripts/WhiteboardServerSideInfo.js | 3 +++ src/js/services/ConfigService.js | 3 +++ src/js/services/ThrottlingService.js | 3 +++ 3 files changed, 9 insertions(+) diff --git a/scripts/WhiteboardServerSideInfo.js b/scripts/WhiteboardServerSideInfo.js index 04bf79f..ba442c5 100644 --- a/scripts/WhiteboardServerSideInfo.js +++ b/scripts/WhiteboardServerSideInfo.js @@ -1,5 +1,8 @@ const config = require("./config/config"); +/** + * Class to hold information related to a whiteboard + */ class WhiteboardServerSideInfo { static defaultScreenResolution = { w: 1000, h: 1000 }; diff --git a/src/js/services/ConfigService.js b/src/js/services/ConfigService.js index 0405f65..c00b70e 100644 --- a/src/js/services/ConfigService.js +++ b/src/js/services/ConfigService.js @@ -1,5 +1,8 @@ import { getThrottling } from "./ConfigService.utils"; +/** + * Class to hold the configuration sent by the backend + */ class ConfigService { /** * @type {object} diff --git a/src/js/services/ThrottlingService.js b/src/js/services/ThrottlingService.js index b73921e..73c69b8 100644 --- a/src/js/services/ThrottlingService.js +++ b/src/js/services/ThrottlingService.js @@ -2,6 +2,9 @@ import Point from "../classes/Point"; import { getCurrentTimeMs } from "../utils"; import ConfigService from "./ConfigService"; +/** + * Class to handle all the throttling logic + */ class ThrottlingService { /** * @type {number}