diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 5eaa47a..0000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.env b/.env
new file mode 100644
index 0000000..fff8dc8
--- /dev/null
+++ b/.env
@@ -0,0 +1,2 @@
+HTTP_PORT = 8080
+HTTPS_PORT = 8443
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..e502fc9
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,2 @@
+CREDENTIALS_ENABLED = 0
+WEBHOOK_ENABLED = 0
diff --git a/.env.production b/.env.production
new file mode 100644
index 0000000..8cabc5a
--- /dev/null
+++ b/.env.production
@@ -0,0 +1,2 @@
+CREDENTIALS_ENABLED = 1
+WEBHOOK_ENABLED = 1
diff --git a/.gitignore b/.gitignore
index 689bc6a..ed81866 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,16 @@
+# WebStorm settings
/.idea
+
+# npm
/node_modules
/npm-debug.log
-/public
-/pm2.config.js
+
+# macOS
.DS_Store
+
+# local .env* files
+.env.local
+.env.*.local
+
+# downloaded files
+/public
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
deleted file mode 100644
index 15a15b2..0000000
--- a/.idea/encodings.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
deleted file mode 100644
index 28a804d..0000000
--- a/.idea/misc.xml
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
deleted file mode 100644
index fbd90b4..0000000
--- a/.idea/modules.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/server.iml b/.idea/server.iml
deleted file mode 100644
index 24643cc..0000000
--- a/.idea/server.iml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
deleted file mode 100644
index d6eacee..0000000
--- a/.idea/vcs.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/webResources.xml b/.idea/webResources.xml
deleted file mode 100644
index edf30b3..0000000
--- a/.idea/webResources.xml
+++ /dev/null
@@ -1,14 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
deleted file mode 100644
index 3b2c9de..0000000
--- a/.idea/workspace.xml
+++ /dev/null
@@ -1,266 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- robot
-
-
- $PROJECT_DIR$
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 1559885518055
-
-
- 1559885518055
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/app/backend.js b/app/backend.js
deleted file mode 100644
index ebec15a..0000000
--- a/app/backend.js
+++ /dev/null
@@ -1,42 +0,0 @@
-const {
- __DEV__,
- backendBuildPath,
-} = require('../environment');
-
-if (__DEV__) {
- const webpack = require('webpack');
- const webpackConfig = require('../webpack.backend.config.js');
- const compiler = webpack(webpackConfig);
-
- let backend = null;
- let lastHash = null;
- compiler.watch({
- watchOptions: {
- ignored: /public/,
- },
- }, (err, stats) => {
- if (err) {
- lastHash = null;
- compiler.purgeInputFileSystem();
- console.error(err);
- } else if (stats.hash !== lastHash) {
- lastHash = stats.hash;
- console.info(stats.toString({
- cached: false,
- colors: true,
- }));
-
- delete require.cache[require.resolve(backendBuildPath)];
- backend = require(backendBuildPath).default;
- }
- });
-
- const backendWrapper = (req, res, next) => backend(req, res, next);
- backendWrapper.getHierarchy = () => backend.hierarchy;
- module.exports = backendWrapper;
-} else {
- const backend = require(backendBuildPath).default;
- const backendWrapper = (req, res, next) => backend(req, res, next);
- backendWrapper.getHierarchy = () => backend.hierarchy;
- module.exports = backendWrapper;
-}
diff --git a/app/frontend.js b/app/frontend.js
deleted file mode 100644
index d54c3fb..0000000
--- a/app/frontend.js
+++ /dev/null
@@ -1,78 +0,0 @@
-const express = require('express');
-const path = require('path');
-const fs = require('fs');
-const url = require('url');
-const packageJson = require('../package');
-
-const {
- __DEV__,
- frontendSrcPath,
- frontendBuildPath,
-} = require('../environment');
-
-const app = express();
-
-if (__DEV__) {
- const webpack = require('webpack');
- const webpackDev = require('webpack-dev-middleware');
- const webpackHot = require('webpack-hot-middleware');
- const webpackConfig = require('../webpack.frontend.config.js');
- const compiler = webpack(webpackConfig);
- app.use(express.static(path.resolve(frontendSrcPath, 'static')));
- app.use(webpackDev(compiler, {
- stats: {
- cached: false,
- colors: true,
- },
- serverSideRender: true,
- index: false,
- }));
- app.use(webpackHot(compiler));
- app.use((req, res, next) => {
- const { fs } = res.locals;
- const outputPath = res.locals.webpackStats.toJson().outputPath;
- const filePath = path.resolve(outputPath, 'index.html');
- fs.readFile(filePath, 'utf8', (err, data) => {
- if (err) return next(err);
- res.indexFile = data;
- next();
- });
- });
-} else {
- app.use(express.static(frontendBuildPath, { index: false }));
- app.use((req, res, next) => {
- const filePath = path.resolve(frontendBuildPath, 'index.html');
- fs.readFile(filePath, 'utf8', (err, data) => {
- if (err) return next(err);
- res.indexFile = data;
- next();
- });
- });
-}
-
-app.use((req, res) => {
- const backend = require('./backend');
- const hierarchy = backend.getHierarchy();
-
- const [, categoryKey, algorithmKey] = url.parse(req.originalUrl).pathname.split('/');
- let { title, description } = packageJson;
- let algorithm = undefined;
- if (categoryKey && categoryKey !== 'scratch-paper') {
- algorithm = hierarchy.find(categoryKey, algorithmKey) || null;
- if (algorithm) {
- title = [algorithm.categoryName, algorithm.algorithmName].join(' - ');
- description = algorithm.description;
- } else {
- res.status(404);
- }
- }
-
- const indexFile = res.indexFile
- .replace(/\$TITLE/g, title)
- .replace(/\$DESCRIPTION/g, description)
- .replace(/\$ALGORITHM/g, algorithm === undefined ? 'undefined' :
- JSON.stringify(algorithm).replace(/ {
- if (req.hostname === 'algo-visualizer.jasonpark.me') {
- res.redirect(301, 'https://algorithm-visualizer.org/');
- } else if (credentials && !req.secure) {
- res.redirect(301, `https://${req.hostname}${req.url}`);
- } else {
- next();
- }
-});
-app.get('/robots.txt', (req, res) => {
- res.sendFile(path.resolve(__dirname, '..', 'robots.txt'));
-});
-app.use(apiEndpoint, backend);
-app.use(frontend);
-
-module.exports = app;
diff --git a/bin/pull b/bin/pull
deleted file mode 100755
index e0acb4d..0000000
--- a/bin/pull
+++ /dev/null
@@ -1,7 +0,0 @@
-#!/usr/bin/env bash
-
-git fetch &&
-! git diff-index --quiet origin/master -- ':!package-lock.json' &&
-git reset --hard origin/master &&
-npm install &&
-npm run build
diff --git a/bin/www b/bin/www
deleted file mode 100755
index 19bd763..0000000
--- a/bin/www
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env node
-
-const http = require('http');
-const https = require('https');
-
-const app = require('../app');
-const {
- httpPort,
- httpsPort,
- credentials,
-} = require('../environment');
-
-const httpServer = http.createServer(app);
-httpServer.listen(httpPort);
-console.info(`http: listening on port ${httpPort}`);
-
-if (credentials) {
- const httpsServer = https.createServer(credentials, app);
- httpsServer.listen(httpsPort);
- console.info(`https: listening on port ${httpsPort}`);
-}
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..c6f3005
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,1640 @@
+{
+ "name": "@algorithm-visualizer/server",
+ "version": "2.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.0.0.tgz",
+ "integrity": "sha512-OfC2uemaknXr87bdLUkWog7nYuliM9Ij5HUcajsVcMCpQrcLmtxRbVFTIqmcSkSeYRBFBRxs2FiUqFJDLdiebA==",
+ "dev": true,
+ "requires": {
+ "@babel/highlight": "^7.0.0"
+ }
+ },
+ "@babel/highlight": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.0.0.tgz",
+ "integrity": "sha512-UFMC4ZeFC48Tpvj7C8UgLvtkaUuovQX+5xNWrsIoMG8o2z+XFKjKaN9iVmS84dPwVN00W4wPmqvYoZF3EGAsfw==",
+ "dev": true,
+ "requires": {
+ "chalk": "^2.0.0",
+ "esutils": "^2.0.2",
+ "js-tokens": "^4.0.0"
+ }
+ },
+ "@types/body-parser": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.17.0.tgz",
+ "integrity": "sha512-a2+YeUjPkztKJu5aIF2yArYFQQp8d51wZ7DavSHjFuY1mqVgidGyzEQ41JIVNy82fXj8yPgy2vJmfIywgESW6w==",
+ "dev": true,
+ "requires": {
+ "@types/connect": "*",
+ "@types/node": "*"
+ }
+ },
+ "@types/compression": {
+ "version": "0.0.36",
+ "resolved": "https://registry.npmjs.org/@types/compression/-/compression-0.0.36.tgz",
+ "integrity": "sha512-B66iZCIcD2eB2F8e8YDIVtCUKgfiseOR5YOIbmMN2tM57Wu55j1xSdxdSw78aVzsPmbZ6G+hINc+1xe1tt4NBg==",
+ "dev": true,
+ "requires": {
+ "@types/express": "*"
+ }
+ },
+ "@types/connect": {
+ "version": "3.4.32",
+ "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
+ "integrity": "sha512-4r8qa0quOvh7lGD0pre62CAb1oni1OO6ecJLGCezTmhQ8Fz50Arx9RUszryR8KlgK6avuSXvviL6yWyViQABOg==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/execa": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@types/execa/-/execa-0.9.0.tgz",
+ "integrity": "sha512-mgfd93RhzjYBUHHV532turHC2j4l/qxsF/PbfDmprHDEUHmNZGlDn1CEsulGK3AfsPdhkWzZQT/S/k0UGhLGsA==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/express": {
+ "version": "4.17.0",
+ "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.0.tgz",
+ "integrity": "sha512-CjaMu57cjgjuZbh9DpkloeGxV45CnMGlVd+XpG7Gm9QgVrd7KFq+X4HY0vM+2v0bczS48Wg7bvnMY5TN+Xmcfw==",
+ "dev": true,
+ "requires": {
+ "@types/body-parser": "*",
+ "@types/express-serve-static-core": "*",
+ "@types/serve-static": "*"
+ }
+ },
+ "@types/express-serve-static-core": {
+ "version": "4.16.7",
+ "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.16.7.tgz",
+ "integrity": "sha512-847KvL8Q1y3TtFLRTXcVakErLJQgdpFSaq+k043xefz9raEf0C7HalpSY7OW5PyjCnY8P7bPW5t/Co9qqp+USg==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*",
+ "@types/range-parser": "*"
+ }
+ },
+ "@types/fs-extra": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-7.0.0.tgz",
+ "integrity": "sha512-ndoMMbGyuToTy4qB6Lex/inR98nPiNHacsgMPvy+zqMLgSxbt8VtWpDArpGp69h1fEDQHn1KB+9DWD++wgbwYA==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "@types/mime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz",
+ "integrity": "sha512-FwI9gX75FgVBJ7ywgnq/P7tw+/o1GUbtP0KzbtusLigAOgIgNISRK0ZPl4qertvXSIE8YbsVJueQ90cDt9YYyw==",
+ "dev": true
+ },
+ "@types/morgan": {
+ "version": "1.7.35",
+ "resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.7.35.tgz",
+ "integrity": "sha512-E9qFi0seOkdlQnCTPv54brNfGWeFdRaEhI5tSue4pdx/V+xfxvMETsxXhOEcj1cYL+0n/jcTEmj/jD2gjzCwMg==",
+ "dev": true,
+ "requires": {
+ "@types/express": "*"
+ }
+ },
+ "@types/node": {
+ "version": "12.0.7",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.7.tgz",
+ "integrity": "sha512-1YKeT4JitGgE4SOzyB9eMwO0nGVNkNEsm9qlIt1Lqm/tG2QEiSMTD4kS3aO6L+w5SClLVxALmIBESK6Mk5wX0A==",
+ "dev": true
+ },
+ "@types/range-parser": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.3.tgz",
+ "integrity": "sha512-ewFXqrQHlFsgc09MK5jP5iR7vumV/BYayNC6PgJO2LPe8vrnNFyjQjSppfEngITi0qvfKtzFvgKymGheFM9UOA==",
+ "dev": true
+ },
+ "@types/remove-markdown": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@types/remove-markdown/-/remove-markdown-0.1.1.tgz",
+ "integrity": "sha512-SCYOFMHUqyiJU5M0V2gBB6UDdBhPwma34j0vYX0JgWaqp/74ila2Ops1jt5tB/C1UQXVXqK+is61884bITn3LQ==",
+ "dev": true
+ },
+ "@types/serve-static": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.2.tgz",
+ "integrity": "sha512-/BZ4QRLpH/bNYgZgwhKEh+5AsboDBcUdlBYgzoLX0fpj3Y2gp6EApyOlM3bK53wQS/OE1SrdSYBAbux2D1528Q==",
+ "dev": true,
+ "requires": {
+ "@types/express-serve-static-core": "*",
+ "@types/mime": "*"
+ }
+ },
+ "@types/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@types/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-FKjsOVbC6B7bdSB5CuzyHCkK69I=",
+ "dev": true
+ },
+ "@types/strip-json-comments": {
+ "version": "0.0.30",
+ "resolved": "https://registry.npmjs.org/@types/strip-json-comments/-/strip-json-comments-0.0.30.tgz",
+ "integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
+ "dev": true
+ },
+ "@types/uuid": {
+ "version": "3.4.4",
+ "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-3.4.4.tgz",
+ "integrity": "sha512-tPIgT0GUmdJQNSHxp0X2jnpQfBSTfGxUMc/2CXBU2mnyTFVYVa2ojpoQ74w0U2yn2vw3jnC640+77lkFFpdVDw==",
+ "dev": true,
+ "requires": {
+ "@types/node": "*"
+ }
+ },
+ "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"
+ }
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "arg": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.0.tgz",
+ "integrity": "sha512-ZWc51jO3qegGkVh8Hwpv636EkbesNV5ZNQPCtRa+0qytRYPEs9IYT9qITY9buezqUH5uqyzlWLcufrzU2rffdg=="
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "dev": true,
+ "requires": {
+ "sprintf-js": "~1.0.2"
+ }
+ },
+ "array-find-index": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=",
+ "dev": true
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "axios": {
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-0.19.0.tgz",
+ "integrity": "sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ==",
+ "requires": {
+ "follow-redirects": "1.5.10",
+ "is-buffer": "^2.0.2"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
+ "dev": true
+ },
+ "basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "requires": {
+ "safe-buffer": "5.1.2"
+ }
+ },
+ "body-parser": {
+ "version": "1.19.0",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz",
+ "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==",
+ "requires": {
+ "bytes": "3.1.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "on-finished": "~2.3.0",
+ "qs": "6.7.0",
+ "raw-body": "2.4.0",
+ "type-is": "~1.6.17"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "brace-expansion": {
+ "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"
+ }
+ },
+ "buffer-equal-constant-time": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
+ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk="
+ },
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
+ },
+ "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
+ },
+ "bytes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz",
+ "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg=="
+ },
+ "camelcase": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz",
+ "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=",
+ "dev": true
+ },
+ "camelcase-keys": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz",
+ "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=",
+ "dev": true,
+ "requires": {
+ "camelcase": "^2.0.0",
+ "map-obj": "^1.0.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
+ "dev": true
+ },
+ "commander": {
+ "version": "2.20.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz",
+ "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==",
+ "dev": true
+ },
+ "compressible": {
+ "version": "2.0.17",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.17.tgz",
+ "integrity": "sha512-BGHeLCK1GV7j1bSmQQAi26X+GgWcTjLr/0tzSvMCl3LH1w1IJ4PFSPoV5316b30cneTziC+B1a+3OjoSUcQYmw==",
+ "requires": {
+ "mime-db": ">= 1.40.0 < 2"
+ }
+ },
+ "compression": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz",
+ "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==",
+ "requires": {
+ "accepts": "~1.3.5",
+ "bytes": "3.0.0",
+ "compressible": "~2.0.16",
+ "debug": "2.6.9",
+ "on-headers": "~1.0.2",
+ "safe-buffer": "5.1.2",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "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
+ },
+ "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"
+ }
+ },
+ "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=="
+ },
+ "cookie": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz",
+ "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg=="
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "cross-spawn": {
+ "version": "6.0.5",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz",
+ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==",
+ "requires": {
+ "nice-try": "^1.0.4",
+ "path-key": "^2.0.1",
+ "semver": "^5.5.0",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "currently-unhandled": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+ "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+ "dev": true,
+ "requires": {
+ "array-find-index": "^1.0.1"
+ }
+ },
+ "dateformat": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz",
+ "integrity": "sha1-nxJLZ1lMk3/3BpMuSmQsyo27/uk=",
+ "dev": true,
+ "requires": {
+ "get-stdin": "^4.0.1",
+ "meow": "^3.3.0"
+ }
+ },
+ "debounce": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.0.tgz",
+ "integrity": "sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg==",
+ "dev": true
+ },
+ "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"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "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="
+ },
+ "diff": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.1.tgz",
+ "integrity": "sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q=="
+ },
+ "dotenv": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.0.0.tgz",
+ "integrity": "sha512-30xVGqjLjiUOArT4+M5q9sYdvuR4riM6yK9wMcas9Vbp6zZa+ocC9dp6QoftuhTPhFAiLK/0C5Ni2nou/Bk8lg=="
+ },
+ "dotenv-flow": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/dotenv-flow/-/dotenv-flow-2.0.0.tgz",
+ "integrity": "sha512-Gfey3wkr644VCq8e0EsX7d2ywbX9WTCOu5cakwE8seUXuoSUqr/aA1Ft1VqfZcnPaPPD0ADHhQ5IrjuZieMlVw==",
+ "requires": {
+ "dotenv": "^8.0.0"
+ }
+ },
+ "dynamic-dedupe": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz",
+ "integrity": "sha1-BuRMIj9eTpTXjvnbI6ZRXOL5YqE=",
+ "dev": true,
+ "requires": {
+ "xtend": "^4.0.0"
+ }
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
+ "integrity": "sha512-1MkrZNvWTKCaigbn+W15elq2BB/L22nqrSY5DKlo3X6+vclJm8Bb5djXJBmEX6fS3+zCh/F4VBK5Z2KxJt4s2Q==",
+ "requires": {
+ "once": "^1.4.0"
+ }
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "dev": true,
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "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",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
+ "dev": true
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
+ "dev": true
+ },
+ "esutils": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=",
+ "dev": true
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "execa": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
+ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==",
+ "requires": {
+ "cross-spawn": "^6.0.0",
+ "get-stream": "^4.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ }
+ },
+ "express": {
+ "version": "4.17.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz",
+ "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==",
+ "requires": {
+ "accepts": "~1.3.7",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.19.0",
+ "content-disposition": "0.5.3",
+ "content-type": "~1.0.4",
+ "cookie": "0.4.0",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.1.2",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.5",
+ "qs": "6.7.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.1.2",
+ "send": "0.17.1",
+ "serve-static": "1.14.1",
+ "setprototypeof": "1.1.1",
+ "statuses": "~1.5.0",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "express-github-webhook": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/express-github-webhook/-/express-github-webhook-1.0.6.tgz",
+ "integrity": "sha1-qYDKCriLjL2ke5Sh6RjD8KaPTK0=",
+ "requires": {
+ "buffer-equal-constant-time": "^1.0.1"
+ }
+ },
+ "filewatcher": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/filewatcher/-/filewatcher-3.0.1.tgz",
+ "integrity": "sha1-9KGVc1Xdr0Q8zXiolfPVXiPIoDQ=",
+ "dev": true,
+ "requires": {
+ "debounce": "^1.0.0"
+ }
+ },
+ "finalhandler": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz",
+ "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.3",
+ "statuses": "~1.5.0",
+ "unpipe": "~1.0.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "find-up": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+ "dev": true,
+ "requires": {
+ "path-exists": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "follow-redirects": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.5.10.tgz",
+ "integrity": "sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ==",
+ "requires": {
+ "debug": "=3.1.0"
+ }
+ },
+ "forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "fs-extra": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-6.0.1.tgz",
+ "integrity": "sha512-GnyIkKhhzXZUWFCaJzvyDLEEgDkPfb4/TPvJCJVuS8MWZgoSsErf++QpiAlDnKFcqhRlm+tIOcencCjyJE6ZCA==",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "jsonfile": "^4.0.0",
+ "universalify": "^0.1.0"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
+ "dev": true
+ },
+ "get-stdin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz",
+ "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=",
+ "dev": true
+ },
+ "get-stream": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz",
+ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==",
+ "requires": {
+ "pump": "^3.0.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz",
+ "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==",
+ "dev": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.1.15",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.15.tgz",
+ "integrity": "sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA=="
+ },
+ "growly": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz",
+ "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=",
+ "dev": true
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
+ "dev": true
+ },
+ "hosted-git-info": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
+ "dev": true
+ },
+ "http-errors": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz",
+ "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.1",
+ "statuses": ">= 1.5.0 < 2",
+ "toidentifier": "1.0.0"
+ }
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "indent-string": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz",
+ "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=",
+ "dev": true,
+ "requires": {
+ "repeating": "^2.0.0"
+ }
+ },
+ "inflight": {
+ "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"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "ipaddr.js": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.0.tgz",
+ "integrity": "sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA=="
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=",
+ "dev": true
+ },
+ "is-buffer": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
+ "integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
+ },
+ "is-finite": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz",
+ "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=",
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
+ },
+ "is-utf8": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
+ "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=",
+ "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
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "dev": true
+ },
+ "js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "dev": true,
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "jsonfile": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz",
+ "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=",
+ "requires": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "load-json-file": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz",
+ "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^2.2.0",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0",
+ "strip-bom": "^2.0.0"
+ }
+ },
+ "loud-rejection": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+ "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+ "dev": true,
+ "requires": {
+ "currently-unhandled": "^0.4.1",
+ "signal-exit": "^3.0.0"
+ }
+ },
+ "make-error": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.5.tgz",
+ "integrity": "sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g=="
+ },
+ "map-obj": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz",
+ "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=",
+ "dev": true
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "meow": {
+ "version": "3.7.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz",
+ "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=",
+ "dev": true,
+ "requires": {
+ "camelcase-keys": "^2.0.0",
+ "decamelize": "^1.1.2",
+ "loud-rejection": "^1.0.0",
+ "map-obj": "^1.0.1",
+ "minimist": "^1.1.3",
+ "normalize-package-data": "^2.3.4",
+ "object-assign": "^4.0.1",
+ "read-pkg-up": "^1.0.1",
+ "redent": "^1.0.0",
+ "trim-newlines": "^1.0.0"
+ }
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="
+ },
+ "mime-db": {
+ "version": "1.40.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.40.0.tgz",
+ "integrity": "sha512-jYdeOMPy9vnxEqFRRo6ZvTZ8d9oPb+k18PKoYNYUe2stVEBPPwsln/qWzdbmaIvnhZ9v2P+CuecK+fpUfsV2mA=="
+ },
+ "mime-types": {
+ "version": "2.1.24",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.24.tgz",
+ "integrity": "sha512-WaFHS3MCl5fapm3oLxU4eYDw77IQM2ACcxQ9RIxfaC3ooc6PFuBMGZZsYpvoXS5D5QTWPieo1jjLdAm3TBP3cQ==",
+ "requires": {
+ "mime-db": "1.40.0"
+ }
+ },
+ "minimatch": {
+ "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"
+ }
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
+ "dev": true
+ }
+ }
+ },
+ "morgan": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.9.1.tgz",
+ "integrity": "sha512-HQStPIV4y3afTiCYVxirakhlCfGkI161c76kKFca7Fk1JusM//Qeo1ej2XaMniiNeaZklMVrh3vTtIzpzwbpmA==",
+ "requires": {
+ "basic-auth": "~2.0.0",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "on-headers": "~1.0.1"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ }
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "negotiator": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz",
+ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw=="
+ },
+ "nice-try": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz",
+ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ=="
+ },
+ "node-notifier": {
+ "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.5.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
+ "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
+ "dev": true,
+ "requires": {
+ "hosted-git-info": "^2.1.4",
+ "resolve": "^1.10.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+ "requires": {
+ "path-key": "^2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
+ "dev": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=",
+ "dev": true
+ },
+ "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"
+ }
+ },
+ "on-headers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz",
+ "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
+ },
+ "parse-json": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+ "dev": true,
+ "requires": {
+ "error-ex": "^1.2.0"
+ }
+ },
+ "parseurl": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="
+ },
+ "path-exists": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+ "dev": true,
+ "requires": {
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "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
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
+ "dev": true
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "path-type": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz",
+ "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=",
+ "dev": true
+ },
+ "pinkie": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=",
+ "dev": true
+ },
+ "pinkie-promise": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+ "dev": true,
+ "requires": {
+ "pinkie": "^2.0.0"
+ }
+ },
+ "proxy-addr": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz",
+ "integrity": "sha512-t/7RxHXPH6cJtP0pRG6smSr9QJidhB+3kXu0KgXnbGYMgzEnUxRQ4/LDdfOwZEMyIh3/xHb8PX3t+lfL9z+YVQ==",
+ "requires": {
+ "forwarded": "~0.1.2",
+ "ipaddr.js": "1.9.0"
+ }
+ },
+ "pump": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
+ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==",
+ "requires": {
+ "end-of-stream": "^1.1.0",
+ "once": "^1.3.1"
+ }
+ },
+ "qs": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz",
+ "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ=="
+ },
+ "range-parser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="
+ },
+ "raw-body": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz",
+ "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==",
+ "requires": {
+ "bytes": "3.1.0",
+ "http-errors": "1.7.2",
+ "iconv-lite": "0.4.24",
+ "unpipe": "1.0.0"
+ }
+ },
+ "read-pkg": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
+ "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=",
+ "dev": true,
+ "requires": {
+ "load-json-file": "^1.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^1.0.0"
+ }
+ },
+ "read-pkg-up": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz",
+ "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=",
+ "dev": true,
+ "requires": {
+ "find-up": "^1.0.0",
+ "read-pkg": "^1.0.0"
+ }
+ },
+ "redent": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz",
+ "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=",
+ "dev": true,
+ "requires": {
+ "indent-string": "^2.1.0",
+ "strip-indent": "^1.0.1"
+ }
+ },
+ "remove-markdown": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.3.0.tgz",
+ "integrity": "sha1-XktmdJOpNXlyjz1S7MHbnKUF3Jg="
+ },
+ "repeating": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz",
+ "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=",
+ "dev": true,
+ "requires": {
+ "is-finite": "^1.0.0"
+ }
+ },
+ "resolve": {
+ "version": "1.11.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz",
+ "integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==",
+ "dev": true,
+ "requires": {
+ "path-parse": "^1.0.6"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ },
+ "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=="
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "semver": {
+ "version": "5.7.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.0.tgz",
+ "integrity": "sha512-Ya52jSX2u7QKghxeoFGpLwCtGlt7j0oY9DYb5apt9nPlJ42ID+ulTXESnt/qAQcoSERyZ5sl3LDIOw0nAn/5DA=="
+ },
+ "send": {
+ "version": "0.17.1",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz",
+ "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.7.2",
+ "mime": "1.6.0",
+ "ms": "2.1.1",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.1",
+ "statuses": "~1.5.0"
+ },
+ "dependencies": {
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ },
+ "dependencies": {
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ }
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ }
+ }
+ },
+ "serve-static": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz",
+ "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==",
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "0.17.1"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz",
+ "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw=="
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
+ },
+ "shellwords": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz",
+ "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==",
+ "dev": true
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ },
+ "source-map-support": {
+ "version": "0.5.12",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.12.tgz",
+ "integrity": "sha512-4h2Pbvyy15EE02G+JOZpUCmqWJuqrs+sEkzewTm++BPi7Hvn/HwcqLAcNxYAyI0x13CpPPn+kMjl+hplXMHITQ==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "spdx-correct": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.0.tgz",
+ "integrity": "sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q==",
+ "dev": true,
+ "requires": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-exceptions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz",
+ "integrity": "sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA==",
+ "dev": true
+ },
+ "spdx-expression-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+ "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+ "dev": true,
+ "requires": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-license-ids": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.4.tgz",
+ "integrity": "sha512-7j8LYJLeY/Yb6ACbQ7F76qy5jHkp0U6jgBfJsk97bwWlVUnUWsAgpyaCvo17h0/RQGnQ036tVDomiwoI4pDkQA==",
+ "dev": true
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
+ "dev": true
+ },
+ "statuses": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz",
+ "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow="
+ },
+ "strip-bom": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz",
+ "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=",
+ "dev": true,
+ "requires": {
+ "is-utf8": "^0.2.0"
+ }
+ },
+ "strip-eof": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
+ },
+ "strip-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz",
+ "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=",
+ "dev": true,
+ "requires": {
+ "get-stdin": "^4.0.1"
+ }
+ },
+ "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
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ },
+ "toidentifier": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz",
+ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw=="
+ },
+ "tree-kill": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.1.tgz",
+ "integrity": "sha512-4hjqbObwlh2dLyW4tcz0Ymw0ggoaVDMveUB9w8kFSQScdRLo0gxO9J7WFcUBo+W3C1TLdFIEwNOWebgZZ0RH9Q==",
+ "dev": true
+ },
+ "trim-newlines": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz",
+ "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=",
+ "dev": true
+ },
+ "ts-httpexceptions": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/ts-httpexceptions/-/ts-httpexceptions-4.1.0.tgz",
+ "integrity": "sha512-eNppUDEIt7zE/YnJ9tIvDnqzvs41qssS23+QsgOrUm+MZXoVsohAP+XBDAah5QWMk0nOyaLkS7EUpN8HeAdhSA==",
+ "requires": {
+ "tslib": "^1.9.3"
+ }
+ },
+ "ts-node": {
+ "version": "8.2.0",
+ "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.2.0.tgz",
+ "integrity": "sha512-m8XQwUurkbYqXrKqr3WHCW310utRNvV5OnRVeISeea7LoCWVcdfeB/Ntl8JYWFh+WRoUAdBgESrzKochQt7sMw==",
+ "requires": {
+ "arg": "^4.1.0",
+ "diff": "^4.0.1",
+ "make-error": "^1.1.1",
+ "source-map-support": "^0.5.6",
+ "yn": "^3.0.0"
+ }
+ },
+ "ts-node-dev": {
+ "version": "1.0.0-pre.39",
+ "resolved": "https://registry.npmjs.org/ts-node-dev/-/ts-node-dev-1.0.0-pre.39.tgz",
+ "integrity": "sha512-yOg9nMAi6U2HcAkhnFuWxfg53XDqpdbeBESo+7DfmlDpQX4RrEzBNV6szOjlm/OH0KiRgG5J0emvg/BS/gQmXQ==",
+ "dev": true,
+ "requires": {
+ "dateformat": "~1.0.4-1.2.3",
+ "dynamic-dedupe": "^0.3.0",
+ "filewatcher": "~3.0.0",
+ "minimist": "^1.1.3",
+ "mkdirp": "^0.5.1",
+ "node-notifier": "^5.4.0",
+ "resolve": "^1.0.0",
+ "rimraf": "^2.6.1",
+ "tree-kill": "^1.2.1",
+ "ts-node": "*",
+ "tsconfig": "^7.0.0"
+ }
+ },
+ "tsconfig": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz",
+ "integrity": "sha512-vZXmzPrL+EmC4T/4rVlT2jNVMWCi/O4DIiSj3UHg1OE5kCKbk4mfrXc6dZksLgRM/TZlKnousKH9bbTazUWRRw==",
+ "dev": true,
+ "requires": {
+ "@types/strip-bom": "^3.0.0",
+ "@types/strip-json-comments": "0.0.30",
+ "strip-bom": "^3.0.0",
+ "strip-json-comments": "^2.0.0"
+ },
+ "dependencies": {
+ "strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=",
+ "dev": true
+ }
+ }
+ },
+ "tslib": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
+ "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ=="
+ },
+ "tslint": {
+ "version": "5.17.0",
+ "resolved": "https://registry.npmjs.org/tslint/-/tslint-5.17.0.tgz",
+ "integrity": "sha512-pflx87WfVoYepTet3xLfDOLDm9Jqi61UXIKePOuca0qoAZyrGWonDG9VTbji58Fy+8gciUn8Bt7y69+KEVjc/w==",
+ "dev": true,
+ "requires": {
+ "@babel/code-frame": "^7.0.0",
+ "builtin-modules": "^1.1.1",
+ "chalk": "^2.3.0",
+ "commander": "^2.12.1",
+ "diff": "^3.2.0",
+ "glob": "^7.1.1",
+ "js-yaml": "^3.13.1",
+ "minimatch": "^3.0.4",
+ "mkdirp": "^0.5.1",
+ "resolve": "^1.3.2",
+ "semver": "^5.3.0",
+ "tslib": "^1.8.0",
+ "tsutils": "^2.29.0"
+ },
+ "dependencies": {
+ "diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+ "dev": true
+ }
+ }
+ },
+ "tsutils": {
+ "version": "2.29.0",
+ "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
+ "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
+ "dev": true,
+ "requires": {
+ "tslib": "^1.8.1"
+ }
+ },
+ "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"
+ }
+ },
+ "typescript": {
+ "version": "3.5.1",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.1.tgz",
+ "integrity": "sha512-64HkdiRv1yYZsSe4xC1WVgamNigVYjlssIoaH2HcZF0+ijsk5YK2g0G34w9wJkze8+5ow4STd22AynfO6ZYYLw=="
+ },
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "uuid": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+ },
+ "validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "dev": true,
+ "requires": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "xtend": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=",
+ "dev": true
+ },
+ "yn": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.0.tgz",
+ "integrity": "sha512-kKfnnYkbTfrAdd0xICNFw7Atm8nKpLcLv9AZGEt+kczL/WQVai4e2V6ZN8U/O+iI6WrNuJjNNOyu4zfhl9D3Hg=="
+ }
+ }
+}
diff --git a/package.json b/package.json
index c212fb9..e0afbc1 100644
--- a/package.json
+++ b/package.json
@@ -3,11 +3,42 @@
"version": "2.0.0",
"description": "Server for Algorithm Visualizer",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "watch": "NODE_ENV=development NODE_PATH=src ts-node-dev --respawn --ignore-watch node_modules --no-notify src",
+ "start": "NODE_ENV=production NODE_PATH=src ts-node --transpile-only src",
+ "tslint": "tslint -c tslint.json -p tsconfig.json"
},
"repository": {
"type": "git",
"url": "git+https://github.com/algorithm-visualizer/server.git"
},
- "license": "MIT"
+ "license": "MIT",
+ "devDependencies": {
+ "@types/compression": "0.0.36",
+ "@types/execa": "^0.9.0",
+ "@types/express": "^4.16.1",
+ "@types/fs-extra": "^7.0.0",
+ "@types/morgan": "^1.7.35",
+ "@types/node": "^12.0.0",
+ "@types/remove-markdown": "^0.1.1",
+ "@types/uuid": "^3.4.4",
+ "ts-node-dev": "^1.0.0-pre.39",
+ "tslint": "^5.16.0"
+ },
+ "dependencies": {
+ "axios": "^0.19.0",
+ "body-parser": "^1.18.2",
+ "compression": "^1.7.3",
+ "dotenv": "^8.0.0",
+ "dotenv-flow": "^2.0.0",
+ "execa": "^1.0.0",
+ "express": "^4.16.4",
+ "express-github-webhook": "^1.0.6",
+ "fs-extra": "^6.0.1",
+ "morgan": "^1.9.1",
+ "remove-markdown": "^0.3.0",
+ "ts-httpexceptions": "^4.1.0",
+ "ts-node": "^8.1.0",
+ "typescript": "^3.4.5",
+ "uuid": "^3.3.2"
+ }
}
diff --git a/pm2.config.js b/pm2.config.js
deleted file mode 100644
index 92786cc..0000000
--- a/pm2.config.js
+++ /dev/null
@@ -1,23 +0,0 @@
-const env = {
- NODE_ENV: 'production',
- HTTP_PORT: '80',
- HTTPS_PORT: '443',
- GITHUB_CLIENT_ID: '4ca41e5d1e4ae21b6f75',
- GITHUB_CLIENT_SECRET: '72fb530eb676b89a1a77e5371f8e21c5965f56ea',
- GITHUB_WEBHOOK_SECRET: 'flapdoodle_blandiloquence',
- CREDENTIALS_ENABLED: '1',
- CREDENTIALS_PATH: '/etc/letsencrypt/live/algorithm-visualizer.org',
- CREDENTIALS_CA: 'chain.pem',
- CREDENTIALS_KEY: 'privkey.pem',
- CREDENTIALS_CERT: 'cert.pem',
-};
-
-module.exports = {
- apps: [
- {
- name: 'algorithm-visualizer',
- script: 'npm run pm2',
- env,
- },
- ],
-};
diff --git a/public/algorithm-visualizer.js b/public/algorithm-visualizer.js
deleted file mode 100644
index ff8035b..0000000
--- a/public/algorithm-visualizer.js
+++ /dev/null
@@ -1 +0,0 @@
-!function webpackUniversalModuleDefinition(t,r){"object"==typeof exports&&"object"==typeof module?module.exports=r():"function"==typeof define&&define.amd?define([],r):"object"==typeof exports?exports.AlgorithmVisualizer=r():t.AlgorithmVisualizer=r()}("undefined"!=typeof self?self:this,function(){return function(t){var r={};function __webpack_require__(e){if(r[e])return r[e].exports;var o=r[e]={i:e,l:!1,exports:{}};return t[e].call(o.exports,o,o.exports,__webpack_require__),o.l=!0,o.exports}return __webpack_require__.m=t,__webpack_require__.c=r,__webpack_require__.d=function(t,r,e){__webpack_require__.o(t,r)||Object.defineProperty(t,r,{enumerable:!0,get:e})},__webpack_require__.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},__webpack_require__.t=function(t,r){if(1&r&&(t=__webpack_require__(t)),8&r)return t;if(4&r&&"object"==typeof t&&t&&t.__esModule)return t;var e=Object.create(null);if(__webpack_require__.r(e),Object.defineProperty(e,"default",{enumerable:!0,value:t}),2&r&&"string"!=typeof t)for(var o in t)__webpack_require__.d(e,o,function(r){return t[r]}.bind(null,o));return e},__webpack_require__.n=function(t){var r=t&&t.__esModule?function getDefault(){return t.default}:function getModuleExports(){return t};return __webpack_require__.d(r,"a",r),r},__webpack_require__.o=function(t,r){return Object.prototype.hasOwnProperty.call(t,r)},__webpack_require__.p="",__webpack_require__(__webpack_require__.s=0)}([function(t,r,e){"use strict";Object.defineProperty(r,"__esModule",{value:!0});var o=e(1);r.Randomize=o.default;var n=e(2);r.Commander=n.default;var a=e(5);r.Layout=a.default;var i=e(6);r.VerticalLayout=i.default;var c=e(7);r.HorizontalLayout=c.default;var u=e(8);r.Tracer=u.default;var p=e(9);r.LogTracer=p.default;var s=e(10);r.Array2DTracer=s.default;var _=e(11);r.Array1DTracer=_.default;var f=e(12);r.ChartTracer=f.default;var d=e(13);r.GraphTracer=d.default},function(t,r,e){"use strict";var o=this&&this.__extends||function(){var t=function(r,e){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,r){t.__proto__=r}||function(t,r){for(var e in r)r.hasOwnProperty(e)&&(t[e]=r[e])})(r,e)};return function(r,e){function __(){this.constructor=r}t(r,e),r.prototype=null===e?Object.create(e):(__.prototype=e.prototype,new __)}}();Object.defineProperty(r,"__esModule",{value:!0});var n=function(){function Randomizer(){}return Randomizer.prototype.create=function(){return null},Randomizer}(),a=function(t){function Integer(r,e){void 0===r&&(r=1),void 0===e&&(e=9);var o=t.call(this)||this;return o._min=r,o._max=e,o}return o(Integer,t),Integer.prototype.create=function(){return Math.random()*(this._max-this._min+1)+this._min|0},Integer}(n),i=(function(t){function Double(r,e){void 0===r&&(r=0),void 0===e&&(e=1);var o=t.call(this)||this;return o._min=r,o._max=e,o}o(Double,t),Double.prototype.create=function(){return Math.random()*(this._max-this._min)+this._min}}(n),function(t){function String(r,e){void 0===r&&(r=16),void 0===e&&(e="abcdefghijklmnopqrstuvwxyz");var o=t.call(this)||this;return o._length=r,o._letters=e,o}return o(String,t),String.prototype.create=function(){for(var t="",r=new a(0,this._letters.length-1),e=0;e1e6)throw new Error("Too Many Commands");if(this.objectCount>100)throw new Error("Too Many Objects")},Commander.prototype.destroy=function(){Commander.objectCount--,this.command("destroy",arguments)},Commander.prototype.command=function(t,r){Commander.command(this.key,t,r)},Commander.prototype.toJSON=function(){return this.key},Commander.keyRandomizer=new o.Randomize.String(8,"abcdefghijklmnopqrstuvwxyz0123456789"),Commander.objectCount=0,Commander.commands=[],Commander}();if(!process.env.ALGORITHM_VISUALIZER){var a=e(3),i=e(4);process.on("beforeExit",function(){a.post("https://algorithm-visualizer.org/api/visualizations",{content:JSON.stringify(n.commands)}).then(function(t){return i(t.data,{wait:!1})}).catch(console.error).finally(function(){return process.exit()})})}r.default=n},function(t,r){t.exports=require("axios")},function(t,r){t.exports=require("opn")},function(t,r,e){"use strict";var o=this&&this.__extends||function(){var t=function(r,e){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,r){t.__proto__=r}||function(t,r){for(var e in r)r.hasOwnProperty(e)&&(t[e]=r[e])})(r,e)};return function(r,e){function __(){this.constructor=r}t(r,e),r.prototype=null===e?Object.create(e):(__.prototype=e.prototype,new __)}}();Object.defineProperty(r,"__esModule",{value:!0});var n=function(t){function Layout(r){return t.call(this,arguments)||this}return o(Layout,t),Layout.setRoot=function(t){this.command(null,"setRoot",arguments)},Layout.prototype.add=function(t,r){this.command("add",arguments)},Layout.prototype.remove=function(t){this.command("remove",arguments)},Layout.prototype.removeAll=function(){this.command("removeAll",arguments)},Layout}(e(0).Commander);r.default=n},function(t,r,e){"use strict";var o=this&&this.__extends||function(){var t=function(r,e){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,r){t.__proto__=r}||function(t,r){for(var e in r)r.hasOwnProperty(e)&&(t[e]=r[e])})(r,e)};return function(r,e){function __(){this.constructor=r}t(r,e),r.prototype=null===e?Object.create(e):(__.prototype=e.prototype,new __)}}();Object.defineProperty(r,"__esModule",{value:!0});var n=function(t){function VerticalLayout(){return null!==t&&t.apply(this,arguments)||this}return o(VerticalLayout,t),VerticalLayout}(e(0).Layout);r.default=n},function(t,r,e){"use strict";var o=this&&this.__extends||function(){var t=function(r,e){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,r){t.__proto__=r}||function(t,r){for(var e in r)r.hasOwnProperty(e)&&(t[e]=r[e])})(r,e)};return function(r,e){function __(){this.constructor=r}t(r,e),r.prototype=null===e?Object.create(e):(__.prototype=e.prototype,new __)}}();Object.defineProperty(r,"__esModule",{value:!0});var n=function(t){function HorizontalLayout(){return null!==t&&t.apply(this,arguments)||this}return o(HorizontalLayout,t),HorizontalLayout}(e(0).Layout);r.default=n},function(t,r,e){"use strict";var o=this&&this.__extends||function(){var t=function(r,e){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,r){t.__proto__=r}||function(t,r){for(var e in r)r.hasOwnProperty(e)&&(t[e]=r[e])})(r,e)};return function(r,e){function __(){this.constructor=r}t(r,e),r.prototype=null===e?Object.create(e):(__.prototype=e.prototype,new __)}}();Object.defineProperty(r,"__esModule",{value:!0});var n=function(t){function Tracer(r){return t.call(this,arguments)||this}return o(Tracer,t),Tracer.delay=function(t){this.command(null,"delay",arguments)},Tracer.prototype.set=function(){this.command("set",arguments)},Tracer.prototype.reset=function(){this.command("reset",arguments)},Tracer}(e(0).Commander);r.default=n},function(t,r,e){"use strict";var o=this&&this.__extends||function(){var t=function(r,e){return(t=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,r){t.__proto__=r}||function(t,r){for(var e in r)r.hasOwnProperty(e)&&(t[e]=r[e])})(r,e)};return function(r,e){function __(){this.constructor=r}t(r,e),r.prototype=null===e?Object.create(e):(__.prototype=e.prototype,new __)}}();Object.defineProperty(r,"__esModule",{value:!0});var n=function(t){function LogTracer(){return null!==t&&t.apply(this,arguments)||this}return o(LogTracer,t),LogTracer.prototype.set=function(t){this.command("set",arguments)},LogTracer.prototype.print=function(t){this.command("print",arguments)},LogTracer.prototype.println=function(t){this.command("println",arguments)},LogTracer.prototype.printf=function(t){for(var r=[],e=1;e
-#include
-#include "algorithm-visualizer/GraphTracer.h"
-#include "algorithm-visualizer/Randomize.h"
-
-#define N 5
-
-using namespace std;
-
-int main() {
- int array[N][N];
- Randomize::Graph(N, 1, *(new Randomize::Integer(1, 9))).weighted().directed(false).fill(&array[0][0]);
- GraphTracer graphTracer;
- graphTracer.set(array);
- for (int i = 0; i < N; i++) {
- for (int j = 0; j < N; j++) {
- cout << array[i][j] << " ";
- }
- cout << endl;
- }
- return 0;
-}
diff --git a/src/Server.ts b/src/Server.ts
new file mode 100644
index 0000000..f478451
--- /dev/null
+++ b/src/Server.ts
@@ -0,0 +1,104 @@
+import express, { NextFunction, Request, Response } from 'express';
+import morgan from 'morgan';
+import bodyParser from 'body-parser';
+import * as Controllers from 'controllers';
+import { NotFound } from 'ts-httpexceptions';
+import compression from 'compression';
+import { __PROD__, credentials, httpPort, httpsPort, webhookOptions } from 'config/environments';
+import http from 'http';
+import https from 'https';
+import { Hierarchy } from 'models';
+import execa from 'execa';
+import * as Tracers from 'tracers';
+import { errorHandlerMiddleware, frontendMiddleware, redirectMiddleware } from 'middlewares';
+import { pull } from 'utils/misc';
+import { frontendBuildDir, frontendBuiltDir, frontendDir, rootDir } from 'config/paths';
+
+const Webhook = require('express-github-webhook');
+
+export default class Server {
+ private readonly app = express();
+ readonly hierarchy = new Hierarchy();
+ readonly tracers = Object.values(Tracers).map(Tracer => new Tracer());
+ private readonly webhook = webhookOptions && Webhook(webhookOptions);
+
+ constructor() {
+ this.app
+ .use(compression())
+ .use(morgan(__PROD__ ? 'tiny' : 'dev'))
+ .use(redirectMiddleware())
+ .use(bodyParser.json())
+ .use(bodyParser.urlencoded({extended: true}))
+ .use('/api', this.getApiRouter())
+ .use(frontendMiddleware(this));
+ if (this.webhook) {
+ this.app.use('/webhook', this.webhook);
+ }
+ this.app.use(errorHandlerMiddleware());
+
+ if (this.webhook) {
+ this.webhook.on('push', async (repo: string, data: any) => {
+ const {ref, head_commit} = data;
+ if (ref !== 'refs/heads/master') return;
+ if (!head_commit) throw new Error('The `head_commit` is empty.');
+
+ switch (repo) {
+ case 'server':
+ await this.update(head_commit.id);
+ break;
+ case 'algorithm-visualizer':
+ await this.updateFrontend(head_commit.id);
+ break;
+ case 'algorithms':
+ await this.hierarchy.update(head_commit.id);
+ break;
+ default:
+ throw new Error(`Webhook from unknown repository '${repo}'.`);
+ }
+ });
+
+ this.webhook.on('release', async (repo: string, data: any) => {
+ const tracer = this.tracers.find(tracer => repo === `tracers.${tracer.lang}`);
+ if (!tracer) throw new Error(`Tracer not found for repository '${repo}'.`);
+ await tracer.update(data.release);
+ });
+ }
+ }
+
+ getApiRouter() {
+ const router = express.Router();
+ Object.values(Controllers).forEach(Controller => new Controller(this).route(router));
+ router.use((req: Request, res: Response, next: NextFunction) => {
+ next(new NotFound('API not found.'));
+ });
+ return router;
+ };
+
+ async update(commit?: string) {
+ await pull(rootDir, 'server', commit);
+ await execa.shell('npm install', {cwd: rootDir});
+ process.exit(0);
+ };
+
+ async updateFrontend(commit?: string) {
+ await pull(frontendDir, 'algorithm-visualizer', commit);
+ await execa.shell([
+ 'npm install',
+ 'npm run build',
+ `rm -rf ${frontendBuiltDir}`,
+ `mv ${frontendBuildDir} ${frontendBuiltDir}`,
+ ].join(' && '), {cwd: frontendDir});
+ }
+
+ start() {
+ const httpServer = http.createServer(this.app);
+ httpServer.listen(httpPort);
+ console.info(`http: listening on port ${httpPort}`);
+
+ if (credentials) {
+ const httpsServer = https.createServer(credentials, this.app);
+ httpsServer.listen(httpsPort);
+ console.info(`https: listening on port ${httpsPort}`);
+ }
+ }
+}
diff --git a/src/apis/index.js b/src/apis/index.js
deleted file mode 100644
index 5aedb72..0000000
--- a/src/apis/index.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import Promise from 'bluebird';
-import axios from 'axios';
-import fs from 'fs';
-import { githubClientId, githubClientSecret } from '/environment';
-
-const instance = axios.create();
-
-instance.interceptors.request.use(request => {
- request.params = { client_id: githubClientId, client_secret: githubClientSecret, ...request.params };
- return request;
-});
-
-instance.interceptors.response.use(response => {
- return response.data;
-}, error => {
- return Promise.reject(error.response.data);
-});
-
-const request = (url, process) => {
- const tokens = url.split('/');
- const baseURL = /^https?:\/\//i.test(url) ? '' : 'https://api.github.com';
- return (...args) => {
- return new Promise((resolve, reject) => {
- const mappedURL = baseURL + tokens.map((token, i) => token.startsWith(':') ? args.shift() : token).join('/');
- return resolve(process(mappedURL, args));
- });
- };
-};
-
-const GET = URL => {
- return request(URL, (mappedURL, args) => {
- const [params] = args;
- return instance.get(mappedURL, { params });
- });
-};
-
-const DELETE = URL => {
- return request(URL, (mappedURL, args) => {
- const [params] = args;
- return instance.delete(mappedURL, { params });
- });
-};
-
-const POST = URL => {
- return request(URL, (mappedURL, args) => {
- const [body, params] = args;
- return instance.post(mappedURL, body, { params });
- });
-};
-
-const PUT = URL => {
- return request(URL, (mappedURL, args) => {
- const [body, params] = args;
- return instance.put(mappedURL, body, { params });
- });
-};
-
-const PATCH = URL => {
- return request(URL, (mappedURL, args) => {
- const [body, params] = args;
- return instance.patch(mappedURL, body, { params });
- });
-};
-
-const GitHubApi = {
- listCommits: GET('/repos/:owner/:repo/commits'),
- getAccessToken: code => instance.post('https://github.com/login/oauth/access_token', { code }, { headers: { Accept: 'application/json' } }),
- getLatestRelease: GET('/repos/:owner/:repo/releases/latest'),
- download: (url, path) => instance({
- method: 'get',
- url,
- responseType: 'stream',
- }).then(data => new Promise((resolve, reject) => {
- data.pipe(fs.createWriteStream(path));
- data.on('end', resolve);
- data.on('error', reject);
- })),
-};
-
-export {
- GitHubApi,
-};
\ No newline at end of file
diff --git a/src/common/config.js b/src/common/config.js
deleted file mode 100644
index 6e06a99..0000000
--- a/src/common/config.js
+++ /dev/null
@@ -1,7 +0,0 @@
-const memoryLimit = 256; // in megabytes
-const timeLimit = 5000; // in milliseconds
-
-export {
- memoryLimit,
- timeLimit,
-};
diff --git a/src/common/error.js b/src/common/error.js
deleted file mode 100644
index c089470..0000000
--- a/src/common/error.js
+++ /dev/null
@@ -1,18 +0,0 @@
-class ClientError extends Error {
-}
-
-class NotFoundError extends ClientError {
-}
-
-class ForbiddenError extends ClientError {
-}
-
-class UnauthorizedError extends ClientError {
-}
-
-export {
- ClientError,
- NotFoundError,
- ForbiddenError,
- UnauthorizedError,
-};
diff --git a/src/common/hierarchy.js b/src/common/hierarchy.js
deleted file mode 100644
index 61635f1..0000000
--- a/src/common/hierarchy.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Hierarchy } from '/models';
-import path from 'path';
-
-const repoPath = path.resolve(__dirname, '..', 'public', 'algorithms');
-const hierarchy = new Hierarchy(repoPath);
-
-export default hierarchy;
diff --git a/src/common/util.js b/src/common/util.js
deleted file mode 100644
index 8c9ddfa..0000000
--- a/src/common/util.js
+++ /dev/null
@@ -1,52 +0,0 @@
-import Promise from 'bluebird';
-import axios from 'axios';
-import child_process from 'child_process';
-import path from 'path';
-import fs from 'fs-extra';
-import removeMarkdown from 'remove-markdown';
-
-const execute = (command, { stdout = process.stdout, stderr = process.stderr, ...options } = {}) => new Promise((resolve, reject) => {
- const child = child_process.exec(command, options, (error, stdout, stderr) => {
- if (error) return reject(error.code ? new Error(stderr) : error);
- resolve(stdout);
- });
- if (stdout) child.stdout.pipe(stdout);
- if (stderr) child.stderr.pipe(stderr);
-});
-
-const createKey = name => name.toLowerCase().trim().replace(/[^\w \-]/g, '').replace(/ /g, '-');
-
-const isDirectory = dirPath => fs.lstatSync(dirPath).isDirectory();
-
-const listFiles = dirPath => fs.readdirSync(dirPath).filter(fileName => !fileName.startsWith('.'));
-
-const listDirectories = dirPath => listFiles(dirPath).filter(fileName => isDirectory(path.resolve(dirPath, fileName)));
-
-const getDescription = files => {
- const readmeFile = files.find(file => file.name === 'README.md');
- if (!readmeFile) return '';
- const lines = readmeFile.content.split('\n');
- lines.shift();
- while (lines.length && !lines[0].trim()) lines.shift();
- let descriptionLines = [];
- while (lines.length && lines[0].trim()) descriptionLines.push(lines.shift());
- return removeMarkdown(descriptionLines.join(' '));
-};
-
-const download = (url, localPath) => axios({ url, method: 'GET', responseType: 'stream' })
- .then(response => new Promise((resolve, reject) => {
- const writer = fs.createWriteStream(localPath);
- writer.on('finish', resolve);
- writer.on('error', reject);
- response.data.pipe(writer);
- }));
-
-export {
- execute,
- createKey,
- isDirectory,
- listFiles,
- listDirectories,
- getDescription,
- download,
-};
diff --git a/src/common/webhook.js b/src/common/webhook.js
deleted file mode 100644
index 5f7599a..0000000
--- a/src/common/webhook.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import GithubWebHook from 'express-github-webhook';
-import { githubWebhookSecret } from '/environment';
-
-const webhook = GithubWebHook({ path: '/', secret: githubWebhookSecret });
-
-export default webhook;
diff --git a/src/config/constants.ts b/src/config/constants.ts
new file mode 100644
index 0000000..34f9787
--- /dev/null
+++ b/src/config/constants.ts
@@ -0,0 +1,2 @@
+export const memoryLimit = 256; // in megabytes
+export const timeLimit = 5000; // in milliseconds
diff --git a/src/config/environments.ts b/src/config/environments.ts
new file mode 100644
index 0000000..0a8ae31
--- /dev/null
+++ b/src/config/environments.ts
@@ -0,0 +1,73 @@
+import fs from 'fs';
+import { ServerOptions } from 'https';
+import path from 'path';
+
+require('dotenv-flow').config();
+
+declare var process: {
+ env: {
+ [key: string]: string,
+ }
+};
+
+const {
+ NODE_ENV,
+
+ HTTP_PORT,
+ HTTPS_PORT,
+
+ CREDENTIALS_ENABLED,
+ CREDENTIALS_PATH,
+ CREDENTIALS_CA,
+ CREDENTIALS_KEY,
+ CREDENTIALS_CERT,
+
+ WEBHOOK_ENABLED,
+ WEBHOOK_SECRET,
+
+ GITHUB_CLIENT_ID,
+ GITHUB_CLIENT_SECRET,
+} = process.env;
+
+const isEnabled = (v: string) => v === '1';
+
+const missingVars = [
+ 'NODE_ENV',
+ 'HTTP_PORT',
+ 'HTTPS_PORT',
+ 'CREDENTIALS_ENABLED',
+ ...(isEnabled(CREDENTIALS_ENABLED) ? [
+ 'CREDENTIALS_PATH',
+ 'CREDENTIALS_CA',
+ 'CREDENTIALS_KEY',
+ 'CREDENTIALS_CERT',
+ ] : []),
+ 'WEBHOOK_ENABLED',
+ ...(isEnabled(WEBHOOK_ENABLED) ? [
+ 'WEBHOOK_SECRET',
+ ] : []),
+ 'GITHUB_CLIENT_ID',
+ 'GITHUB_CLIENT_SECRET',
+].filter(variable => process.env[variable] === undefined);
+if (missingVars.length) throw new Error(`The following environment variables are missing: ${missingVars.join(', ')}`);
+
+export const __PROD__ = NODE_ENV === 'production';
+export const __DEV__ = NODE_ENV === 'development';
+
+export const httpPort = parseInt(HTTP_PORT);
+export const httpsPort = parseInt(HTTPS_PORT);
+
+export const webhookOptions = isEnabled(WEBHOOK_ENABLED) ? {
+ path: '/webhook',
+ secret: WEBHOOK_SECRET,
+} : undefined;
+
+const readCredentials = (file: string) => fs.readFileSync(path.resolve(CREDENTIALS_PATH, file));
+export const credentials: ServerOptions | undefined = isEnabled(CREDENTIALS_ENABLED) ? {
+ ca: readCredentials(CREDENTIALS_CA),
+ key: readCredentials(CREDENTIALS_KEY),
+ cert: readCredentials(CREDENTIALS_CERT),
+} : undefined;
+
+export const githubClientId = GITHUB_CLIENT_ID;
+export const githubClientSecret = GITHUB_CLIENT_SECRET;
diff --git a/src/config/paths.ts b/src/config/paths.ts
new file mode 100644
index 0000000..3d69c81
--- /dev/null
+++ b/src/config/paths.ts
@@ -0,0 +1,10 @@
+import path from 'path';
+
+export const rootDir = path.resolve(__dirname, '..', '..');
+export const publicDir = path.resolve(rootDir, 'public');
+export const algorithmsDir = path.resolve(publicDir, 'algorithms');
+export const codesDir = path.resolve(publicDir, 'codes');
+export const visualizationsDir = path.resolve(publicDir, 'visualizations');
+export const frontendDir = path.resolve(publicDir, 'frontend');
+export const frontendBuildDir = path.resolve(frontendDir, 'build');
+export const frontendBuiltDir = path.resolve(publicDir, 'frontend-built');
diff --git a/src/controllers/AlgorithmsController.ts b/src/controllers/AlgorithmsController.ts
new file mode 100644
index 0000000..3db320b
--- /dev/null
+++ b/src/controllers/AlgorithmsController.ts
@@ -0,0 +1,38 @@
+import express from 'express';
+import { Controller } from 'controllers/Controller';
+import { NotFound } from 'ts-httpexceptions';
+import Server from 'Server';
+
+export class AlgorithmsController extends Controller {
+ constructor(server: Server) {
+ super(server);
+ this.router
+ .get('/', this.getHierarchy)
+ .get('/:categoryKey/:algorithmKey', this.getAlgorithm)
+ .get('/sitemap.txt', this.getSitemap);
+ }
+
+ route = (router: express.Router): void => {
+ router.use('/algorithms', this.router);
+ };
+
+ getHierarchy = (req: express.Request, res: express.Response) => {
+ res.json(this.server.hierarchy);
+ };
+
+ getAlgorithm = (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ const {categoryKey, algorithmKey} = req.params;
+ const algorithm = this.server.hierarchy.find(categoryKey, algorithmKey);
+ if (!algorithm) return next(new NotFound('Algorithm not found.'));
+ res.json({algorithm});
+ };
+
+ getSitemap = (req: express.Request, res: express.Response) => {
+ const urls: string[] = [];
+ this.server.hierarchy.iterate((category, algorithm) => {
+ urls.push(`https://algorithm-visualizer.org/${category.key}/${algorithm.key}`);
+ });
+ res.set('Content-Type', 'text/plain');
+ res.send(urls.join('\n'));
+ };
+}
diff --git a/src/controllers/AuthController.ts b/src/controllers/AuthController.ts
new file mode 100644
index 0000000..74080ed
--- /dev/null
+++ b/src/controllers/AuthController.ts
@@ -0,0 +1,36 @@
+import express from 'express';
+import { githubClientId } from 'config/environments';
+import { GitHubApi } from 'utils/apis';
+import { Controller } from 'controllers/Controller';
+import Server from 'Server';
+
+export class AuthController extends Controller {
+ constructor(server: Server) {
+ super(server);
+ this.router
+ .get('/request', this.request)
+ .get('/response', this.response)
+ .get('/destroy', this.destroy);
+ }
+
+ route = (router: express.Router): void => {
+ router.use('/auth', this.router);
+ };
+
+ request = (req: express.Request, res: express.Response) => {
+ res.redirect(`https://github.com/login/oauth/authorize?client_id=${githubClientId}&scope=user,gist`);
+ };
+
+ response = (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ const {code} = req.query;
+
+ GitHubApi.getAccessToken(code).then(({data}) => {
+ const {access_token} = data;
+ res.send(``);
+ }).catch(next);
+ };
+
+ destroy = (req: express.Request, res: express.Response) => {
+ res.send(``);
+ };
+}
diff --git a/src/controllers/Controller.ts b/src/controllers/Controller.ts
new file mode 100644
index 0000000..9be63b6
--- /dev/null
+++ b/src/controllers/Controller.ts
@@ -0,0 +1,12 @@
+import express from "express";
+import Server from 'Server';
+
+export abstract class Controller {
+ protected readonly router: express.Router;
+
+ protected constructor(protected server: Server) {
+ this.router = express.Router();
+ }
+
+ abstract route(router: express.Router): void;
+}
diff --git a/src/controllers/TracersController.ts b/src/controllers/TracersController.ts
new file mode 100644
index 0000000..51a4070
--- /dev/null
+++ b/src/controllers/TracersController.ts
@@ -0,0 +1,14 @@
+import express from 'express';
+import { Controller } from 'controllers/Controller';
+import Server from 'Server';
+
+export class TracersController extends Controller {
+ constructor(server: Server) {
+ super(server);
+ this.server.tracers.forEach(tracer => tracer.route(this.router));
+ }
+
+ route = (router: express.Router): void => {
+ router.use('/tracers', this.router);
+ };
+}
diff --git a/src/controllers/VisualizationsController.ts b/src/controllers/VisualizationsController.ts
new file mode 100644
index 0000000..3b9cfa9
--- /dev/null
+++ b/src/controllers/VisualizationsController.ts
@@ -0,0 +1,41 @@
+import express from 'express';
+import path from 'path';
+import uuid from 'uuid';
+import fs from 'fs-extra';
+import { Controller } from 'controllers/Controller';
+import Server from 'Server';
+import { visualizationsDir } from 'config/paths';
+
+export class VisualizationsController extends Controller {
+ constructor(server: Server) {
+ super(server);
+ this.router
+ .post('/', this.uploadVisualization)
+ .get('/:visualizationId', this.getVisualization);
+
+ fs.remove(visualizationsDir).catch(console.error);
+ }
+
+ route = (router: express.Router): void => {
+ router.use('/visualizations', this.router);
+ };
+
+ uploadVisualization = (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ const {content} = req.body;
+ const visualizationId = uuid.v4();
+ const visualizationPath = path.resolve(visualizationsDir, `${visualizationId}.json`);
+ const url = `https://algorithm-visualizer.org/scratch-paper/new?visualizationId=${visualizationId}`;
+ fs.outputFile(visualizationPath, content)
+ .then(() => res.send(url))
+ .catch(next);
+ };
+
+ getVisualization = (req: express.Request, res: express.Response, next: express.NextFunction) => {
+ const {visualizationId} = req.params;
+ const visualizationPath = path.resolve(visualizationsDir, `${visualizationId}.json`);
+ res.sendFile(visualizationPath, err => {
+ if (err) next(new Error('Visualization Expired'));
+ fs.remove(visualizationPath);
+ });
+ };
+}
diff --git a/src/controllers/algorithms.js b/src/controllers/algorithms.js
deleted file mode 100644
index 6bac374..0000000
--- a/src/controllers/algorithms.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import express from 'express';
-import fs from 'fs-extra';
-import { execute } from '/common/util';
-import webhook from '/common/webhook';
-import hierarchy from '/common/hierarchy';
-import { NotFoundError } from '/common/error';
-
-const router = express.Router();
-
-const downloadCategories = () => (
- fs.pathExistsSync(hierarchy.path) ?
- execute(`git fetch && git reset --hard origin/master`, { cwd: hierarchy.path }) :
- execute(`git clone https://github.com/algorithm-visualizer/algorithms.git ${hierarchy.path}`)
-).then(() => hierarchy.refresh());
-
-downloadCategories().catch(console.error);
-
-webhook.on('algorithms', event => {
- switch (event) {
- case 'push':
- downloadCategories().catch(console.error);
- break;
- }
-});
-
-router.route('/')
- .get((req, res, next) => {
- res.json(hierarchy);
- });
-
-router.route('/:categoryKey/:algorithmKey')
- .get((req, res, next) => {
- const { categoryKey, algorithmKey } = req.params;
- const algorithm = hierarchy.find(categoryKey, algorithmKey);
- if (!algorithm) return next(new NotFoundError());
- res.json({ algorithm });
- });
-
-router.route('/sitemap.txt')
- .get((req, res, next) => {
- const urls = [];
- hierarchy.iterate((category, algorithm) => {
- urls.push(`https://algorithm-visualizer.org/${category.key}/${algorithm.key}`);
- });
- res.set('Content-Type', 'text/plain');
- res.send(urls.join('\n'));
- });
-
-export default router;
diff --git a/src/controllers/auth.js b/src/controllers/auth.js
deleted file mode 100644
index 45e156e..0000000
--- a/src/controllers/auth.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import express from 'express';
-import { githubClientId } from '/environment';
-import { GitHubApi } from '/apis';
-
-const router = express.Router();
-
-const request = (req, res, next) => {
- res.redirect(`https://github.com/login/oauth/authorize?client_id=${githubClientId}&scope=user,gist`);
-};
-
-const response = (req, res, next) => {
- const { code } = req.query;
-
- GitHubApi.getAccessToken(code).then(({ access_token }) => {
- res.send(``);
- }).catch(next);
-};
-
-const destroy = (req, res, next) => {
- res.send(``);
-};
-
-router.route('/request')
- .get(request);
-
-router.route('/response')
- .get(response);
-
-router.route('/destroy')
- .get(destroy);
-
-export default router;
diff --git a/src/controllers/index.js b/src/controllers/index.js
deleted file mode 100644
index a971818..0000000
--- a/src/controllers/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export { default as auth } from './auth';
-export { default as algorithms } from './algorithms';
-export { default as tracers } from './tracers';
-export { default as visualizations } from './visualizations';
diff --git a/src/controllers/index.ts b/src/controllers/index.ts
new file mode 100644
index 0000000..7d3ebf0
--- /dev/null
+++ b/src/controllers/index.ts
@@ -0,0 +1,4 @@
+export { AlgorithmsController } from './AlgorithmsController';
+export { AuthController } from './AuthController';
+export { TracersController } from './TracersController';
+export { VisualizationsController } from './VisualizationsController';
diff --git a/src/controllers/tracers.js b/src/controllers/tracers.js
deleted file mode 100644
index 04806e4..0000000
--- a/src/controllers/tracers.js
+++ /dev/null
@@ -1,81 +0,0 @@
-import express from 'express';
-import fs from 'fs-extra';
-import Promise from 'bluebird';
-import uuid from 'uuid';
-import path from 'path';
-import { GitHubApi } from '/apis';
-import { execute } from '/common/util';
-import webhook from '/common/webhook';
-import { ImageBuilder, WorkerBuilder } from '/tracers';
-import { memoryLimit, timeLimit } from '/common/config';
-
-const router = express.Router();
-
-const trace = lang => (req, res, next) => {
- const { code } = req.body;
- const tempPath = path.resolve(__dirname, '..', 'public', 'codes', uuid.v4());
- fs.outputFile(path.resolve(tempPath, `Main.${lang}`), code)
- .then(() => {
- const builder = builderMap[lang];
- const containerName = uuid.v4();
- let killed = false;
- const timer = setTimeout(() => {
- execute(`docker kill ${containerName}`).then(() => {
- killed = true;
- });
- }, timeLimit);
- return execute([
- 'docker run --rm',
- `--name=${containerName}`,
- '-w=/usr/visualization',
- `-v=${tempPath}:/usr/visualization:rw`,
- `-m=${memoryLimit}m`,
- '-e ALGORITHM_VISUALIZER=1',
- builder.imageName,
- ].join(' '), { stdout: null, stderr: null }).catch(error => {
- if (killed) throw new Error('Time Limit Exceeded');
- throw error;
- }).finally(() => clearTimeout(timer));
- })
- .then(() => new Promise((resolve, reject) => {
- const visualizationPath = path.resolve(tempPath, 'visualization.json');
- res.sendFile(visualizationPath, err => {
- if (err) return reject(new Error('Visualization Not Found'));
- resolve();
- });
- }))
- .catch(next)
- .finally(() => fs.remove(tempPath));
-};
-
-const builderMap = {
- js: new WorkerBuilder(),
- cpp: new ImageBuilder('cpp'),
- java: new ImageBuilder('java'),
-};
-
-Promise.map(Object.keys(builderMap), lang => {
- const builder = builderMap[lang];
- return GitHubApi.getLatestRelease('algorithm-visualizer', `tracers.${lang}`).then(builder.build);
-}).catch(console.error);
-
-webhook.on('release', (repo, data) => {
- const result = /^tracers\.(\w+)$/.exec(repo);
- if (result) {
- const [, lang] = result;
- const builder = builderMap[lang];
- builder.build(data.release).catch(console.error);
- }
-});
-
-Object.keys(builderMap).forEach(lang => {
- const builder = builderMap[lang];
- if (builder instanceof ImageBuilder) {
- router.post(`/${lang}`, trace(lang));
- } else if (builder instanceof WorkerBuilder) {
- router.get(`/${lang}`, (req, res) => res.sendFile(builder.tracerPath));
- router.get(`/${lang}/worker`, (req, res) => res.sendFile(builder.workerPath));
- }
-});
-
-export default router;
diff --git a/src/controllers/visualizations.js b/src/controllers/visualizations.js
deleted file mode 100644
index 968f78a..0000000
--- a/src/controllers/visualizations.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import express from 'express';
-import path from 'path';
-import uuid from 'uuid';
-import fs from 'fs-extra';
-import Promise from 'bluebird';
-
-const router = express.Router();
-
-const uploadPath = path.resolve(__dirname, '..', 'public', 'visualizations');
-const getVisualizationPath = visualizationId => path.resolve(uploadPath, `${visualizationId}.json`);
-
-fs.remove(uploadPath).catch(console.error);
-
-const uploadVisualization = (req, res, next) => {
- const { content } = req.body;
- const visualizationId = uuid.v4();
- const tracesPath = getVisualizationPath(visualizationId);
- const url = `https://algorithm-visualizer.org/scratch-paper/new?visualizationId=${visualizationId}`;
- fs.outputFile(tracesPath, content)
- .then(() => res.send(url))
- .catch(next);
-};
-
-const getVisualization = (req, res, next) => {
- const { visualizationId } = req.params;
- const visualizationPath = getVisualizationPath(visualizationId);
- new Promise((resolve, reject) => {
- res.sendFile(visualizationPath, err => {
- if (err) return reject(new Error('Visualization Expired'));
- resolve();
- });
- }).catch(next)
- .finally(() => fs.remove(visualizationPath));
-};
-
-router.route('/')
- .post(uploadVisualization);
-
-router.route('/:visualizationId')
- .get(getVisualization);
-
-export default router;
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index 3f9df62..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import express from 'express';
-import morgan from 'morgan';
-import cookieParser from 'cookie-parser';
-import bodyParser from 'body-parser';
-import * as controllers from '/controllers';
-import { ClientError, ForbiddenError, NotFoundError, UnauthorizedError } from '/common/error';
-import webhook from '/common/webhook';
-import hierarchy from '/common/hierarchy';
-
-const app = express();
-app.use(morgan('tiny'));
-app.use(cookieParser());
-app.use(bodyParser.json());
-app.use(bodyParser.urlencoded({ extended: true }));
-Object.keys(controllers).forEach(key => app.use(`/${key}`, controllers[key]));
-app.use('/webhook', webhook);
-app.use((req, res, next) => next(new NotFoundError()));
-app.use((err, req, res, next) => {
- const statusMap = [
- [UnauthorizedError, 401],
- [ForbiddenError, 403],
- [NotFoundError, 404],
- [ClientError, 400],
- [Error, 500],
- ];
- const [, status] = statusMap.find(([Error]) => err instanceof Error);
- res.status(status);
- res.send(err.message);
- console.error(err);
-});
-app.hierarchy = hierarchy;
-
-webhook.on('algorithm-visualizer', event => {
- switch (event) {
- case 'push':
- process.exit(0);
- break;
- }
-});
-
-export default app;
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..329043e
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,3 @@
+import Server from 'Server';
+
+new Server().start();
diff --git a/src/middlewares/errorHandlerMiddleware.ts b/src/middlewares/errorHandlerMiddleware.ts
new file mode 100644
index 0000000..8a1936a
--- /dev/null
+++ b/src/middlewares/errorHandlerMiddleware.ts
@@ -0,0 +1,14 @@
+import { NextFunction, Request, Response } from 'express';
+import { Exception, InternalServerError } from 'ts-httpexceptions';
+
+export function errorHandlerMiddleware() {
+ return (err: any, req: Request, res: Response, next: NextFunction) => {
+ if (!(err instanceof Exception)) {
+ console.error(err);
+ err = new InternalServerError(err.message, err);
+ }
+
+ const {name, message, status} = err;
+ res.status(status).json({name, message, status});
+ };
+}
diff --git a/src/middlewares/frontendMiddleware.ts b/src/middlewares/frontendMiddleware.ts
new file mode 100644
index 0000000..d793b1c
--- /dev/null
+++ b/src/middlewares/frontendMiddleware.ts
@@ -0,0 +1,51 @@
+import express, { NextFunction, Request, Response } from 'express';
+import path from 'path';
+import fs from 'fs-extra';
+import url from 'url';
+import Server from 'Server';
+import { frontendBuiltDir } from 'config/paths';
+
+const packageJson = require('../../package.json');
+
+export function frontendMiddleware(server: Server) {
+ const staticMiddleware = express.static(frontendBuiltDir, {index: false});
+
+ if (!fs.pathExistsSync(frontendBuiltDir)) {
+ server.updateFrontend().catch(console.error);
+ }
+
+ return (req: Request, res: Response, next: NextFunction) => {
+ staticMiddleware(req, res, err => {
+ if (err) return next(err);
+ if (req.method !== 'GET') return next();
+
+ const filePath = path.resolve(frontendBuiltDir, 'index.html');
+ fs.readFile(filePath, 'utf8', (err, data) => {
+ if (err) return next(err);
+
+ const {pathname} = url.parse(req.originalUrl);
+ if (!pathname) return next(new Error('Failed to get `pathname`.'));
+ const [, categoryKey, algorithmKey] = pathname.split('/');
+ let {title, description} = packageJson;
+ let algorithm = undefined;
+ if (categoryKey && categoryKey !== 'scratch-paper') {
+ algorithm = server.hierarchy.find(categoryKey, algorithmKey) || null;
+ if (algorithm) {
+ title = [algorithm.categoryName, algorithm.algorithmName].join(' - ');
+ description = algorithm.description;
+ } else {
+ res.status(404);
+ return;
+ }
+ }
+
+ const indexFile = data
+ .replace(/\$TITLE/g, title)
+ .replace(/\$DESCRIPTION/g, description)
+ .replace(/\$ALGORITHM/g, algorithm === undefined ? 'undefined' :
+ JSON.stringify(algorithm).replace(/ {
+ if (req.hostname === 'algo-visualizer.jasonpark.me') {
+ res.redirect(301, 'https://algorithm-visualizer.org/');
+ } else if (credentials && !req.secure) {
+ res.redirect(301, `https://${req.hostname}${req.url}`);
+ } else {
+ next();
+ }
+ };
+}
diff --git a/src/models/Algorithm.js b/src/models/Algorithm.js
deleted file mode 100644
index 45dfde2..0000000
--- a/src/models/Algorithm.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import path from 'path';
-import { createKey, getDescription, listFiles } from '/common/util';
-import { File } from '/models';
-
-class Algorithm {
- constructor(path, name) {
- this.path = path;
- this.key = createKey(name);
- this.name = name;
- this.refresh();
- }
-
- refresh() {
- this.files = listFiles(this.path)
- .map(fileName => new File(path.resolve(this.path, fileName), fileName));
- this.description = getDescription(this.files);
- }
-
- toJSON() {
- const { key, name } = this;
- return { key, name };
- }
-}
-
-export default Algorithm;
diff --git a/src/models/Algorithm.ts b/src/models/Algorithm.ts
new file mode 100644
index 0000000..d886799
--- /dev/null
+++ b/src/models/Algorithm.ts
@@ -0,0 +1,26 @@
+import path from 'path';
+import { createKey, listFiles } from 'utils/hierarchy';
+import { File } from 'models';
+import { getDescription } from 'utils/misc';
+
+export class Algorithm {
+ key: string;
+ files!: File[];
+ description!: string;
+
+ constructor(private path: string, public name: string) {
+ this.key = createKey(name);
+ this.refresh();
+ }
+
+ refresh() {
+ this.files = listFiles(this.path)
+ .map(fileName => new File(path.resolve(this.path, fileName), fileName));
+ this.description = getDescription(this.files);
+ }
+
+ toJSON() {
+ const {key, name} = this;
+ return {key, name};
+ }
+}
diff --git a/src/models/Category.js b/src/models/Category.js
deleted file mode 100644
index 03ea980..0000000
--- a/src/models/Category.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import path from 'path';
-import { createKey, listDirectories } from '/common/util';
-import { Algorithm } from '/models';
-
-class Category {
- constructor(path, name) {
- this.path = path;
- this.key = createKey(name);
- this.name = name;
- this.refresh();
- }
-
- refresh() {
- this.algorithms = listDirectories(this.path)
- .map(algorithmName => new Algorithm(path.resolve(this.path, algorithmName), algorithmName));
- }
-
- toJSON() {
- const { key, name, algorithms } = this;
- return { key, name, algorithms };
- }
-}
-
-export default Category;
diff --git a/src/models/Category.ts b/src/models/Category.ts
new file mode 100644
index 0000000..52c9048
--- /dev/null
+++ b/src/models/Category.ts
@@ -0,0 +1,25 @@
+import path from 'path';
+import { createKey, listDirectories } from 'utils/hierarchy';
+import { Algorithm } from 'models';
+
+export class Category {
+ key: string;
+ algorithms!: Algorithm[];
+
+ constructor(private path: string, public name: string) {
+ this.key = createKey(name);
+ this.refresh();
+ }
+
+ refresh() {
+ this.algorithms = listDirectories(this.path)
+ .map(algorithmName => new Algorithm(path.resolve(this.path, algorithmName), algorithmName));
+ }
+
+ toJSON() {
+ const {key, name, algorithms} = this;
+ return {key, name, algorithms};
+ }
+}
+
+export default Category;
diff --git a/src/models/File.js b/src/models/File.js
deleted file mode 100644
index 3bb69da..0000000
--- a/src/models/File.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import fs from 'fs-extra';
-
-class File {
- constructor(path, name) {
- this.path = path;
- this.name = name;
- this.refresh();
- }
-
- refresh() {
- this.content = fs.readFileSync(this.path, 'utf-8');
- this.contributors = [];
- }
-
- toJSON() {
- const { name, content, contributors } = this;
- return { name, content, contributors };
- }
-}
-
-export default File;
diff --git a/src/models/File.ts b/src/models/File.ts
new file mode 100644
index 0000000..d97aa26
--- /dev/null
+++ b/src/models/File.ts
@@ -0,0 +1,25 @@
+import fs from 'fs-extra';
+
+export type Author = {
+ login: string,
+ avatar_url: string,
+}
+
+export class File {
+ content!: string;
+ contributors!: Author[];
+
+ constructor(public path: string, public name: string) {
+ this.refresh();
+ }
+
+ refresh() {
+ this.content = fs.readFileSync(this.path, 'utf-8');
+ this.contributors = [];
+ }
+
+ toJSON() {
+ const {name, content, contributors} = this;
+ return {name, content, contributors};
+ }
+}
diff --git a/src/models/Hierarchy.js b/src/models/Hierarchy.js
deleted file mode 100644
index 589910e..0000000
--- a/src/models/Hierarchy.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import Promise from 'bluebird';
-import path from 'path';
-import { execute, listDirectories } from '/common/util';
-import { GitHubApi } from '/apis';
-import { Category } from '/models';
-
-class Hierarchy {
- constructor(path) {
- this.path = path;
- this.refresh();
- }
-
- refresh() {
- this.categories = listDirectories(this.path)
- .map(categoryName => new Category(path.resolve(this.path, categoryName), categoryName));
-
- const files = [];
- this.categories.forEach(category => category.algorithms.forEach(algorithm => files.push(...algorithm.files)));
- this.cacheCommitAuthors().then(commitAuthors => this.cacheContributors(files, commitAuthors));
- }
-
- cacheCommitAuthors(page = 1, commitAuthors = {}) {
- const per_page = 100;
- return GitHubApi.listCommits('algorithm-visualizer', 'algorithms', {
- per_page,
- page,
- }).then(commits => {
- commits.forEach(({ sha, commit, author }) => {
- if (!author) return;
- const { login, avatar_url } = author;
- commitAuthors[sha] = { login, avatar_url };
- });
- if (commits.length < per_page) {
- return commitAuthors;
- } else {
- return this.cacheCommitAuthors(page + 1, commitAuthors);
- }
- });
- }
-
- cacheContributors(files, commitAuthors) {
- return Promise.each(files, file => {
- return execute(`git --no-pager log --follow --no-merges --format="%H" "${file.path}"`, {
- cwd: this.path, stdout: null,
- }).then(stdout => {
- const output = stdout.toString().replace(/\n$/, '');
- const shas = output.split('\n').reverse();
- const contributors = [];
- for (const sha of shas) {
- const author = commitAuthors[sha];
- if (author && !contributors.find(contributor => contributor.login === author.login)) {
- contributors.push(author);
- }
- }
- file.contributors = contributors;
- });
- });
- }
-
- find(categoryKey, algorithmKey) {
- const category = this.categories.find(category => category.key === categoryKey);
- if (!category) return;
- const algorithm = category.algorithms.find(algorithm => algorithm.key === algorithmKey);
- if (!algorithm) return;
-
- const categoryName = category.name;
- const algorithmName = algorithm.name;
- const files = algorithm.files;
- const description = algorithm.description;
-
- return { categoryKey, categoryName, algorithmKey, algorithmName, files, description };
- }
-
- iterate(callback) {
- this.categories.forEach(category => category.algorithms.forEach(algorithm => callback(category, algorithm)));
- }
-
- toJSON() {
- const { categories } = this;
- return { categories };
- }
-}
-
-export default Hierarchy;
diff --git a/src/models/Hierarchy.ts b/src/models/Hierarchy.ts
new file mode 100644
index 0000000..c5d6ff1
--- /dev/null
+++ b/src/models/Hierarchy.ts
@@ -0,0 +1,93 @@
+import path from 'path';
+import { listDirectories } from 'utils/hierarchy';
+import { GitHubApi } from 'utils/apis';
+import { Algorithm, Category, File } from 'models';
+import { Author } from 'models/File';
+import { algorithmsDir } from 'config/paths';
+import { pull } from 'utils/misc';
+import execa from 'execa';
+
+type CommitAuthors = {
+ [sha: string]: Author,
+}
+
+export class Hierarchy {
+ private categories!: Category[];
+ readonly path: string = algorithmsDir;
+
+ constructor() {
+ this.refresh();
+ this.update().catch(console.error);
+ }
+
+ refresh() {
+ this.categories = listDirectories(this.path)
+ .map(categoryName => new Category(path.resolve(this.path, categoryName), categoryName));
+
+ const files: File[] = [];
+ this.categories.forEach(category => category.algorithms.forEach(algorithm => files.push(...algorithm.files)));
+ this.cacheCommitAuthors().then(commitAuthors => this.cacheContributors(files, commitAuthors));
+ }
+
+ async update(commit?: string) {
+ await pull(this.path, 'algorithms', commit);
+ this.refresh();
+ };
+
+ async cacheCommitAuthors(page = 1, commitAuthors: CommitAuthors = {}): Promise {
+ const per_page = 100;
+ const {data} = await GitHubApi.listCommits('algorithm-visualizer', 'algorithms', {per_page, page});
+ const commits: any[] = data;
+ for (const {sha, author} of commits) {
+ if (!author) continue;
+ const {login, avatar_url} = author;
+ commitAuthors[sha] = {login, avatar_url};
+ }
+ if (commits.length < per_page) {
+ return commitAuthors;
+ } else {
+ return this.cacheCommitAuthors(page + 1, commitAuthors);
+ }
+ }
+
+ async cacheContributors(files: File[], commitAuthors: CommitAuthors) {
+ for (const file of files) {
+ const stdout = await execa('git', ['--no-pager', 'log', '--follow', '--no-merges', '--format="%H"', '--', `"${file.path}"`], {
+ cwd: this.path,
+ });
+ const output = stdout.toString().replace(/\n$/, '');
+ const shas = output.split('\n').reverse();
+ const contributors: Author[] = [];
+ for (const sha of shas) {
+ const author = commitAuthors[sha];
+ if (author && !contributors.find(contributor => contributor.login === author.login)) {
+ contributors.push(author);
+ }
+ }
+ file.contributors = contributors;
+ }
+ }
+
+ find(categoryKey: string, algorithmKey: string) {
+ const category = this.categories.find(category => category.key === categoryKey);
+ if (!category) return;
+ const algorithm = category.algorithms.find(algorithm => algorithm.key === algorithmKey);
+ if (!algorithm) return;
+
+ const categoryName = category.name;
+ const algorithmName = algorithm.name;
+ const files = algorithm.files;
+ const description = algorithm.description;
+
+ return {categoryKey, categoryName, algorithmKey, algorithmName, files, description};
+ }
+
+ iterate(callback: (category: Category, algorithm: Algorithm) => void) {
+ this.categories.forEach(category => category.algorithms.forEach(algorithm => callback(category, algorithm)));
+ }
+
+ toJSON() {
+ const {categories} = this;
+ return {categories};
+ }
+}
diff --git a/src/models/index.js b/src/models/index.js
deleted file mode 100644
index 64dea13..0000000
--- a/src/models/index.js
+++ /dev/null
@@ -1,4 +0,0 @@
-export { default as Algorithm } from './Algorithm';
-export { default as Category } from './Category';
-export { default as File } from './File';
-export { default as Hierarchy } from './Hierarchy';
diff --git a/src/models/index.ts b/src/models/index.ts
new file mode 100644
index 0000000..4ace972
--- /dev/null
+++ b/src/models/index.ts
@@ -0,0 +1,4 @@
+export { Algorithm } from './Algorithm';
+export { Category } from './Category';
+export { File } from './File';
+export { Hierarchy } from './Hierarchy';
diff --git a/src/tracers/DockerTracer.ts b/src/tracers/DockerTracer.ts
new file mode 100644
index 0000000..ae40663
--- /dev/null
+++ b/src/tracers/DockerTracer.ts
@@ -0,0 +1,64 @@
+import path from 'path';
+import { Release, Tracer } from 'tracers/Tracer';
+import express from 'express';
+import uuid from 'uuid';
+import fs from 'fs-extra';
+import { memoryLimit, timeLimit } from 'config/constants';
+import { codesDir } from 'config/paths';
+import execa = require('execa');
+
+export class DockerTracer extends Tracer {
+ private readonly directory: string;
+ private readonly imageName: string;
+
+ constructor(lang: string) {
+ super(lang);
+ this.directory = path.resolve(__dirname, lang);
+ this.imageName = `tracer-${this.lang}`;
+
+ this.build = this.build.bind(this);
+ }
+
+ build(release: Release) {
+ const {tag_name} = release;
+ return execa('docker', ['build', '-t', this.imageName, '.', '--build-arg', `tag_name=${tag_name}`], {cwd: this.directory});
+ }
+
+ route(router: express.Router) {
+ router.post(`/${this.lang}`, (req, res, next) => {
+ const {code} = req.body;
+ const tempPath = path.resolve(codesDir, uuid.v4());
+ fs.outputFile(path.resolve(tempPath, `Main.${this.lang}`), code)
+ .then(() => {
+ const containerName = uuid.v4();
+ let killed = false;
+ const timer = setTimeout(() => {
+ execa('docker', ['kill', containerName]).then(() => {
+ killed = true;
+ });
+ }, timeLimit);
+ return execa('docker', [
+ 'run', '--rm',
+ `--name=${containerName}`,
+ '-w=/usr/visualization',
+ `-v=${tempPath}:/usr/visualization:rw`,
+ `-m=${memoryLimit}m`,
+ '-e', 'ALGORITHM_VISUALIZER=1',
+ this.imageName,
+ ]).catch(error => {
+ if (killed) throw new Error('Time Limit Exceeded');
+ throw error;
+ }).finally(() => clearTimeout(timer));
+ })
+ .then(() => new Promise((resolve, reject) => {
+ const visualizationPath = path.resolve(tempPath, 'visualization.json');
+ res.sendFile(visualizationPath, (err: any) => {
+ if (err) return reject(new Error('Visualization Not Found'));
+ resolve();
+ });
+ }))
+ .catch(next)
+ .finally(() => fs.remove(tempPath));
+ });
+ }
+}
diff --git a/src/tracers/ImageBuilder.js b/src/tracers/ImageBuilder.js
deleted file mode 100644
index e25d7c1..0000000
--- a/src/tracers/ImageBuilder.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import path from 'path';
-import { execute } from '/common/util';
-
-class ImageBuilder {
- constructor(lang) {
- this.lang = lang;
- this.directory = path.resolve(__dirname, lang);
- this.imageName = `tracer-${this.lang}`;
-
- this.build = this.build.bind(this);
- }
-
- build(release) {
- const { tag_name } = release;
- return execute(`docker build -t ${this.imageName} . --build-arg tag_name=${tag_name}`, { cwd: this.directory });
- }
-}
-
-export default ImageBuilder;
diff --git a/src/tracers/Tracer.ts b/src/tracers/Tracer.ts
new file mode 100644
index 0000000..400afe2
--- /dev/null
+++ b/src/tracers/Tracer.ts
@@ -0,0 +1,24 @@
+import express from 'express';
+import { GitHubApi } from 'utils/apis';
+
+export type Release = {
+ tag_name: string;
+}
+
+export abstract class Tracer {
+ protected constructor(public lang: string) {
+ this.update().catch(console.error);
+ }
+
+ abstract build(release: Release): Promise;
+
+ abstract route(router: express.Router): void;
+
+ async update(release?: Release) {
+ if (release) {
+ return this.build(release);
+ }
+ const {data} = await GitHubApi.getLatestRelease('algorithm-visualizer', `tracers.${this.lang}`);
+ return this.build(data);
+ }
+}
diff --git a/src/tracers/WorkerBuilder.js b/src/tracers/WorkerBuilder.js
deleted file mode 100644
index 07369cd..0000000
--- a/src/tracers/WorkerBuilder.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import path from 'path';
-import { download } from '/common/util';
-
-class WorkerBuilder {
- constructor() {
- this.tracerPath = path.resolve(__dirname, '..', 'public', 'algorithm-visualizer.js');
- this.workerPath = path.resolve(__dirname, 'js', 'worker.js');
-
- this.build = this.build.bind(this);
- }
-
- build(release) {
- const { tag_name } = release;
- return download(`https://github.com/algorithm-visualizer/tracers.js/releases/download/${tag_name}/algorithm-visualizer.js`, this.tracerPath);
- }
-}
-
-export default WorkerBuilder;
diff --git a/src/tracers/cpp/CppTracer.ts b/src/tracers/cpp/CppTracer.ts
new file mode 100644
index 0000000..a2bc807
--- /dev/null
+++ b/src/tracers/cpp/CppTracer.ts
@@ -0,0 +1,7 @@
+import { DockerTracer } from 'tracers/DockerTracer';
+
+export class CppTracer extends DockerTracer {
+ constructor() {
+ super('cpp');
+ }
+}
diff --git a/src/tracers/index.js b/src/tracers/index.js
deleted file mode 100644
index d41b049..0000000
--- a/src/tracers/index.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export { default as ImageBuilder } from './ImageBuilder';
-export { default as WorkerBuilder } from './WorkerBuilder';
diff --git a/src/tracers/index.ts b/src/tracers/index.ts
new file mode 100644
index 0000000..ae5ad72
--- /dev/null
+++ b/src/tracers/index.ts
@@ -0,0 +1,3 @@
+export { CppTracer } from './cpp/CppTracer';
+export { JavaTracer } from './java/JavaTracer';
+export { JsTracer } from './js/JsTracer';
diff --git a/src/tracers/java/JavaTracer.ts b/src/tracers/java/JavaTracer.ts
new file mode 100644
index 0000000..043c3c6
--- /dev/null
+++ b/src/tracers/java/JavaTracer.ts
@@ -0,0 +1,7 @@
+import { DockerTracer } from 'tracers/DockerTracer';
+
+export class JavaTracer extends DockerTracer {
+ constructor() {
+ super('java');
+ }
+}
diff --git a/src/tracers/js/JsTracer.ts b/src/tracers/js/JsTracer.ts
new file mode 100644
index 0000000..b253a92
--- /dev/null
+++ b/src/tracers/js/JsTracer.ts
@@ -0,0 +1,26 @@
+import path from 'path';
+import { download } from 'utils/misc';
+import { Release, Tracer } from 'tracers/Tracer';
+import express from 'express';
+import { publicDir } from 'config/paths';
+
+export class JsTracer extends Tracer {
+ readonly tracerPath: string;
+ readonly workerPath: string;
+
+ constructor() {
+ super('js');
+ this.tracerPath = path.resolve(publicDir, 'algorithm-visualizer.js');
+ this.workerPath = path.resolve(__dirname, 'worker.js');
+ }
+
+ build(release: Release) {
+ const {tag_name} = release;
+ return download(`https://github.com/algorithm-visualizer/tracers.js/releases/download/${tag_name}/algorithm-visualizer.js`, this.tracerPath);
+ }
+
+ route(router: express.Router) {
+ router.get(`/${this.lang}`, (req, res) => res.sendFile(this.tracerPath));
+ router.get(`/${this.lang}/worker`, (req, res) => res.sendFile(this.workerPath));
+ }
+}
diff --git a/src/utils/apis.ts b/src/utils/apis.ts
new file mode 100644
index 0000000..73355d8
--- /dev/null
+++ b/src/utils/apis.ts
@@ -0,0 +1,61 @@
+import axios, { AxiosResponse } from 'axios';
+import { githubClientId, githubClientSecret } from 'config/environments';
+
+const instance = axios.create();
+
+instance.interceptors.request.use(request => {
+ request.params = {client_id: githubClientId, client_secret: githubClientSecret, ...request.params};
+ return request;
+});
+
+const request = (url: string, process: (mappedUrl: string, args: any[]) => Promise>) => {
+ const tokens = url.split('/');
+ const baseURL = /^https?:\/\//i.test(url) ? '' : 'https://api.github.com';
+ return (...args: any[]) => {
+ const mappedUrl = baseURL + tokens.map(token => token.startsWith(':') ? args.shift() : token).join('/');
+ return process(mappedUrl, args);
+ };
+};
+
+const GET = (url: string) => {
+ return request(url, (mappedUrl: string, args: any[]) => {
+ const [params] = args;
+ return instance.get(mappedUrl, {params});
+ });
+};
+
+const DELETE = (url: string) => {
+ return request(url, (mappedUrl: string, args: any[]) => {
+ const [params] = args;
+ return instance.delete(mappedUrl, {params});
+ });
+};
+
+const POST = (url: string) => {
+ return request(url, (mappedUrl: string, args: any[]) => {
+ const [body, params] = args;
+ return instance.post(mappedUrl, body, {params});
+ });
+};
+
+const PUT = (url: string) => {
+ return request(url, (mappedUrl: string, args: any[]) => {
+ const [body, params] = args;
+ return instance.put(mappedUrl, body, {params});
+ });
+};
+
+const PATCH = (url: string) => {
+ return request(url, (mappedUrl: string, args: any[]) => {
+ const [body, params] = args;
+ return instance.patch(mappedUrl, body, {params});
+ });
+};
+
+export const GitHubApi = {
+ listCommits: GET('/repos/:owner/:repo/commits'),
+
+ getAccessToken: (code: string) => instance.post('https://github.com/login/oauth/access_token', {code}, {headers: {Accept: 'application/json'}}),
+
+ getLatestRelease: GET('/repos/:owner/:repo/releases/latest'),
+};
diff --git a/src/utils/hierarchy.ts b/src/utils/hierarchy.ts
new file mode 100644
index 0000000..50baa82
--- /dev/null
+++ b/src/utils/hierarchy.ts
@@ -0,0 +1,18 @@
+import path from 'path';
+import fs from 'fs-extra';
+
+export function createKey(name: string) {
+ return name.toLowerCase().trim().replace(/[^\w \-]/g, '').replace(/ /g, '-');
+}
+
+export function isDirectory(dirPath: string) {
+ return fs.lstatSync(dirPath).isDirectory();
+}
+
+export function listFiles(dirPath: string) {
+ return fs.pathExistsSync(dirPath) ? fs.readdirSync(dirPath).filter(fileName => !fileName.startsWith('.')) : [];
+}
+
+export function listDirectories(dirPath: string) {
+ return listFiles(dirPath).filter(fileName => isDirectory(path.resolve(dirPath, fileName)));
+}
diff --git a/src/utils/misc.ts b/src/utils/misc.ts
new file mode 100644
index 0000000..ed4303e
--- /dev/null
+++ b/src/utils/misc.ts
@@ -0,0 +1,39 @@
+import axios from 'axios';
+import fs from 'fs-extra';
+import execa from 'execa';
+import { File } from 'models';
+import removeMarkdown from 'remove-markdown';
+
+export function download(url: string, localPath: string) {
+ return axios({url, method: 'GET', responseType: 'stream'})
+ .then(response => new Promise((resolve, reject) => {
+ const writer = fs.createWriteStream(localPath);
+ writer.on('finish', resolve);
+ writer.on('error', reject);
+ response.data.pipe(writer);
+ }));
+}
+
+export async function pull(dir: string, repo: string, commit = 'origin/master') {
+ if (fs.pathExistsSync(dir)) {
+ await execa.shell(`git fetch`, {cwd: dir});
+ } else {
+ await execa.shell(`git clone https://github.com/algorithm-visualizer/${repo}.git ${dir}`);
+ }
+ await execa.shell(`git reset --hard ${commit}`, {cwd: dir});
+}
+
+export function getDescription(files: File[]) {
+ const readmeFile = files.find(file => file.name === 'README.md');
+ if (!readmeFile) return '';
+ const lines = readmeFile.content.split('\n');
+ lines.shift();
+ while (lines.length && !lines[0].trim()) lines.shift();
+ let descriptionLines = [];
+ while (lines.length && lines[0].trim()) descriptionLines.push(lines.shift());
+ return removeMarkdown(descriptionLines.join(' '));
+}
+
+export function rethrow(err: any) {
+ throw err;
+}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..eaedd03
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,22 @@
+{
+ "compilerOptions": {
+ "target": "es2015",
+ "lib": [
+ "es2015",
+ "esnext"
+ ],
+ "module": "commonjs",
+ "moduleResolution": "node",
+ "esModuleInterop": true,
+ "experimentalDecorators": true,
+ "emitDecoratorMetadata": true,
+ "sourceMap": true,
+ "declaration": false,
+ "strict": true,
+ "baseUrl": "src",
+ "resolveJsonModule": true
+ },
+ "exclude": [
+ "node_modules",
+ ]
+}
diff --git a/tslint.json b/tslint.json
new file mode 100644
index 0000000..655d091
--- /dev/null
+++ b/tslint.json
@@ -0,0 +1,61 @@
+{
+ "rules": {
+ "class-name": true,
+ "comment-format": [
+ true,
+ "check-space"
+ ],
+ "indent": [
+ true,
+ "spaces",
+ 2
+ ],
+ "one-line": [
+ true,
+ "check-open-brace",
+ "check-whitespace"
+ ],
+ "no-var-keyword": true,
+ "quotemark": [
+ true,
+ "single",
+ "avoid-escape"
+ ],
+ "semicolon": [
+ true,
+ "always",
+ "ignore-bound-class-methods"
+ ],
+ "whitespace": [
+ true,
+ "check-branch",
+ "check-decl",
+ "check-operator",
+ "check-module",
+ "check-separator",
+ "check-type"
+ ],
+ "typedef-whitespace": [
+ true,
+ {
+ "call-signature": "nospace",
+ "index-signature": "nospace",
+ "parameter": "nospace",
+ "property-declaration": "nospace",
+ "variable-declaration": "nospace"
+ },
+ {
+ "call-signature": "onespace",
+ "index-signature": "onespace",
+ "parameter": "onespace",
+ "property-declaration": "onespace",
+ "variable-declaration": "onespace"
+ }
+ ],
+ "no-internal-module": true,
+ "no-trailing-whitespace": true,
+ "no-null-keyword": true,
+ "prefer-const": true,
+ "jsdoc-format": true
+ }
+}
diff --git a/webpack.backend.config.js b/webpack.backend.config.js
deleted file mode 100644
index 5185c70..0000000
--- a/webpack.backend.config.js
+++ /dev/null
@@ -1,45 +0,0 @@
-const CleanWebpackPlugin = require('clean-webpack-plugin');
-const nodeExternals = require('webpack-node-externals');
-const path = require('path');
-const fs = require('fs');
-
-const {
- __DEV__,
- backendBuildPath: buildPath,
- backendSrcPath: srcPath,
-} = require('./environment');
-
-const alias = {
- '/environment': path.resolve(__dirname, 'environment.js'),
-};
-fs.readdirSync(srcPath).forEach(name => {
- alias['/' + name] = path.resolve(srcPath, name);
-});
-
-module.exports = {
- target: 'node',
- node: {
- __dirname: true,
- },
- entry: srcPath,
- externals: [nodeExternals()],
- resolve: {
- modules: [srcPath],
- extensions: ['.js'],
- alias,
- },
- output: {
- path: buildPath,
- filename: 'index.js',
- libraryTarget: 'umd',
- },
- module: {
- rules: [
- { test: /\.js$/, use: 'babel-loader', include: srcPath },
- ],
- },
- plugins: [
- new CleanWebpackPlugin([buildPath]),
- ],
- mode: __DEV__ ? 'development' : 'production',
-};
\ No newline at end of file