Adding Routing. Fixes #16

Avoiding index.html redirect. Fixes #17
Adding Favicon. Fixes #14
This commit is contained in:
Dessalines 2018-11-27 14:29:56 -07:00
parent ada3df08dc
commit 2d8bb48ba8
10 changed files with 388 additions and 209 deletions

View File

@ -5,7 +5,7 @@ extern crate serde_derive;
extern crate grep; extern crate grep;
extern crate time; extern crate time;
use actix_web::{fs, http, server, App, HttpResponse, Query}; use actix_web::{fs, fs::NamedFile, http, server, App, HttpRequest, HttpResponse, Query};
use std::error::Error; use std::error::Error;
@ -15,14 +15,17 @@ use grep::searcher::{BinaryDetection, SearcherBuilder};
use std::fs::File; use std::fs::File;
fn main() { fn main() {
server::new(|| { server::new(|| {
App::new() App::new()
.route("/service/search", http::Method::GET, search) .route("/service/search", http::Method::GET, search)
.resource("/favicon.ico", |r| r.f(favicon))
.resource("/", |r| r.f(index))
.handler( .handler(
"/", "/static",
fs::StaticFiles::new("../ui/dist/") fs::StaticFiles::new("../ui/dist/")
.unwrap() .unwrap()
.index_file("index.html"), // .index_file("index.html"),
) )
.finish() .finish()
}).bind("127.0.0.1:8080") }).bind("127.0.0.1:8080")
@ -30,6 +33,14 @@ fn main() {
.run(); .run();
} }
fn index(_req: &HttpRequest) -> Result<NamedFile, actix_web::error::Error> {
Ok(NamedFile::open("../ui/dist/index.html")?)
}
fn favicon(_req: &HttpRequest) -> Result<NamedFile, actix_web::error::Error> {
Ok(NamedFile::open("../ui/src/favicon.ico")?)
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct SearchQuery { struct SearchQuery {
q: String, q: String,

View File

@ -27,10 +27,10 @@ Sparky.task('config', _ => {
plugins: [ plugins: [
EnvPlugin({ NODE_ENV: isProduction ? 'production' : 'development' }), EnvPlugin({ NODE_ENV: isProduction ? 'production' : 'development' }),
CSSPlugin(), CSSPlugin(),
CopyPlugin({ files: ["src/favicon.ico"] }),
WebIndexPlugin({ WebIndexPlugin({
title: 'Inferno Typescript FuseBox Example', title: 'Inferno Typescript FuseBox Example',
template: 'src/index.html', template: 'src/index.html',
path: isProduction ? "/static" : "/"
}), }),
isProduction && isProduction &&
QuantumPlugin({ QuantumPlugin({

View File

@ -13,6 +13,7 @@
"dependencies": { "dependencies": {
"classcat": "^1.1.3", "classcat": "^1.1.3",
"inferno": "^6.0.3", "inferno": "^6.0.3",
"inferno-router": "^6.3.1",
"moment": "^2.22.2" "moment": "^2.22.2"
}, },
"devDependencies": { "devDependencies": {

View File

@ -0,0 +1,28 @@
import { Component } from 'inferno';
export class Home extends Component<any, any> {
render() {
return (
<div class="container">
{this.onboard()}
</div>
)
}
onboard() {
let site: string = "https://gitlab.com/dessalines/torrents.csv";
return (
<div>
<br />
<a href={site}>Torrents.csv</a> is a collaborative, <b>vetted</b> git repository of torrents, consisting of a single, searchable <code>torrents.csv</code> file. Its initially populated with a January 2017 backup of the pirate bay, and new torrents are periodically added from various torrents sites via a rust script.<br></br><br></br>
<a href={site}>Torrents.csv</a> will only store torrents with at least one seeder to keep the file small, and will be periodically purged of non-seeded torrents, and sorted by seeders descending.<br></br><br></br>
To request more torrents, or add your own to the file, go <a href={site}>here</a>.<br></br><br></br>
Made with <a href="https://www.rust-lang.org">Rust</a>, <a href="https://github.com/BurntSushi/ripgrep">ripgrep</a>, <a href="https://actix.rs/">Actix</a>, <a href="https://www.infernojs.org">Inferno</a>, and <a href="https://www.typescriptlang.org/">Typescript</a>.
</div>
);
}
}

View File

@ -0,0 +1,68 @@
import { Component, linkEvent } from 'inferno';
import { SearchParams } from '../interfaces';
interface State {
searchParams: SearchParams;
}
export class Navbar extends Component<any, State> {
state: State = {
searchParams: {
page: 1,
q: ""
}
}
constructor(props, context) {
super(props, context);
}
render() {
return (
<div>{this.navbar()}</div>
)
}
navbar() {
return (
<nav class="navbar navbar-dark" style="background-color: #673ab7">
<a class="navbar-brand" href="#">
<i class="fas fa-fw fa-database mr-1"></i>Torrents.csv
</a>
<div class="col-12 col-sm-6">
{this.searchForm()}
</div>
</nav>
);
}
// TODO
// https://www.codeply.com/go/xBVaM3q5X4/bootstrap-4-navbar-search-full-width
searchForm() {
return (
<form class="my-2 my-lg-0 d-inline w-100" onSubmit={linkEvent(this, this.search)}>
<div class="input-group">
<input value={this.state.searchParams.q} onInput={linkEvent(this, this.searchChange)} type="text" class="form-control" placeholder="Search..." />
<button type="submit" class="btn btn-light border border-left-0 border-radius-left-0">
<i className="fas fa-fw fa-search"></i>
</button>
</div>
</form>
);
}
search(i: Navbar, event) {
event.preventDefault();
i.context.router.history.push(`/search/${i.state.searchParams.q}/${i.state.searchParams.page}`);
}
searchChange(i: Navbar, event) {
let searchParams: SearchParams = {
q: event.target.value,
page: 1
}
i.setState({ searchParams: searchParams });
}
}

View File

@ -0,0 +1,178 @@
import { Component, linkEvent } from 'inferno';
import * as moment from 'moment';
import { endpoint } from '../env';
import { SearchParams, Results } from '../interfaces';
import { convertCsvToJson, humanFileSize, magnetLink } 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
},
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
}
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
}
this.search();
}
}
search() {
if (!!this.state.searchParams.q) {
this.setState({ searching: true, results: { torrents: [] } });
this.fetchData(this.state.searchParams)
.then(results => {
if (!!results) {
this.setState({
results: results
});
}
}).catch(error => {
console.error('request failed', error);
}).then(() => this.setState({ searching: false }));
} else {
this.setState({ results: { torrents: [] } });
}
}
fetchData(searchParams: SearchParams): Promise<Results> {
let q = encodeURI(searchParams.q);
return fetch(`${endpoint}/service/search?q=${q}&page=${searchParams.page}`)
.then(data => data.text())
.then(csv => convertCsvToJson(csv));
}
render() {
return (
<div>
<div className={this.state.results.torrents[0] ? "container-fluid" : "container"}>
<div class="row mt-2">
<div class="col-12">
{
this.state.searching ?
this.spinner()
:
this.state.results.torrents[0] ? this.table() : this.noResults()
}
</div>
</div>
</div>
</div>
);
}
noResults() {
return (
<h1>No Results</h1>
)
}
table() {
return (
<div>
<table style="table-layout: fixed;" class="table table-fixed table-hover table-sm table-striped">
<thead>
<tr>
<th style="width: 50%">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>
<td style="word-wrap: break-word;">{torrent.name}</td>
<td class="text-right text-muted">{humanFileSize(torrent.size_bytes, true)}</td>
<td class="text-right text-success"><div><i class="fas fa-fw fa-arrow-up"></i>{torrent.seeders}</div></td>
<td class="text-right text-danger d-none d-md-table-cell"><i class="fas fa-fw fa-arrow-down"></i>{torrent.leechers}</td>
<td class="text-right text-muted d-none d-md-table-cell" title={`Scraped ${moment(torrent.scraped_date * 1000).fromNow()}`}>
{moment(torrent.created_unix * 1000).fromNow()}
</td>
<td class="text-right">
<a class="btn btn-sm" href={magnetLink(torrent.infohash)}>
<i title="magnet link" class="fas fa-fw fa-magnet"></i>
</a>
<a class="btn btn-sm d-none d-md-inline" href={`https://gitlab.com/dessalines/torrents.csv/issues/new?issue[assignee_id]=&issue[milestone_id]=&issue[title]=Report%20Torrent%20infohash%20${torrent.infohash}`}>
<i title="Report torrent" class="fas fa-fw fa-flag"></i>
</a>
</td>
</tr>
))}
</tbody>
</table>
{this.paginator()}
</div>
);
}
spinner() {
return (
<div class="text-center">
<i class="fas fa-spinner fa-spin fa-5x"></i>
</div>
);
}
paginator() {
return (
<nav>
<ul class="pagination">
<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.q}/${newSearch.page}`);
}
}

BIN
server/ui/src/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,209 +1,27 @@
import { render, Component, linkEvent } from 'inferno'; import { render, Component } from 'inferno';
import * as moment from 'moment'; import { HashRouter, Route, Switch, Link } from 'inferno-router';
import { endpoint } from './env'; import { Navbar } from './components/navbar';
import { SearchParams, Results, State } from './interfaces'; import { Home } from './components/home';
import { convertCsvToJson, humanFileSize, magnetLink } from './utils'; import { Search } from './components/search';
import './Main.css'; import './Main.css';
const container = document.getElementById('app'); const container = document.getElementById('app');
class TorrentSearchComponent extends Component<any, State> { class Index extends Component<any, any> {
state: State = {
results: {
torrents: []
},
searchParams: {
q: "",
page: 1
},
searching: false
};
constructor(props, context) {
super(props, context);
}
search(i: TorrentSearchComponent, event) {
event.preventDefault();
if (!!i.state.searchParams.q) {
i.setState({ searching: true, results: { torrents: [] } });
i.fetchData(i.state.searchParams)
.then(results => {
if (!!results) {
i.setState({
results: results
});
}
}).catch(error => {
console.error('request failed', error);
}).then(() => i.setState({ searching: false }));
} else {
i.setState({ results: { torrents: [] } });
}
}
fetchData(searchParams: SearchParams): Promise<Results> {
let q = encodeURI(searchParams.q);
return fetch(`${endpoint}/service/search?q=${q}&page=${searchParams.page}`)
.then(data => data.text())
.then(csv => convertCsvToJson(csv));
}
render() { render() {
return ( return (
<HashRouter>
<div> <div>
{this.navbar()} <Navbar />
<div className={this.state.results.torrents[0] ? "container-fluid" : "container"}> <Switch>
<div class="row mt-2"> <Route exact path="/" component={Home} />
<div class="col-12"> <Route path={`/search/:q/:page`} component={Search} />
{ </Switch>
this.state.searching ?
this.spinner()
:
this.state.results.torrents[0] ? this.table() : this.onboard()
}
</div> </div>
</div> </HashRouter>
</div> )
</div>
);
} }
table() {
return (
<div>
<table style="table-layout: fixed;" class="table table-fixed table-hover table-sm table-striped">
<thead>
<tr>
<th style="width: 50%">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>
<td style="word-wrap: break-word;">{torrent.name}</td>
<td class="text-right text-muted">{humanFileSize(torrent.size_bytes, true)}</td>
<td class="text-right text-success"><div><i class="fas fa-fw fa-arrow-up"></i>{torrent.seeders}</div></td>
<td class="text-right text-danger d-none d-md-table-cell"><i class="fas fa-fw fa-arrow-down"></i>{torrent.leechers}</td>
<td class="text-right text-muted d-none d-md-table-cell" title={`Scraped ${moment(torrent.scraped_date * 1000).fromNow()}`}>
{moment(torrent.created_unix * 1000).fromNow()}
</td>
<td class="text-right">
<a class="btn btn-sm" href={magnetLink(torrent.infohash)}>
<i title="magnet link" class="fas fa-fw fa-magnet"></i>
</a>
<a class="btn btn-sm d-none d-md-inline" href={`https://gitlab.com/dessalines/torrents.csv/issues/new?issue[assignee_id]=&issue[milestone_id]=&issue[title]=Report%20Torrent%20infohash%20${torrent.infohash}`}>
<i title="Report torrent" class="fas fa-fw fa-flag"></i>
</a>
</td>
</tr>
))}
</tbody>
</table>
{this.paginator()}
</div>
);
} }
render(<Index />, container);
navbar() {
return (
<nav class="navbar navbar-dark" style="background-color: #673ab7">
<a class="navbar-brand" href="#">
<i class="fas fa-fw fa-database mr-1"></i>Torrents.csv
</a>
<div class="col-12 col-sm-6">
{this.searchForm()}
</div>
</nav>
);
}
// TODO
// https://www.codeply.com/go/xBVaM3q5X4/bootstrap-4-navbar-search-full-width
searchForm() {
return (
<form class="my-2 my-lg-0 d-inline w-100" onSubmit={linkEvent(this, this.search)}>
<div class="input-group">
<input value={this.state.searchParams.q} onInput={linkEvent(this, this.searchChange)} type="text" class="form-control" placeholder="Search..." />
<button type="submit" class="btn btn-light border border-left-0 border-radius-left-0">
<i className={(this.state.searching) ? "fas fa-spinner fa-spin" : "fas fa-fw fa-search"}></i>
</button>
</div>
</form>
);
}
spinner() {
return (
<div class="text-center">
<i class="fas fa-spinner fa-spin fa-5x"></i>
</div>
);
}
onboard() {
let site: string = "https://gitlab.com/dessalines/torrents.csv";
return (
<div class="mt-2">
<a href={site}>Torrents.csv</a> is a collaborative, <b>vetted</b> git repository of torrents, consisting of a single, searchable <code>torrents.csv</code> file. Its initially populated with a January 2017 backup of the pirate bay, and new torrents are periodically added from various torrents sites via a rust script.<br></br><br></br>
<a href={site}>Torrents.csv</a> will only store torrents with at least one seeder to keep the file small, and will be periodically purged of non-seeded torrents, and sorted by seeders descending.<br></br><br></br>
To request more torrents, or add your own to the file, go <a href={site}>here</a>.<br></br><br></br>
Made with <a href="https://www.rust-lang.org">Rust</a>, <a href="https://github.com/BurntSushi/ripgrep">ripgrep</a>, <a href="https://actix.rs/">Actix</a>, <a href="https://www.infernojs.org">Inferno</a>, and <a href="https://www.typescriptlang.org/">Typescript</a>.
</div>
);
}
paginator() {
return (
<nav>
<ul class="pagination">
<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>
);
}
searchChange(i, event) {
let searchParams: SearchParams = {
q: event.target.value,
page: 1
}
i.setState({ searchParams: searchParams });
}
switchPage(a: { i: TorrentSearchComponent, nextPage: boolean }, event) {
let newSearch = a.i.state.searchParams;
newSearch.page += (a.nextPage) ? 1 : -1;
a.i.setState({
searchParams: newSearch
});
a.i.search(a.i, event);
}
}
render(<TorrentSearchComponent />, container);

