From 2d8bb48ba897dff6e8d5a60aa75def6cafaaf573 Mon Sep 17 00:00:00 2001 From: Dessalines Date: Tue, 27 Nov 2018 14:29:56 -0700 Subject: [PATCH] Adding Routing. Fixes #16 Avoiding index.html redirect. Fixes #17 Adding Favicon. Fixes #14 --- server/service/src/main.rs | 17 ++- server/ui/fuse.js | 2 +- server/ui/package.json | 1 + server/ui/src/components/home.tsx | 28 ++++ server/ui/src/components/navbar.tsx | 68 +++++++++ server/ui/src/components/search.tsx | 178 +++++++++++++++++++++++ server/ui/src/favicon.ico | Bin 0 -> 1150 bytes server/ui/src/index.tsx | 214 +++------------------------- server/ui/src/interfaces.ts | 6 - server/ui/yarn.lock | 83 ++++++++++- 10 files changed, 388 insertions(+), 209 deletions(-) create mode 100644 server/ui/src/components/home.tsx create mode 100644 server/ui/src/components/navbar.tsx create mode 100644 server/ui/src/components/search.tsx create mode 100644 server/ui/src/favicon.ico diff --git a/server/service/src/main.rs b/server/service/src/main.rs index d316a63..14a3e0c 100644 --- a/server/service/src/main.rs +++ b/server/service/src/main.rs @@ -5,7 +5,7 @@ extern crate serde_derive; extern crate grep; 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; @@ -15,14 +15,17 @@ use grep::searcher::{BinaryDetection, SearcherBuilder}; use std::fs::File; fn main() { + server::new(|| { App::new() .route("/service/search", http::Method::GET, search) + .resource("/favicon.ico", |r| r.f(favicon)) + .resource("/", |r| r.f(index)) .handler( - "/", + "/static", fs::StaticFiles::new("../ui/dist/") .unwrap() - .index_file("index.html"), + // .index_file("index.html"), ) .finish() }).bind("127.0.0.1:8080") @@ -30,6 +33,14 @@ fn main() { .run(); } +fn index(_req: &HttpRequest) -> Result { + Ok(NamedFile::open("../ui/dist/index.html")?) +} + +fn favicon(_req: &HttpRequest) -> Result { + Ok(NamedFile::open("../ui/src/favicon.ico")?) +} + #[derive(Deserialize)] struct SearchQuery { q: String, diff --git a/server/ui/fuse.js b/server/ui/fuse.js index 187d7c1..cc0f075 100644 --- a/server/ui/fuse.js +++ b/server/ui/fuse.js @@ -27,10 +27,10 @@ Sparky.task('config', _ => { plugins: [ EnvPlugin({ NODE_ENV: isProduction ? 'production' : 'development' }), CSSPlugin(), - CopyPlugin({ files: ["src/favicon.ico"] }), WebIndexPlugin({ title: 'Inferno Typescript FuseBox Example', template: 'src/index.html', + path: isProduction ? "/static" : "/" }), isProduction && QuantumPlugin({ diff --git a/server/ui/package.json b/server/ui/package.json index c042b44..4d0f230 100644 --- a/server/ui/package.json +++ b/server/ui/package.json @@ -13,6 +13,7 @@ "dependencies": { "classcat": "^1.1.3", "inferno": "^6.0.3", + "inferno-router": "^6.3.1", "moment": "^2.22.2" }, "devDependencies": { diff --git a/server/ui/src/components/home.tsx b/server/ui/src/components/home.tsx new file mode 100644 index 0000000..c4f2694 --- /dev/null +++ b/server/ui/src/components/home.tsx @@ -0,0 +1,28 @@ +import { Component } from 'inferno'; + +export class Home extends Component { + + render() { + return ( +
+ {this.onboard()} +
+ ) + } + + onboard() { + let site: string = "https://gitlab.com/dessalines/torrents.csv"; + return ( +
+
+ Torrents.csv is a collaborative, vetted git repository of torrents, consisting of a single, searchable torrents.csv 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.



+ Torrents.csv 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.



+ To request more torrents, or add your own to the file, go here.



+ Made with Rust, ripgrep, Actix, Inferno, and Typescript. + +
+ ); + } + +} + diff --git a/server/ui/src/components/navbar.tsx b/server/ui/src/components/navbar.tsx new file mode 100644 index 0000000..8aecc70 --- /dev/null +++ b/server/ui/src/components/navbar.tsx @@ -0,0 +1,68 @@ +import { Component, linkEvent } from 'inferno'; +import { SearchParams } from '../interfaces'; + + +interface State { + searchParams: SearchParams; +} + +export class Navbar extends Component { + + state: State = { + searchParams: { + page: 1, + q: "" + } + } + + constructor(props, context) { + super(props, context); + } + + render() { + return ( +
{this.navbar()}
+ ) + } + + navbar() { + return ( + + ); + } + + // TODO + // https://www.codeply.com/go/xBVaM3q5X4/bootstrap-4-navbar-search-full-width + searchForm() { + return ( +
+
+ + +
+
+ ); + } + + 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 }); + } +} \ No newline at end of file diff --git a/server/ui/src/components/search.tsx b/server/ui/src/components/search.tsx new file mode 100644 index 0000000..f8db1a9 --- /dev/null +++ b/server/ui/src/components/search.tsx @@ -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 { + + 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 { + 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 ( +
+
+
+
+ { + this.state.searching ? + this.spinner() + : + this.state.results.torrents[0] ? this.table() : this.noResults() + } +
+
+
+
+ ); + } + + noResults() { + return ( +

No Results

+ ) + } + + table() { + return ( +
+ + + + + + + + + + + + + {this.state.results.torrents.map(torrent => ( + + + + + + + + + ))} + +
NameSizeSeedsLeechesCreated
{torrent.name}{humanFileSize(torrent.size_bytes, true)}
{torrent.seeders}
{torrent.leechers} + {moment(torrent.created_unix * 1000).fromNow()} + + + + + + + +
+ {this.paginator()} +
+ ); + } + + spinner() { + return ( +
+ +
+ ); + } + + paginator() { + return ( + + ); + } + + 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}`); + } + +} \ No newline at end of file diff --git a/server/ui/src/favicon.ico b/server/ui/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..13f310e9f127f7d158d22478ffd2413d1a46e585 GIT binary patch literal 1150 zcmeH`%?`mp6os#reQkuDjl|C0L-Mq;CruXB5D z&-A8u#;n3f664;sMb)ffW?eu+dFf1MWCIaDp@3J&L8RU<`5o`)GS` z1<3;bD_*TXgC5kO=20wd3QM>I{4Mk$XpMLE56CwI{uI4v{o8>5U;l6af6?AO-wX6F m`=I~3fIq@N0`0#e=*-+ailr&1JsRcxWuKihJ8&AyAnyZdacNfo literal 0 HcmV?d00001 diff --git a/server/ui/src/index.tsx b/server/ui/src/index.tsx index 7d54aa5..6cf899c 100644 --- a/server/ui/src/index.tsx +++ b/server/ui/src/index.tsx @@ -1,209 +1,27 @@ -import { render, Component, linkEvent } from 'inferno'; -import * as moment from 'moment'; +import { render, Component } from 'inferno'; +import { HashRouter, Route, Switch, Link } from 'inferno-router'; -import { endpoint } from './env'; -import { SearchParams, Results, State } from './interfaces'; -import { convertCsvToJson, humanFileSize, magnetLink } from './utils'; +import { Navbar } from './components/navbar'; +import { Home } from './components/home'; +import { Search } from './components/search'; import './Main.css'; const container = document.getElementById('app'); -class TorrentSearchComponent extends Component { - - 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 { - let q = encodeURI(searchParams.q); - return fetch(`${endpoint}/service/search?q=${q}&page=${searchParams.page}`) - .then(data => data.text()) - .then(csv => convertCsvToJson(csv)); - } - +class Index extends Component { render() { return ( -
- {this.navbar()} -
-
-
- { - this.state.searching ? - this.spinner() - : - this.state.results.torrents[0] ? this.table() : this.onboard() - } -
+ +
+ + + + +
-
-
- ); + + ) } - - table() { - return ( -
- - - - - - - - - - - - - {this.state.results.torrents.map(torrent => ( - - - - - - - - - ))} - -
NameSizeSeedsLeechesCreated
{torrent.name}{humanFileSize(torrent.size_bytes, true)}
{torrent.seeders}
{torrent.leechers} - {moment(torrent.created_unix * 1000).fromNow()} - - - - - - - -
- {this.paginator()} -
- ); - } - - navbar() { - return ( - - ); - } - - // TODO - // https://www.codeply.com/go/xBVaM3q5X4/bootstrap-4-navbar-search-full-width - searchForm() { - return ( -
-
- - -
-
- ); - } - - spinner() { - return ( -
- -
- ); - } - - onboard() { - let site: string = "https://gitlab.com/dessalines/torrents.csv"; - return ( -
- Torrents.csv is a collaborative, vetted git repository of torrents, consisting of a single, searchable torrents.csv 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.



- Torrents.csv 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.



- To request more torrents, or add your own to the file, go here.



- Made with Rust, ripgrep, Actix, Inferno, and Typescript. -
- ); - } - - paginator() { - return ( - - ); - } - - 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(, container); +render(, container); diff --git a/server/ui/src/interfaces.ts b/server/ui/src/interfaces.ts index 32fb3e7..e43eeb7 100644 --- a/server/ui/src/interfaces.ts +++ b/server/ui/src/interfaces.ts @@ -17,10 +17,4 @@ export interface Torrent { leechers: number; completed: number; scraped_date: number; -} - -export interface State { - results: Results; - searchParams: SearchParams; - searching: Boolean; } \ No newline at end of file diff --git a/server/ui/yarn.lock b/server/ui/yarn.lock index 486f93d..423bab7 100644 --- a/server/ui/yarn.lock +++ b/server/ui/yarn.lock @@ -1104,6 +1104,22 @@ has-values@^1.0.0: is-number "^3.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: version "1.6.3" resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" @@ -1149,6 +1165,16 @@ ignore-walk@^3.0.1: dependencies: 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: version "6.3.1" 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" integrity sha512-Wr6xrWVxjjk4QRwDc6chbSkCko7birNLV60LnWoczILwFezPZHYg5U4fz/CMOc6u+0/cnDdfdV433SynwzW7Qw== -inferno@^6.0.3: +inferno@6.3.1, inferno@^6.0.3: version "6.3.1" resolved "https://registry.yarnpkg.com/inferno/-/inferno-6.3.1.tgz#51c11787c9dbd311e8df82dacb0efef0d2ff51c8" integrity sha512-FejSGGzymfFqvWG3wrOfRrv+cP1TOeq7aiBN7hZsCd0mcKnqMjEgdSXm1YltFekk+lpTFWEvCzD7SBETmYWwhg== @@ -1225,6 +1251,13 @@ inquirer@^3.0.6: strip-ansi "^4.0.0" 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: version "1.8.0" 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" 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: version "1.0.0" 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" 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: version "0.1.1" 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" 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: version "0.2.2" 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" 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: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 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: version "2.1.0" 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" 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: version "0.2.1" 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" 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: version "1.1.2" 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" 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: version "1.0.2" resolved "https://registry.yarnpkg.com/watch/-/watch-1.0.2.tgz#340a717bde765726fa0aa07d721e0147a551df0c"