diff options
author | Dessalines <tyhou13@gmx.com> | 2019-03-20 18:22:31 -0700 |
---|---|---|
committer | Dessalines <tyhou13@gmx.com> | 2019-03-20 18:22:31 -0700 |
commit | 816aa0b15f3766e340d8722f03e8b3a7633ab6fb (patch) | |
tree | 23dd0fc329e8f08c71dc6f10dd398b35d92c047c /ui | |
parent | 064d7f84b25236195eeb33a8671935bc9df37e57 (diff) |
Adding initial UI and Websocket server.
Diffstat (limited to 'ui')
-rw-r--r-- | ui/.gitignore | 30 | ||||
-rw-r--r-- | ui/assets/favicon.ico | bin | 0 -> 1150 bytes | |||
-rw-r--r-- | ui/fuse.js | 55 | ||||
-rw-r--r-- | ui/package.json | 31 | ||||
-rw-r--r-- | ui/src/components/home.tsx | 14 | ||||
-rw-r--r-- | ui/src/components/login.tsx | 145 | ||||
-rw-r--r-- | ui/src/components/navbar.tsx | 38 | ||||
-rw-r--r-- | ui/src/components/search.tsx | 205 | ||||
-rw-r--r-- | ui/src/env.ts | 3 | ||||
-rw-r--r-- | ui/src/index.html | 19 | ||||
-rw-r--r-- | ui/src/index.tsx | 42 | ||||
-rw-r--r-- | ui/src/interfaces.ts | 14 | ||||
-rw-r--r-- | ui/src/main.css | 0 | ||||
-rw-r--r-- | ui/src/services.ts | 57 | ||||
-rw-r--r-- | ui/src/utils.ts | 2 | ||||
-rw-r--r-- | ui/tsconfig.json | 12 | ||||
-rw-r--r-- | ui/tslint.json | 28 | ||||
-rw-r--r-- | ui/yarn.lock | 3084 |
18 files changed, 3779 insertions, 0 deletions
diff --git a/ui/.gitignore b/ui/.gitignore new file mode 100644 index 00000000..cc0ab540 --- /dev/null +++ b/ui/.gitignore @@ -0,0 +1,30 @@ +dist +.fusebox +_site +.alm +.history +.git +build +.build +.git +.history +.idea +.jshintrc +.nyc_output +.sass-cache +.vscode +build +coverage +jsconfig.json +Gemfile.lock +node_modules +.DS_Store +*.map +*.log +*.swp +*~ +test/data/result.json + +package-lock.json +*.orig + diff --git a/ui/assets/favicon.ico b/ui/assets/favicon.ico Binary files differnew file mode 100644 index 00000000..13f310e9 --- /dev/null +++ b/ui/assets/favicon.ico diff --git a/ui/fuse.js b/ui/fuse.js new file mode 100644 index 00000000..ff1e6d15 --- /dev/null +++ b/ui/fuse.js @@ -0,0 +1,55 @@ +const { + FuseBox, + Sparky, + EnvPlugin, + CSSPlugin, + WebIndexPlugin, + QuantumPlugin +} = require('fuse-box'); +// const transformInferno = require('../../dist').default +const transformInferno = require('ts-transform-inferno').default; +const transformClasscat = require('ts-transform-classcat').default; +let fuse, app; +let isProduction = false; + +Sparky.task('config', _ => { + fuse = new FuseBox({ + homeDir: 'src', + hash: isProduction, + output: 'dist/$name.js', + experimentalFeatures: true, + cache: !isProduction, + sourceMaps: !isProduction, + transformers: { + before: [transformClasscat(), transformInferno()], + }, + plugins: [ + EnvPlugin({ NODE_ENV: isProduction ? 'production' : 'development' }), + CSSPlugin(), + WebIndexPlugin({ + title: 'Inferno Typescript FuseBox Example', + template: 'src/index.html', + path: isProduction ? "/static" : "/" + }), + isProduction && + QuantumPlugin({ + bakeApiIntoBundle: 'app', + treeshake: true, + uglify: true, + }), + ], + }); + app = fuse.bundle('app').instructions('>index.tsx'); +}); +Sparky.task('clean', _ => Sparky.src('dist/').clean('dist/')); +Sparky.task('env', _ => (isProduction = true)); +Sparky.task('copy-assets', () => Sparky.src('assets/*.ico').dest('dist/')); +Sparky.task('dev', ['clean', 'config', 'copy-assets'], _ => { + fuse.dev(); + app.hmr().watch(); + return fuse.run(); +}); +Sparky.task('prod', ['clean', 'env', 'config', 'copy-assets'], _ => { + // fuse.dev({ reload: true }); // remove after demo + return fuse.run(); +}); diff --git a/ui/package.json b/ui/package.json new file mode 100644 index 00000000..ca4fa368 --- /dev/null +++ b/ui/package.json @@ -0,0 +1,31 @@ +{ + "name": "rust_reddit_fediverse", + "version": "1.0.0", + "description": "A simple UI for rust_reddit_fediverse", + "main": "index.js", + "scripts": { + "start": "node fuse dev", + "build": "node fuse prod" + }, + "keywords": [], + "author": "Dessalines", + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=8.9.0" + }, + "engineStrict": true, + "dependencies": { + "classcat": "^1.1.3", + "dotenv": "^6.1.0", + "inferno": "^7.0.1", + "inferno-router": "^7.0.1", + "moment": "^2.22.2" + }, + "devDependencies": { + "fuse-box": "3.1.3", + "ts-transform-classcat": "^0.0.2", + "ts-transform-inferno": "^4.0.2", + "typescript": "^3.3.3333", + "uglify-es": "^3.3.9" + } +} diff --git a/ui/src/components/home.tsx b/ui/src/components/home.tsx new file mode 100644 index 00000000..07cb94f5 --- /dev/null +++ b/ui/src/components/home.tsx @@ -0,0 +1,14 @@ +import { Component } from 'inferno'; +import { repoUrl } from '../utils'; + +export class Home extends Component<any, any> { + + render() { + return ( + <div class="container"> + hola this is me. + </div> + ) + } + +} diff --git a/ui/src/components/login.tsx b/ui/src/components/login.tsx new file mode 100644 index 00000000..fd6f5045 --- /dev/null +++ b/ui/src/components/login.tsx @@ -0,0 +1,145 @@ +import { Component, linkEvent } from 'inferno'; + +import { LoginForm, RegisterForm } from '../interfaces'; +import { WebSocketService } from '../services'; + +interface State { + loginForm: LoginForm; + registerForm: RegisterForm; +} + +let emptyState: State = { + loginForm: { + username: null, + password: null + }, + registerForm: { + username: null, + password: null, + password_verify: null + } +} + +export class Login extends Component<any, State> { + + constructor(props, context) { + super(props, context); + + this.state = emptyState; + + } + render() { + return ( + <div class="container"> + <div class="row"> + <div class="col-12 col-lg-6 mb-4"> + {this.loginForm()} + </div> + <div class="col-12 col-lg-6"> + {this.registerForm()} + </div> + </div> + </div> + ) + } + + loginForm() { + return ( + <div> + <form onSubmit={linkEvent(this, this.handleLoginSubmit)}> + <h3>Login</h3> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Email or Username</label> + <div class="col-sm-10"> + <input type="text" class="form-control" value={this.state.loginForm.username} onInput={linkEvent(this, this.handleLoginUsernameChange)} required minLength={3} /> + </div> + </div> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Password</label> + <div class="col-sm-10"> + <input type="password" value={this.state.loginForm.password} onInput={linkEvent(this, this.handleLoginPasswordChange)} class="form-control" required /> + </div> + </div> + <div class="form-group row"> + <div class="col-sm-10"> + <button type="submit" class="btn btn-secondary">Login</button> + </div> + </div> + </form> + Forgot your password or deleted your account? Reset your password. TODO + </div> + ); + } + registerForm() { + return ( + <form onSubmit={linkEvent(this, this.handleRegisterSubmit)}> + <h3>Sign Up</h3> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Username</label> + <div class="col-sm-10"> + <input type="text" class="form-control" value={this.state.registerForm.username} onInput={linkEvent(this, this.handleRegisterUsernameChange)} required minLength={3} /> + </div> + </div> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Email</label> + <div class="col-sm-10"> + <input type="email" class="form-control" value={this.state.registerForm.email} onInput={linkEvent(this, this.handleRegisterEmailChange)} minLength={3} /> + </div> + </div> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Password</label> + <div class="col-sm-10"> + <input type="password" value={this.state.registerForm.password} onInput={linkEvent(this, this.handleRegisterPasswordChange)} class="form-control" required /> + </div> + </div> + <div class="form-group row"> + <label class="col-sm-2 col-form-label">Verify Password</label> + <div class="col-sm-10"> + <input type="password" value={this.state.registerForm.password_verify} onInput={linkEvent(this, this.handleRegisterPasswordVerifyChange)} class="form-control" required /> + </div> + </div> + <div class="form-group row"> + <div class="col-sm-10"> + <button type="submit" class="btn btn-secondary">Sign Up</button> + </div> + </div> + </form> + ); + } + + handleLoginSubmit(i: Login, event) { + console.log(i.state); + event.preventDefault(); + WebSocketService.Instance.login(i.state.loginForm); + } + + handleLoginUsernameChange(i: Login, event) { + i.state.loginForm.username = event.target.value; + } + + handleLoginPasswordChange(i: Login, event) { + i.state.loginForm.password = event.target.value; + } + + handleRegisterSubmit(i: Login, event) { + console.log(i.state); + event.preventDefault(); + WebSocketService.Instance.register(i.state.registerForm); + } + + handleRegisterUsernameChange(i: Login, event) { + i.state.registerForm.username = event.target.value; + } + + handleRegisterEmailChange(i: Login, event) { + i.state.registerForm.email = event.target.value; + } + + handleRegisterPasswordChange(i: Login, event) { + i.state.registerForm.password = event.target.value; + } + + handleRegisterPasswordVerifyChange(i: Login, event) { + i.state.registerForm.password_verify = event.target.value; + } +} diff --git a/ui/src/components/navbar.tsx b/ui/src/components/navbar.tsx new file mode 100644 index 00000000..86d5d1d2 --- /dev/null +++ b/ui/src/components/navbar.tsx @@ -0,0 +1,38 @@ +import { Component, linkEvent } from 'inferno'; +import { Link } from 'inferno-router'; +import { repoUrl } from '../utils'; + +export class Navbar extends Component<any, any> { + + constructor(props, context) { + super(props, context); + } + + render() { + return ( + <div class="sticky-top">{this.navbar()}</div> + ) + } + + // TODO class active corresponding to current page + navbar() { + return ( + <nav class="navbar navbar-light bg-light p-0 px-3 shadow"> + <a class="navbar-brand mx-1" href="#"> + rrf + </a> + <ul class="navbar-nav mr-auto"> + <li class="nav-item"> + <a class="nav-item nav-link" href={repoUrl}>github</a> + </li> + </ul> + <ul class="navbar-nav ml-auto mr-2"> + <li class="nav-item"> + <Link class="nav-item nav-link" to="/login">Login</Link> + </li> + </ul> + </nav> + ); + } + +} diff --git a/ui/src/components/search.tsx b/ui/src/components/search.tsx new file mode 100644 index 00000000..080761f9 --- /dev/null +++ b/ui/src/components/search.tsx @@ -0,0 +1,205 @@ +import { Component, linkEvent } from 'inferno'; +import * as moment from 'moment'; + +import { endpoint } from '../env'; +import { SearchParams, Results, Torrent } from '../interfaces'; +import { humanFileSize, magnetLink, getFileName } from '../utils'; + +interface State { + results: Results; + searchParams: SearchParams; + searching: Boolean; +} + +export class Search extends Component<any, State> { + + state: State = { + results: { + torrents: [] + }, + searchParams: { + q: "", + page: 1, + type_: 'torrent' + }, + searching: false + }; + + constructor(props, context) { + super(props, context); + } + + componentDidMount() { + this.state.searchParams = { + page: Number(this.props.match.params.page), + q: this.props.match.params.q, + type_: this.props.match.params.type_ + } + this.search(); + } + + // Re-do search if the props have changed + componentDidUpdate(lastProps, lastState, snapshot) { + if (lastProps.match && lastProps.match.params !== this.props.match.params) { + this.state.searchParams = { + page: Number(this.props.match.params.page), + q: this.props.match.params.q, + type_: this.props.match.params.type_ + } + this.search(); + } + + } + + search() { + if (!!this.state.searchParams.q) { + this.setState({ searching: true, results: { torrents: [] } }); + this.fetchData(this.state.searchParams) + .then(torrents => { + if (!!torrents) { + this.setState({ + results: { + torrents: torrents + } + }); + } + }).catch(error => { + console.error('request failed', error); + }).then(() => this.setState({ searching: false })); + } else { + this.setState({ results: { torrents: [] } }); + } + } + + fetchData(searchParams: SearchParams): Promise<Array<Torrent>> { + let q = encodeURI(searchParams.q); + return fetch(`${endpoint}/service/search?q=${q}&page=${searchParams.page}&type_=${searchParams.type_}`) + .then(data => data.json()); + } + + render() { + return ( + <div> + { + this.state.searching ? + this.spinner() : this.state.results.torrents[0] ? + this.torrentsTable() + : this.noResults() + } + </div> + ); + } + + spinner() { + return ( + <div class="text-center m-5 p-5"> + <svg class="icon icon-spinner spinner"><use xlinkHref="#icon-spinner"></use></svg> + </div> + ); + } + + noResults() { + return ( + <div class="text-center m-5 p-5"> + <h1>No Results</h1> + </div> + ) + } + + torrentsTable() { + return ( + <div> + <table class="table table-fixed table-hover table-sm table-striped table-hover-purple table-padding"> + <thead> + <tr> + <th class="search-name-col">Name</th> + <th class="text-right">Size</th> + <th class="text-right">Seeds</th> + <th class="text-right d-none d-md-table-cell">Leeches</th> + <th class="text-right d-none d-md-table-cell">Created</th> + <th></th> + </tr> + </thead> + <tbody> + {this.state.results.torrents.map(torrent => ( + <tr> + { !torrent.name ? ( + <td className="path_column"> + <a class="text-body" + href={magnetLink(torrent.infohash, torrent.path, torrent.index_)}> + {getFileName(torrent.path)} + </a> + </td> + ) : ( + <td class="search-name-cell"> + <a class="text-body" + href={magnetLink(torrent.infohash, torrent.name, torrent.index_)}> + {torrent.name} + </a> + </td> + )} + <td class="text-right text-muted">{humanFileSize(torrent.size_bytes, true)}</td> + <td class="text-right text-success"> + <svg class="icon icon-arrow-up d-none d-sm-inline mr-1"><use xlinkHref="#icon-arrow-up"></use></svg> + {torrent.seeders} + </td> + <td class="text-right text-danger d-none d-md-table-cell"> + <svg class="icon icon-arrow-down mr-1"><use xlinkHref="#icon-arrow-down"></use></svg> + {torrent.leechers} + </td> + <td class="text-right text-muted d-none d-md-table-cell" + data-balloon={`Scraped ${moment(torrent.scraped_date * 1000).fromNow()}`} + data-balloon-pos="down"> + {moment(torrent.created_unix * 1000).fromNow()} + </td> + <td class="text-right"> + <a class="btn btn-sm no-outline p-1" + href={magnetLink(torrent.infohash, (torrent.name) ? torrent.name : torrent.path, torrent.index_)} + data-balloon="Magnet link" + data-balloon-pos="left"> + <svg class="icon icon-magnet"><use xlinkHref="#icon-magnet"></use></svg> + </a> + <a class="btn btn-sm no-outline p-1 d-none d-sm-inline" + href={`https://gitlab.com/dessalines/torrents.csv/issues/new?issue[title]=Report%20Torrent%20infohash%20${torrent.infohash}`} + target="_blank" + data-balloon="Report Torrent" + data-balloon-pos="left"> + <svg class="icon icon-flag"><use xlinkHref="#icon-flag"></use></svg> + </a> + </td> + </tr> + ))} + </tbody> + </table> + {this.paginator()} + </div> + ); + } + + paginator() { + return ( + <nav> + <ul class="pagination justify-content-center"> + <li className={(this.state.searchParams.page == 1) ? "page-item disabled" : "page-item"}> + <button class="page-link" + onClick={linkEvent({ i: this, nextPage: false }, this.switchPage)}> + Previous + </button> + </li> + <li class="page-item"> + <button class="page-link" + onClick={linkEvent({ i: this, nextPage: true }, this.switchPage)}> + Next + </button> + </li> + </ul> + </nav> + ); + } + + switchPage(a: { i: Search, nextPage: boolean }, event) { + let newSearch = a.i.state.searchParams; + newSearch.page += (a.nextPage) ? 1 : -1; + a.i.props.history.push(`/search/${newSearch.type_}/${newSearch.q}/${newSearch.page}`); + } +} diff --git a/ui/src/env.ts b/ui/src/env.ts new file mode 100644 index 00000000..a8e72d90 --- /dev/null +++ b/ui/src/env.ts @@ -0,0 +1,3 @@ +// export const endpoint = window.location.origin; +export const endpoint = "http://localhost:8080"; +export let wsUri = (window.location.protocol=='https:') ? 'wss://' : 'ws://' + endpoint.substr(7) + '/service/ws'; diff --git a/ui/src/index.html b/ui/src/index.html new file mode 100644 index 00000000..2a3e4198 --- /dev/null +++ b/ui/src/index.html @@ -0,0 +1,19 @@ +<html lang="en"> + +<head> + <!-- Required meta tags --> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> + <link rel="shortcut icon" type="image/ico" href="/static/assets/favicon.ico" /> + + <title>rust-reddit-fediverse</title> + <link rel="stylesheet" href="https://bootswatch.com/4/darkly/bootstrap.min.css"> + <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/balloon-css/0.5.0/balloon.min.css"> +</head> + +<body> + <div id="app"></div> + $bundles +</body> + +</html> diff --git a/ui/src/index.tsx b/ui/src/index.tsx new file mode 100644 index 00000000..36e5681d --- /dev/null +++ b/ui/src/index.tsx @@ -0,0 +1,42 @@ +import { render, Component } from 'inferno'; +import { HashRouter, Route, Switch } from 'inferno-router'; + +import { Navbar } from './components/navbar'; +import { Home } from './components/home'; +import { Login } from './components/login'; + +import './main.css'; + +import { WebSocketService } from './services'; + +const container = document.getElementById('app'); + +class Index extends Component<any, any> { + + constructor(props, context) { + super(props, context); + WebSocketService.Instance; + } + + render() { + return ( + <HashRouter> + <Navbar /> + <div class="mt-3 p-0"> + <Switch> + <Route exact path="/" component={Home} /> + <Route path={`/login`} component={Login} /> + {/* + <Route path={`/search/:type_/:q/:page`} component={Search} /> + <Route path={`/submit`} component={Submit} /> + <Route path={`/user/:id`} component={Login} /> + <Route path={`/community/:id`} component={Login} /> + */} + </Switch> + </div> + </HashRouter> + ); + } +} + +render(<Index />, container); diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts new file mode 100644 index 00000000..c1550cc1 --- /dev/null +++ b/ui/src/interfaces.ts @@ -0,0 +1,14 @@ +export interface LoginForm { + username: string; + password: string; +} +export interface RegisterForm { + username: string; + email?: string; + password: string; + password_verify: string; +} + +export enum UserOperation { + Login, Register +} diff --git a/ui/src/main.css b/ui/src/main.css new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/ui/src/main.css diff --git a/ui/src/services.ts b/ui/src/services.ts new file mode 100644 index 00000000..b9536aed --- /dev/null +++ b/ui/src/services.ts @@ -0,0 +1,57 @@ +import { wsUri } from './env'; +import { LoginForm, RegisterForm, UserOperation } from './interfaces'; + +export class WebSocketService { + private static _instance: WebSocketService; + private _ws; + private conn: WebSocket; + + private constructor() { + console.log("Creating WSS"); + this.connect(); + console.log(wsUri); + } + + public static get Instance(){ + return this._instance || (this._instance = new this()); + } + + private connect() { + this.disconnect(); + this.conn = new WebSocket(wsUri); + console.log('Connecting...'); + this.conn.onopen = (() => { + console.log('Connected.'); + }); + this.conn.onmessage = (e => { + console.log('Received: ' + e.data); + }); + this.conn.onclose = (() => { + console.log('Disconnected.'); + this.conn = null; + }); + } + private disconnect() { + if (this.conn != null) { + console.log('Disconnecting...'); + this.conn.close(); + this.conn = null; + } + } + + public login(loginForm: LoginForm) { + this.conn.send(this.wsSendWrapper(UserOperation.Login, loginForm)); + } + + public register(registerForm: RegisterForm) { + this.conn.send(this.wsSendWrapper(UserOperation.Register, registerForm)); + } + + private wsSendWrapper(op: UserOperation, data: any): string { + let send = { op: UserOperation[op], data: data }; + console.log(send); + return JSON.stringify(send); + } + + +} diff --git a/ui/src/utils.ts b/ui/src/utils.ts new file mode 100644 index 00000000..e141c681 --- /dev/null +++ b/ui/src/utils.ts @@ -0,0 +1,2 @@ +export let repoUrl = 'https://github.com/dessalines/rust-reddit-fediverse'; +export let wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/service/ws/'; diff --git a/ui/tsconfig.json b/ui/tsconfig.json new file mode 100644 index 00000000..f58da758 --- /dev/null +++ b/ui/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2015", + "sourceMap": true, + "inlineSources": true, + "jsx": "preserve", + "importHelpers": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true + } +} diff --git a/ui/tslint.json b/ui/tslint.json new file mode 100644 index 00000000..d3e7a8a9 --- /dev/null +++ b/ui/tslint.json @@ -0,0 +1,28 @@ +{ + "extends": "tslint:recommended", + "rules": { + "forin": false, + "indent": [ true, "tabs" ], + "interface-name": false, + "ban-types": true, + "max-classes-per-file": true, + "max-line-length": false, + "member-access": true, + "member-ordering": false, + "no-bitwise": false, + "no-conditional-assignment": false, + "no-debugger": false, + "no-empty": true, + "no-namespace": false, + "no-unused-expression": true, + "object-literal-sort-keys": true, + "one-variable-per-declaration": [true, "ignore-for-loop"], + "only-arrow-functions": [false], + "ordered-imports": true, + "prefer-const": true, + "prefer-for-of": false, + "quotemark": [ true, "single", "jsx-double" ], + "trailing-comma": [true, {"multiline": "never", "singleline": "never"}], + "variable-name": false + } +} diff --git a/ui/yarn.lock b/ui/yarn.lock new file mode 100644 index 00000000..92a1250a --- /dev/null +++ b/ui/yarn.lock @@ -0,0 +1,3084 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/runtime@^7.1.2": + version "7.3.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.4.tgz#73d12ba819e365fcf7fd152aed56d6df97d21c83" + integrity sha512-IvfvnMdSaLBateu0jfsYIpZTxAc2cKEXEMiezGGN75QcBcecDUKd3PgLAncT0oOgxKy8dd8hrJKj9MfzgfZd6g== + dependencies: + regenerator-runtime "^0.12.0" + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== + +accepts@~1.3.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" + integrity sha1-63d99gEXI6OxTopywIBcjoZ0a9I= + dependencies: + mime-types "~2.1.18" + negotiator "0.6.1" + +acorn-jsx@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-4.1.1.tgz#e8e41e48ea2fe0c896740610ab6a4ffd8add225e" + integrity sha512-JY+iV6r+cO21KtntVvFkD+iqjtdpRUpGqKWgfkCdZq1R+kbreEl8EcdcJR4SmiIgsIQT33s6QzheQ9a275Q8xw== + dependencies: + acorn "^5.0.3" + +acorn@^5.0.3, acorn@^5.1.2: + version "5.7.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.3.tgz#67aa231bf8812974b85235a96771eb6bd07ea279" + integrity sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw== + +ajax-request@^1.2.0: + version "1.2.3" + resolved "https://registry.yarnpkg.com/ajax-request/-/ajax-request-1.2.3.tgz#99fcbec1d6d2792f85fa949535332bd14f5f3790" + integrity sha1-mfy+wdbSeS+F+pSVNTMr0U9fN5A= + dependencies: + file-system "^2.1.1" + utils-extend "^1.0.7" + +ajv@^6.5.5: + version "6.10.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1" + integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + integrity sha1-w7M6te42DYbg5ijwRorn7yfWVN8= + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/ansi/-/ansi-0.3.1.tgz#0c42d4fb17160d5a9af1e484bace1c66922c1b21" + integrity sha1-DELU+xcWDVqa8eSEus4cZpIsGyE= + +anymatch@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" + integrity sha512-0XNayC8lTHQ2OI8aljNCN3sSx6hsr/1+rlcDAotXJR7C1oZZHCNsfpbKwMjRA3Uqb5tF1Rae2oloTr4xpq+WjA== + dependencies: + micromatch "^2.1.5" + normalize-path "^2.0.0" + +app-root-path@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-1.4.0.tgz#6335d865c9640d0fad99004e5a79232238e92dfa" + integrity sha1-YzXYZclkDQ+tmQBOWnkjIjjpLfo= + +app-root-path@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.1.0.tgz#98bf6599327ecea199309866e8140368fd2e646a" + integrity sha1-mL9lmTJ+zqGZMJhm6BQDaP0uZGo= + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + integrity sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw== + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + integrity sha512-5hYdAkZlcG8tOLujVDTgCT+uPX0VnpAH28gWsLfzpXYm7wP6mp5Q/gYyR7YQ0cKVJcXJnl3j2kpBan13PtQf6w== + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + integrity sha1-jzuCf5Vai9ZpaX5KQlasPOrjVs8= + dependencies: + arr-flatten "^1.0.1" + +arr-diff@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520" + integrity sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA= + +arr-flatten@^1.0.1, arr-flatten@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + integrity sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg== + +arr-union@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" + integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI= + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + integrity sha1-odl8yvy8JiXMcPrc6zalDFiwGlM= + +array-unique@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" + integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= + +asn1@~0.2.3: + version "0.2.4" + resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.4.tgz#8d2475dfab553bb33e77b54e59e880bb8ce23136" + integrity sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg== + dependencies: + safer-buffer "~2.1.0" + +assert-plus@1.0.0, assert-plus@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525" + integrity sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU= + +assign |