View File

@ -18,9 +18,3 @@ export interface Torrent {
completed: number; completed: number;
scraped_date: number; scraped_date: number;
} }
export interface State {
results: Results;
searchParams: SearchParams;
searching: Boolean;
}

View File

@ -1104,6 +1104,22 @@ has-values@^1.0.0:
is-number "^3.0.0" is-number "^3.0.0"
kind-of "^4.0.0" kind-of "^4.0.0"
history@^4.7.2:
version "4.7.2"
resolved "https://registry.yarnpkg.com/history/-/history-4.7.2.tgz#22b5c7f31633c5b8021c7f4a8a954ac139ee8d5b"
integrity sha512-1zkBRWW6XweO0NBcjiphtVJVsIQ+SXF29z9DVkceeaSLVMFXHool+fdCZD4spDCfZJCILPILc3bm7Bc+HRi0nA==
dependencies:
invariant "^2.2.1"
loose-envify "^1.2.0"
resolve-pathname "^2.2.0"
value-equal "^0.4.0"
warning "^3.0.0"
hoist-non-inferno-statics@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/hoist-non-inferno-statics/-/hoist-non-inferno-statics-1.1.3.tgz#7d870f4160bfb6a59269b45c343c027f0e30ab35"
integrity sha1-fYcPQWC/tqWSabRcNDwCfw4wqzU=
http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3: http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3:
version "1.6.3" version "1.6.3"
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
@ -1149,6 +1165,16 @@ ignore-walk@^3.0.1:
dependencies: dependencies:
minimatch "^3.0.4" minimatch "^3.0.4"
inferno-router@^6.3.1:
version "6.3.1"
resolved "https://registry.yarnpkg.com/inferno-router/-/inferno-router-6.3.1.tgz#dc31d979e1ebeb313fc2c1d94f21f06969fd366b"
integrity sha512-Ze7SkL28aydxffONhjE0Dcl0uWdXPNVcNBQ1Jxfa2HQjT/09DPcDO5nwdWEyYkU3zJcdyPsThh2YESbfxk0eJg==
dependencies:
history "^4.7.2"
hoist-non-inferno-statics "^1.1.3"
inferno "6.3.1"
path-to-regexp-es6 "1.7.0"
inferno-shared@6.3.1: inferno-shared@6.3.1:
version "6.3.1" version "6.3.1"
resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-6.3.1.tgz#0ebb51aa397cabaaced4ef48b4e794d6f41f0455" resolved "https://registry.yarnpkg.com/inferno-shared/-/inferno-shared-6.3.1.tgz#0ebb51aa397cabaaced4ef48b4e794d6f41f0455"
@ -1159,7 +1185,7 @@ inferno-vnode-flags@6.3.1:
resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-6.3.1.tgz#8ef65130f1966c6ad38a0582a19d64c331094077" resolved "https://registry.yarnpkg.com/inferno-vnode-flags/-/inferno-vnode-flags-6.3.1.tgz#8ef65130f1966c6ad38a0582a19d64c331094077"
integrity sha512-Wr6xrWVxjjk4QRwDc6chbSkCko7birNLV60LnWoczILwFezPZHYg5U4fz/CMOc6u+0/cnDdfdV433SynwzW7Qw== integrity sha512-Wr6xrWVxjjk4QRwDc6chbSkCko7birNLV60LnWoczILwFezPZHYg5U4fz/CMOc6u+0/cnDdfdV433SynwzW7Qw==
inferno@^6.0.3: inferno@6.3.1, inferno@^6.0.3:
version "6.3.1" version "6.3.1"
resolved "https://registry.yarnpkg.com/inferno/-/inferno-6.3.1.tgz#51c11787c9dbd311e8df82dacb0efef0d2ff51c8" resolved "https://registry.yarnpkg.com/inferno/-/inferno-6.3.1.tgz#51c11787c9dbd311e8df82dacb0efef0d2ff51c8"
integrity sha512-FejSGGzymfFqvWG3wrOfRrv+cP1TOeq7aiBN7hZsCd0mcKnqMjEgdSXm1YltFekk+lpTFWEvCzD7SBETmYWwhg== integrity sha512-FejSGGzymfFqvWG3wrOfRrv+cP1TOeq7aiBN7hZsCd0mcKnqMjEgdSXm1YltFekk+lpTFWEvCzD7SBETmYWwhg==
@ -1225,6 +1251,13 @@ inquirer@^3.0.6:
strip-ansi "^4.0.0" strip-ansi "^4.0.0"
through "^2.3.6" through "^2.3.6"
invariant@^2.2.1:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==
dependencies:
loose-envify "^1.0.0"
ipaddr.js@1.8.0: ipaddr.js@1.8.0:
version "1.8.0" version "1.8.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.8.0.tgz#eaa33d6ddd7ace8f7f6fe0c9ca0440e706738b1e"
@ -1392,6 +1425,11 @@ is-windows@^1.0.2:
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA== integrity sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==
isarray@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=
isarray@1.0.0, isarray@~1.0.0: isarray@1.0.0, isarray@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
@ -1414,6 +1452,11 @@ isstream@~0.1.2:
resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo= integrity sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=
"js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
jsbn@~0.1.0: jsbn@~0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@ -1500,6 +1543,13 @@ lodash@^4.3.0:
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d"
integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==
loose-envify@^1.0.0, loose-envify@^1.2.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
map-cache@^0.2.2: map-cache@^0.2.2:
version "0.2.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
@ -1914,11 +1964,25 @@ path-is-absolute@^1.0.0:
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18=
path-to-regexp-es6@1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/path-to-regexp-es6/-/path-to-regexp-es6-1.7.0.tgz#965657c9f8ea8f660e103ccb839abf038cba4caf"
integrity sha512-QdT7okCAMGv7FR7w6KWFH9OSMivOgtXAGKodD6MDZBNR/XNL16W+hHoj6qBmV6cy/7eR1fr0Qujrg9OhBf5QPw==
dependencies:
path-to-regexp "1.7.0"
path-to-regexp@0.1.7: path-to-regexp@0.1.7:
version "0.1.7" version "0.1.7"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w= integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.7.0.tgz#59fde0f435badacba103a84e9d3bc64e96b9937d"
integrity sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=
dependencies:
isarray "0.0.1"
performance-now@^2.1.0: performance-now@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b" resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@ -2172,6 +2236,11 @@ request@^2.79.0:
tunnel-agent "^0.6.0" tunnel-agent "^0.6.0"
uuid "^3.3.2" uuid "^3.3.2"
resolve-pathname@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879"
integrity sha512-bAFz9ld18RzJfddgrO2e/0S2O81710++chRMUxHjXOYKF6jTAMrUNZrEZ1PvV0zlhfjidm08iRPdTLPno1FuRg==
resolve-url@^0.2.1: resolve-url@^0.2.1:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" resolved "https://registry.yarnpkg.com/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a"
@ -2681,6 +2750,11 @@ uuid@^3.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
value-equal@^0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-0.4.0.tgz#c5bdd2f54ee093c04839d71ce2e4758a6890abc7"
integrity sha512-x+cYdNnaA3CxvMaTX0INdTCN8m8aF2uY9BvEqmxuYp8bL09cs/kWVQPVGcA35fMktdOsP69IgU7wFj/61dJHEw==
vary@~1.1.2: vary@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
@ -2695,6 +2769,13 @@ verror@1.10.0:
core-util-is "1.0.2" core-util-is "1.0.2"
extsprintf "^1.2.0" extsprintf "^1.2.0"
warning@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/warning/-/warning-3.0.0.tgz#32e5377cb572de4ab04753bdf8821c01ed605b7c"
integrity sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=
dependencies:
loose-envify "^1.0.0"
watch@^1.0.1: watch@^1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c" resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c"