diff --git a/.travis.yml b/.travis.yml index 0e87d6e..70147af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ language: node_js node_js: - - '11' + - '10' os: osx script: npm run test:ci && npm run release diff --git a/package-lock.json b/package-lock.json index 7576dbb..0c6151c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "netlify-menubar", - "version": "1.4.0", + "version": "1.5.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -39,18 +39,18 @@ } }, "@babel/runtime": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.1.tgz", - "integrity": "sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==", + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.3.4.tgz", + "integrity": "sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g==", "dev": true, "requires": { "regenerator-runtime": "^0.12.0" } }, "@types/auto-launch": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@types/auto-launch/-/auto-launch-5.0.0.tgz", - "integrity": "sha512-yxT2pVPPhFJNuiYX9kyAqRBA4EV7J/QeA/igZK6c4b/hS5Jn71R/Jl5J866WHNG4+r8uteLo3gojSh0sZmlqew==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@types/auto-launch/-/auto-launch-5.0.1.tgz", + "integrity": "sha512-+KQ+/koZ7sJXnf5cnCANofY6yXAdYJNEoVZEuWcwJfuWbUp9u6l09I7KhwD+ivU+cdz7JId4V5ukxscWtHdSuw==", "dev": true }, "@types/electron-settings": { @@ -63,21 +63,21 @@ } }, "@types/jest": { - "version": "23.3.13", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-23.3.13.tgz", - "integrity": "sha512-ePl4l+7dLLmCucIwgQHAgjiepY++qcI6nb8eAwGNkB6OxmTe3Z9rQU3rSpomqu42PCCnlThZbOoxsf+qylJsLA==", + "version": "23.3.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-23.3.14.tgz", + "integrity": "sha512-Q5hTcfdudEL2yOmluA1zaSyPbzWPmJ3XfSWeP3RyoYvS9hnje1ZyagrZOuQ6+1nQC1Gw+7gap3pLNL3xL6UBug==", "dev": true }, "@types/node": { - "version": "10.12.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.19.tgz", - "integrity": "sha512-2NVovndCjJQj6fUUn9jCgpP4WSqr+u1SoUZMZyJkhGeBFsm6dE46l31S7lPUYt9uQ28XI+ibrJA1f5XyH5HNtA==", + "version": "11.10.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-11.10.5.tgz", + "integrity": "sha512-DuIRlQbX4K+d5I+GMnv+UfnGh+ist0RdlvOp+JZ7ePJ6KQONCFQv/gKYSU1ZzbVdFSUCKZOltjmpFAGGv5MdYA==", "dev": true }, "@types/node-fetch": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.1.4.tgz", - "integrity": "sha512-tR1ekaXUGpmzOcDXWU9BW73YfA2/VW1DF1FH+wlJ82BbCSnWTbdX+JkqWQXWKIGsFPnPsYadbXfNgz28g+ccWg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.1.6.tgz", + "integrity": "sha512-Hv1jgh3pfpUEl2F2mqUd1AfLSk1YbUCeBJFaP36t7esAO617dErqdxWb5cdG2NfJGOofkmBW36fdx0dVewxDRg==", "dev": true, "requires": { "@types/node": "*" @@ -106,9 +106,9 @@ }, "dependencies": { "acorn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.6.tgz", - "integrity": "sha512-5M3G/A4uBSMIlfJ+h9W125vJvPFH/zirISsW5qfxF5YzEvXJCtolLoQvM5yZft0DvMcUrPGKPOlgEu55I6iUtA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.1.1.tgz", + "integrity": "sha512-jPTiwtOxaHNaAPg/dmrJ/beuzLRnXtB0kQPQ8JpotKJgTB6rX6c8mlf315941pyjBSaPg8NHXS9fhP4u17DpGA==", "dev": true } } @@ -119,10 +119,19 @@ "integrity": "sha512-OtUw6JUTgxA2QoqqmrmQ7F2NYqiBPi/L2jqHyFtllhOUvXYQXf0Z1CYUinIfyT4bTCGmrA7gX9FvHA81uzCoVw==", "dev": true }, + "agent-base": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz", + "integrity": "sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==", + "dev": true, + "requires": { + "es6-promisify": "^5.0.0" + } + }, "ajv": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.7.0.tgz", - "integrity": "sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==", + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.0.tgz", + "integrity": "sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==", "dev": true, "requires": { "fast-deep-equal": "^2.0.1", @@ -132,9 +141,9 @@ } }, "ajv-keywords": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.3.0.tgz", - "integrity": "sha512-CMzN9S62ZOO4sA/mJZIO4S++ZM7KFWzH3PPWkveLhy4OZ9i1/VatgwWMD46w/XbGCBy7Ye0gCk+Za6mmyfKK7g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.4.0.tgz", + "integrity": "sha512-aUjdRFISbuFOl0EIZc+9e4FfZp0bDZgAdOOf30bJmw8VM9v84SHyVyxDfbWxpGYbdZD/9XoKxfHVNmxPkhwyGw==", "dev": true }, "all-contributors-cli": { @@ -623,12 +632,12 @@ "dev": true }, "async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", - "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.2.tgz", + "integrity": "sha512-H1qVYh1MYhEEFLsP97cVKqCGo7KfCyTt6uEWqsTBr9SO84oK9Uwbyd/yCW+6rKJLHksBNUVWZDAjfS+Ccx0Bbg==", "dev": true, "requires": { - "lodash": "^4.17.10" + "lodash": "^4.17.11" } }, "async-exit-hook": { @@ -1135,11 +1144,11 @@ "integrity": "sha512-/qKPUQlaW1OyR51WeCPBvRnAlnZFUJkCSG5HzGnuIqhgyJtF+T94lFnn33eiazjRm2LAHVy2guNnaq48X9SJuw==" }, "bluebird-lst": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.6.tgz", - "integrity": "sha512-CBWFoPuUPpcvMUxfyr8DKdI5d4kjxFl1h39+VbKxP3KJWJHEsLtuT4pPLkjpxCGU6Ask21tvbnftWXdqIxYldQ==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/bluebird-lst/-/bluebird-lst-1.0.7.tgz", + "integrity": "sha512-5ix04IbXVIZ6nSRM4aZnwQfk40Td0D57WAl8LfhnICF6XwT4efCZYh0veOHvfDmgpbqE4ju5L5XEAMIcAe13Kw==", "requires": { - "bluebird": "^3.5.2" + "bluebird": "^3.5.3" } }, "boxen": { @@ -1199,6 +1208,14 @@ "dev": true, "requires": { "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", + "dev": true + } } }, "bs-logger": { @@ -1278,9 +1295,9 @@ } }, "builtin-modules": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.0.0.tgz", - "integrity": "sha512-hMIeU4K2ilbXV6Uv93ZZ0Avg/M91RaKXucQ+4me2Do1txxBDyDZWCBa5bJSLqoNTRpXTLwEzIk1KmloenDDjhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", "dev": true }, "cache-base": { @@ -1333,9 +1350,9 @@ "dev": true }, "camelcase": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.0.0.tgz", - "integrity": "sha512-faqwZqnWxbxn+F1d399ygeamQNy3lPp/H9H6rNrqYh4FSVCtcY+3cub1MxA8o9mDd55mM8Aghuu/kuyYA6VTsA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.2.0.tgz", + "integrity": "sha512-IXFsBS2pC+X0j0N/GE7Dm7j3bsEBp+oTpb7F50dwEVX7rf3IgwO9XatnegTsDtniKCUtEJH4fSU6Asw7uoVLfQ==", "dev": true }, "camelcase-keys": { @@ -1496,15 +1513,15 @@ "dev": true }, "codecov": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.1.0.tgz", - "integrity": "sha512-aWQc/rtHbcWEQLka6WmBAOpV58J2TwyXqlpAQGhQaSiEUoigTTUk6lLd2vB3kXkhnDyzyH74RXfmV4dq2txmdA==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/codecov/-/codecov-3.2.0.tgz", + "integrity": "sha512-3NJvNARXxilqnqVfgzDHyVrF4oeVgaYW1c1O6Oi5mn93exE7HTSSFNiYdwojWW6IwrCZABJ8crpNbKoo9aUHQw==", "dev": true, "requires": { "argv": "^0.0.2", "ignore-walk": "^3.0.1", "js-yaml": "^3.12.0", - "request": "^2.87.0", + "teeny-request": "^3.7.0", "urlgrey": "^0.4.4" } }, @@ -1640,9 +1657,9 @@ "dev": true }, "core-js": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.3.tgz", - "integrity": "sha512-l00tmFFZOBHtYhN4Cz7k32VM7vTn3rE2ANjQDxdEN6zmXZ/xq1jQuutnmHvMG1ZJ7xd72+TA5YpUK8wz3rWsfQ==", + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==", "dev": true }, "core-util-is": { @@ -1652,14 +1669,15 @@ "dev": true }, "cosmiconfig": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.0.7.tgz", - "integrity": "sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.1.0.tgz", + "integrity": "sha512-kCNPvthka8gvLtzAxQXvWo4FxqRB+ftRZyPZNuab5ngvM9Y7yw7hbEysglptLgpkGX9nAOKTBVkHUAe8xtYR6Q==", "dev": true, "requires": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.9.0", + "lodash.get": "^4.4.2", "parse-json": "^4.0.0" }, "dependencies": { @@ -1704,15 +1722,15 @@ "dev": true }, "cssom": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.4.tgz", - "integrity": "sha512-+7prCSORpXNeR4/fUP3rL+TzqtiFfhMvTd7uEqMdgPvLPt4+uzFUeufx5RHjGTACCargg/DiEt/moMQmvnfkog==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.6.tgz", + "integrity": "sha512-DtUeseGk9/GBW0hl0vVPpU22iHL6YB5BUX7ml1hB+GMpo0NX5G4voX3kdWiMSEguFtcW3Vh3djqNF4aIe6ne0A==", "dev": true }, "cssstyle": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.1.1.tgz", - "integrity": "sha512-364AI1l/M5TYcFH83JnOH/pSqgaNnKmYgKrm0didZMGKWjQB60dymwWy1rKUgL3J1ffdq9xVi2yGLHdSjjSNog==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-1.2.1.tgz", + "integrity": "sha512-7DYm8qe+gPx/h77QlCyFmX80+fGaE/6A/Ekl0zaszYOubvySO2saYFdQ78P29D0UsULxFKCetDGNaNRUdSF+2A==", "dev": true, "requires": { "cssom": "0.3.x" @@ -1816,9 +1834,9 @@ }, "dependencies": { "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", + "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", "dev": true } } @@ -1972,14 +1990,22 @@ "dev": true }, "electron": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/electron/-/electron-4.0.3.tgz", - "integrity": "sha512-wOBYnlv3Xgbwh9DAHBktP3sQcbCBuXMQi1NzPH3EMnbdYNqj+FnTzVLq0RYp0uSzdjR3fhAZ/E6wFSMLaAc5iw==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-4.0.7.tgz", + "integrity": "sha512-KYQ9SJZFWNKqoq6XjKW1bLFHjmAGeSC3XNuhHK/Sd2MK5H5sO3iKjvZU/YhiBUtkB/cBSkOdQTVEaLcMwU8l3A==", "dev": true, "requires": { "@types/node": "^10.12.18", "electron-download": "^4.1.0", "extract-zip": "^1.0.3" + }, + "dependencies": { + "@types/node": { + "version": "10.12.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-10.12.30.tgz", + "integrity": "sha512-nsqTN6zUcm9xtdJiM9OvOJ5EF0kOI8f1Zuug27O/rgtxCRJHGqncSWfCMZUP852dCKPsDsYXGvBhxfRjDBkF5Q==", + "dev": true + } } }, "electron-builder": { @@ -2143,6 +2169,11 @@ "once": "^1.4.0" } }, + "entities": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", + "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + }, "env-paths": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-1.0.0.tgz", @@ -2173,9 +2204,9 @@ }, "dependencies": { "object-keys": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz", - "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.0.tgz", + "integrity": "sha512-6OO5X1+2tYkNyNEx6TsCxEqFfRWaqx6EtMiSbGrw8Ob8v9Ne+Hl8rBAgLBZn5wjEz3s/s6U1WXFUFOcxxAwUpg==", "dev": true } } @@ -2191,6 +2222,21 @@ "is-symbol": "^1.0.2" } }, + "es6-promise": { + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.6.tgz", + "integrity": "sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==", + "dev": true + }, + "es6-promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", + "dev": true, + "requires": { + "es6-promise": "^4.0.3" + } + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -2198,9 +2244,9 @@ "dev": true }, "escodegen": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.0.tgz", - "integrity": "sha512-IeMV45ReixHS53K/OmfKAIztN/igDHzTJUhZM3k1jMhIZWjk45SMwAtBsEXiJp3vSPmTcu6CXn7mDvFHRN66fw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.11.1.tgz", + "integrity": "sha512-JwiqFD9KdGVVpeuRa68yU3zZnBEOcPs0nKW7wZzXky8Z7tffdYUHbe11bPCV5jYlK6DVdKLWLm0f5I/QlL0Kmw==", "dev": true, "requires": { "esprima": "^3.1.3", @@ -2514,12 +2560,12 @@ } }, "fs-extra-p": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-7.0.0.tgz", - "integrity": "sha512-5tg5jBOd0xIXjwj4PDnafOXL5TyPVzjxLby4DPKev53wurEXp7IsojBaD4Lj5M5w7jxw0pbkEU0fFEPmcKoMnA==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra-p/-/fs-extra-p-7.0.1.tgz", + "integrity": "sha512-yhd2OV0HnHt2oitlp+X9hl2ReX4X/7kQeL7/72qzPHTZj5eUPGzAKOvEglU02Fa1OeG2rSy/aKB4WGVaLiF8tw==", "requires": { - "bluebird-lst": "^1.0.6", - "fs-extra": "^7.0.0" + "bluebird-lst": "^1.0.7", + "fs-extra": "^7.0.1" } }, "fs.realpath": { @@ -3186,9 +3232,9 @@ "dev": true }, "handlebars": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz", - "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.0.tgz", + "integrity": "sha512-l2jRuU1NAWK6AW5qqcTATWQJvNPEwkM7NEKSiv/gqOsoSQbVoWyqVEY5GS+XPQ88zLNmqASRpzfdm8d79hJS+w==", "dev": true, "requires": { "async": "^2.5.0", @@ -3347,6 +3393,27 @@ "sshpk": "^1.7.0" } }, + "https-proxy-agent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz", + "integrity": "sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==", + "dev": true, + "requires": { + "agent-base": "^4.1.0", + "debug": "^3.1.0" + }, + "dependencies": { + "debug": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + } + } + }, "husky": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/husky/-/husky-1.3.1.tgz", @@ -3536,21 +3603,21 @@ "dev": true }, "inquirer": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.1.tgz", - "integrity": "sha512-088kl3DRT2dLU5riVMKKr1DlImd6X7smDhpXUCkJDCKvTEJeRiXh0G132HG9u5a+6Ylw9plFRY7RuTnwohYSpg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-6.2.2.tgz", + "integrity": "sha512-Z2rREiXA6cHRR9KBOarR3WuLlFzlIfAEIiB45ll5SSadMg7WqOh1MKEjjndfuH5ewXdixWCxqnVfGOQzPeiztA==", "dev": true, "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", + "ansi-escapes": "^3.2.0", + "chalk": "^2.4.2", "cli-cursor": "^2.1.0", "cli-width": "^2.0.0", - "external-editor": "^3.0.0", + "external-editor": "^3.0.3", "figures": "^2.0.0", - "lodash": "^4.17.10", + "lodash": "^4.17.11", "mute-stream": "0.0.7", "run-async": "^2.2.0", - "rxjs": "^6.1.0", + "rxjs": "^6.4.0", "string-width": "^2.1.0", "strip-ansi": "^5.0.0", "through": "^2.3.6" @@ -3592,15 +3659,6 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", "dev": true }, - "is-builtin-module": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.0.0.tgz", - "integrity": "sha512-/93sDihsAD652hrMEbJGbMAVBf1qc96kyThHQ0CAOONHaE3aROLpTjDe4WQ5aoC5ITHFxEq1z8XqSU7km+8amw==", - "dev": true, - "requires": { - "builtin-modules": "^3.0.0" - } - }, "is-callable": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.4.tgz", @@ -3842,6 +3900,12 @@ "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", "dev": true }, + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "dev": true + }, "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -4753,9 +4817,9 @@ "dev": true }, "js-yaml": { - "version": "3.12.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.1.tgz", - "integrity": "sha512-um46hB9wNOKlwkHgiuyEVAybXBjwFUV0Z/RaHJblRd9DXltue9FTYvzCr9ErQrK9Adz5MU4gHWVaNUfdmrC8qA==", + "version": "3.12.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.2.tgz", + "integrity": "sha512-QHn/Lh/7HhZ/Twc7vJYQTkjuCa0kaCcDcjK5Zlk2rvnUpy7DxMJ23+Jc2dcyvltwQVg1nygAVlB2oRDFHoRS5Q==", "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -4893,9 +4957,9 @@ } }, "lazy-val": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.3.tgz", - "integrity": "sha512-pjCf3BYk+uv3ZcPzEVM0BFvO9Uw58TmlrU0oG5tTrr9Kcid3+kdKxapH8CjdYmVa2nO5wOoZn2rdvZx2PKj/xg==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.4.tgz", + "integrity": "sha512-u93kb2fPbIrfzBuLjZE+w+fJbUUMhNDXxNmMfaqNgpfQf1CO5ZSe2LfsnBqVAk7i/2NF48OSoRj+Xe2VT+lE8Q==" }, "lcid": { "version": "2.0.0", @@ -4965,6 +5029,12 @@ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==", "dev": true }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", + "dev": true + }, "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", @@ -5191,18 +5261,18 @@ "dev": true }, "mime-db": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", - "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "version": "1.38.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.38.0.tgz", + "integrity": "sha512-bqVioMFFzc2awcdJZIzR3HjZFX20QhilVS7hytkKrv7xFAn8bM1gzc/FOX2awLISvWe0PV8ptFKcon+wZ5qYkg==", "dev": true }, "mime-types": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.21.tgz", - "integrity": "sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==", + "version": "2.1.22", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.22.tgz", + "integrity": "sha512-aGl6TZGnhm/li6F7yx82bJiBZwgiEa4Hf6CNr8YO+r5UHr53tSTYZb102zyU50DOWWKeOv0uQLRL0/9EiKWCog==", "dev": true, "requires": { - "mime-db": "~1.37.0" + "mime-db": "~1.38.0" } }, "mimic-fn": { @@ -5335,25 +5405,26 @@ "dev": true }, "node-notifier": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.3.0.tgz", - "integrity": "sha512-AhENzCSGZnZJgBARsUjnQ7DnZbzyP+HxlVXuD0xqAnvL8q+OqtSX7lGg9e8nHzwXkMMXNdVeqq4E2M3EUAqX6Q==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz", + "integrity": "sha512-SUDEb+o71XR5lXSTyivXd9J7fCloE3SyP4lSgt3lU2oSANiox+SxlNRGPjDKrwU1YN3ix2KN/VGGCg0t01rttQ==", "dev": true, "requires": { "growly": "^1.3.0", + "is-wsl": "^1.1.0", "semver": "^5.5.0", "shellwords": "^0.1.1", "which": "^1.3.0" } }, "normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-ZVuHxWJv1bopjv/SD5uPhgwUhLqxdJ+SsdUQbGR9HWlXrvnd/C08Cn9Bq48PbvX3y5V97GIpAHpL5Bk9BwChGg==", + "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", - "is-builtin-module": "^3.0.0", + "resolve": "^1.10.0", "semver": "2 || 3 || 4 || 5", "validate-npm-package-license": "^3.0.1" } @@ -5421,9 +5492,9 @@ "dev": true }, "nwsapi": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.0.9.tgz", - "integrity": "sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.1.1.tgz", + "integrity": "sha512-T5GaA1J/d34AC8mkrFD2O0DR17kwJ702ZOtJOsS8RpbsQZVOC2/xYFb1i/cw+xdM54JIlMuojjDOYct8GIWtwg==", "dev": true }, "oauth-sign": { @@ -5612,9 +5683,9 @@ "dev": true }, "p-limit": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.1.0.tgz", - "integrity": "sha512-NhURkNcrVB+8hNfLuysU8enY5xn2KXphsHBaC2YmRNTZRc7RWusw6apSpdEj3jo4CMb6W9nrF6tTnsJsJeyu6g==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", + "integrity": "sha512-pZbTJpoUsCzV48Mc9Nh51VbwO0X9cuPFE8gYwx9BTCt9SF8/b7Zljd2fVgOxhIF/HDTKgpVzs+GPhyKfjLLFRQ==", "dev": true, "requires": { "p-try": "^2.0.0" @@ -5648,9 +5719,9 @@ } }, "pako": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.8.tgz", - "integrity": "sha512-6i0HVbUfcKaTv+EG8ZTr75az7GFXcLYk9UyLEg7Notv/Ma+z/UG3TCoz6GiNeOrn1E/e63I0X/Hpw18jHOTUnA==" + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", + "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==" }, "parse-color": { "version": "1.0.0", @@ -6045,9 +6116,9 @@ } }, "realpath-native": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.0.2.tgz", - "integrity": "sha512-+S3zTvVt9yTntFrBpm7TQmQ3tzpCrnA1a/y+3cUHAc9ZR6aIjG0WNLR+Rj79QpJktY+VeW/TQtFlQ1bzsehI8g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-1.1.0.tgz", + "integrity": "sha512-wlgPA6cCIIg9gKz0fgAPjnzh4yR/LnXovwuo9hvyGvx3h8nX4+/iLZplfUWasXpqD8BdnGnP5njOFjkUwPzvjA==", "dev": true, "requires": { "util.promisify": "^1.0.0" @@ -6163,23 +6234,23 @@ } }, "request-promise-core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.1.tgz", - "integrity": "sha1-Pu4AssWqgyOc+wTFcA2jb4HNCLY=", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/request-promise-core/-/request-promise-core-1.1.2.tgz", + "integrity": "sha512-UHYyq1MO8GsefGEt7EprS8UrXsm1TxEvFUX1IMTuSLU2Rh7fTIdFtl8xD7JiEYiWU2dl+NYAjCTksTehQUxPag==", "dev": true, "requires": { - "lodash": "^4.13.1" + "lodash": "^4.17.11" } }, "request-promise-native": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.5.tgz", - "integrity": "sha1-UoF3D2jgyXGeUWP9P6tIIhX0/aU=", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/request-promise-native/-/request-promise-native-1.0.7.tgz", + "integrity": "sha512-rIMnbBdgNViL37nZ1b3L/VfPOpSi0TqVDQPAvO6U14lMzOLrt5nilxCQqtDKhZeDiW0/hkCXGoQjhgJd/tCh6w==", "dev": true, "requires": { - "request-promise-core": "1.1.1", - "stealthy-require": "^1.1.0", - "tough-cookie": ">=2.3.3" + "request-promise-core": "1.1.2", + "stealthy-require": "^1.1.1", + "tough-cookie": "^2.3.3" } }, "require-directory": { @@ -6195,10 +6266,13 @@ "dev": true }, "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", + "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", + "dev": true, + "requires": { + "path-parse": "^1.0.6" + } }, "resolve-cwd": { "version": "2.0.0", @@ -6246,6 +6320,15 @@ "glob": "^7.1.3" } }, + "rss-parser": { + "version": "3.6.3", + "resolved": "https://registry.npmjs.org/rss-parser/-/rss-parser-3.6.3.tgz", + "integrity": "sha512-HASaMXqkUUw7mS+Sz780ml3Da3dG4ddIA3FRyXE8ae1KIiiDxG+jzrsNk/bpK1zDrDKe2WztjfQ5MRF323Kg3g==", + "requires": { + "entities": "^1.1.1", + "xml2js": "^0.4.19" + } + }, "rsvp": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-3.6.2.tgz", @@ -7165,6 +7248,17 @@ "integrity": "sha1-rifbOPZgp64uHDt9G8KQgZuFGeY=", "dev": true }, + "teeny-request": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-3.11.3.tgz", + "integrity": "sha512-CKncqSF7sH6p4rzCgkb/z/Pcos5efl0DmolzvlqRQUNcpRIruOhY9+T1FsIlyEbfWd7MsFpodROOwHYh2BaXzw==", + "dev": true, + "requires": { + "https-proxy-agent": "^2.2.1", + "node-fetch": "^2.2.0", + "uuid": "^3.3.2" + } + }, "temp-file": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/temp-file/-/temp-file-3.3.2.tgz", @@ -7418,9 +7512,9 @@ "dev": true }, "tslint": { - "version": "5.12.1", - "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.12.1.tgz", - "integrity": "sha512-sfodBHOucFg6egff8d1BvuofoOQ/nOeYNfbp7LDlKBcLNrL3lmS5zoiDGyOMdT7YsEXAwWpTdAHwOGOc8eRZAw==", + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.13.1.tgz", + "integrity": "sha512-fplQqb2miLbcPhyHoMV4FU9PtNRbgmm/zI5d3SZwwmJQM6V0eodju+hplpyfhLWpmwrDNfNYU57uYRb8s0zZoQ==", "dev": true, "requires": { "babel-code-frame": "^6.22.0", @@ -7431,33 +7525,17 @@ "glob": "^7.1.1", "js-yaml": "^3.7.0", "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", "resolve": "^1.3.2", "semver": "^5.3.0", "tslib": "^1.8.0", "tsutils": "^2.27.2" - }, - "dependencies": { - "builtin-modules": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", - "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", - "dev": true - }, - "resolve": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.10.0.tgz", - "integrity": "sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg==", - "dev": true, - "requires": { - "path-parse": "^1.0.6" - } - } } }, "tslint-config-prettier": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.17.0.tgz", - "integrity": "sha512-NKWNkThwqE4Snn4Cm6SZB7lV5RMDDFsBwz6fWUkTxOKGjMx8ycOHnjIbhn7dZd5XmssW3CwqUjlANR6EhP9YQw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz", + "integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==", "dev": true }, "tsutils": { @@ -7500,9 +7578,9 @@ "dev": true }, "typescript": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.2.4.tgz", - "integrity": "sha512-0RNDbSdEokBeEAkgNbxJ+BLwSManFy9TeXz8uW+48j/xhEXv1ePME60olyzw2XzUqUBNAYFeJadIqAgNqIACwg==", + "version": "3.3.3333", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.3333.tgz", + "integrity": "sha512-JjSKsAfuHBE/fB2oZ8NxtRTk5iGcg6hkYXMnZ3Wc+b2RSqejEqTaem11mHASMnFilHrax3sLK0GDzcJrekZYLw==", "dev": true }, "uglify-js": { @@ -7936,11 +8014,19 @@ "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", "dev": true }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, "xmlbuilder": { "version": "9.0.7", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", - "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=", - "dev": true + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xmldom": { "version": "0.1.27", diff --git a/package.json b/package.json index 8f69c5a..93d7234 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,8 @@ "date-fns": "^1.30.1", "electron-settings": "^3.2.0", "electron-updater": "^4.0.6", - "node-fetch": "^2.3.0" + "node-fetch": "^2.3.0", + "rss-parser": "^3.6.2" }, "devDependencies": { "@types/auto-launch": "^5.0.0", diff --git a/src/incidentFeed.test.ts b/src/incidentFeed.test.ts new file mode 100644 index 0000000..a5db6cb --- /dev/null +++ b/src/incidentFeed.test.ts @@ -0,0 +1,81 @@ +const firstResponse = { + items: [ + { link: 'id=1', content: 'not updated' }, + { link: 'id=2', content: 'not updated' } + ] +}; +const secondResponse = { + items: [ + { link: 'id=1', content: 'not updated' }, + { link: 'id=2', content: 'not updated' }, + { link: 'id=3', content: 'not updated' } + ] +}; +const thirdResponse = { + items: [ + { link: 'id=1', content: 'updated' }, + { link: 'id=2', content: 'not updated' }, + { link: 'id=3', content: 'not updated' } + ] +}; +const fourthResponse = { + items: [ + { link: 'id=1', content: 'updated' }, + { link: 'id=2', content: 'not updated' }, + { link: 'id=3', content: 'not updated' } + ] +}; + +jest.doMock( + 'rss-parser', + () => + // has to use function keyword to be called with new keyword (ie act as a constructor) + /* tslint:disable-line only-arrow-functions */ function() { + return { + parseURL: jest + .fn() + .mockReturnValueOnce(firstResponse) + .mockReturnValueOnce(secondResponse) + .mockReturnValueOnce(thirdResponse) + .mockReturnValueOnce(fourthResponse) + }; + } +); + +import IncidentFeed from './incidentFeed'; +const incidentFeed = new IncidentFeed(); + +describe('IncidentFeed', () => { + test('before :update is called, :getFeed returns an empty array', () => { + expect(incidentFeed.getFeed()).toMatchObject([]); + expect(incidentFeed.getFeed()).not.toMatchObject(['some value']); + }); + test('first update', async () => { + await incidentFeed.update(); + expect(incidentFeed.getFeed()).toBe(firstResponse.items); + expect(incidentFeed.newIncidents()).toMatchObject([]); + expect(incidentFeed.updatedIncidents()).toMatchObject([]); + }); + test('second update', async () => { + await incidentFeed.update(); + expect(incidentFeed.getFeed()).toBe(secondResponse.items); + expect(incidentFeed.newIncidents()).toMatchObject([ + { link: 'id=3', content: 'not updated' } + ]); + expect(incidentFeed.updatedIncidents()).toMatchObject([]); + }); + test('third update', async () => { + await incidentFeed.update(); + expect(incidentFeed.getFeed()).toBe(thirdResponse.items); + expect(incidentFeed.newIncidents()).toMatchObject([]); + expect(incidentFeed.updatedIncidents()).toMatchObject([ + { link: 'id=1', content: 'updated' } + ]); + }); + test('fourth update', async () => { + await incidentFeed.update(); + expect(incidentFeed.getFeed()).toBe(fourthResponse.items); + expect(incidentFeed.newIncidents()).toMatchObject([]); + expect(incidentFeed.updatedIncidents()).toMatchObject([]); + }); +}); diff --git a/src/incidentFeed.ts b/src/incidentFeed.ts new file mode 100644 index 0000000..7552bef --- /dev/null +++ b/src/incidentFeed.ts @@ -0,0 +1,59 @@ +import Parser from 'rss-parser'; + +const FEED_URL = 'https://www.netlifystatus.com/history.rss'; + +export interface IFeedItem { + title: string; + pubDate: string; + content: string; + link: string; +} + +export default class IncidentFeed { + private parser: { parseURL(feedUrl: string) }; + private currentFeed: IFeedItem[]; + private previousFeed: IFeedItem[]; + + constructor() { + this.parser = new Parser(); + this.currentFeed = []; + this.previousFeed = []; + } + + public async update(): Promise { + const fetchedFeed: IFeedItem[] = await this.fetchAndParseFeed(); + this.previousFeed = this.currentFeed; + this.currentFeed = fetchedFeed; + } + + public newIncidents(): ReadonlyArray { + if (this.previousFeed.length === 0) { + return []; + } + return this.currentFeed.filter(currentItem => { + return !this.previousFeed.some(previousItem => { + return previousItem.link === currentItem.link; + }); + }); + } + + public updatedIncidents(): ReadonlyArray { + return this.currentFeed.filter(currentItem => { + return this.previousFeed.find(previousItem => { + return ( + previousItem.link === currentItem.link && + previousItem.content !== currentItem.content + ); + }); + }); + } + + public getFeed(): ReadonlyArray { + return this.currentFeed as ReadonlyArray; + } + + private async fetchAndParseFeed(): Promise { + const response = await this.parser.parseURL(FEED_URL); + return response.items; + } +} diff --git a/src/index.ts b/src/index.ts index 3419575..74b52e7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { app, powerMonitor } from 'electron'; // tslint:disable-line no-implicit import settings from 'electron-settings'; import { autoUpdater } from 'electron-updater'; import Connection from './connection'; +import IncidentFeed from './incidentFeed'; import MenuUI from './menubar'; import Netlify from './netlify'; @@ -44,6 +45,7 @@ const configureAutoLauncher = ( */ const onAppReady = async (): Promise => { const connection = await getOnlineConnection(); + const incidentFeed = new IncidentFeed(); const apiClient = await getNetlifyClient(settings.get( 'accessToken' ) as string); @@ -64,7 +66,8 @@ const onAppReady = async (): Promise => { const ui = new MenuUI({ apiClient, - connection + connection, + incidentFeed }); // only hide dock icon when everything's running diff --git a/src/menubar.ts b/src/menubar.ts index 2f18848..d1ad026 100644 --- a/src/menubar.ts +++ b/src/menubar.ts @@ -1,18 +1,19 @@ -import { - app, - Menu, - MenuItemConstructorOptions, - Notification, - shell, - Tray -} from 'electron'; // tslint:disable-line no-implicit-dependencies +import { isToday, isYesterday } from 'date-fns'; +import { app, Menu, MenuItemConstructorOptions, shell, Tray } from 'electron'; // tslint:disable-line no-implicit-dependencies import settings from 'electron-settings'; import { EventEmitter } from 'events'; import { POLL_DURATIONS } from './config'; import Connection from './connection'; import ICONS from './icons'; -import { getCheckboxMenu, getDeploysMenu, getSitesMenu } from './menus'; +import IncidentFeed from './incidentFeed'; +import { + getCheckboxMenu, + getDeploysMenu, + getIncidentsMenu, + getSitesMenu +} from './menus'; import Netlify, { INetlifyDeploy, INetlifySite, INetlifyUser } from './netlify'; +import notify from './notify'; import { getFormattedDeploys, getNotificationOptions, @@ -60,6 +61,7 @@ const DEFAULT_SETTINGS: IAppSettings = { export default class UI extends EventEmitter { private apiClient: Netlify; + private incidentFeed: IncidentFeed; private connection: Connection; private state: IAppState; private tray: Tray; @@ -68,13 +70,16 @@ export default class UI extends EventEmitter { public constructor({ apiClient, - connection + connection, + incidentFeed }: { apiClient: Netlify; connection: Connection; + incidentFeed: IncidentFeed; }) { super(); + this.incidentFeed = incidentFeed; this.tray = new Tray(ICONS.loading); this.apiClient = apiClient; this.connection = connection; @@ -95,11 +100,22 @@ export default class UI extends EventEmitter { updateAvailable: false }; + // TODO: move this to a dedicated Scheduler this.setup().then(() => { + // Scheduler should have some method like: doOnce() + let first = true; const repeat = () => { setTimeout(async () => { if (this.connection.isOnline) { await this.updateDeploys(); + // every 10 seconds is probably too frequent to be checking the incidents rss + // incident feed should get its own polling interval when Scheduler is implemented + await this.incidentFeed.update(); + if (first) { + first = false; + this.notifyForIncidentsPastTwoDays(); + } + this.notifyForNewAndUpdatedIncidents(); } else { this.tray.setImage(ICONS.offline); await this.render(); @@ -108,7 +124,6 @@ export default class UI extends EventEmitter { repeat(); }, this.settings.pollInterval); }; - repeat(); }); } @@ -157,7 +172,6 @@ export default class UI extends EventEmitter { private async fetchData(fn: () => void): Promise { if (this.connection.isOnline) { this.tray.setImage(ICONS.loading); - // catch possible network hickups try { await fn(); @@ -187,6 +201,40 @@ export default class UI extends EventEmitter { }); } + private notifyForIncidentsPastTwoDays(): void { + const recentIncidents = this.incidentFeed.getFeed().filter(item => { + const publicationDate = new Date(item.pubDate); + return isToday(publicationDate) || isYesterday(publicationDate); + }); + if (recentIncidents.length) { + this.notifyIncident(recentIncidents[0], 'Recently reported incident'); + } + } + + private notifyForNewAndUpdatedIncidents(): void { + const newIncidents = this.incidentFeed.newIncidents(); + const updatedIncidents = this.incidentFeed.updatedIncidents(); + if (newIncidents.length) { + this.notifyIncident(newIncidents[0], 'New incident reported'); + } + if (updatedIncidents.length) { + this.notifyIncident(updatedIncidents[0], 'Incident report updated'); + } + } + + private notifyIncident( + incident: { title: string; link: string }, + title: string + ): void { + notify({ + body: incident.title, + onClick: () => { + shell.openExternal(incident.link); + }, + title + }); + } + private evaluateDeployState(): void { const { deploys } = this.netlifyData; const { previousDeploy, currentSite } = this.state; @@ -205,32 +253,25 @@ export default class UI extends EventEmitter { return; } - if (this.settings.showNotifications && previousDeploy) { + if (previousDeploy) { const notificationOptions = getNotificationOptions( previousDeploy, currentDeploy ); if (notificationOptions) { - const notification = new Notification(notificationOptions); - - notification.on('click', event => { - if (currentSite && currentDeploy) { - shell.openExternal( - `https://app.netlify.com/sites/${currentSite.name}/deploys/${ - currentDeploy.id - }` - ); + notify({ + ...notificationOptions, + onClick: () => { + if (currentSite && currentDeploy) { + shell.openExternal( + `https://app.netlify.com/sites/${currentSite.name}/deploys/${ + currentDeploy.id + }` + ); + } } }); - - // notifications with an attached click handler - // won't disappear by itself - // -> close it after certain timeframe automatically - notification.on('show', () => - setTimeout(() => notification.close(), 4000) - ); - notification.show(); } } @@ -277,6 +318,11 @@ export default class UI extends EventEmitter { label: `Netlify Menubar ${app.getVersion()}` }, { type: 'separator' }, + { + label: 'Reported Incidents', + submenu: getIncidentsMenu(this.incidentFeed) + }, + { type: 'separator' }, { enabled: false, label: user && user.email diff --git a/src/menus.ts b/src/menus.ts index 460f2b5..c3549d9 100644 --- a/src/menus.ts +++ b/src/menus.ts @@ -1,7 +1,9 @@ -import { distanceInWords } from 'date-fns'; +import { distanceInWords, isWithinRange, subMonths } from 'date-fns'; import { MenuItemConstructorOptions, shell } from 'electron'; // tslint:disable-line no-implicit-dependencies +import IncidentFeed, { IFeedItem } from './incidentFeed'; import { IAppDeploys, IAppSettings } from './menubar'; import { INetlifySite } from './netlify'; +import { shortenString } from './util'; interface IDeployMenuOptions { currentSite: INetlifySite; @@ -9,6 +11,48 @@ interface IDeployMenuOptions { onItemClick: (deployId: string) => void; } +const isOlderThanAMonth = (incident: IFeedItem): boolean => { + const pubDate = new Date(incident.pubDate); + const today = new Date(); + const aMonthAgo = subMonths(today, 1); + return isWithinRange(pubDate, aMonthAgo, today); +}; + +export const getIncidentsMenu = ( + incidentFeed: IncidentFeed +): MenuItemConstructorOptions[] => { + const recentIncidents = incidentFeed + .getFeed() + .filter(isOlderThanAMonth) + // create menu option objects from incidents + .map(incident => { + return { + click: () => shell.openExternal(incident.link), + label: `${shortenString(incident.title, 60)} | ${distanceInWords( + new Date(incident.pubDate), + new Date() + )} ago` + }; + }); + // if there are no recent incidents, replace with message + const renderedItems = recentIncidents.length + ? recentIncidents + : [ + { + enabled: false, + label: 'no recent incidents' + } + ]; + return [ + { + click: () => shell.openExternal('https://www.netlifystatus.com/history'), + label: 'History' + }, + { type: 'separator' }, + ...renderedItems + ]; +}; + export const getDeploysMenu = ({ currentSite, deploys, diff --git a/src/netlify.test.ts b/src/netlify.test.ts index 98fd865..ac41f1c 100644 --- a/src/netlify.test.ts +++ b/src/netlify.test.ts @@ -1,4 +1,4 @@ -import * as fetch from 'node-fetch'; +const fetch = require('node-fetch').default; // tslint:disable-line no-var-requires import Netlify, { API_URL } from './netlify'; // TODO place this in global config somehwere @@ -20,17 +20,21 @@ interface IFetchResponse { json: () => {}; } -const getFetchPromise = (json: {} = {}, response: {} = {}): IFetchResponse => { - return { +const getFetchPromise = async ( + json: {} = {}, + response: {} = {} +): Promise => { + const result = await { ...(response && response), json: () => new Promise(res => res(json)) }; + return result; }; describe('netlify api client', () => { const apiToken = 'awesomeToken'; let apiClient: Netlify; - const mFetch = fetch.default as jest.Mock; + const mFetch = fetch as jest.Mock>; beforeEach(() => { apiClient = new Netlify(apiToken); @@ -120,12 +124,11 @@ describe('netlify api client', () => { test('invalid token', async () => { const newToken = 'yeah-awesome-token'; - mFetch - .mockResolvedValueOnce(getFetchPromise({}, { status: 401 })) - .mockResolvedValueOnce(getFetchPromise({ id: 'ticketId' })) - .mockResolvedValueOnce(getFetchPromise({ authorized: false })) - .mockResolvedValueOnce(getFetchPromise({ authorized: true })) - .mockResolvedValueOnce(getFetchPromise({ access_token: newToken })); + mFetch.mockResolvedValueOnce(getFetchPromise({}, { status: 401 })); + mFetch.mockResolvedValueOnce(getFetchPromise({ id: 'ticketId' })); + mFetch.mockResolvedValueOnce(getFetchPromise({ authorized: false })); + mFetch.mockResolvedValueOnce(getFetchPromise({ authorized: true })); + mFetch.mockResolvedValueOnce(getFetchPromise({ access_token: newToken })); const client = await apiClient.authorize('clientId2'); diff --git a/src/notify.test.ts b/src/notify.test.ts new file mode 100644 index 0000000..8787b22 --- /dev/null +++ b/src/notify.test.ts @@ -0,0 +1,38 @@ +const mockGetSettings = jest.fn(); +const mockOn = jest.fn(); +const mockShow = jest.fn(); +const inMockConstructor = jest.fn(); +class Notification { + public on = mockOn; + public show = mockShow; + constructor() { + inMockConstructor(); + } +} +/* doMock instead of mock to prevent hoisting above class and const declarations*/ +jest.doMock('electron', () => ({ + Notification +})); +jest.doMock('electron-settings', () => ({ + get: mockGetSettings +})); + +// this import must come after jest.doMock(... +import notify from './notify'; + +describe('notify function', () => { + test('if showNotifications setting is false, does not create a Notification ', () => { + mockGetSettings.mockImplementation(() => false); // getting any setting will return false + notify({ title: 'test title', body: 'test body', onClick: jest.fn() }); + expect(mockGetSettings).toHaveBeenCalledWith('showNotifications'); + expect(mockGetSettings.mock.results[0].value).toEqual(false); + expect(inMockConstructor).not.toHaveBeenCalled(); + }); + test('if showNotifications setting is true, it creates a Notification, registers callbacks, calls notifation.show()', () => { + mockGetSettings.mockImplementation(() => true); + notify({ title: 'test title', body: 'test body', onClick: jest.fn() }); + expect(inMockConstructor).toHaveBeenCalled(); + expect(mockOn).toHaveBeenCalledTimes(2); + expect(mockShow).toHaveBeenCalled(); + }); +}); diff --git a/src/notify.ts b/src/notify.ts new file mode 100644 index 0000000..614c994 --- /dev/null +++ b/src/notify.ts @@ -0,0 +1,20 @@ +import { Notification, NotificationConstructorOptions } from 'electron'; // tslint:disable-line no-implicit-dependencies +import settings from 'electron-settings'; + +interface INotificationOptions extends NotificationConstructorOptions { + onClick: () => void; +} + +const notify = (options: INotificationOptions): void => { + if (settings.get('showNotifications')) { + const notification = new Notification(options); + notification.on('click', options.onClick); + // notifications with an attached click handler + // won't disappear by itself + // -> close it after certain timeframe automatically + notification.on('show', () => setTimeout(() => notification.close(), 4000)); + notification.show(); + } +}; + +export default notify; diff --git a/src/util.ts b/src/util.ts index c15003e..8d2d83d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -98,3 +98,11 @@ export const getSuspendedDeployCount = (deployCount: number): string => { return ''; } }; + +// shorten strings without splitting the last word +export const shortenString = (str: string, maxLen: number) => { + if (str.length <= maxLen) { + return str; + } + return str.substr(0, str.lastIndexOf(' ', maxLen)); +};