diff --git a/.eslintrc b/.eslintrc index 4c22ba42b1..992139b41c 100644 --- a/.eslintrc +++ b/.eslintrc @@ -65,6 +65,7 @@ rules: quote-props: [2, 'consistent-as-needed'] quotes: [2, 'single', 'avoid-escape'] radix: 2 + require-atomic-updates: 0 react/display-name: 0 react/no-deprecated: 1 react/jsx-boolean-value: 2 diff --git a/package-lock.json b/package-lock.json index 05b3ee36f2..a4b65316ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -359,6 +359,29 @@ "resolved": "https://registry.npmjs.org/@inukshuk/exif/-/exif-2.0.0.tgz", "integrity": "sha512-fAvsxo5Fq68hu02adHpMnh+zBJqlnghVg/NCKE6CzHafxmxUClk8RXuGAuduFAZkQEi1DJiqxUJOBITGEeLq1w==" }, + "@koa/router": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@koa/router/-/router-8.0.2.tgz", + "integrity": "sha512-7Wa8yXBmz9HjmZOr+xfMVuxFPNObdkiQFBiwF9SQ8zFqHykwBHcJA/mLqqxU2NKoeXRPBKUOPeOjwgR+gyadcA==", + "requires": { + "debug": "^3.1.0", + "http-errors": "^1.3.1", + "koa-compose": "^3.0.0", + "methods": "^1.0.1", + "path-to-regexp": "^1.1.1", + "urijs": "^1.19.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, "@mrmlnc/readdir-enhanced": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", @@ -582,6 +605,15 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", + "requires": { + "mime-types": "~2.1.24", + "negotiator": "0.6.2" + } + }, "accessibility-developer-tools": { "version": "2.12.0", "resolved": "https://registry.npmjs.org/accessibility-developer-tools/-/accessibility-developer-tools-2.12.0.tgz", @@ -660,6 +692,11 @@ "color-convert": "^1.9.0" } }, + "any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha1-q8av7tzqUugJzcA3au0845Y10X8=" + }, "anymatch": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", @@ -1144,6 +1181,15 @@ "unset-value": "^1.0.0" } }, + "cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "requires": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + } + }, "cacheable-request": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", @@ -1507,6 +1553,11 @@ "mimic-response": "^1.0.0" } }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=" + }, "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", @@ -1622,6 +1673,26 @@ "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, + "content-disposition": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", + "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "requires": { + "safe-buffer": "5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "content-type": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + }, "convert-source-map": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.6.0.tgz", @@ -1639,6 +1710,22 @@ } } }, + "cookies": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.8.0.tgz", + "integrity": "sha512-8aPsApQfebXnuI+537McwYsDtjVxGm8gTIzQI3FDW6t5t/DAhERxtnbEPN/8RX+uZthoz4eCOgloXaE5cYyNow==", + "requires": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "dependencies": { + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + } + } + }, "copy-descriptor": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", @@ -1910,6 +1997,11 @@ "type-detect": "^4.0.0" } }, + "deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=" + }, "deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -2004,6 +2096,16 @@ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=" }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" + }, + "destroy": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", + "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=" + }, "detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", @@ -2165,6 +2267,11 @@ } } }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" + }, "electron": { "version": "6.0.12", "resolved": "https://registry.npmjs.org/electron/-/electron-6.0.12.tgz", @@ -2406,6 +2513,11 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -2512,6 +2624,11 @@ } } }, + "error-inject": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/error-inject/-/error-inject-1.0.0.tgz", + "integrity": "sha1-4rPZG1Su1nLzCdlQ0VSFD6EdTzc=" + }, "es-abstract": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.15.0.tgz", @@ -2555,6 +2672,11 @@ "integrity": "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==", "dev": true }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" + }, "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", @@ -3378,6 +3500,11 @@ "map-cache": "^0.2.2" } }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=" + }, "fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", @@ -4063,12 +4190,33 @@ } } }, + "http-assert": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.4.1.tgz", + "integrity": "sha512-rdw7q6GTlibqVVbXr0CKelfV5iY8G2HqEUkhSk297BMbSpSL8crXC+9rjKoMcZZEsksX30le6f/4ul4E28gegw==", + "requires": { + "deep-equal": "~1.0.1", + "http-errors": "~1.7.2" + } + }, "http-cache-semantics": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", "integrity": "sha512-TcIMG3qeVLgDr1TEd2XvHaTnMPwYQUQMIBLy+5pLSDKYFc7UIqj39w8EGzZkaxoLv/l2K8HaI0t5AVA+YYgUew==", "dev": true }, + "http-errors": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.3.tgz", + "integrity": "sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.1.1", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.0" + } + }, "http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -4443,6 +4591,11 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" }, + "is-generator-function": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.7.tgz", + "integrity": "sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw==" + }, "is-glob": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", @@ -4902,6 +5055,14 @@ "integrity": "sha512-FrLwOgm+iXrPV+5zDU6Jqu4gCRXbWEQg2O3SKONsWE4w7AXFRkryS53bpWdaL9cNol+AmR3AEYz6kn+o0fCPnw==", "dev": true }, + "keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "requires": { + "tsscmp": "1.0.6" + } + }, "keyv": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", @@ -4923,6 +5084,82 @@ "integrity": "sha512-0g5vDDPvNnQk7WM/aE92dTDxXJoOE0biiIcUb3qkn/F6h/ZQZPlZIbE2XSXH2vFPfphkgCxuR2vH6HHnobEOaQ==", "dev": true }, + "koa": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.10.0.tgz", + "integrity": "sha512-vcZopGEWHDokchYtjU6jF1BCy+2MA2hnvGP7xPi26qWoIS0OiAUb4+lCqkqf05qG5ULnGYUFTvFnSK9RyOoiKw==", + "requires": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.8.0", + "debug": "~3.1.0", + "delegates": "^1.0.0", + "depd": "^1.1.2", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "error-inject": "^1.0.0", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^1.2.0", + "koa-is-json": "^1.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "dependencies": { + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==" + }, + "koa-convert": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-1.2.0.tgz", + "integrity": "sha1-2kCHXfSd4FOQmNFwC1CCDOvNIdA=", + "requires": { + "co": "^4.6.0", + "koa-compose": "^3.0.0" + }, + "dependencies": { + "koa-compose": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-3.2.1.tgz", + "integrity": "sha1-qFzLQLfZhtjlo0Wzoazo6rz1Tec=", + "requires": { + "any-promise": "^1.1.0" + } + } + } + }, + "koa-is-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/koa-is-json/-/koa-is-json-1.0.0.tgz", + "integrity": "sha1-JzwH7c3Ljfaiwat9We52SRRR7BQ=" + }, "lcid": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz", @@ -5221,6 +5458,11 @@ "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=" }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + }, "mem": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz", @@ -5368,6 +5610,11 @@ "integrity": "sha512-2j4DAdlBOkiSZIsaXk4mTE3sRS02yBHAtfy127xRV3bQUFqXkjHCHLW6Scv7DwNRbIWNHH8zpnz9zMaKXIdvYw==", "dev": true }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=" + }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -5705,6 +5952,11 @@ "sax": "^1.2.4" } }, + "negotiator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", + "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" + }, "neo-async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", @@ -6329,6 +6581,14 @@ "has": "^1.0.3" } }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", + "requires": { + "ee-first": "1.1.1" + } + }, "once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6354,6 +6614,11 @@ } } }, + "only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=" + }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -6540,6 +6805,11 @@ "@types/node": "*" } }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, "pascalcase": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", @@ -6578,7 +6848,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", - "dev": true, "requires": { "isarray": "0.0.1" }, @@ -6586,8 +6855,7 @@ "isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" } } }, @@ -8063,6 +8331,11 @@ } } }, + "setprototypeof": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", + "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" + }, "shallow-equal": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.0.tgz", @@ -8735,6 +9008,11 @@ } } }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" + }, "stdout-stream": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/stdout-stream/-/stdout-stream-1.4.1.tgz", @@ -9530,6 +9808,11 @@ "is-number": "^7.0.0" } }, + "toidentifier": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", + "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -9598,6 +9881,11 @@ "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", "dev": true }, + "tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -9626,6 +9914,15 @@ "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", "dev": true }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -9865,6 +10162,11 @@ "punycode": "^2.1.0" } }, + "urijs": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/urijs/-/urijs-1.19.2.tgz", + "integrity": "sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w==" + }, "urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -9923,6 +10225,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" + }, "verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", @@ -10229,6 +10536,11 @@ "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } + }, + "ylru": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.2.1.tgz", + "integrity": "sha512-faQrqNMzcPCHGVC2aaOINk13K+aaBDUPjGWl0teOXywElLjyVAB6Oe2jj62jHYtwsU49jXhScYbvPENK+6zAvQ==" } } } diff --git a/package.json b/package.json index ebfa66b382..a0dc81b378 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,7 @@ }, "dependencies": { "@inukshuk/exif": "^2.0.0", + "@koa/router": "^8.0.2", "@pixi/filter-adjustment": "^2.7.0", "@tweenjs/tween.js": "^18.3.1", "bluebird": "^3.7.1", @@ -110,6 +111,7 @@ "generic-pool": "^3.7.1", "js-yaml": "^3.13.1", "jsonld": "^1.8.0", + "koa": "^2.10.0", "lodash.debounce": "^4.0.7", "lodash.throttle": "^4.1.1", "memoize-one": "^5.1.1", diff --git a/scripts/log-viewer.js b/scripts/log-viewer.js index 8812dc606e..856225c4d7 100755 --- a/scripts/log-viewer.js +++ b/scripts/log-viewer.js @@ -17,10 +17,14 @@ const format = log => const end = log => log.quit ? '\n\n' : '\n' -const body = log => - log.action ? - `${log.action} ${chalk.gray(meta(log))}` : - log.msg || log.message +const body = log => { + if (log.action) + return `${log.action} ${chalk.gray(meta(log))}` + if (log.url) + return `${log.url} ${chalk.gray(`${log.status} Δ${ms(log.ms)}`)}` + else + return log.msg || log.message +} const error = ({ stack }) => stack ? @@ -43,6 +47,7 @@ const symbol = log => const SYMBOL = { about: 'α', + api: 'λ', main: 'β', prefs: 'σ', print: 'π', diff --git a/src/actions/activity.js b/src/actions/activity.js index 59d6b601e6..e50e446976 100644 --- a/src/actions/activity.js +++ b/src/actions/activity.js @@ -17,6 +17,7 @@ module.exports = { meta: { ipc: action.meta.ipc, idx: action.meta.idx, + rsvp: action.meta.rsvp, search: action.meta.search, ...meta, done: true, diff --git a/src/actions/api.js b/src/actions/api.js new file mode 100644 index 0000000000..ec7bd21406 --- /dev/null +++ b/src/actions/api.js @@ -0,0 +1,100 @@ +'use strict' + +const { API, ITEM } = require('../constants') +const { array } = require('../common/util') + +module.exports = { + import({ files, ...payload }, meta) { + return { + type: ITEM.IMPORT, + payload: { + ...payload, + files: array(files) + }, + meta: { + cmd: 'project', + history: 'add', + search: true, + prompt: false, + ...meta + } + } + }, + + item: { + find({ tags, ...payload }, meta) { + return { + type: API.ITEM.FIND, + payload: { + ...payload, + tags: array(tags) + }, + meta: { + cmd: 'project', + ...meta + } + } + }, + + show(payload, meta) { + return { + type: API.ITEM.SHOW, + payload, + meta: { + cmd: 'project', + ...meta + } + } + } + }, + + note: { + show(payload, meta) { + return { + type: API.NOTE.SHOW, + payload, + meta: { + cmd: 'project', + ...meta + } + } + } + }, + + photo: { + find(payload, meta) { + return { + type: API.PHOTO.FIND, + payload, + meta: { + cmd: 'project', + ...meta + } + } + }, + + show(payload, meta) { + return { + type: API.PHOTO.SHOW, + payload, + meta: { + cmd: 'project', + ...meta + } + } + } + }, + + selection: { + show(payload, meta) { + return { + type: API.SELECTION.SHOW, + payload, + meta: { + cmd: 'project', + ...meta + } + } + } + } +} diff --git a/src/actions/index.js b/src/actions/index.js index ad351e0329..7940fc3318 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -2,6 +2,7 @@ module.exports = { activity: require('./activity'), + api: require('./api'), cache: require('./cache'), classes: require('./classes'), context: require('./context'), diff --git a/src/browser/actions.js b/src/browser/actions.js index 216e22e552..10e9a57de4 100644 --- a/src/browser/actions.js +++ b/src/browser/actions.js @@ -1,6 +1,7 @@ 'use strict' module.exports = { + api: require('../actions/api'), cache: require('../actions/cache'), context: require('../actions/context'), edit: require('../actions/edit'), diff --git a/src/browser/api.js b/src/browser/api.js new file mode 100644 index 0000000000..f15c5786be --- /dev/null +++ b/src/browser/api.js @@ -0,0 +1,42 @@ +'use strict' + +const { info, logger } = require('../common/log') + +class Server { + constructor(app) { + this.app = app + } + + dispatch = (type, action) => { + this.app.wm.current(type).webContents.send('dispatch', action) + } + + rsvp = (type, action) => ( + this.app.wm.rsvp(type, action) + ) + + start() { + if (this.app.state.api || this.app.opts.port) { + let api = require('../common/api') + + this.koa = api.create({ + dispatch: this.dispatch, + log: logger.child({ name: 'api' }), + rsvp: this.rsvp, + version: this.app.version + }) + + let port = this.app.opts.port || this.app.state.port + + info(`api.start on port ${port}`) + this.koa.listen(port) + } + } + + stop() { + } +} + +module.exports = { + Server +} diff --git a/src/browser/args.js b/src/browser/args.js index c564cf062d..08b2cc256a 100644 --- a/src/browser/args.js +++ b/src/browser/args.js @@ -72,6 +72,13 @@ module.exports = default: false }) + .option('port', { + alias: 'p', + type: 'number', + describe: 'Set API listening port', + default: null + }) + .help('help') .version(version) diff --git a/src/browser/main.js b/src/browser/main.js index 2983c2f59f..6f397b6e2d 100644 --- a/src/browser/main.js +++ b/src/browser/main.js @@ -92,6 +92,10 @@ if (!(win32 && require('./squirrel')(opts))) { info(`ready after ${tropy.ready - START}ms [req:${T2 - T1}ms]`) }) + if (app.isPackaged) { + app.setAsDefaultProtocolClient('tropy') + } + if (darwin) { app.on('open-file', (event, file) => { if (tropy.ready) { diff --git a/src/browser/tropy.js b/src/browser/tropy.js index 235572de53..4cbe453182 100644 --- a/src/browser/tropy.js +++ b/src/browser/tropy.js @@ -32,6 +32,7 @@ const { Strings } = require('../common/res') const Storage = require('./storage') const Updater = require('./updater') const dialog = require('./dialog') +const API = require('./api') const WindowManager = require('./wm') const { addIdleObserver } = require('./idle') const { migrate } = require('./migrate') @@ -53,6 +54,7 @@ class Tropy extends EventEmitter { static defaults = { frameless: darwin, debug: false, + port: 2019, theme: darwin ? 'system' : 'light', recent: [], updater: true, @@ -78,6 +80,7 @@ class Tropy extends EventEmitter { this.updater = new Updater({ enable: process.env.NODE_ENV === 'production' && opts['auto-updates'] }) + this.api = new API.Server(this) prop(this, 'cache', { value: new Cache(opts.cache || join(opts.data, 'cache')) @@ -100,9 +103,11 @@ class Tropy extends EventEmitter { await this.restore() this.listen() this.wm.start() + this.api.start() } stop() { + this.api.stop() this.updater.stop() this.plugins.stop() this.persist() @@ -395,15 +400,15 @@ class Tropy extends EventEmitter { this.dispatch(act.item.export(target.id, { plugin }), win)) this.on('app:restore-item', (win, { target }) => { - this.dispatch(act.item.restore(target.id)) + this.dispatch(act.item.restore(target.id), win) }) this.on('app:destroy-item', (win, { target }) => { - this.dispatch(act.item.destroy(target.id)) + this.dispatch(act.item.destroy(target.id), win) }) this.on('app:create-item-photo', (win, { target }) => { - this.dispatch(act.photo.create({ item: target.id })) + this.dispatch(act.photo.create({ item: target.id }), win) }) this.on('app:toggle-item-tag', (win, { id, tag }) => { @@ -411,7 +416,7 @@ class Tropy extends EventEmitter { }) this.on('app:clear-item-tags', (win, { id }) => { - this.dispatch(act.item.tags.clear(id)) + this.dispatch(act.item.tags.clear(id), win) }) this.on('app:list-item-remove', (win, { target }) => { diff --git a/src/browser/wm.js b/src/browser/wm.js index 07987c5609..e13a15652c 100644 --- a/src/browser/wm.js +++ b/src/browser/wm.js @@ -8,9 +8,18 @@ const { debug, error, trace, warn } = require('../common/log') const { darwin, EL_CAPITAN } = require('../common/os') const { channel } = require('../common/release') const res = require('../common/res') -const { array, blank, get, once, remove, restrict } = require('../common/util') const { BODY, PANEL, ESPER } = require('../constants/sass') +const { + array, + blank, + counter, + get, + once, + remove, + restrict +} = require('../common/util') + const { app, BrowserWindow, @@ -39,6 +48,8 @@ class WindowManager extends EventEmitter { } this.windows = {} + this.pending = {} + this.seq = counter() } broadcast(...args) { @@ -230,11 +241,30 @@ class WindowManager extends EventEmitter { else win.minimize() break + case 'rsvp': + this.handlePendingResponse(...args) + break default: win.emit(type, ...args) } } + handlePendingResponse(action) { + try { + var id = action.meta.rsvp + + if (action.error) + this.pending[id].reject(action.payload) + else + this.pending[id].resolve(action) + + } catch (e) { + warn({ + stack: e.stack + }, `failed to resolve pending message ${id}`) + } + } + handleScrollBarsChange = () => { this.broadcast('scrollbars', !WindowManager.hasOverlayScrollBars()) } @@ -342,6 +372,26 @@ class WindowManager extends EventEmitter { this.each(type, win => win.webContents.send(...args)) } + rsvp(type, action) { + let id + + return new Promise((resolve, reject) => { + id = this.seq.next().value + let win = this.current(type) + + if (win == null) + return reject(new Error(`no ${type} window open`)) + + action.meta.rsvp = id + this.pending[id] = { resolve, reject } + + win.webContents.send('dispatch', action) + + // TODO reject pending after timeout! + + }).finally(() => { delete this.pending[id] }) + } + setTitle(type, title, frameless = false) { if (!frameless || !(darwin && EL_CAPITAN)) { this.each(type, win => win.setTitle(title)) diff --git a/src/commands/api.js b/src/commands/api.js new file mode 100644 index 0000000000..8448c63f62 --- /dev/null +++ b/src/commands/api.js @@ -0,0 +1,112 @@ +'use strict' + +const { select } = require('redux-saga/effects') +const { Command } = require('./command') +const { API } = require('../constants') +const { pluck } = require('../common/util') +const { serialize } = require('../export/note') +//const act = require('../actions') +//const mod = require('../models') + + +class ItemFind extends Command { + static get ACTION() { + return API.ITEM.FIND + } + + *exec() { + } +} + +class ItemShow extends Command { + static get ACTION() { + return API.ITEM.SHOW + } + + *exec() { + let { id } = this.action.payload + let item = yield select(state => state.items[id]) + return item + } +} + +class NoteShow extends Command { + static get ACTION() { + return API.NOTE.SHOW + } + + *exec() { + let { id, format } = this.action.payload + + let note = yield select(state => state.notes[id]) + + if (note == null) + return null + + switch (format) { + case 'html': + return serialize(note, { format: { html: true }, localize: false }).html + case 'plain': + case 'text': + return note.text + case 'md': + case 'markdown': + return serialize(note, { + format: { markdown: true }, + localize: false + }).markdown + default: + return note + } + } +} + +class PhotoFind extends Command { + static get ACTION() { + return API.PHOTO.FIND + } + + *exec() { + let { item } = this.action.payload + let { items, photos } = yield select() + + if (!(item in items)) + return null + + return pluck(photos, items[item].photos) + } +} + +class PhotoShow extends Command { + static get ACTION() { + return API.PHOTO.SHOW + } + + *exec() { + let { id } = this.action.payload + let photo = yield select(state => state.photos[id]) + return photo + } +} + +class SelectionShow extends Command { + static get ACTION() { + return API.SELECTION.SHOW + } + + *exec() { + let { id } = this.action.payload + let selection = yield select(state => state.selections[id]) + return selection + } +} + + +module.exports = { + ItemFind, + ItemShow, + NoteShow, + PhotoFind, + PhotoShow, + SelectionShow +} diff --git a/src/commands/index.js b/src/commands/index.js index 0f1fd9120d..9ac6eb6ffd 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -6,6 +6,7 @@ const handles = map(([, cmd]) => [cmd.ACTION, cmd]) module.exports = { ...seq(require('./cache'), handles), + ...seq(require('./api'), handles), ...seq(require('./item'), handles), ...seq(require('./list'), handles), ...seq(require('./metadata'), handles), diff --git a/src/common/api.js b/src/common/api.js new file mode 100644 index 0000000000..24613b11e9 --- /dev/null +++ b/src/common/api.js @@ -0,0 +1,155 @@ +'use strict' + +const Koa = require('koa') +const Router = require('@koa/router') +const act = require('../actions/api') + +const show = (type) => + async (ctx) => { + let { params, rsvp } = ctx + + let { payload } = await rsvp('project', act[type].show({ + id: params[type] + })) + + if (payload != null) + ctx.body = payload + else + ctx.status = 404 + } + +const project = { + async import(ctx) { + let { assert, query, rsvp } = ctx + + assert.ok(query.file, 400, 'missing file parameter') + + let { payload } = await rsvp('project', act.import({ + files: query.file, + list: query.list + })) + + ctx.body = payload + }, + + items: { + async find(ctx) { + let { query, rsvp } = ctx + + let { payload } = await rsvp('project', act.item.find({ + tags: query.tag + })) + + ctx.body = payload + }, + + show: show('item') + }, + + notes: { + async show(ctx) { + let { assert, params, query, rsvp } = ctx + + if (query.format) + assert( + (/^(json|html|plain|text|md|markdown)$/).test(query.format), + 400, + 'format unknown') + + let { payload } = await rsvp('project', act.note.show({ + id: params.note, + format: query.format + })) + + if (payload != null) { + if (query.format === 'html') + ctx.type = 'text/html' + if (query.format === 'markdown' || query.format === 'md') + ctx.type = 'text/markdown' + + ctx.body = payload + + } else { + ctx.status = 404 + } + } + }, + + photos: { + async find(ctx) { + let { params, rsvp } = ctx + + let { payload } = await rsvp('project', act.photo.find({ + item: params.item + })) + + if (payload != null) + ctx.body = payload + else + ctx.status = 404 + }, + + show: show('photo') + }, + + selections: { + show: show('selection') + } +} + +const create = ({ dispatch, log, rsvp, version }) => { + let app = new Koa + let api = new Router + + app.silent = true + app.on('error', e => { + log.error({ + stack: e.stack, + status: e.status + }, e.message) + }) + + app.context.dispatch = dispatch + app.context.log = log + app.context.rsvp = rsvp + + api + .get('/project/import', project.import) + + .get('/project/items', project.items.find) + .get('/project/items/:item', project.items.show) + .get('/project/items/:item/photos', project.photos.find) + + .get('/project/notes/:note', project.notes.show) + .get('/project/photos/:photo', project.photos.show) + .get('/project/selections/:selection', project.photos.show) + + + .get('/version', (ctx) => { + ctx.body = { version } + }) + + app + .use(logging) + .use(api.routes()) + .use(api.allowedMethods()) + + return app +} + +const logging = async (ctx, next) => { + const START = Date.now() + await next() + + ctx.log.debug({ + ms: Date.now() - START, + status: ctx.status, + method: ctx.method, + query: ctx.query, + url: ctx.url + }) +} + +module.exports = { + create +} diff --git a/src/constants/api.js b/src/constants/api.js new file mode 100644 index 0000000000..0760f4a94e --- /dev/null +++ b/src/constants/api.js @@ -0,0 +1,26 @@ +'use strict' + +module.exports = { + ITEM: { + FIND: 'api.item.find', + SHOW: 'api.item.show' + }, + + PHOTO: { + FIND: 'api.photo.find', + SHOW: 'api.photo.show' + }, + + SELECTION: { + SHOW: 'api.selection.show' + }, + + NOTE: { + SHOW: 'api.note.show' + }, + + TAG: { + LIST: 'api.tag.list', + SHOW: 'api.tag.show' + } +} diff --git a/src/constants/index.js b/src/constants/index.js index 32241a402d..00c5808862 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -2,6 +2,7 @@ module.exports = { ACTIVITY: require('./activity'), + API: require('./api'), CACHE: require('./cache'), CONTEXT: require('./context'), DND: require('./dnd'), diff --git a/src/sagas/ipc.js b/src/sagas/ipc.js index ba6ffc45ae..20051ea4fc 100644 --- a/src/sagas/ipc.js +++ b/src/sagas/ipc.js @@ -7,49 +7,11 @@ const { const { ipcRenderer: ipc } = require('electron') const { warn } = require('../common/log') -const { identity } = require('../common/util') const history = require('../selectors/history') const { getAllTags } = require('../selectors') const { TAG, HISTORY } = require('../constants') - -module.exports = { - - *forward(filter, { type, payload, meta }) { - try { - const event = meta.ipc === true ? type : meta.ipc - const data = yield call(filter[event] || identity, payload) - - yield call([ipc, ipc.send], event, data) - - } catch (e) { - warn({ stack: e.stack }, 'unexpected error in *ipc:forward') - } - }, - - *receive() { - const disp = yield call(channel, 'dispatch') - - while (true) { - try { - const action = yield take(disp) - yield put(action) - - } catch (e) { - warn({ stack: e.stack }, 'unexpected error in *ipc:receive') - } - } - }, - - *ipc() { - yield every(({ meta }) => meta && meta.ipc, module.exports.forward, FILTER) - yield fork(module.exports.receive) - } - -} - - -const FILTER = { +const filters = { *[HISTORY.CHANGED]() { const summary = yield select(history.summary) const messages = yield select(state => state.intl.messages) @@ -70,6 +32,39 @@ const FILTER = { } } +function *forward({ type, payload, meta }) { + try { + let name = meta.ipc === true ? type : meta.ipc + + let data = (name in filters) ? + yield call(filters[name], payload) : + payload + + yield call([ipc, ipc.send], name, data) + + } catch (e) { + warn({ stack: e.stack }, 'unexpected error in *ipc:forward') + } +} + +function *rsvp(action) { + try { + yield call([ipc, ipc.send], 'wm', 'rsvp', action) + + } catch (e) { + warn({ stack: e.stack }, 'unexpected error in *ipc:rsvp') + } +} + +function *receive() { + let dispatches = yield call(channel, 'dispatch') + + while (true) { + let action = yield take(dispatches) + yield put(action) + } +} + function channel(name) { return eventChannel(emitter => { const listener = (_, ...actions) => { @@ -85,3 +80,12 @@ function channel(name) { return () => ipc.removeListener(name, listener) }) } + +module.exports = { + *ipc() { + yield every(({ error, meta }) => !error && meta && meta.ipc, forward) + yield every(({ meta }) => meta && meta.rsvp && meta.done, rsvp) + + yield fork(receive) + } +}