Compare commits
No commits in common. 'master' and 'main' have entirely different histories.
@ -1,2 +0,0 @@
|
|||||||
CREDENTIALS_ENABLED = 0
|
|
||||||
WEBHOOK_ENABLED = 0
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
CREDENTIALS_ENABLED = 0
|
|
||||||
WEBHOOK_ENABLED = 0
|
|
||||||
@ -1,5 +0,0 @@
|
|||||||
# Optional: fill for GitHub auth / AWS when running `npm run start` (NODE_ENV=production).
|
|
||||||
GITHUB_CLIENT_ID=
|
|
||||||
GITHUB_CLIENT_SECRET=
|
|
||||||
AWS_ACCESS_KEY_ID=
|
|
||||||
AWS_SECRET_ACCESS_KEY=
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
# WebStorm settings
|
|
||||||
/.idea
|
|
||||||
|
|
||||||
# npm
|
|
||||||
/node_modules
|
|
||||||
/npm-debug.log
|
|
||||||
|
|
||||||
# macOS
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# downloaded files
|
|
||||||
/public
|
|
||||||
@ -1,54 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
> #### Table of Contents
|
|
||||||
> - [Running Locally](#running-locally)
|
|
||||||
> - [Directory Structure](#directory-structure)
|
|
||||||
|
|
||||||
Are you a first-timer in contributing to open source? [These guidelines](https://opensource.guide/how-to-contribute/#how-to-submit-a-contribution) from GitHub might help!
|
|
||||||
|
|
||||||
## Running Locally
|
|
||||||
|
|
||||||
1. Fork this repository.
|
|
||||||
|
|
||||||
2. Clone your forked repo to your machine.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/<your-username>/server.git
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Install [Docker](https://docs.docker.com/install/), if not done already.
|
|
||||||
|
|
||||||
4. Create `.env.local` in the project root:
|
|
||||||
```bash
|
|
||||||
# By putting dummy values, GitHub sign in will not work locally
|
|
||||||
GITHUB_CLIENT_ID = dummy
|
|
||||||
GITHUB_CLIENT_SECRET = dummy
|
|
||||||
|
|
||||||
# By putting dummy values, extracting visualizing commands will not work locally (except for JavaScript).
|
|
||||||
AWS_ACCESS_KEY_ID = dummy
|
|
||||||
AWS_SECRET_ACCESS_KEY = dummy
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Install dependencies, and run the server.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd server
|
|
||||||
|
|
||||||
npm install
|
|
||||||
|
|
||||||
npm run watch
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Open [`http://localhost:8080/`](http://localhost:8080/) in a web browser.
|
|
||||||
|
|
||||||
## Directory Structure
|
|
||||||
|
|
||||||
- [**src/**](src) contains source code.
|
|
||||||
- [**config/**](src/config) contains configuration files.
|
|
||||||
- [**controllers/**](src/controllers) routes and processes incoming requests.
|
|
||||||
- [**middlewares/**](src/middlewares) contains Express middlewares.
|
|
||||||
- [**models/**](src/models) manages algorithm visualizations and their hierarchy.
|
|
||||||
- [**tracers/**](src/tracers) build visualization libraries and compiles/runs code.
|
|
||||||
- [**utils/**](src/utils) contains utility files.
|
|
||||||
|
|
||||||
**NOTE** that for JavaScript, it builds a web worker rather than a docker image. Once a browser fetches the web worker, it will submit users' code to the web worker locally, instead of submitting to the remote server, to extract visualizing commands.
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
config-dir = /home/ubuntu/.certbot/config
|
|
||||||
work-dir = /home/ubuntu/.certbot/work
|
|
||||||
logs-dir = /home/ubuntu/.certbot/logs
|
|
||||||
email = parkjs814@gmail.com
|
|
||||||
authenticator = webroot
|
|
||||||
webroot-path = /home/ubuntu/server/public/frontend-built
|
|
||||||
domains = algorithm-visualizer.org
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,47 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@algorithm-visualizer/server",
|
|
||||||
"version": "2.0.0",
|
|
||||||
"title": "Algorithm Visualizer",
|
|
||||||
"description": "Algorithm Visualizer is an interactive online platform that visualizes algorithms from code.",
|
|
||||||
"scripts": {
|
|
||||||
"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",
|
|
||||||
"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/node-cron": "^3.0.1",
|
|
||||||
"@types/remove-markdown": "^0.1.1",
|
|
||||||
"@types/uuid": "^3.4.4",
|
|
||||||
"ts-node-dev": "^1.0.0-pre.39",
|
|
||||||
"tslint": "^5.16.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"aws-sdk": "^2.814.0",
|
|
||||||
"axios": "^0.21.2",
|
|
||||||
"body-parser": "^1.18.2",
|
|
||||||
"compression": "^1.7.3",
|
|
||||||
"dotenv": "^8.0.0",
|
|
||||||
"dotenv-flow": "^2.0.0",
|
|
||||||
"express": "^4.16.4",
|
|
||||||
"express-github-webhook": "^1.0.6",
|
|
||||||
"fs-extra": "^6.0.1",
|
|
||||||
"morgan": "^1.9.1",
|
|
||||||
"node-cron": "^3.0.0",
|
|
||||||
"remove-markdown": "^0.3.0",
|
|
||||||
"ts-httpexceptions": "^4.1.0",
|
|
||||||
"ts-node": "^8.1.0",
|
|
||||||
"typescript": "^3.4.5",
|
|
||||||
"uuid": "^3.3.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"apps": [
|
|
||||||
{
|
|
||||||
"name": "algorithm-visualizer",
|
|
||||||
"script": "npm start"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,118 +0,0 @@
|
|||||||
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 cron from 'node-cron';
|
|
||||||
import { Hierarchy } from 'models';
|
|
||||||
import * as Tracers from 'tracers';
|
|
||||||
import { errorHandlerMiddleware, frontendMiddleware, redirectMiddleware } from 'middlewares';
|
|
||||||
import { execute, issueHttpsCertificate, pull } from 'utils/misc';
|
|
||||||
import { frontendBuildDir, frontendBuiltDir, frontendDir, rootDir } from 'config/paths';
|
|
||||||
|
|
||||||
const Webhook = require('express-github-webhook');
|
|
||||||
|
|
||||||
export default class Server {
|
|
||||||
readonly hierarchy = new Hierarchy();
|
|
||||||
readonly tracers = Object.values(Tracers).map(Tracer => new Tracer());
|
|
||||||
private readonly app = express();
|
|
||||||
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(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);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (credentials) {
|
|
||||||
cron.schedule('0 0 1 * *', () => {
|
|
||||||
issueHttpsCertificate();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 execute('npm install', {
|
|
||||||
cwd: rootDir,
|
|
||||||
stdout: process.stdout,
|
|
||||||
stderr: process.stderr,
|
|
||||||
});
|
|
||||||
process.exit(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
async updateFrontend(commit?: string) {
|
|
||||||
await pull(frontendDir, 'algorithm-visualizer', commit);
|
|
||||||
await execute([
|
|
||||||
'npm install',
|
|
||||||
'npm run build',
|
|
||||||
`rm -rf ${frontendBuiltDir}`,
|
|
||||||
`mv ${frontendBuildDir} ${frontendBuiltDir}`,
|
|
||||||
].join(' && '), {
|
|
||||||
cwd: frontendDir,
|
|
||||||
stdout: process.stdout,
|
|
||||||
stderr: process.stderr,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export const memoryLimit = 256; // in megabytes
|
|
||||||
export const timeLimit = 5000; // in milliseconds
|
|
||||||
@ -1,88 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import { ServerOptions } from 'https';
|
|
||||||
import path from 'path';
|
|
||||||
import { issueHttpsCertificate } from '../utils/misc';
|
|
||||||
|
|
||||||
require('dotenv-flow').config();
|
|
||||||
|
|
||||||
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,
|
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID,
|
|
||||||
AWS_SECRET_ACCESS_KEY,
|
|
||||||
} = process.env as {
|
|
||||||
[key: string]: string,
|
|
||||||
};
|
|
||||||
|
|
||||||
const isEnabled = (v: string) => v === '1';
|
|
||||||
const isDev = NODE_ENV === 'development';
|
|
||||||
|
|
||||||
const missingVars = [
|
|
||||||
'NODE_ENV',
|
|
||||||
'HTTP_PORT',
|
|
||||||
'CREDENTIALS_ENABLED',
|
|
||||||
...(isEnabled(CREDENTIALS_ENABLED) ? [
|
|
||||||
'HTTPS_PORT',
|
|
||||||
'CREDENTIALS_PATH',
|
|
||||||
'CREDENTIALS_CA',
|
|
||||||
'CREDENTIALS_KEY',
|
|
||||||
'CREDENTIALS_CERT',
|
|
||||||
] : []),
|
|
||||||
'WEBHOOK_ENABLED',
|
|
||||||
...(isEnabled(WEBHOOK_ENABLED) ? [
|
|
||||||
'WEBHOOK_SECRET',
|
|
||||||
] : []),
|
|
||||||
...(!isDev ? [
|
|
||||||
'GITHUB_CLIENT_ID',
|
|
||||||
'GITHUB_CLIENT_SECRET',
|
|
||||||
'AWS_ACCESS_KEY_ID',
|
|
||||||
'AWS_SECRET_ACCESS_KEY',
|
|
||||||
] : []),
|
|
||||||
].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, 10);
|
|
||||||
export const httpsPort = isEnabled(CREDENTIALS_ENABLED) ? parseInt(HTTPS_PORT || '8443', 10) : 0;
|
|
||||||
|
|
||||||
export const webhookOptions = isEnabled(WEBHOOK_ENABLED) ? {
|
|
||||||
path: '/webhook',
|
|
||||||
secret: WEBHOOK_SECRET,
|
|
||||||
} : undefined;
|
|
||||||
|
|
||||||
export let credentials: ServerOptions | undefined;
|
|
||||||
if (isEnabled(CREDENTIALS_ENABLED)) {
|
|
||||||
if (fs.existsSync(CREDENTIALS_PATH)) {
|
|
||||||
const readCredentials = (file: string) => fs.readFileSync(path.resolve(CREDENTIALS_PATH, file));
|
|
||||||
credentials = {
|
|
||||||
ca: readCredentials(CREDENTIALS_CA),
|
|
||||||
key: readCredentials(CREDENTIALS_KEY),
|
|
||||||
cert: readCredentials(CREDENTIALS_CERT),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
issueHttpsCertificate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const githubClientId = GITHUB_CLIENT_ID ?? '';
|
|
||||||
export const githubClientSecret = GITHUB_CLIENT_SECRET ?? '';
|
|
||||||
|
|
||||||
export const awsAccessKeyId = AWS_ACCESS_KEY_ID ?? '';
|
|
||||||
export const awsSecretAccessKey = AWS_SECRET_ACCESS_KEY ?? '';
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
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');
|
|
||||||
@ -1,38 +0,0 @@
|
|||||||
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'));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
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(`<script>window.opener.signIn('${access_token}');window.close();</script>`);
|
|
||||||
}).catch(next);
|
|
||||||
};
|
|
||||||
|
|
||||||
destroy = (req: express.Request, res: express.Response) => {
|
|
||||||
res.send(`<script>window.opener.signOut();window.close();</script>`);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,12 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export { AlgorithmsController } from './AlgorithmsController';
|
|
||||||
export { AuthController } from './AuthController';
|
|
||||||
export { TracersController } from './TracersController';
|
|
||||||
export { VisualizationsController } from './VisualizationsController';
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
import Server from 'Server';
|
|
||||||
|
|
||||||
new Server().start();
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
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 {message, status} = err;
|
|
||||||
res.status(status).send(message);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,51 +0,0 @@
|
|||||||
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(/</g, '\\u003c'));
|
|
||||||
res.send(indexFile);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export { errorHandlerMiddleware } from './errorHandlerMiddleware';
|
|
||||||
export { frontendMiddleware } from './frontendMiddleware';
|
|
||||||
export { redirectMiddleware } from './redirectMiddleware';
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
import { NextFunction, Request, Response } from 'express';
|
|
||||||
import { credentials } from 'config/environments';
|
|
||||||
|
|
||||||
export function redirectMiddleware() {
|
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
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;
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
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 { execute, pull } from 'utils/misc';
|
|
||||||
|
|
||||||
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<CommitAuthors> {
|
|
||||||
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 execute(`git --no-pager log --follow --no-merges --format="%H" -- "${path.relative(this.path, 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};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
export { Algorithm } from './Algorithm';
|
|
||||||
export { Category } from './Category';
|
|
||||||
export { File } from './File';
|
|
||||||
export { Hierarchy } from './Hierarchy';
|
|
||||||
@ -1,66 +0,0 @@
|
|||||||
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 { execute } from 'utils/misc';
|
|
||||||
|
|
||||||
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}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
build(release: Release) {
|
|
||||||
const {tag_name} = release;
|
|
||||||
return execute(`docker build -t ${this.imageName} . --build-arg tag_name=${tag_name}`, {
|
|
||||||
cwd: this.directory,
|
|
||||||
stdout: process.stdout,
|
|
||||||
stderr: process.stderr,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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(() => {
|
|
||||||
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',
|
|
||||||
this.imageName,
|
|
||||||
].join(' ')).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));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
import AWS from 'aws-sdk';
|
|
||||||
import express from 'express';
|
|
||||||
import { Release, Tracer } from 'tracers/Tracer';
|
|
||||||
import { awsAccessKeyId, awsSecretAccessKey } from 'config/environments';
|
|
||||||
import { BadRequest } from 'ts-httpexceptions';
|
|
||||||
|
|
||||||
export class LambdaTracer extends Tracer {
|
|
||||||
static lambda = new AWS.Lambda({
|
|
||||||
region: 'us-east-2',
|
|
||||||
accessKeyId: awsAccessKeyId,
|
|
||||||
secretAccessKey: awsSecretAccessKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
async build(release: Release) {
|
|
||||||
}
|
|
||||||
|
|
||||||
route(router: express.Router) {
|
|
||||||
router.post(`/${this.lang}`, (req, res, next) => {
|
|
||||||
const {code} = req.body;
|
|
||||||
LambdaTracer.lambda.invoke({
|
|
||||||
FunctionName: `extractor-${this.lang}`,
|
|
||||||
InvocationType: 'RequestResponse',
|
|
||||||
Payload: JSON.stringify(code),
|
|
||||||
}, function (err, data) {
|
|
||||||
if (err) return next(err);
|
|
||||||
if (typeof data.Payload !== 'string') return next(new Error('Unexpected Payload Type'));
|
|
||||||
const payload = JSON.parse(data.Payload);
|
|
||||||
if (!payload.success) return next(new BadRequest(payload.errorMessage));
|
|
||||||
res.send(payload.commands);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
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<any>;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { DockerTracer } from 'tracers/DockerTracer';
|
|
||||||
|
|
||||||
export class CppTracer extends DockerTracer {
|
|
||||||
constructor() {
|
|
||||||
super('cpp');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
FROM rikorose/gcc-cmake
|
|
||||||
|
|
||||||
ARG tag_name
|
|
||||||
|
|
||||||
RUN curl --create-dirs -o /usr/local/include/nlohmann/json.hpp -L "https://github.com/nlohmann/json/releases/download/v3.1.2/json.hpp" \
|
|
||||||
&& curl --create-dirs -o /usr/tmp/algorithm-visualizer.tar.gz -L "https://github.com/algorithm-visualizer/tracers.cpp/archive/${tag_name}.tar.gz" \
|
|
||||||
&& cd /usr/tmp \
|
|
||||||
&& mkdir algorithm-visualizer \
|
|
||||||
&& tar xvzf algorithm-visualizer.tar.gz -C algorithm-visualizer --strip-components=1 \
|
|
||||||
&& cd /usr/tmp/algorithm-visualizer \
|
|
||||||
&& mkdir build \
|
|
||||||
&& cd build \
|
|
||||||
&& cmake .. \
|
|
||||||
&& make install
|
|
||||||
|
|
||||||
CMD g++ Main.cpp -o Main -O2 -std=c++11 -lcurl \
|
|
||||||
&& ./Main
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
export { CppTracer } from './cpp/CppTracer';
|
|
||||||
export { JavaTracer } from './java/JavaTracer';
|
|
||||||
export { JsTracer } from './js/JsTracer';
|
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { LambdaTracer } from 'tracers/LambdaTracer';
|
|
||||||
|
|
||||||
export class JavaTracer extends LambdaTracer {
|
|
||||||
constructor() {
|
|
||||||
super('java');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
import path from 'path';
|
|
||||||
import { Release, Tracer } from 'tracers/Tracer';
|
|
||||||
import express from 'express';
|
|
||||||
|
|
||||||
export class JsTracer extends Tracer {
|
|
||||||
readonly workerPath: string;
|
|
||||||
tagName?: string;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super('js');
|
|
||||||
this.workerPath = path.resolve(__dirname, 'worker.js');
|
|
||||||
}
|
|
||||||
|
|
||||||
async build(release: Release) {
|
|
||||||
const {tag_name} = release;
|
|
||||||
this.tagName = tag_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
route(router: express.Router) {
|
|
||||||
router.get(`/${this.lang}`, (req, res) => {
|
|
||||||
if (!this.tagName) throw new Error('JsTracer has not been built yet.');
|
|
||||||
const version = this.tagName.slice(1);
|
|
||||||
res.redirect(`https://unpkg.com/algorithm-visualizer@${version}/dist/algorithm-visualizer.umd.js`);
|
|
||||||
});
|
|
||||||
router.get(`/${this.lang}/worker`, (req, res) => res.sendFile(this.workerPath));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
const process = { env: { ALGORITHM_VISUALIZER: '1' } };
|
|
||||||
importScripts('/api/tracers/js');
|
|
||||||
|
|
||||||
const sandbox = code => {
|
|
||||||
const require = name => ({ 'algorithm-visualizer': AlgorithmVisualizer }[name]); // fake require
|
|
||||||
eval(code);
|
|
||||||
};
|
|
||||||
|
|
||||||
onmessage = e => {
|
|
||||||
const lines = e.data.split('\n').map((line, i) => line.replace(/(\.\s*delay\s*)\(\s*\)/g, `$1(${i})`));
|
|
||||||
const code = lines.join('\n');
|
|
||||||
sandbox(code);
|
|
||||||
postMessage(AlgorithmVisualizer.Commander.commands);
|
|
||||||
};
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
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<AxiosResponse<any>>) => {
|
|
||||||
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'),
|
|
||||||
};
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
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)));
|
|
||||||
}
|
|
||||||
@ -1,80 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import fs from 'fs-extra';
|
|
||||||
import { File } from 'models';
|
|
||||||
import removeMarkdown from 'remove-markdown';
|
|
||||||
import * as child_process from 'child_process';
|
|
||||||
import { ExecOptions, spawn } from 'child_process';
|
|
||||||
import { rootDir } from '../config/paths';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
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 execute(`git fetch`, {
|
|
||||||
cwd: dir,
|
|
||||||
stdout: process.stdout,
|
|
||||||
stderr: process.stderr,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await execute(`git clone https://github.com/algorithm-visualizer/${repo}.git ${dir}`, {
|
|
||||||
stdout: process.stdout,
|
|
||||||
stderr: process.stderr,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await execute(`git reset --hard ${commit}`, {
|
|
||||||
cwd: dir,
|
|
||||||
stdout: process.stdout,
|
|
||||||
stderr: process.stderr,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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();
|
|
||||||
const descriptionLines = [];
|
|
||||||
while (lines.length && lines[0].trim()) descriptionLines.push(lines.shift());
|
|
||||||
return removeMarkdown(descriptionLines.join(' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExecuteOptions = ExecOptions & {
|
|
||||||
stdout?: NodeJS.WriteStream;
|
|
||||||
stderr?: NodeJS.WriteStream;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function execute(command: string, { stdout, stderr, ...options }: ExecuteOptions = {}): Promise<string> {
|
|
||||||
return 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 (child.stdout && stdout) child.stdout.pipe(stdout);
|
|
||||||
if (child.stderr && stderr) child.stderr.pipe(stderr);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function issueHttpsCertificate() {
|
|
||||||
const certbotIniPath = path.resolve(rootDir, 'certbot.ini');
|
|
||||||
const childProcess = spawn('certbot', ['certonly', '--non-interactive', '--agree-tos', '--config', certbotIniPath]);
|
|
||||||
childProcess.stdout.pipe(process.stdout);
|
|
||||||
childProcess.stderr.pipe(process.stderr);
|
|
||||||
childProcess.on('error', console.error);
|
|
||||||
childProcess.on('exit', code => {
|
|
||||||
if (code === 0) {
|
|
||||||
process.exit(0);
|
|
||||||
} else {
|
|
||||||
console.error(new Error(`certbot failed with exit code ${code}.`));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"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",
|
|
||||||
"public"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Reference in new issue