diff --git a/package-lock.json b/package-lock.json index 51c34284aa..2593aeafa2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9785,8 +9785,7 @@ "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" }, "arch": { "version": "2.1.1", @@ -9798,7 +9797,6 @@ "version": "1.1.5", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz", "integrity": "sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w==", - "dev": true, "requires": { "delegates": "^1.0.0", "readable-stream": "^2.0.6" @@ -11241,8 +11239,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -11522,7 +11519,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -11936,6 +11932,65 @@ "integrity": "sha512-7RR4Uh04t9K1uYRWzOJmzplgEOAXbfK72oVNokCdMzA67trrhPzy93ahKk1AWHiA0c58tD2P+NHqxrA8FZ+Trg==", "dev": true }, + "canvas": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.6.1.tgz", + "integrity": "sha512-S98rKsPcuhfTcYbtF53UIJhcbgIAK533d1kJKMwsMwAIFgfd58MOyxRud3kktlzWiEkFliaJtvyZCBtud/XVEA==", + "requires": { + "nan": "^2.14.0", + "node-pre-gyp": "^0.11.0", + "simple-get": "^3.0.3" + }, + "dependencies": { + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "requires": { + "minimist": "^1.2.5" + } + }, + "node-pre-gyp": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.11.0.tgz", + "integrity": "sha512-TwWAOZb0j7e9eGaf9esRx3ZcLaE5tQ2lvYy1pb5IAaG1a2e2Kv5Lms1Y4hpj+ciXJRofIxxlt5haeQ/2ANeE0Q==", + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.1", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.2.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "requires": { + "glob": "^7.1.3" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" + } + } + }, "canvas-confetti": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.2.0.tgz", @@ -12291,8 +12346,7 @@ "chownr": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", - "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", - "dev": true + "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==" }, "chrome-trace-event": { "version": "1.0.2", @@ -12587,8 +12641,7 @@ "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 + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "collapse-white-space": { "version": "1.0.5", @@ -12707,8 +12760,7 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { "version": "1.6.2", @@ -12798,8 +12850,7 @@ "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "consolidate": { "version": "0.15.1", @@ -14048,6 +14099,14 @@ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", "dev": true }, + "decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "requires": { + "mimic-response": "^2.0.0" + } + }, "deep-equal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", @@ -14065,9 +14124,7 @@ "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "optional": true + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" }, "deep-is": { "version": "0.1.3", @@ -14165,8 +14222,7 @@ "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", - "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", - "dev": true + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, "depd": { "version": "1.1.2", @@ -14220,9 +14276,7 @@ "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", - "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", - "dev": true, - "optional": true + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=" }, "detect-newline": { "version": "3.1.0", @@ -16797,8 +16851,6 @@ "version": "1.2.7", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz", "integrity": "sha512-GWSSJGFy4e9GUeCcbIkED+bgAoFyj7XF1mV8rma3QW4NIqX9Kyx79N/PF61H5udOV3aY1IaMLs6pGbH71nlCTA==", - "dev": true, - "optional": true, "requires": { "minipass": "^2.6.0" } @@ -16818,8 +16870,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "1.2.9", @@ -16927,7 +16978,6 @@ "version": "2.7.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", - "dev": true, "requires": { "aproba": "^1.0.3", "console-control-strings": "^1.0.0", @@ -17049,7 +17099,6 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -17459,8 +17508,7 @@ "has-unicode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", - "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", - "dev": true + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=" }, "has-value": { "version": "1.0.0", @@ -18184,8 +18232,6 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz", "integrity": "sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw==", - "dev": true, - "optional": true, "requires": { "minimatch": "^3.0.4" } @@ -18346,7 +18392,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -18360,8 +18405,7 @@ "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "inject-loader": { "version": "4.0.1", @@ -18927,7 +18971,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, "requires": { "number-is-nan": "^1.0.0" } @@ -23638,6 +23681,11 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, + "mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==" + }, "min-document": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", @@ -23709,7 +23757,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -23748,8 +23795,6 @@ "version": "2.9.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz", "integrity": "sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg==", - "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -23837,8 +23882,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz", "integrity": "sha512-6ZYMOEnmVsdCeTJVE0W9ZD+pVnE8h9Hma/iOwwRDsdQoePpoX56/8B6z3P9VNwppJuBKNRuFDRNRqRWexT9G9Q==", - "dev": true, - "optional": true, "requires": { "minipass": "^2.9.0" } @@ -24024,8 +24067,6 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/needle/-/needle-2.4.0.tgz", "integrity": "sha512-4Hnwzr3mi5L97hMYeNl8wRW/Onhy4nUKR/lVemJ8gJedxxUyBLm9kkrDColJvoSfwi0jCNhD+xCdOtiGDQiRZg==", - "dev": true, - "optional": true, "requires": { "debug": "^3.2.6", "iconv-lite": "^0.4.4", @@ -24036,8 +24077,6 @@ "version": "3.2.6", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "optional": true, "requires": { "ms": "^2.1.1" } @@ -24045,9 +24084,7 @@ "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, - "optional": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -24370,8 +24407,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz", "integrity": "sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA==", - "dev": true, - "optional": true, "requires": { "npm-normalize-package-bin": "^1.0.1" } @@ -24379,16 +24414,12 @@ "npm-normalize-package-bin": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", - "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", - "dev": true, - "optional": true + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==" }, "npm-packlist": { "version": "1.4.7", "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.7.tgz", "integrity": "sha512-vAj7dIkp5NhieaGZxBJB8fF4R0078rqsmhJcAfXZ6O7JJhjhPK96n5Ry1oZcfLXgfun0GWTZPOxaEyqv8GBykQ==", - "dev": true, - "optional": true, "requires": { "ignore-walk": "^3.0.1", "npm-bundled": "^1.0.1" @@ -24407,7 +24438,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", - "dev": true, "requires": { "are-we-there-yet": "~1.1.2", "console-control-strings": "~1.1.0", @@ -24767,7 +24797,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -25054,8 +25083,7 @@ "os-homedir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", - "dev": true + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=" }, "os-locale": { "version": "3.1.0", @@ -25097,14 +25125,12 @@ "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", - "dev": true + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=" }, "osenv": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", - "dev": true, "requires": { "os-homedir": "^1.0.0", "os-tmpdir": "^1.0.0" @@ -25436,8 +25462,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-is-inside": { "version": "1.0.2", @@ -27299,8 +27324,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "optional": true, "requires": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -29124,8 +29147,7 @@ "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" }, "saxes": { "version": "5.0.1", @@ -29284,8 +29306,7 @@ "set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", - "dev": true + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, "set-cookie-parser": { "version": "2.4.0", @@ -29492,8 +29513,22 @@ "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", - "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", - "dev": true + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" + }, + "simple-concat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz", + "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY=" + }, + "simple-get": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.0.tgz", + "integrity": "sha512-bCR6cP+aTdScaQCnQKbPKtJOKDp/hj9EDLJo3Nw4y1QksqaovlW/bnptB6/c1e+qmNIDHRK+oXFDdEqBT8WzUA==", + "requires": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } }, "simple-lru-cache": { "version": "0.0.2", @@ -30037,7 +30072,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -30475,8 +30509,7 @@ "strip-json-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=" }, "style-loader": { "version": "1.2.1", @@ -31338,8 +31371,6 @@ "version": "4.4.13", "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz", "integrity": "sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA==", - "dev": true, - "optional": true, "requires": { "chownr": "^1.1.1", "fs-minipass": "^1.2.5", @@ -31353,16 +31384,12 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "mkdirp": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -33718,7 +33745,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.3.tgz", "integrity": "sha512-QGkOQc8XL6Bt5PwnsExKBPuMKBxnGxWWW3fU55Xt4feHozMUhdUMaBCk290qpm/wG5u/RSKzwdAC4i51YigihA==", - "dev": true, "requires": { "string-width": "^1.0.2 || 2" } @@ -33964,8 +33990,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "1.0.3", diff --git a/package.json b/package.json index 9ae428cdb1..4b24568e33 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "apollo-server-express": "^2.14.4", "auth0-js": "^9.13.2", "body-parser": "^1.19.0", + "canvas": "^2.6.1", "braintree-web-drop-in": "^1.22.1", "canvas-confetti": "^1.2.0", "change-case": "^4.1.1", diff --git a/server/dev-server.js b/server/dev-server.js index 7d66d1d660..4695fc2f7e 100644 --- a/server/dev-server.js +++ b/server/dev-server.js @@ -19,6 +19,7 @@ const authRouter = require('./auth-router'); const mockGraphQLRouter = require('./mock-graphql-router'); const sessionRouter = require('./session-router'); const timesyncRouter = require('./timesync-router'); +const liveLoanRouter = require('./live-loan-router'); const vueMiddleware = require('./vue-middleware'); const serverConfig = require('../build/webpack.server.conf'); const clientConfig = require('../build/webpack.client.dev.conf'); @@ -153,6 +154,9 @@ app.use('/ui-routes', serverRoutes); // Handle time sychronization requests app.use('/', timesyncRouter()); +// dynamic personalized loan routes +app.use('/live-loan', liveLoanRouter(cache)); + // install dev/hot middleware app.use(devMiddleware); app.use(hotMiddleware); diff --git a/server/index.js b/server/index.js index 1d074c9447..e5fcbec624 100644 --- a/server/index.js +++ b/server/index.js @@ -9,6 +9,7 @@ const authRouter = require('./auth-router'); const mockGraphQLRouter = require('./mock-graphql-router'); const sessionRouter = require('./session-router'); const timesyncRouter = require('./timesync-router'); +const liveLoanRouter = require('./live-loan-router'); const vueMiddleware = require('./vue-middleware'); const serverBundle = require('../dist/vue-ssr-server-bundle.json'); const clientManifest = require('../dist/vue-ssr-client-manifest.json'); @@ -64,6 +65,9 @@ app.use('/ui-routes', serverRoutes); // Handle time sychronization requests app.use('/', timesyncRouter()); +// dynamic personalized loan routes +app.use('/live-loan', liveLoanRouter(cache)); + // Configure session app.set('trust proxy', 1); app.use('/', sessionRouter(config.server)); diff --git a/server/live-loan-router.js b/server/live-loan-router.js new file mode 100644 index 0000000000..d1094d5d7e --- /dev/null +++ b/server/live-loan-router.js @@ -0,0 +1,150 @@ +const get = require('lodash/get'); +const express = require('express'); +const argv = require('./util/argv'); +const config = require('../config/selectConfig')(argv.config); +const fetch = require('./util/fetch'); + +const drawLoanCard = require('./util/live-loan/live-loan-draw'); + +function getLoansFromCache(loginId, cache) { + return new Promise(resolve => { + cache.get(`recommendations-by-login-id-${loginId}`, (error, data) => { + let parsedData = []; + if (error) { + console.error(JSON.stringify({ + meta: {}, + level: 'error', + message: `MemJS Error Getting recommendations-by-login-id-${loginId}, Error: ${error}` + })); + } + if (data) { + parsedData = JSON.parse(data); + } + resolve(parsedData); + }); + }); +} + +function setLoansToCache(loginId, loans, cache) { + return new Promise((resolve, reject) => { + const expires = 10 * 60; // 10 minutes + cache.set(`recommendations-by-login-id-${loginId}`, JSON.stringify(loans), { expires }, (error, success) => { + if (error) { + console.error(JSON.stringify({ + meta: {}, + level: 'error', + message: `MemJS Error Setting Cache for recommendations-by-login-id-${loginId}, Error: ${error}` + })); + reject(); + } + if (success) { + console.info(JSON.stringify({ + meta: {}, + level: 'info', + message: `MemJS Success Setting Cache for recommendations-by-login-id-${loginId}, Success: ${success}` + })); + resolve(); + } + }); + }); +} + +function fetchRecommendedLoans(loginId, cache) { + return new Promise((resolve, reject) => { + getLoansFromCache(loginId, cache).then(data => { + if (data.length) { + resolve(data); + } else { + const endpoint = config.app.graphqlUri; + const query = `{ + ml { + recommendationsByLoginId( + segment: all + loginId: ${loginId} + offset: 0 + limit: 4 + ) { + values { + name + id + borrowerCount + geocode { + country { + name + } + } + use + loanAmount + status + loanFundraisingInfo { + fundedAmount + } + image { + retina: url(customSize: "w960h720") + } + } + } + } + }`; + + fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + query + }), + }) + .then(result => result.json()) + .then(result => { + const loanData = get(result, 'data.ml.recommendationsByLoginId.values'); + setLoansToCache(loginId, loanData, cache).then(() => { + resolve(loanData); + }); + }).catch(err => { + console.error(err); + reject(err); + }); + } + }).catch(err => { + console.error(err); + }); + }); +} + +module.exports = function liveLoanRouter(cache) { + const router = express.Router(); + + // URL Router + router.use('/u/:userId/url/:offset', async (req, res) => { + const { userId, offset } = req.params; + try { + const loanData = await fetchRecommendedLoans(userId, cache); + const offsetLoanId = loanData[offset - 1].id; + res.redirect(302, `/lend/${offsetLoanId}`); + } catch (err) { + console.error(err); + res.redirect(302, '/lend-by-category/'); + } + }); + + // IMG Router + router.use('/u/:userId/img/:offset', async (req, res) => { + const { userId, offset } = req.params; + try { + const loanData = await fetchRecommendedLoans(userId, cache); + const loanCardImgBuffer = await drawLoanCard(loanData[offset - 1]); + res.contentType('image/jpeg'); + res.send(loanCardImgBuffer); + } catch (err) { + console.error(err); + res.sendStatus(500); + } + }); + + // 404 any /live-loan/* routes that don't match above + router.use((req, res) => { + res.sendStatus(404); + }); + + return router; +}; diff --git a/server/util/live-loan/canvas-utils.js b/server/util/live-loan/canvas-utils.js new file mode 100644 index 0000000000..cc445261f2 --- /dev/null +++ b/server/util/live-loan/canvas-utils.js @@ -0,0 +1,78 @@ +module.exports = { + /** + * Returns a single line of text. Adds ellipsis if text overflows the desired canvas width + * @param {CanvasRenderingContext2D} ctx + * @param {String} str The text to draw + * @param {Number} maxWidth The width in pixels of the line + */ + ellipsisLine(ctx, str, maxWidth) { + let { width } = ctx.measureText(str); + const ellipsis = '…'; + const ellipsisWidth = ctx.measureText(ellipsis).width; + if (width <= maxWidth || width <= ellipsisWidth) { + return str; + } + let len = str.length; + while (width >= maxWidth - ellipsisWidth && len-- > 0) { + str = str.substring(0, len); + width = ctx.measureText(str).width; + } + return str + ellipsis; + }, + + /** + * Draws a rounded rectangle to the current state of the canvas. + * @param {CanvasRenderingContext2D} ctx + * @param {Number} x The top left x coordinate + * @param {Number} y The top left y coordinate + * @param {Number} width The width of the rectangle + * @param {Number} height The height of the rectangle + * @param {Number} [radius = 0] The corner radius; + */ + roundRect(ctx, x, y, w, h, r) { + if (w < 2 * r) r = w / 2; + if (h < 2 * r) r = h / 2; + ctx.beginPath(); + ctx.moveTo(x + r, y); + ctx.arcTo(x + w, y, x + w, y + h, r); + ctx.arcTo(x + w, y + h, x, y + h, r); + ctx.arcTo(x, y + h, x, y, r); + ctx.arcTo(x, y, x + w, y, r); + }, + + /** + * Draws multiple lines of text to the current state of the canvas. + * Adds an ellispsis if the text overflows. + * @param {CanvasRenderingContext2D} ctx + * @param {String} text The st + * @param {Number} x The top left x coordinate + * @param {Number} y The top left y coordinate + * @param {Number} maxWidth The width of the rectangle + * @param {Number} maxLines The max number of lines in the paragraph + * @param {Number} lineHeight The lineHeight in px of the text + */ + wrapText(ctx, text, x, y, maxWidth, maxLines, lineHeight) { + const words = text.split(' '); + let line = ''; + let numLines = 1; + + for (let n = 0; n < words.length; n++) { + const testLine = `${line + words[n]} `; + const metrics = ctx.measureText(testLine); + const testWidth = metrics.width; + if (testWidth > maxWidth && n > 0) { + if (numLines < maxLines) { + ctx.fillText(line, x, y); + line = `${words[n]} `; + y += lineHeight; + numLines += 1; + } else if (numLines === maxLines) { + line = module.exports.ellipsisLine(ctx, testLine, maxWidth); + } + } else { + line = testLine; + } + } + ctx.fillText(line, x, y); + } +}; diff --git a/server/util/live-loan/live-loan-draw.js b/server/util/live-loan/live-loan-draw.js new file mode 100644 index 0000000000..9b4dd826de --- /dev/null +++ b/server/util/live-loan/live-loan-draw.js @@ -0,0 +1,130 @@ +const path = require('path'); +const { createCanvas, registerFont, loadImage } = require('canvas'); +const { ellipsisLine, wrapText, roundRect } = require('./canvas-utils'); +const loanUseFilter = require('../../../src/plugins/loan-use-filter'); + +module.exports = loanData => { + return new Promise((resolve, reject) => { + async function draw() { + const resizeFactor = 3; + const borderThickness = 2 * resizeFactor; + const cardWidth = 300 * resizeFactor; + const cardHeight = 525 * resizeFactor; + const borrowerImgAspectRatio = 0.75; + const kivaColors = { + blue: '#118aec', + darkBlue: '#006ed3', + green: '#4faf4e', + textLight: '#999999', + strokeGrey: '#d8d8d8', + textDark: '#484848', + white: '#ffffff' + }; + + function fontFile(name) { + return path.join(__dirname, '../../../src/assets/fonts', name); + } + registerFont(fontFile('PostGrotesk-Medium.ttf'), { family: 'Kiva Post Grot', weight: '400' }); + registerFont(fontFile('PostGrotesk-MediumItalic.ttf'), { family: 'Kiva Post Grot', weight: '400', style: 'italic' }); + registerFont(fontFile('PostGrotesk-Light.ttf'), { family: 'Kiva Post Grot', weight: '100' }); + registerFont(fontFile('PostGrotesk-Book.ttf'), { family: 'Kiva Post Grot', weight: '300' }); + registerFont(fontFile('PostGrotesk-Bold.ttf'), { family: 'Kiva Post Grot', weight: '700' }); + + try { + // Canvas prep + const canvas = createCanvas(cardWidth, cardHeight); + const ctx = canvas.getContext('2d'); + ctx.textAlign = 'center'; + ctx.textBaseline = 'top'; + ctx.fillStyle = '#fff'; + ctx.fillRect(0, 0, cardWidth, cardHeight); + + // Borrower name + const nameXPos = cardWidth / 2; + const nameYPos = 245 * resizeFactor; + ctx.fillStyle = kivaColors.blue; + ctx.font = `700 ${24 * resizeFactor}px "Kiva Post Grot"`; + ctx.fillText(ellipsisLine(ctx, loanData.name, cardWidth * 0.95), nameXPos, nameYPos); + + // Borrower country + const countryXPos = cardWidth / 2; + const countryYPos = 280 * resizeFactor; + const countryWidth = cardWidth * 0.9; + ctx.fillStyle = kivaColors.textLight; + ctx.font = `700 ${16 * resizeFactor}px "Kiva Post Grot"`; + ctx.fillText(ellipsisLine(ctx, loanData.geocode.country.name, countryWidth), countryXPos, countryYPos); + + // Borrower use + const useXPos = cardWidth / 2; + const useYPos = 320 * resizeFactor; + const useWidth = cardWidth * 0.9; + const useMaxLines = 3; + const useMaxLineHeight = 16 * 1.3 * resizeFactor; + const useText = loanUseFilter(loanData.use, loanData.name, loanData.status, loanData.loanAmount, loanData.borrowerCount, 100); + ctx.fillStyle = kivaColors.textDark; + ctx.font = `400 ${16 * resizeFactor}px "Kiva Post Grot"`; + wrapText(ctx, useText, useXPos, useYPos, useWidth, useMaxLines, useMaxLineHeight); + + // Fundraising info + const fundingXPos = cardWidth * 0.05; + const fundingYPos = 412 * resizeFactor; + const fundingWidth = cardWidth * 0.9; + const fundingHeight = 8 * resizeFactor; + const fundingBorderRadius = 10; + const fundingTextYPos = fundingYPos + fundingHeight + (8 * resizeFactor); + const fundraisingPercent = loanData.loanFundraisingInfo.fundedAmount / loanData.loanAmount; + const fundraisingRemaining = loanData.loanAmount - loanData.loanFundraisingInfo.fundedAmount; + // Fundraising info - bar + roundRect(ctx, fundingXPos, fundingYPos, fundingWidth, fundingHeight, fundingBorderRadius); + ctx.fillStyle = kivaColors.strokeGrey; + ctx.fill(); + roundRect(ctx, fundingXPos, fundingYPos, fundingWidth * fundraisingPercent, fundingHeight, fundingBorderRadius); + ctx.fillStyle = kivaColors.green; + ctx.fill(); + // Fundraising info - text + ctx.font = `italic 400 ${14 * resizeFactor}px "Kiva Post Grot"`; + ctx.fillText(`$${fundraisingRemaining} to go`, cardWidth / 2, fundingTextYPos); + + // Button + const btnXPos = cardWidth * 0.05; + const btnYPos = 460 * resizeFactor; + const btnWidth = cardWidth * 0.9; + const btnHeight = 50 * resizeFactor; + const btnBorderRadius = 4 * resizeFactor; + const btnFontSize = 19; + ctx.shadowBlur = 0; + ctx.shadowOffsetX = 0; + ctx.shadowOffsetY = 2 * resizeFactor; + ctx.shadowColor = kivaColors.darkBlue; + roundRect(ctx, btnXPos, btnYPos, btnWidth, btnHeight, btnBorderRadius); + ctx.fillStyle = kivaColors.blue; + ctx.fill(); + ctx.shadowColor = 'transparent'; + ctx.fillStyle = kivaColors.white; + ctx.font = `700 ${btnFontSize * resizeFactor}px "Kiva Post Grot"`; + ctx.fillText('Lend now', cardWidth / 2, btnYPos + btnHeight / 2 - btnFontSize); + + // Borrower Image + const borrowerImg = await loadImage(loanData.image.retina); + ctx.drawImage(borrowerImg, 0, 0, cardWidth, cardWidth * borrowerImgAspectRatio); + + // Add a border around everything + ctx.strokeStyle = kivaColors.strokeGrey; + ctx.lineWidth = borderThickness; + ctx.strokeRect( + borderThickness / 2, + borderThickness / 2, + cardWidth - borderThickness, + cardHeight - borderThickness + ); + + // export it + const buffer = canvas.toBuffer('image/jpeg', { quality: 0.7 }); + resolve(buffer); + } catch (err) { + reject(err); + } + } + draw(); + }); +}; diff --git a/src/plugins/loan-use-filter.js b/src/plugins/loan-use-filter.js index 0bc501ee63..c62f9bd6bc 100644 --- a/src/plugins/loan-use-filter.js +++ b/src/plugins/loan-use-filter.js @@ -1,19 +1,19 @@ -import numeral from 'numeral'; - +// This file uses common JS imports/exports so our express router can use it as well. +const numeral = require('numeral'); /** * Uses the loan status to return the proper verbiage and tense for 'helped' * * @param {string} loan status * @returns {string} */ -export function helpedLanguage(status) { +const helpedLanguage = status => { if (status === 'fundraising' || status === 'inactive' || status === 'reviewed') { return 'helps'; } return 'helped'; -} +}; /** * Uses the borrowerCount to `a member` or empty string. @@ -21,12 +21,12 @@ export function helpedLanguage(status) { * @param {string} borrowerCount * @returns {string} */ -export function borrowerCountLanguage(borrowerCount) { +const borrowerCountLanguage = borrowerCount => { if (borrowerCount > 1) { return 'a member '; } return ''; -} +}; /** * Uses the borrowerCount to `a member` or empty string. * @@ -34,7 +34,7 @@ export function borrowerCountLanguage(borrowerCount) { * @param {number} loanUseMaxLength required * @returns {string} */ -export function shortenedLoanUse(loanUse, loanName, loanUseMaxLength) { +const shortenedLoanUse = (loanUse, loanName, loanUseMaxLength) => { const lowerCaseUse = loanUse.toString().charAt(0).toLowerCase() + loanUse.toString().slice(1); const convertedUse = (loanUse.substring(0, loanName.length) === loanName) ? loanUse : lowerCaseUse; @@ -45,7 +45,7 @@ export function shortenedLoanUse(loanUse, loanName, loanUseMaxLength) { return `${convertedUse.substring(0, loanUseMaxLength)}...`; } return convertedUse; -} +}; /** * Combines all of the above in a filter that returns a string to present to the UI @@ -55,7 +55,7 @@ export function shortenedLoanUse(loanUse, loanName, loanUseMaxLength) { * @param {number} loanAmount, loanBorrowerCount, loanUseMaxLength optional * @returns {string} */ -export default (loanUse, +const loanUseFilter = (loanUse, loanName = '', loanStatus = '', loanAmount = 0, @@ -67,3 +67,8 @@ export default (loanUse, + `${borrowerCountLanguage(loanBorrowerCount)}` + `${shortenedLoanUse(loanUse, loanName, loanUseMaxLength)}`; }; + +module.exports = loanUseFilter; +module.exports.borrowerCountLanguage = borrowerCountLanguage; +module.exports.shortenedLoanUse = shortenedLoanUse; +module.exports.helpedLanguage = helpedLanguage;